From 41d359589c6a3b6c7cb86dce5819006aee5b15a9 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 15:13:00 -0500 Subject: [PATCH 001/199] add multiple gpu support to backend --- desc/__init__.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++-- desc/backend.py | 17 ++++++++++++---- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 840b9985b9..9951062d56 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -61,7 +61,7 @@ def __getattr__(name): config = {"device": None, "avail_mem": None, "kind": None} -def set_device(kind="cpu"): +def set_device(kind="cpu", multigpu=False): # noqa: C901 """Sets the device to use for computation. If kind==``'gpu'``, checks available GPUs and selects the one with the most @@ -73,6 +73,8 @@ def set_device(kind="cpu"): ---------- kind : {``'cpu'``, ``'gpu'``} whether to use CPU or GPU. + multigpu : bool + whether to use multiple GPUs or not. Default is False. """ config["kind"] = kind @@ -85,7 +87,7 @@ def set_device(kind="cpu"): config["device"] = "CPU" config["avail_mem"] = cpu_mem - if kind == "gpu": + if kind == "gpu" and not multigpu: # Set CUDA_DEVICE_ORDER so the IDs assigned by CUDA match those from nvidia-smi os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" import nvgpu @@ -139,3 +141,49 @@ def set_device(kind="cpu"): selected_gpu["mem_total"] - selected_gpu["mem_used"] ) / 1024 # in GB os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_gpu["index"]) + + if kind == "gpu" and multigpu: + # Set CUDA_DEVICE_ORDER so the IDs assigned by CUDA match those from nvidia-smi + os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" + import nvgpu + + try: + devices = nvgpu.gpu_info() + except FileNotFoundError: + devices = [] + if len(devices) == 0: + warnings.warn(colored("No GPU found, falling back to CPU", "yellow")) + set_device(kind="cpu") + return + + gpu_ids = [dev["index"] for dev in devices] + if len(gpu_ids) == 0: + # cuda visible devices = '' -> don't use any gpu + warnings.warn( + colored( + ( + "CUDA_VISIBLE_DEVICES={} ".format( + os.environ["CUDA_VISIBLE_DEVICES"] + ) + + "did not match any physical GPU " + + "(id={}), falling back to CPU".format( + [dev["index"] for dev in devices] + ) + ), + "yellow", + ) + ) + set_device(kind="cpu") + return + + devices = [dev for dev in devices if dev["index"] in gpu_ids] + memories = {} + for dev in devices: + mem = dev["mem_total"] - dev["mem_used"] + memories[dev["index"]] = mem + config["devices"] = [ + dev["type"] + " (id={})".format(dev["index"]) for dev in devices + ] + config["avail_mems"] = [ + memories[dev["index"]] / 1024 for dev in devices + ] # in GB diff --git a/desc/backend.py b/desc/backend.py index 3704cb0bb1..44bc90dd25 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -59,11 +59,20 @@ desc.__version__, np.__version__, y.dtype ) ) -print( - "Using device: {}, with {:.2f} GB available memory".format( - desc_config.get("device"), desc_config.get("avail_mem") + +if hasattr(desc_config, "device"): + print( + "Using device: {}, with {:.2f} GB available memory".format( + desc_config.get("device"), desc_config.get("avail_mem") + ) ) -) +elif hasattr(desc_config, "devices"): + print(f"Using {len(desc_config["devices"])} devices:") + for i, dev in enumerate(desc_config["devices"]): + print( + f"\t Device {i}: {dev} with {desc_config["avail_mems"][i]:.2f} " + "GB available memory" + ) if use_jax: # noqa: C901 from jax import custom_jvp, jit, vmap From 860b1ff2e762460fef8cede498db7137c4847aee Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 15:29:35 -0500 Subject: [PATCH 002/199] fix if statement --- desc/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 44bc90dd25..6ed2ac3348 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -60,13 +60,13 @@ ) ) -if hasattr(desc_config, "device"): +if "device" in desc_config.keys(): print( "Using device: {}, with {:.2f} GB available memory".format( desc_config.get("device"), desc_config.get("avail_mem") ) ) -elif hasattr(desc_config, "devices"): +elif "devices" in desc_config.keys(): print(f"Using {len(desc_config["devices"])} devices:") for i, dev in enumerate(desc_config["devices"]): print( From 003eca90aca86d6d99c4cec2224f66ef195002c1 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 15:45:19 -0500 Subject: [PATCH 003/199] fix stuff --- desc/__init__.py | 6 ++++++ desc/backend.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 9951062d56..37a7755a22 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -157,6 +157,12 @@ def set_device(kind="cpu", multigpu=False): # noqa: C901 return gpu_ids = [dev["index"] for dev in devices] + if "CUDA_VISIBLE_DEVICES" in os.environ: + cuda_ids = [ + s for s in re.findall(r"\b\d+\b", os.environ["CUDA_VISIBLE_DEVICES"]) + ] + # check that the visible devices actually exist and are gpus + gpu_ids = [i for i in cuda_ids if i in gpu_ids] if len(gpu_ids) == 0: # cuda visible devices = '' -> don't use any gpu warnings.warn( diff --git a/desc/backend.py b/desc/backend.py index 6ed2ac3348..7aa8b0e69c 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -60,13 +60,13 @@ ) ) -if "device" in desc_config.keys(): +if "devices" not in desc_config.keys(): print( "Using device: {}, with {:.2f} GB available memory".format( desc_config.get("device"), desc_config.get("avail_mem") ) ) -elif "devices" in desc_config.keys(): +else: print(f"Using {len(desc_config["devices"])} devices:") for i, dev in enumerate(desc_config["devices"]): print( From 74f5f3d4f2b0c6aab2560543170f5cdfaebdfbca Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 15:47:06 -0500 Subject: [PATCH 004/199] try --- desc/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 37a7755a22..6af645128a 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -143,8 +143,6 @@ def set_device(kind="cpu", multigpu=False): # noqa: C901 os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_gpu["index"]) if kind == "gpu" and multigpu: - # Set CUDA_DEVICE_ORDER so the IDs assigned by CUDA match those from nvidia-smi - os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" import nvgpu try: From eed3f6d02da415c96c95d5a9ff82028054e36c14 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 15:58:20 -0500 Subject: [PATCH 005/199] fix issue --- desc/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desc/__init__.py b/desc/__init__.py index 6af645128a..3097d9bc2f 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -185,6 +185,8 @@ def set_device(kind="cpu", multigpu=False): # noqa: C901 for dev in devices: mem = dev["mem_total"] - dev["mem_used"] memories[dev["index"]] = mem + + config["device"] = "gpu" config["devices"] = [ dev["type"] + " (id={})".format(dev["index"]) for dev in devices ] From b7e9435b442f0b83d5514619074d18e0a52cf344 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 16:11:32 -0500 Subject: [PATCH 006/199] update jac_chunk_size assignment --- desc/objectives/objective_funs.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 3e3af89836..1f69ccd047 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -350,10 +350,13 @@ def build(self, use_jit=None, verbose=1): # Heuristic estimates of fwd mode Jacobian memory usage, # slightly conservative, based on using ForceBalance as the objective estimated_memory_usage = 2.4e-7 * self.dim_f * self.dim_x + 1 # in GB + mem_avail = ( + desc_config.get("avail_mem") + if desc_config.get("avail_mem") is not None + else sum(desc_config["avail_mems"]) + ) max_chunk_size = round( - (desc_config.get("avail_mem") / estimated_memory_usage - 0.22) - / 0.85 - * self.dim_x + (mem_avail / estimated_memory_usage - 0.22) / 0.85 * self.dim_x ) self._jac_chunk_size = max([1, max_chunk_size]) if self._deriv_mode == "blocked": From ab7402ea4eb767c404225543d3a5d55d924b0fa0 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 16:48:16 -0500 Subject: [PATCH 007/199] try putting the grid accross devices --- desc/grid.py | 6 +++--- desc/objectives/objective_funs.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/desc/grid.py b/desc/grid.py index e6ebbb6831..d86aead70c 100644 --- a/desc/grid.py +++ b/desc/grid.py @@ -1211,7 +1211,7 @@ def _create_nodes( # noqa: C901 nodes = np.column_stack([r, t, z]) spacing = np.column_stack([dr, dt, dz]) - return nodes, spacing + return jnp.asarray(nodes), jnp.asarray(spacing) def change_resolution(self, L, M, N, NFP=None): """Change the resolution of the grid. @@ -1349,7 +1349,7 @@ def _create_nodes(self, L=1, M=1, N=1, NFP=1): nodes = np.column_stack([r, t, z]) spacing = np.column_stack([dr, dt, dz]) - return nodes, spacing + return jnp.asarray(nodes), jnp.asarray(spacing) def change_resolution(self, L, M, N, NFP=None): """Change the resolution of the grid. @@ -1551,7 +1551,7 @@ def ocs(L): nodes = np.column_stack([r, t, z]) spacing = np.column_stack([dr, dt, dz]) - return nodes, spacing + return jnp.asarray(nodes), jnp.asarray(spacing) def change_resolution(self, L, M, N, NFP=None): """Change the resolution of the grid. diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 1f69ccd047..04260417b6 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -8,6 +8,7 @@ from desc.backend import ( desc_config, execute_on_cpu, + jax, jit, jnp, tree_flatten, @@ -1143,6 +1144,23 @@ def build(self, use_jit=True, verbose=1): self._check_dimensions() self._set_derivatives() + if "avail_mems" in desc_config.keys(): + grid = self._constants["transforms"]["grid"] + num_gpu = len(desc_config["avail_mems"]) + mesh = jax.make_mesh((num_gpu,), ("grid")) + grid._nodes = jax.device_put( + grid.nodes, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), + ) + grid._spacing = jax.device_put( + grid.spacing, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), + ) + grid._weights = jax.device_put( + grid.weights, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), + ) + # set quadrature weights if they haven't been if hasattr(self, "_constants") and ("quad_weights" not in self._constants): grid = self._constants["transforms"]["grid"] From 2a7ab0debc4df41148eb06e9f2b8c6e84caa975b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 16:50:11 -0500 Subject: [PATCH 008/199] fix issue with none constants --- desc/objectives/objective_funs.py | 37 ++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 04260417b6..81c54025f5 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1145,21 +1145,28 @@ def build(self, use_jit=True, verbose=1): self._set_derivatives() if "avail_mems" in desc_config.keys(): - grid = self._constants["transforms"]["grid"] - num_gpu = len(desc_config["avail_mems"]) - mesh = jax.make_mesh((num_gpu,), ("grid")) - grid._nodes = jax.device_put( - grid.nodes, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), - ) - grid._spacing = jax.device_put( - grid.spacing, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), - ) - grid._weights = jax.device_put( - grid.weights, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), - ) + if hasattr(self, "_constants"): + grid = self._constants["transforms"]["grid"] + num_gpu = len(desc_config["avail_mems"]) + mesh = jax.make_mesh((num_gpu,), ("grid")) + grid._nodes = jax.device_put( + grid.nodes, + jax.sharding.NamedSharding( + mesh, jax.sharding.PartitionSpec("grid") + ), + ) + grid._spacing = jax.device_put( + grid.spacing, + jax.sharding.NamedSharding( + mesh, jax.sharding.PartitionSpec("grid") + ), + ) + grid._weights = jax.device_put( + grid.weights, + jax.sharding.NamedSharding( + mesh, jax.sharding.PartitionSpec("grid") + ), + ) # set quadrature weights if they haven't been if hasattr(self, "_constants") and ("quad_weights" not in self._constants): From afa349cb2d4fde702139326bc9bb299677976e69 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 16:53:23 -0500 Subject: [PATCH 009/199] revert jnp.asarrays in grid --- desc/grid.py | 22 ++++++++++------------ desc/objectives/objective_funs.py | 6 +++--- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/desc/grid.py b/desc/grid.py index d86aead70c..b3757ea376 100644 --- a/desc/grid.py +++ b/desc/grid.py @@ -706,8 +706,8 @@ class Grid(_Grid): nodes.reshape((num_poloidal, num_radial, num_toroidal, 3), order="F"). jitable : bool Whether to skip certain checks and conditionals that don't work under jit. - Allows grid to be created on the fly with custom nodes, but weights, - symmetry etc. may be wrong if grid contains duplicate nodes. + Allows grid to be created on the fly with custom nodes, but weights, symmetry + etc. may be wrong if grid contains duplicate nodes. """ def __init__( @@ -793,7 +793,6 @@ def create_meshgrid( coordinates="rtz", period=(np.inf, 2 * np.pi, 2 * np.pi), NFP=1, - jitable=True, **kwargs, ): """Create a tensor-product grid from the given coordinates in a jitable manner. @@ -820,10 +819,6 @@ def create_meshgrid( Only makes sense to change from 1 if last coordinate is periodic with some constant divided by ``NFP`` and the nodes are placed within one field period. - jitable : bool - Whether to skip certain checks and conditionals that don't work under jit. - Allows grid to be created on the fly with custom nodes, but weights, - symmetry etc. may be wrong if grid contains duplicate nodes. Returns ------- @@ -866,7 +861,10 @@ def create_meshgrid( repeat(unique_a_idx // b.size, b.size, total_repeat_length=a.size * b.size), c.size, ) - inverse_b_idx = jnp.tile(unique_b_idx, a.size * c.size) + inverse_b_idx = jnp.tile( + unique_b_idx, + a.size * c.size, + ) inverse_c_idx = repeat(unique_c_idx // (a.size * b.size), (a.size * b.size)) return Grid( nodes=nodes, @@ -877,7 +875,7 @@ def create_meshgrid( NFP=NFP, sort=False, is_meshgrid=True, - jitable=jitable, + jitable=True, _unique_rho_idx=unique_a_idx, _unique_poloidal_idx=unique_b_idx, _unique_zeta_idx=unique_c_idx, @@ -1211,7 +1209,7 @@ def _create_nodes( # noqa: C901 nodes = np.column_stack([r, t, z]) spacing = np.column_stack([dr, dt, dz]) - return jnp.asarray(nodes), jnp.asarray(spacing) + return nodes, spacing def change_resolution(self, L, M, N, NFP=None): """Change the resolution of the grid. @@ -1349,7 +1347,7 @@ def _create_nodes(self, L=1, M=1, N=1, NFP=1): nodes = np.column_stack([r, t, z]) spacing = np.column_stack([dr, dt, dz]) - return jnp.asarray(nodes), jnp.asarray(spacing) + return nodes, spacing def change_resolution(self, L, M, N, NFP=None): """Change the resolution of the grid. @@ -1551,7 +1549,7 @@ def ocs(L): nodes = np.column_stack([r, t, z]) spacing = np.column_stack([dr, dt, dz]) - return jnp.asarray(nodes), jnp.asarray(spacing) + return nodes, spacing def change_resolution(self, L, M, N, NFP=None): """Change the resolution of the grid. diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 81c54025f5..df18031f31 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1150,19 +1150,19 @@ def build(self, use_jit=True, verbose=1): num_gpu = len(desc_config["avail_mems"]) mesh = jax.make_mesh((num_gpu,), ("grid")) grid._nodes = jax.device_put( - grid.nodes, + jnp.asarray(grid.nodes), jax.sharding.NamedSharding( mesh, jax.sharding.PartitionSpec("grid") ), ) grid._spacing = jax.device_put( - grid.spacing, + jnp.asarray(grid.spacing), jax.sharding.NamedSharding( mesh, jax.sharding.PartitionSpec("grid") ), ) grid._weights = jax.device_put( - grid.weights, + jnp.asarray(grid.weights), jax.sharding.NamedSharding( mesh, jax.sharding.PartitionSpec("grid") ), From 04af924d4fce14d88e75a3f2f450bb051cc88cef Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 17:02:11 -0500 Subject: [PATCH 010/199] replicate state vector on all deviecs --- desc/optimize/_constraint_wrappers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 7695a671b4..e2c5c59ce7 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import jit, jnp +from desc.backend import desc_config, jax, jit, jnp from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -129,7 +129,16 @@ def project(self, x): def recover(self, x_reduced): """Recover the full state vector from the reduced optimization vector.""" - return self._recover(x_reduced) + x_full = self._recover(x_reduced) + if "avail_mems" in desc_config.keys(): + if hasattr(self, "_constants"): + num_gpu = len(desc_config["avail_mems"]) + mesh = jax.make_mesh((num_gpu,), ("grid")) + x_full = jax.device_put( + x_full, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec()), + ) + return x_full def x(self, *things): """Return the reduced state vector from the Equilibrium eq.""" From aa4f9aa0981664cc13de9e81b3240ee179ebc209 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 17:51:28 -0500 Subject: [PATCH 011/199] allow variable number of gpus, copy some data to every device --- desc/__init__.py | 54 ++++++++++++++++++++------- desc/objectives/objective_funs.py | 15 ++++++-- desc/optimize/_constraint_wrappers.py | 18 +++++---- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 3097d9bc2f..32410f8af7 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -58,10 +58,10 @@ def __getattr__(name): BANNER = colored(_BANNER, "magenta") -config = {"device": None, "avail_mem": None, "kind": None} +config = {"device": None, "avail_mem": None, "kind": None, "num_device": 1} -def set_device(kind="cpu", multigpu=False): # noqa: C901 +def set_device(kind="cpu", num_device=None): # noqa: C901 """Sets the device to use for computation. If kind==``'gpu'``, checks available GPUs and selects the one with the most @@ -73,8 +73,8 @@ def set_device(kind="cpu", multigpu=False): # noqa: C901 ---------- kind : {``'cpu'``, ``'gpu'``} whether to use CPU or GPU. - multigpu : bool - whether to use multiple GPUs or not. Default is False. + num_device : int + number of devices to use. If None, uses only one device. """ config["kind"] = kind @@ -87,7 +87,7 @@ def set_device(kind="cpu", multigpu=False): # noqa: C901 config["device"] = "CPU" config["avail_mem"] = cpu_mem - if kind == "gpu" and not multigpu: + if kind == "gpu" and num_device is None: # Set CUDA_DEVICE_ORDER so the IDs assigned by CUDA match those from nvidia-smi os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" import nvgpu @@ -142,7 +142,7 @@ def set_device(kind="cpu", multigpu=False): # noqa: C901 ) / 1024 # in GB os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_gpu["index"]) - if kind == "gpu" and multigpu: + if kind == "gpu" and num_device is not None and num_device > 1: import nvgpu try: @@ -186,10 +186,38 @@ def set_device(kind="cpu", multigpu=False): # noqa: C901 mem = dev["mem_total"] - dev["mem_used"] memories[dev["index"]] = mem - config["device"] = "gpu" - config["devices"] = [ - dev["type"] + " (id={})".format(dev["index"]) for dev in devices - ] - config["avail_mems"] = [ - memories[dev["index"]] / 1024 for dev in devices - ] # in GB + if num_device > len(devices): + warnings.warn( + colored( + "Requested {} GPUs, but only {} available".format( + num_device, len(devices) + ), + "yellow", + ) + ) + return + elif num_device < len(devices): + config["device"] = "gpu" + config["devices"] = [ + dev["type"] + " (id={})".format(dev["index"]) + for dev in devices[:num_device] + ] + config["avail_mems"] = [ + memories[dev["index"]] / 1024 for dev in devices[:num_device] + ] # in GB + config["num_device"] = num_device + # make the other gpu's invisible + visible_devices = "0" + for i in range(1, num_device): + visible_devices += f",{i}" + os.environ["CUDA_VISIBLE_DEVICES"] = visible_devices + else: + config["device"] = "gpu" + config["devices"] = [ + dev["type"] + " (id={})".format(dev["index"]) for dev in devices + ] + config["avail_mems"] = [ + memories[dev["index"]] / 1024 for dev in devices + ] # in GB + config["num_device"] = num_device + # by default all gpus are already visible diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index df18031f31..b7b434b6f7 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1144,11 +1144,11 @@ def build(self, use_jit=True, verbose=1): self._check_dimensions() self._set_derivatives() - if "avail_mems" in desc_config.keys(): + if desc_config["num_device"] != 1: if hasattr(self, "_constants"): grid = self._constants["transforms"]["grid"] - num_gpu = len(desc_config["avail_mems"]) - mesh = jax.make_mesh((num_gpu,), ("grid")) + mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) + # shard nodes, spacing, and weights across devices grid._nodes = jax.device_put( jnp.asarray(grid.nodes), jax.sharding.NamedSharding( @@ -1168,6 +1168,15 @@ def build(self, use_jit=True, verbose=1): ), ) + # replicate profiles across devices + # TODO: profiles are dict of arrays, need to shard each array + if False: + profiles = self._constants["profiles"] + profiles = jax.device_put( + profiles, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec()), + ) + # set quadrature weights if they haven't been if hasattr(self, "_constants") and ("quad_weights" not in self._constants): grid = self._constants["transforms"]["grid"] diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index e2c5c59ce7..af9107ed1f 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -130,14 +130,12 @@ def project(self, x): def recover(self, x_reduced): """Recover the full state vector from the reduced optimization vector.""" x_full = self._recover(x_reduced) - if "avail_mems" in desc_config.keys(): - if hasattr(self, "_constants"): - num_gpu = len(desc_config["avail_mems"]) - mesh = jax.make_mesh((num_gpu,), ("grid")) - x_full = jax.device_put( - x_full, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec()), - ) + if desc_config["num_device"] != 1: + mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) + x_full = jax.device_put( + x_full, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec()), + ) return x_full def x(self, *things): @@ -300,6 +298,8 @@ def _jac(self, x_reduced, constants=None, op="scaled"): x = self.recover(x_reduced) v = self._unfixed_idx_mat df = getattr(self._objective, "jvp_" + op)(v.T, x, constants) + if desc_config["num_device"] != 1: + df = jax.device_put(df, jax.devices("gpu")[0]) return df.T def jac_scaled(self, x_reduced, constants=None): @@ -360,6 +360,8 @@ def _jvp(self, v, x_reduced, constants=None, op="jvp_scaled"): x = self.recover(x_reduced) v = self._unfixed_idx_mat @ v df = getattr(self._objective, op)(v, x, constants) + if desc_config["num_device"] != 1: + df = jax.device_put(df, jax.devices("gpu")[0]) return df def jvp_scaled(self, v, x_reduced, constants=None): From d67688815cd0e42bac4ea13f8aba4ece793785d6 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 17:55:00 -0500 Subject: [PATCH 012/199] not put back to one device for testing --- desc/optimize/_constraint_wrappers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index af9107ed1f..61c5e36e2d 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -298,8 +298,6 @@ def _jac(self, x_reduced, constants=None, op="scaled"): x = self.recover(x_reduced) v = self._unfixed_idx_mat df = getattr(self._objective, "jvp_" + op)(v.T, x, constants) - if desc_config["num_device"] != 1: - df = jax.device_put(df, jax.devices("gpu")[0]) return df.T def jac_scaled(self, x_reduced, constants=None): @@ -360,8 +358,6 @@ def _jvp(self, v, x_reduced, constants=None, op="jvp_scaled"): x = self.recover(x_reduced) v = self._unfixed_idx_mat @ v df = getattr(self._objective, op)(v, x, constants) - if desc_config["num_device"] != 1: - df = jax.device_put(df, jax.devices("gpu")[0]) return df def jvp_scaled(self, v, x_reduced, constants=None): From d3d266359f8b485bc10bc30eb109a544e40a50d3 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 17:58:16 -0500 Subject: [PATCH 013/199] handle num_device=1 case --- desc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/__init__.py b/desc/__init__.py index 32410f8af7..94f966a8ce 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -87,7 +87,7 @@ def set_device(kind="cpu", num_device=None): # noqa: C901 config["device"] = "CPU" config["avail_mem"] = cpu_mem - if kind == "gpu" and num_device is None: + if kind == "gpu" and (num_device is None or num_device == 1): # Set CUDA_DEVICE_ORDER so the IDs assigned by CUDA match those from nvidia-smi os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" import nvgpu From ec05139fd46af4e2addbe112fd4aee7f3b1c6fb6 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 18:19:03 -0500 Subject: [PATCH 014/199] update --- desc/__init__.py | 7 ++++--- desc/backend.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 94f966a8ce..9a7227098c 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -61,7 +61,7 @@ def __getattr__(name): config = {"device": None, "avail_mem": None, "kind": None, "num_device": 1} -def set_device(kind="cpu", num_device=None): # noqa: C901 +def set_device(kind="cpu", num_device=1): # noqa: C901 """Sets the device to use for computation. If kind==``'gpu'``, checks available GPUs and selects the one with the most @@ -87,7 +87,7 @@ def set_device(kind="cpu", num_device=None): # noqa: C901 config["device"] = "CPU" config["avail_mem"] = cpu_mem - if kind == "gpu" and (num_device is None or num_device == 1): + if kind == "gpu" and num_device == 1: # Set CUDA_DEVICE_ORDER so the IDs assigned by CUDA match those from nvidia-smi os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" import nvgpu @@ -142,7 +142,8 @@ def set_device(kind="cpu", num_device=None): # noqa: C901 ) / 1024 # in GB os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_gpu["index"]) - if kind == "gpu" and num_device is not None and num_device > 1: + # TODO: merge the "gpu" and "num_device" cases in single if block + if kind == "gpu" and num_device > 1: import nvgpu try: diff --git a/desc/backend.py b/desc/backend.py index 7aa8b0e69c..658d51b1ab 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -60,7 +60,7 @@ ) ) -if "devices" not in desc_config.keys(): +if desc_config["num_devices"] == 1: print( "Using device: {}, with {:.2f} GB available memory".format( desc_config.get("device"), desc_config.get("avail_mem") From ea0b5849f0f9fa9f55ced1aeab7059f77a90ac49 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 18:20:07 -0500 Subject: [PATCH 015/199] fix typo --- desc/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 658d51b1ab..29231354f5 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -60,14 +60,14 @@ ) ) -if desc_config["num_devices"] == 1: +if desc_config["num_device"] == 1: print( "Using device: {}, with {:.2f} GB available memory".format( desc_config.get("device"), desc_config.get("avail_mem") ) ) else: - print(f"Using {len(desc_config["devices"])} devices:") + print(f"Using {desc_config["num_device"]} devices:") for i, dev in enumerate(desc_config["devices"]): print( f"\t Device {i}: {dev} with {desc_config["avail_mems"][i]:.2f} " From f353649c7bd3937cb390f4715172b6b5769e4727 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 18:25:06 -0500 Subject: [PATCH 016/199] fix issue --- desc/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/desc/__init__.py b/desc/__init__.py index 9a7227098c..935d55447f 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -58,7 +58,7 @@ def __getattr__(name): BANNER = colored(_BANNER, "magenta") -config = {"device": None, "avail_mem": None, "kind": None, "num_device": 1} +config = {"device": None, "avail_mem": None, "kind": None, "num_device": None} def set_device(kind="cpu", num_device=1): # noqa: C901 @@ -86,6 +86,7 @@ def set_device(kind="cpu", num_device=1): # noqa: C901 cpu_mem = psutil.virtual_memory().available / 1024**3 # RAM in GB config["device"] = "CPU" config["avail_mem"] = cpu_mem + config["num_device"] = 1 if kind == "gpu" and num_device == 1: # Set CUDA_DEVICE_ORDER so the IDs assigned by CUDA match those from nvidia-smi @@ -140,6 +141,7 @@ def set_device(kind="cpu", num_device=1): # noqa: C901 config["avail_mem"] = ( selected_gpu["mem_total"] - selected_gpu["mem_used"] ) / 1024 # in GB + config["num_device"] = 1 os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_gpu["index"]) # TODO: merge the "gpu" and "num_device" cases in single if block From a7847df0034ef803f51313ccf0e7c73f74442e1b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 18:27:23 -0500 Subject: [PATCH 017/199] it was a stupid mistake --- desc/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 29231354f5..1fa5ae0cb1 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -67,10 +67,10 @@ ) ) else: - print(f"Using {desc_config["num_device"]} devices:") + print(f"Using {desc_config['num_device']} devices:") for i, dev in enumerate(desc_config["devices"]): print( - f"\t Device {i}: {dev} with {desc_config["avail_mems"][i]:.2f} " + f"\t Device {i}: {dev} with {desc_config['avail_mems'][i]:.2f} " "GB available memory" ) From 5c0f8116ed0f2afb1c8a606375bbbb6bd3053f00 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 18:41:53 -0500 Subject: [PATCH 018/199] I don't know why this was changed --- desc/grid.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/desc/grid.py b/desc/grid.py index b3757ea376..e6ebbb6831 100644 --- a/desc/grid.py +++ b/desc/grid.py @@ -706,8 +706,8 @@ class Grid(_Grid): nodes.reshape((num_poloidal, num_radial, num_toroidal, 3), order="F"). jitable : bool Whether to skip certain checks and conditionals that don't work under jit. - Allows grid to be created on the fly with custom nodes, but weights, symmetry - etc. may be wrong if grid contains duplicate nodes. + Allows grid to be created on the fly with custom nodes, but weights, + symmetry etc. may be wrong if grid contains duplicate nodes. """ def __init__( @@ -793,6 +793,7 @@ def create_meshgrid( coordinates="rtz", period=(np.inf, 2 * np.pi, 2 * np.pi), NFP=1, + jitable=True, **kwargs, ): """Create a tensor-product grid from the given coordinates in a jitable manner. @@ -819,6 +820,10 @@ def create_meshgrid( Only makes sense to change from 1 if last coordinate is periodic with some constant divided by ``NFP`` and the nodes are placed within one field period. + jitable : bool + Whether to skip certain checks and conditionals that don't work under jit. + Allows grid to be created on the fly with custom nodes, but weights, + symmetry etc. may be wrong if grid contains duplicate nodes. Returns ------- @@ -861,10 +866,7 @@ def create_meshgrid( repeat(unique_a_idx // b.size, b.size, total_repeat_length=a.size * b.size), c.size, ) - inverse_b_idx = jnp.tile( - unique_b_idx, - a.size * c.size, - ) + inverse_b_idx = jnp.tile(unique_b_idx, a.size * c.size) inverse_c_idx = repeat(unique_c_idx // (a.size * b.size), (a.size * b.size)) return Grid( nodes=nodes, @@ -875,7 +877,7 @@ def create_meshgrid( NFP=NFP, sort=False, is_meshgrid=True, - jitable=True, + jitable=jitable, _unique_rho_idx=unique_a_idx, _unique_poloidal_idx=unique_b_idx, _unique_zeta_idx=unique_c_idx, From c97608864b8c58b2039f6e5906d73e2cc23559e8 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 19:01:13 -0500 Subject: [PATCH 019/199] put the copying inside the jitted part --- desc/objectives/utils.py | 8 +++++++- desc/optimize/_constraint_wrappers.py | 11 ++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 8e0d6c5f34..2258f1acee 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -5,7 +5,7 @@ import numpy as np -from desc.backend import jit, jnp, put, softargmax +from desc.backend import desc_config, jax, jit, jnp, put, softargmax from desc.io import IOAble from desc.utils import Index, errorif, flatten_list, svd_inv_null, unique_list, warnif @@ -264,6 +264,12 @@ def __call__(self, x_reduced): """Recover the full state vector from the reduced optimization vector.""" dx = put(jnp.zeros(self.dim_x), self.unfixed_idx, self.Z @ x_reduced) x_full = self.D * (self.xp + dx) + if desc_config["num_device"] != 1: + mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) + x_full = jax.device_put( + x_full, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec()), + ) return jnp.atleast_1d(jnp.squeeze(x_full)) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 61c5e36e2d..7695a671b4 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import desc_config, jax, jit, jnp +from desc.backend import jit, jnp from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -129,14 +129,7 @@ def project(self, x): def recover(self, x_reduced): """Recover the full state vector from the reduced optimization vector.""" - x_full = self._recover(x_reduced) - if desc_config["num_device"] != 1: - mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) - x_full = jax.device_put( - x_full, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec()), - ) - return x_full + return self._recover(x_reduced) def x(self, *things): """Return the reduced state vector from the Equilibrium eq.""" From e15f7b2e090df7556d92efe623f416bcdb4b1e12 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 19:20:14 -0500 Subject: [PATCH 020/199] shard A, Z and D too --- desc/objectives/utils.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 2258f1acee..11c5529ee1 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -183,6 +183,21 @@ def factorize_linear_constraints(objective, constraint, x_scale="auto"): # noqa Z = jnp.asarray(Z) D = jnp.asarray(D) + if desc_config["num_device"] != 1: + mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) + Z = jax.device_put( + Z, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), + ) + D = jax.device_put( + D, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), + ) + A = jax.device_put( + A, + jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), + ) + project = _Project(Z, D, xp, unfixed_idx) recover = _Recover(Z, D, xp, unfixed_idx, objective.dim_x) From 36cd4e1a597891b2aca539c98257375f08999c65 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 19:23:15 -0500 Subject: [PATCH 021/199] fix --- desc/objectives/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 11c5529ee1..9de7b6bad5 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -185,9 +185,10 @@ def factorize_linear_constraints(objective, constraint, x_scale="auto"): # noqa if desc_config["num_device"] != 1: mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) + mesh2 = jax.make_mesh((1, desc_config["num_device"]), ("grid")) Z = jax.device_put( Z, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), + jax.sharding.NamedSharding(mesh2, jax.sharding.PartitionSpec("grid")), ) D = jax.device_put( D, From 7e82f6d7bf5b1ed2bac68814e7f766ea5d01f06f Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 19:25:58 -0500 Subject: [PATCH 022/199] fix --- desc/objectives/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 9de7b6bad5..29070916b6 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -185,10 +185,12 @@ def factorize_linear_constraints(objective, constraint, x_scale="auto"): # noqa if desc_config["num_device"] != 1: mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) - mesh2 = jax.make_mesh((1, desc_config["num_device"]), ("grid")) + mesh2 = jax.make_mesh((1, desc_config["num_device"]), ("vert", "horz")) Z = jax.device_put( Z, - jax.sharding.NamedSharding(mesh2, jax.sharding.PartitionSpec("grid")), + jax.sharding.NamedSharding( + mesh2, jax.sharding.PartitionSpec("vert", "horz") + ), ) D = jax.device_put( D, From c963c1a9666d13ec8bec850167973d3a551d344b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 19:26:58 -0500 Subject: [PATCH 023/199] don't shard A --- desc/objectives/utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 29070916b6..cbe8f7466b 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -196,10 +196,6 @@ def factorize_linear_constraints(objective, constraint, x_scale="auto"): # noqa D, jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), ) - A = jax.device_put( - A, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), - ) project = _Project(Z, D, xp, unfixed_idx) recover = _Recover(Z, D, xp, unfixed_idx, objective.dim_x) From ebd8dd1f6887e0621b034c662b55dfd99736b7fb Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 25 Dec 2024 19:39:09 -0500 Subject: [PATCH 024/199] clean up --- desc/backend.py | 8 ++++++++ desc/objectives/objective_funs.py | 15 ++++----------- desc/objectives/utils.py | 21 ++------------------- 3 files changed, 14 insertions(+), 30 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 1fa5ae0cb1..c8f3152f54 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -32,6 +32,14 @@ from jax import config as jax_config jax_config.update("jax_enable_x64", True) + if desc_config["num_device"] != 1: + mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) + desc_config["sharding"] = jax.sharding.NamedSharding( + mesh, jax.sharding.PartitionSpec("grid") + ) + desc_config["sharding_replicated"] = jax.sharding.NamedSharding( + mesh, jax.sharding.PartitionSpec() + ) if desc_config.get("kind") == "gpu" and len(jax.devices("gpu")) == 0: warnings.warn( "JAX failed to detect GPU, are you sure you " diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index b7b434b6f7..93012584e9 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1147,25 +1147,18 @@ def build(self, use_jit=True, verbose=1): if desc_config["num_device"] != 1: if hasattr(self, "_constants"): grid = self._constants["transforms"]["grid"] - mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) # shard nodes, spacing, and weights across devices grid._nodes = jax.device_put( jnp.asarray(grid.nodes), - jax.sharding.NamedSharding( - mesh, jax.sharding.PartitionSpec("grid") - ), + desc_config["sharding"], ) grid._spacing = jax.device_put( jnp.asarray(grid.spacing), - jax.sharding.NamedSharding( - mesh, jax.sharding.PartitionSpec("grid") - ), + desc_config["sharding"], ) grid._weights = jax.device_put( jnp.asarray(grid.weights), - jax.sharding.NamedSharding( - mesh, jax.sharding.PartitionSpec("grid") - ), + desc_config["sharding"], ) # replicate profiles across devices @@ -1174,7 +1167,7 @@ def build(self, use_jit=True, verbose=1): profiles = self._constants["profiles"] profiles = jax.device_put( profiles, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec()), + desc_config["sharding"], ) # set quadrature weights if they haven't been diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index cbe8f7466b..b47ac2afcf 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -177,26 +177,13 @@ def factorize_linear_constraints(objective, constraint, x_scale="auto"): # noqa xp = put(xp, unfixed_idx, A_inv @ b) xp = put(xp, fixed_idx, ((1 / D) * xp)[fixed_idx]) # cast to jnp arrays + # TODO: might consider sharding these too xp = jnp.asarray(xp) A = jnp.asarray(A) b = jnp.asarray(b) Z = jnp.asarray(Z) D = jnp.asarray(D) - if desc_config["num_device"] != 1: - mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) - mesh2 = jax.make_mesh((1, desc_config["num_device"]), ("vert", "horz")) - Z = jax.device_put( - Z, - jax.sharding.NamedSharding( - mesh2, jax.sharding.PartitionSpec("vert", "horz") - ), - ) - D = jax.device_put( - D, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec("grid")), - ) - project = _Project(Z, D, xp, unfixed_idx) recover = _Recover(Z, D, xp, unfixed_idx, objective.dim_x) @@ -279,11 +266,7 @@ def __call__(self, x_reduced): dx = put(jnp.zeros(self.dim_x), self.unfixed_idx, self.Z @ x_reduced) x_full = self.D * (self.xp + dx) if desc_config["num_device"] != 1: - mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) - x_full = jax.device_put( - x_full, - jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec()), - ) + x_full = jax.device_put(x_full, desc_config["sharding_replicated"]) return jnp.atleast_1d(jnp.squeeze(x_full)) From 172d2119a3134d195e19fb072431496b88cda3c8 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 26 Dec 2024 01:34:40 -0500 Subject: [PATCH 025/199] shard tangents too --- desc/optimize/_constraint_wrappers.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 7695a671b4..3128e07297 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import jit, jnp +from desc.backend import desc_config, jax, jit, jnp from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -289,8 +289,10 @@ def hess(self, x_reduced, constants=None): def _jac(self, x_reduced, constants=None, op="scaled"): x = self.recover(x_reduced) - v = self._unfixed_idx_mat - df = getattr(self._objective, "jvp_" + op)(v.T, x, constants) + vT = self._unfixed_idx_mat.T + if desc_config["num_device"] != 1: + vT = jax.device_put(vT, desc_config["sharding"]) + df = getattr(self._objective, "jvp_" + op)(vT, x, constants) return df.T def jac_scaled(self, x_reduced, constants=None): From bd986bef6124929073a04bdcfc947cef66b0b1c5 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 26 Dec 2024 01:55:49 -0500 Subject: [PATCH 026/199] shard v in different way --- desc/optimize/_constraint_wrappers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 3128e07297..4f8f86ee9c 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -289,10 +289,10 @@ def hess(self, x_reduced, constants=None): def _jac(self, x_reduced, constants=None, op="scaled"): x = self.recover(x_reduced) - vT = self._unfixed_idx_mat.T + v = self._unfixed_idx_mat if desc_config["num_device"] != 1: - vT = jax.device_put(vT, desc_config["sharding"]) - df = getattr(self._objective, "jvp_" + op)(vT, x, constants) + v = jax.device_put(v, desc_config["sharding"]) + df = getattr(self._objective, "jvp_" + op)(v.T, x, constants) return df.T def jac_scaled(self, x_reduced, constants=None): From 163801ec05fb8c1eb2487cee5092b0aab3d7c662 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 26 Dec 2024 02:18:20 -0500 Subject: [PATCH 027/199] don't cover set_device for coverage --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index ec603ef1f1..72b9a5775a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,10 @@ source = desc/ # _version.py is generated code, no need to count it +# __init__.py deals with device selection that CI cannot test omit = desc/_version.py + desc/__init__.py desc/examples/precise_QH.py desc/examples/precise_QA.py desc/examples/regenerate_all_equilibria.py From 33b7c0b9840fe7f919de212eb1afd62ed741c8cf Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 29 Jan 2025 17:03:16 -0500 Subject: [PATCH 028/199] add getter for parallel force objective --- desc/objectives/getters.py | 49 +++++++++++++++++++++++++++++++ desc/objectives/objective_funs.py | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 03e1b75631..2dcddc636b 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -337,3 +337,52 @@ def maybe_add_self_consistency(thing, constraints): constraints += (FixCurveRotation(curve=thing),) return constraints + + +def get_parallel_forcebalance(eq, num_device, check_device=True): + """Get a list of ForceBalance objectives for parallel computing. + + Parameters + ---------- + eq : Equilibrium + Equilibrium to constrain. + num_device : int + Number of devices to use for parallel computing. + + Returns + ------- + objs : tuple of ForceBalance + A list of the linear constraints used in fixed-boundary problems. + """ + from desc.backend import desc_config, jnp + from desc.grid import LinearGrid + + if desc_config["num_device"] != num_device and check_device: + raise ValueError( + f"Number of devices in desc_config ({desc_config['num_device']}) " + f"does not match the number of devices in input ({num_device})." + ) + if eq.L_grid % num_device == 0: + k = eq.L_grid // num_device + L = eq.L_grid + else: + k = eq.L_grid // num_device + 1 + L = k * num_device + + rhos = jnp.linspace(0.01, 1.0, L) + objs = () + for i in range(num_device): + obj = ForceBalance( + eq, + grid=LinearGrid( + rho=rhos[i * k : (i + 1) * k], + # kind of experimental way of set giving + # less grid points to inner part, but seems + # to make transforms way slower + M=int(eq.M_grid * i / num_device), + N=eq.N_grid, + NFP=eq.NFP, + ), + ) + objs += (obj,) + return objs diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 659d3d5baa..ac6c10dea9 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1148,7 +1148,7 @@ def build(self, use_jit=True, verbose=1): self._check_dimensions() self._set_derivatives() - if desc_config["num_device"] != 1: + if desc_config["num_device"] != 1 and False: # temporarly disable sharding if hasattr(self, "_constants"): grid = self._constants["transforms"]["grid"] # shard nodes, spacing, and weights across devices From 35dd7b037e062588df4ac9e9ae4f9491c01bb297 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 29 Jan 2025 17:11:29 -0500 Subject: [PATCH 029/199] add notebook for testing --- docs/notebooks/tutorials/multi_device.ipynb | 108 ++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 docs/notebooks/tutorials/multi_device.ipynb diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb new file mode 100644 index 0000000000..0540b82026 --- /dev/null +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -0,0 +1,108 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "# Multi-Gpu Equilibrium Solve" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "import os\n", + "\n", + "sys.path.insert(0, os.path.abspath(\".\"))\n", + "sys.path.append(os.path.abspath(\"../../../\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# from desc import set_device\n", + "# set_device(\"gpu\", num_device=4)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DESC version 0.13.0+1107.g33b7c0b98,using JAX backend, jax version=0.4.37, jaxlib version=0.4.36, dtype=float64\n", + "Using device: CPU, with 7.82 GB available memory\n" + ] + } + ], + "source": [ + "from desc.examples import get\n", + "from desc.objectives import *\n", + "from desc.objectives.getters import *" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eq = get(\"HELIOTRON\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "objs = get_parallel_forcebalance(eq, num_device=4, check_device=False)\n", + "obj = ObjectiveFunction(objs)\n", + "cons = get_fixed_boundary_constraints(eq)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eq.solve(objective=obj, constraints=cons, maxiter=2, ftol=0, gtol=0, xtol=0, verbose=3)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "desc-env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From e9c6e637773f518b31547fd2f46a643090470f0d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 29 Jan 2025 17:18:14 -0500 Subject: [PATCH 030/199] build and distribute objectives in getter --- desc/objectives/getters.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 2dcddc636b..3bfe69661b 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -354,7 +354,7 @@ def get_parallel_forcebalance(eq, num_device, check_device=True): objs : tuple of ForceBalance A list of the linear constraints used in fixed-boundary problems. """ - from desc.backend import desc_config, jnp + from desc.backend import desc_config, jax, jnp from desc.grid import LinearGrid if desc_config["num_device"] != num_device and check_device: @@ -372,17 +372,19 @@ def get_parallel_forcebalance(eq, num_device, check_device=True): rhos = jnp.linspace(0.01, 1.0, L) objs = () for i in range(num_device): - obj = ForceBalance( - eq, - grid=LinearGrid( - rho=rhos[i * k : (i + 1) * k], - # kind of experimental way of set giving - # less grid points to inner part, but seems - # to make transforms way slower - M=int(eq.M_grid * i / num_device), - N=eq.N_grid, - NFP=eq.NFP, - ), + grid = LinearGrid( + rho=rhos[i * k : (i + 1) * k], + # kind of experimental way of set giving + # less grid points to inner part, but seems + # to make transforms way slower + M=int(eq.M_grid * i / num_device), + N=eq.N_grid, + NFP=eq.NFP, ) + grid._nodes = jax.device_put(grid._nodes, jax.devices("gpu")[i]) + grid._spacing = jax.device_put(grid.spacing, jax.devices("gpu")[i]) + grid._weights = jax.device_put(grid.weights, jax.devices("gpu")[i]) + obj = ForceBalance(eq, grid=grid) + obj.build() objs += (obj,) return objs From 9f1988591b1ddcd5901c3c61a7fcde7971bf23eb Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 29 Jan 2025 21:42:05 -0500 Subject: [PATCH 031/199] maybe use same grid res --- desc/objectives/getters.py | 3 +- docs/notebooks/tutorials/multi_device.ipynb | 41 +++++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 3bfe69661b..406070122d 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -377,7 +377,8 @@ def get_parallel_forcebalance(eq, num_device, check_device=True): # kind of experimental way of set giving # less grid points to inner part, but seems # to make transforms way slower - M=int(eq.M_grid * i / num_device), + # M=int(eq.M_grid * i / num_device), # noqa: E800 + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, ) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 0540b82026..724699ca36 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -36,22 +36,16 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DESC version 0.13.0+1107.g33b7c0b98,using JAX backend, jax version=0.4.37, jaxlib version=0.4.36, dtype=float64\n", - "Using device: CPU, with 7.82 GB available memory\n" - ] - } - ], + "outputs": [], "source": [ "from desc.examples import get\n", "from desc.objectives import *\n", - "from desc.objectives.getters import *" + "from desc.objectives.getters import *\n", + "from desc.grid import LinearGrid\n", + "from desc.backend import jnp\n", + "from desc.plotting import plot_grid" ] }, { @@ -82,6 +76,29 @@ "source": [ "eq.solve(objective=obj, constraints=cons, maxiter=2, ftol=0, gtol=0, xtol=0, verbose=3)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eq = get(\"HELIOTRON\")\n", + "r_per_gpu = 2\n", + "num_device = 5\n", + "rhos = jnp.linspace(0.01, 1.0, r_per_gpu * num_device)\n", + "for i in range(num_device):\n", + " grid = LinearGrid(\n", + " rho=rhos[i * r_per_gpu : (i + 1) * r_per_gpu],\n", + " # kind of experimental way of set giving\n", + " # less grid points to inner part, but seems\n", + " # to make transforms way slower\n", + " M=int(eq.M_grid * i / num_device),\n", + " N=eq.N_grid,\n", + " NFP=eq.NFP,\n", + " )\n", + " plot_grid(grid)" + ] } ], "metadata": { From 57ab00caaadb3909ebb5d319c7e7a786816c5ca7 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 29 Jan 2025 22:45:13 -0500 Subject: [PATCH 032/199] add build flag to getter --- desc/objectives/getters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 406070122d..d684f09340 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -339,7 +339,7 @@ def maybe_add_self_consistency(thing, constraints): return constraints -def get_parallel_forcebalance(eq, num_device, check_device=True): +def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): """Get a list of ForceBalance objectives for parallel computing. Parameters @@ -386,6 +386,6 @@ def get_parallel_forcebalance(eq, num_device, check_device=True): grid._spacing = jax.device_put(grid.spacing, jax.devices("gpu")[i]) grid._weights = jax.device_put(grid.weights, jax.devices("gpu")[i]) obj = ForceBalance(eq, grid=grid) - obj.build() + obj.build(use_jit=use_jit) objs += (obj,) return objs From b28bc4e836eb6cc84eb160a099de3abd664e797f Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 30 Jan 2025 13:29:10 -0500 Subject: [PATCH 033/199] do not jit the ObjectiveFunction because jax doesn't allow it --- desc/objectives/getters.py | 13 +++++++++---- desc/objectives/objective_funs.py | 6 ++++-- docs/notebooks/tutorials/multi_device.ipynb | 11 +++++------ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index d684f09340..687b252132 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -340,7 +340,7 @@ def maybe_add_self_consistency(thing, constraints): def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): - """Get a list of ForceBalance objectives for parallel computing. + """Get an ObjectiveFunction for parallel computing ForceBalance. Parameters ---------- @@ -351,8 +351,11 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): Returns ------- - objs : tuple of ForceBalance - A list of the linear constraints used in fixed-boundary problems. + obj : ObjectiveFunction + An objective function with force balance objectives. Each objective is + computed on a separate device. The objective function is built with + `use_jit_wrapper=False` to make it compatible with JAX parallel computing. + Each objective will have a grid with same number of flux surfaces. """ from desc.backend import desc_config, jax, jnp from desc.grid import LinearGrid @@ -388,4 +391,6 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): obj = ForceBalance(eq, grid=grid) obj.build(use_jit=use_jit) objs += (obj,) - return objs + obj = ObjectiveFunction(objs) + obj.build(use_jit_wrapper=False) + return obj diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index ac6c10dea9..9b44d02dfc 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -293,7 +293,7 @@ def _unjit(self): pass @execute_on_cpu - def build(self, use_jit=None, verbose=1): + def build(self, use_jit=None, use_jit_wrapper=True, verbose=1): """Build the objective. Parameters @@ -306,6 +306,8 @@ def build(self, use_jit=None, verbose=1): """ if use_jit is not None: self._use_jit = use_jit + if use_jit is False: + use_jit_wrapper = False timer = Timer() timer.start("Objective build") @@ -369,7 +371,7 @@ def build(self, use_jit=None, verbose=1): if obj._jac_chunk_size is None: obj._jac_chunk_size = self._jac_chunk_size - if not self.use_jit: + if not use_jit_wrapper: self._unjit() self._built = True diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 724699ca36..f2459b07be 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -63,8 +63,7 @@ "metadata": {}, "outputs": [], "source": [ - "objs = get_parallel_forcebalance(eq, num_device=4, check_device=False)\n", - "obj = ObjectiveFunction(objs)\n", + "obj = get_parallel_forcebalance(eq, num_device=1, check_device=False)\n", "cons = get_fixed_boundary_constraints(eq)" ] }, From c8f482694500fd32bf58ca129f5c9f16c0da07c3 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 5 Feb 2025 18:22:56 -0500 Subject: [PATCH 034/199] move extra stuff --- desc/objectives/getters.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 687b252132..ce38f838c3 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -390,6 +390,9 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): grid._weights = jax.device_put(grid.weights, jax.devices("gpu")[i]) obj = ForceBalance(eq, grid=grid) obj.build(use_jit=use_jit) + obj._constants["quad_weights"] = jax.device_put( + obj._constants["quad_weights"], jax.devices("gpu")[i] + ) objs += (obj,) obj = ObjectiveFunction(objs) obj.build(use_jit_wrapper=False) From c3a4803848223ceff090c26f8ec3a07cd07c16ca Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 5 Feb 2025 18:23:48 -0500 Subject: [PATCH 035/199] move whole objective on gpu --- desc/objectives/getters.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index ce38f838c3..3c8b93f1e2 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -385,14 +385,9 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): N=eq.N_grid, NFP=eq.NFP, ) - grid._nodes = jax.device_put(grid._nodes, jax.devices("gpu")[i]) - grid._spacing = jax.device_put(grid.spacing, jax.devices("gpu")[i]) - grid._weights = jax.device_put(grid.weights, jax.devices("gpu")[i]) obj = ForceBalance(eq, grid=grid) obj.build(use_jit=use_jit) - obj._constants["quad_weights"] = jax.device_put( - obj._constants["quad_weights"], jax.devices("gpu")[i] - ) + obj = jax.device_put(obj, jax.devices("gpu")[i]) objs += (obj,) obj = ObjectiveFunction(objs) obj.build(use_jit_wrapper=False) From b599b91851eb6c989a5dfcc56695adbab6741ace Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 5 Feb 2025 22:09:51 -0500 Subject: [PATCH 036/199] add pconcat function normal concatenate doesn't accepts arrays from different devices --- desc/backend.py | 17 +++++++++++++++++ desc/objectives/objective_funs.py | 15 ++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index c8f3152f54..66fe406b08 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -444,6 +444,23 @@ def tangent_solve(g, y): x = jax.lax.custom_root(res, x0, solve, tangent_solve, has_aux=False) return x + def pconcat(arrays): + """Concatenate arrays that live on different devices. + + Parameters + ---------- + arrays : list of jnp.ndarray + Arrays to concatenate. + + Returns + ------- + out : jnp.ndarray + Concatenated array that lives in the first device. + """ + return jnp.concatenate( + [jax.device_put(x, device=jax.devices("gpu")[0]) for x in arrays] + ) + # we can't really test the numpy backend stuff in automated testing, so we ignore it # for coverage purposes diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 9b44d02dfc..49aae91b70 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -500,12 +500,21 @@ def compute_scaled_error(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - f = jnp.concatenate( - [ + if desc_config["num_devices"] == 1: + f = jnp.concatenate( + [ + obj.compute_scaled_error(*par, constants=const) + for par, obj, const in zip(params, self.objectives, constants) + ] + ) + else: + fs = [ obj.compute_scaled_error(*par, constants=const) for par, obj, const in zip(params, self.objectives, constants) ] - ) + from desc.backend import pconcat + + f = pconcat(fs) return f @jit From 05f705ace685b8812d368263d1d8024e2898201d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 5 Feb 2025 22:18:51 -0500 Subject: [PATCH 037/199] use more pconcat --- desc/backend.py | 11 +++++++---- desc/objectives/getters.py | 14 ++++++++++---- desc/objectives/objective_funs.py | 20 ++++++-------------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 66fe406b08..f4450f953e 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -445,7 +445,7 @@ def tangent_solve(g, y): return x def pconcat(arrays): - """Concatenate arrays that live on different devices. + """Concatenate arrays that live on same/different devices. Parameters ---------- @@ -457,9 +457,12 @@ def pconcat(arrays): out : jnp.ndarray Concatenated array that lives in the first device. """ - return jnp.concatenate( - [jax.device_put(x, device=jax.devices("gpu")[0]) for x in arrays] - ) + if desc_config["num_device"] == 1: + return jnp.concatenate(arrays) + else: + return jnp.concatenate( + [jax.device_put(x, device=jax.devices("gpu")[0]) for x in arrays] + ) # we can't really test the numpy backend stuff in automated testing, so we ignore it diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 3c8b93f1e2..222d074bf3 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -339,7 +339,9 @@ def maybe_add_self_consistency(thing, constraints): return constraints -def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): +def get_parallel_forcebalance( + eq, num_device, use_jit=True, use_jit_wrapper=False, check_device=True +): """Get an ObjectiveFunction for parallel computing ForceBalance. Parameters @@ -388,7 +390,11 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): obj = ForceBalance(eq, grid=grid) obj.build(use_jit=use_jit) obj = jax.device_put(obj, jax.devices("gpu")[i]) + # if the eq is also distrubuted across GPUs, then some internal logic that + # checks if the things are different will fail, so we need to set the eq + # to be the same manually + obj._things[0] = eq objs += (obj,) - obj = ObjectiveFunction(objs) - obj.build(use_jit_wrapper=False) - return obj + objective = ObjectiveFunction(objs) + objective.build(use_jit_wrapper=use_jit_wrapper) + return objective diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 49aae91b70..434574dc7e 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -11,6 +11,7 @@ jax, jit, jnp, + pconcat, tree_flatten, tree_map, tree_unflatten, @@ -442,7 +443,7 @@ def compute_unscaled(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - f = jnp.concatenate( + f = pconcat( [ obj.compute_unscaled(*par, constants=const) for par, obj, const in zip(params, self.objectives, constants) @@ -471,7 +472,7 @@ def compute_scaled(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - f = jnp.concatenate( + f = pconcat( [ obj.compute_scaled(*par, constants=const) for par, obj, const in zip(params, self.objectives, constants) @@ -500,21 +501,12 @@ def compute_scaled_error(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - if desc_config["num_devices"] == 1: - f = jnp.concatenate( - [ - obj.compute_scaled_error(*par, constants=const) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) - else: - fs = [ + f = pconcat( + [ obj.compute_scaled_error(*par, constants=const) for par, obj, const in zip(params, self.objectives, constants) ] - from desc.backend import pconcat - - f = pconcat(fs) + ) return f @jit From 7c36f3a793679ad61825094ed8d16c4578099342 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 6 Feb 2025 15:41:09 -0500 Subject: [PATCH 038/199] test not passing constants --- desc/objectives/objective_funs.py | 34 ++----------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 434574dc7e..fdc1e91a58 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -8,7 +8,6 @@ from desc.backend import ( desc_config, execute_on_cpu, - jax, jit, jnp, pconcat, @@ -498,13 +497,10 @@ def compute_scaled_error(self, x, constants=None): """ params = self.unpack_state(x) - if constants is None: - constants = self.constants - assert len(params) == len(constants) == len(self.objectives) f = pconcat( [ - obj.compute_scaled_error(*par, constants=const) - for par, obj, const in zip(params, self.objectives, constants) + obj.compute_scaled_error(*par) + for par, obj in zip(params, self.objectives) ] ) return f @@ -1151,32 +1147,6 @@ def build(self, use_jit=True, verbose=1): self._check_dimensions() self._set_derivatives() - if desc_config["num_device"] != 1 and False: # temporarly disable sharding - if hasattr(self, "_constants"): - grid = self._constants["transforms"]["grid"] - # shard nodes, spacing, and weights across devices - grid._nodes = jax.device_put( - jnp.asarray(grid.nodes), - desc_config["sharding"], - ) - grid._spacing = jax.device_put( - jnp.asarray(grid.spacing), - desc_config["sharding"], - ) - grid._weights = jax.device_put( - jnp.asarray(grid.weights), - desc_config["sharding"], - ) - - # replicate profiles across devices - # TODO: profiles are dict of arrays, need to shard each array - if False: - profiles = self._constants["profiles"] - profiles = jax.device_put( - profiles, - desc_config["sharding"], - ) - # set quadrature weights if they haven't been if hasattr(self, "_constants") and ("quad_weights" not in self._constants): grid = self._constants["transforms"]["grid"] From 66a4f95c6495b93c54b45a07807a347249ef319e Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 6 Feb 2025 16:02:44 -0500 Subject: [PATCH 039/199] try something --- desc/objectives/getters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 222d074bf3..cb4bcbfc31 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -393,7 +393,7 @@ def get_parallel_forcebalance( # if the eq is also distrubuted across GPUs, then some internal logic that # checks if the things are different will fail, so we need to set the eq # to be the same manually - obj._things[0] = eq + # obj._things[0] = eq # noqa: E800 objs += (obj,) objective = ObjectiveFunction(objs) objective.build(use_jit_wrapper=use_jit_wrapper) From 293b6f0a6930c610b6256af81fe833b01f6fa0e7 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 6 Feb 2025 16:04:51 -0500 Subject: [PATCH 040/199] try something --- desc/optimize/optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/optimize/optimizer.py b/desc/optimize/optimizer.py index f42c4865eb..ab4a61fb4a 100644 --- a/desc/optimize/optimizer.py +++ b/desc/optimize/optimizer.py @@ -235,7 +235,7 @@ def optimize( # noqa: C901 objective, nonlinear_constraint ) assert set(objective.things) == set(nonlinear_constraint.things) - assert set(objective.things) == set(things) + # assert set(objective.things) == set(things) #noqa E800 # wrap to handle linear constraints if linear_constraint is not None: From 50883958654f2a3bdb7d1203dd04be5a7e6a8e44 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 6 Feb 2025 16:11:00 -0500 Subject: [PATCH 041/199] instead replicate eq every device --- desc/objectives/getters.py | 3 ++- desc/optimize/optimizer.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index cb4bcbfc31..f983c37849 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -362,6 +362,7 @@ def get_parallel_forcebalance( from desc.backend import desc_config, jax, jnp from desc.grid import LinearGrid + eq = jax.device_put(eq, desc_config["sharding_replicated"]) if desc_config["num_device"] != num_device and check_device: raise ValueError( f"Number of devices in desc_config ({desc_config['num_device']}) " @@ -393,7 +394,7 @@ def get_parallel_forcebalance( # if the eq is also distrubuted across GPUs, then some internal logic that # checks if the things are different will fail, so we need to set the eq # to be the same manually - # obj._things[0] = eq # noqa: E800 + obj._things[0] = eq objs += (obj,) objective = ObjectiveFunction(objs) objective.build(use_jit_wrapper=use_jit_wrapper) diff --git a/desc/optimize/optimizer.py b/desc/optimize/optimizer.py index ab4a61fb4a..f42c4865eb 100644 --- a/desc/optimize/optimizer.py +++ b/desc/optimize/optimizer.py @@ -235,7 +235,7 @@ def optimize( # noqa: C901 objective, nonlinear_constraint ) assert set(objective.things) == set(nonlinear_constraint.things) - # assert set(objective.things) == set(things) #noqa E800 + assert set(objective.things) == set(things) # wrap to handle linear constraints if linear_constraint is not None: From 2c93a6a1a08fe8fab5a6e5b0b746d9d63d134718 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 6 Feb 2025 16:19:19 -0500 Subject: [PATCH 042/199] try something --- desc/optimize/optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/optimize/optimizer.py b/desc/optimize/optimizer.py index f42c4865eb..64422c4b01 100644 --- a/desc/optimize/optimizer.py +++ b/desc/optimize/optimizer.py @@ -235,7 +235,7 @@ def optimize( # noqa: C901 objective, nonlinear_constraint ) assert set(objective.things) == set(nonlinear_constraint.things) - assert set(objective.things) == set(things) + # assert set(objective.things) == set(things) #noqa: E800 # wrap to handle linear constraints if linear_constraint is not None: From 84179d12b3b5db48fce615e27841d029fc8b4446 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 6 Feb 2025 16:36:11 -0500 Subject: [PATCH 043/199] return replicated eq and use that otherwise outer eq and obj eq are not the same because one is the copy of the other --- desc/objectives/getters.py | 4 +++- desc/optimize/optimizer.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index f983c37849..685abc9119 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -353,6 +353,8 @@ def get_parallel_forcebalance( Returns ------- + eq : Equilibrium + Equilibrium to constrain replicated to devices. obj : ObjectiveFunction An objective function with force balance objectives. Each objective is computed on a separate device. The objective function is built with @@ -398,4 +400,4 @@ def get_parallel_forcebalance( objs += (obj,) objective = ObjectiveFunction(objs) objective.build(use_jit_wrapper=use_jit_wrapper) - return objective + return eq, objective diff --git a/desc/optimize/optimizer.py b/desc/optimize/optimizer.py index 64422c4b01..f42c4865eb 100644 --- a/desc/optimize/optimizer.py +++ b/desc/optimize/optimizer.py @@ -235,7 +235,7 @@ def optimize( # noqa: C901 objective, nonlinear_constraint ) assert set(objective.things) == set(nonlinear_constraint.things) - # assert set(objective.things) == set(things) #noqa: E800 + assert set(objective.things) == set(things) # wrap to handle linear constraints if linear_constraint is not None: From 1ee3452af0037d4b5e91b4c44db3fc1be3f82496 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 6 Feb 2025 17:04:14 -0500 Subject: [PATCH 044/199] reorder steps --- desc/objectives/getters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 685abc9119..ac33b665ab 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -391,12 +391,12 @@ def get_parallel_forcebalance( NFP=eq.NFP, ) obj = ForceBalance(eq, grid=grid) - obj.build(use_jit=use_jit) obj = jax.device_put(obj, jax.devices("gpu")[i]) # if the eq is also distrubuted across GPUs, then some internal logic that # checks if the things are different will fail, so we need to set the eq # to be the same manually obj._things[0] = eq + obj.build(use_jit=use_jit) objs += (obj,) objective = ObjectiveFunction(objs) objective.build(use_jit_wrapper=use_jit_wrapper) From 088324f08dad15d6427cc8b708eeda5ae64eab54 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 10 Feb 2025 18:47:13 -0500 Subject: [PATCH 045/199] copy params to device before passing to function --- desc/objectives/getters.py | 13 ++--- desc/objectives/objective_funs.py | 85 ++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index ac33b665ab..8f518796e7 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -339,9 +339,7 @@ def maybe_add_self_consistency(thing, constraints): return constraints -def get_parallel_forcebalance( - eq, num_device, use_jit=True, use_jit_wrapper=False, check_device=True -): +def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): """Get an ObjectiveFunction for parallel computing ForceBalance. Parameters @@ -353,8 +351,6 @@ def get_parallel_forcebalance( Returns ------- - eq : Equilibrium - Equilibrium to constrain replicated to devices. obj : ObjectiveFunction An objective function with force balance objectives. Each objective is computed on a separate device. The objective function is built with @@ -364,7 +360,6 @@ def get_parallel_forcebalance( from desc.backend import desc_config, jax, jnp from desc.grid import LinearGrid - eq = jax.device_put(eq, desc_config["sharding_replicated"]) if desc_config["num_device"] != num_device and check_device: raise ValueError( f"Number of devices in desc_config ({desc_config['num_device']}) " @@ -391,13 +386,13 @@ def get_parallel_forcebalance( NFP=eq.NFP, ) obj = ForceBalance(eq, grid=grid) + obj.build(use_jit=use_jit) obj = jax.device_put(obj, jax.devices("gpu")[i]) # if the eq is also distrubuted across GPUs, then some internal logic that # checks if the things are different will fail, so we need to set the eq # to be the same manually obj._things[0] = eq - obj.build(use_jit=use_jit) objs += (obj,) objective = ObjectiveFunction(objs) - objective.build(use_jit_wrapper=use_jit_wrapper) - return eq, objective + objective.build(use_jit=use_jit) + return objective diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index fdc1e91a58..ee9b44621d 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -8,6 +8,7 @@ from desc.backend import ( desc_config, execute_on_cpu, + jax, jit, jnp, pconcat, @@ -293,13 +294,16 @@ def _unjit(self): pass @execute_on_cpu - def build(self, use_jit=None, use_jit_wrapper=True, verbose=1): + def build(self, use_jit=None, use_jit_wrapper=True, verbose=1): # noqa: C901 """Build the objective. Parameters ---------- use_jit : bool, optional Whether to just-in-time compile the objective and derivatives. + use_jit_wrapper : bool, optional + Whether to use the jit wrapper for the objective. If multiple GPUs are + used, this will be set to False. verbose : int, optional Level of output. @@ -308,6 +312,10 @@ def build(self, use_jit=None, use_jit_wrapper=True, verbose=1): self._use_jit = use_jit if use_jit is False: use_jit_wrapper = False + + if use_jit_wrapper and desc_config["num_device"] > 1: + use_jit_wrapper = False + timer = Timer() timer.start("Objective build") @@ -442,12 +450,24 @@ def compute_unscaled(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - f = pconcat( - [ - obj.compute_unscaled(*par, constants=const) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) + if desc_config["num_device"] == 1: + f = jnp.concatenate( + [ + obj.compute_unscaled(*par, constants=const) + for par, obj, const in zip(params, self.objectives, constants) + ] + ) + else: + f = pconcat( + [ + obj.compute_unscaled( + *jax.device_put(par, jax.devices("gpu")[i]), constants=const + ) + for i, (par, obj, const) in enumerate( + zip(params, self.objectives, constants) + ) + ] + ) return f @jit @@ -471,12 +491,24 @@ def compute_scaled(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - f = pconcat( - [ - obj.compute_scaled(*par, constants=const) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) + if desc_config["num_device"] == 1: + f = jnp.concatenate( + [ + obj.compute_scaled(*par, constants=const) + for par, obj, const in zip(params, self.objectives, constants) + ] + ) + else: + f = pconcat( + [ + obj.compute_scaled( + *jax.device_put(par, jax.devices("gpu")[i]), constants=const + ) + for i, (par, obj, const) in enumerate( + zip(params, self.objectives, constants) + ) + ] + ) return f @jit @@ -497,12 +529,27 @@ def compute_scaled_error(self, x, constants=None): """ params = self.unpack_state(x) - f = pconcat( - [ - obj.compute_scaled_error(*par) - for par, obj in zip(params, self.objectives) - ] - ) + if constants is None: + constants = self.constants + assert len(params) == len(constants) == len(self.objectives) + if desc_config["num_device"] == 1: + f = jnp.concatenate( + [ + obj.compute_scaled_error(*par, constants=const) + for par, obj, const in zip(params, self.objectives, constants) + ] + ) + else: + f = pconcat( + [ + obj.compute_scaled_error( + *jax.device_put(par, jax.devices("gpu")[i]), constants=const + ) + for i, (par, obj, const) in enumerate( + zip(params, self.objectives, constants) + ) + ] + ) return f @jit From 97c3dec59315f5da71d78564159aabb7b34fc419 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 10 Feb 2025 19:08:52 -0500 Subject: [PATCH 046/199] add device_id for forcebalance --- desc/backend.py | 11 ++++------- desc/objectives/_equilibrium.py | 2 ++ desc/objectives/getters.py | 2 +- desc/objectives/objective_funs.py | 12 ++++++++---- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index f4450f953e..66fe406b08 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -445,7 +445,7 @@ def tangent_solve(g, y): return x def pconcat(arrays): - """Concatenate arrays that live on same/different devices. + """Concatenate arrays that live on different devices. Parameters ---------- @@ -457,12 +457,9 @@ def pconcat(arrays): out : jnp.ndarray Concatenated array that lives in the first device. """ - if desc_config["num_device"] == 1: - return jnp.concatenate(arrays) - else: - return jnp.concatenate( - [jax.device_put(x, device=jax.devices("gpu")[0]) for x in arrays] - ) + return jnp.concatenate( + [jax.device_put(x, device=jax.devices("gpu")[0]) for x in arrays] + ) # we can't really test the numpy backend stuff in automated testing, so we ignore it diff --git a/desc/objectives/_equilibrium.py b/desc/objectives/_equilibrium.py index 7be04509ea..5f1b27b149 100644 --- a/desc/objectives/_equilibrium.py +++ b/desc/objectives/_equilibrium.py @@ -61,6 +61,7 @@ def __init__( grid=None, name="force", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -76,6 +77,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 8f518796e7..74393ea271 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -385,7 +385,7 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): N=eq.N_grid, NFP=eq.NFP, ) - obj = ForceBalance(eq, grid=grid) + obj = ForceBalance(eq, grid=grid, device_id=i) obj.build(use_jit=use_jit) obj = jax.device_put(obj, jax.devices("gpu")[i]) # if the eq is also distrubuted across GPUs, then some internal logic that diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index ee9b44621d..20c5f1106b 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -265,6 +265,8 @@ def __init__( self._built = False self._compiled = False self._name = name + device_ids = [obj._device_id for obj in objectives] + self._is_multi_device = len(set(device_ids)) > 1 def _unjit(self): """Remove jit compiled methods.""" @@ -313,7 +315,7 @@ def build(self, use_jit=None, use_jit_wrapper=True, verbose=1): # noqa: C901 if use_jit is False: use_jit_wrapper = False - if use_jit_wrapper and desc_config["num_device"] > 1: + if self._is_multi_device: use_jit_wrapper = False timer = Timer() @@ -450,7 +452,7 @@ def compute_unscaled(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - if desc_config["num_device"] == 1: + if not self._is_multi_device: f = jnp.concatenate( [ obj.compute_unscaled(*par, constants=const) @@ -491,7 +493,7 @@ def compute_scaled(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - if desc_config["num_device"] == 1: + if not self._is_multi_device: f = jnp.concatenate( [ obj.compute_scaled(*par, constants=const) @@ -532,7 +534,7 @@ def compute_scaled_error(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - if desc_config["num_device"] == 1: + if not self._is_multi_device: f = jnp.concatenate( [ obj.compute_scaled_error(*par, constants=const) @@ -1094,6 +1096,7 @@ def __init__( deriv_mode="auto", name=None, jac_chunk_size=None, + device_id=0, ): if self._scalar: assert self._coordinates == "" @@ -1107,6 +1110,7 @@ def __init__( assert jac_chunk_size is None or isposint(jac_chunk_size) self._jac_chunk_size = jac_chunk_size + self._device_id = device_id self._target = target self._bounds = bounds From 2b7e007b7a80d5d04ea749d3e3f71f09d7c26f78 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 10 Feb 2025 19:11:18 -0500 Subject: [PATCH 047/199] update notebook --- docs/notebooks/tutorials/multi_device.ipynb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index f2459b07be..107f3de1eb 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -26,12 +26,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "num_device = 1\n", "# from desc import set_device\n", - "# set_device(\"gpu\", num_device=4)" + "\n", + "# set_device(\"gpu\", num_device=num_device)" ] }, { @@ -63,7 +65,7 @@ "metadata": {}, "outputs": [], "source": [ - "obj = get_parallel_forcebalance(eq, num_device=1, check_device=False)\n", + "obj = get_parallel_forcebalance(eq, num_device=num_device, check_device=False)\n", "cons = get_fixed_boundary_constraints(eq)" ] }, From 856a115027e46421cc3c876efbbbc050ec51a497 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 10 Feb 2025 19:15:31 -0500 Subject: [PATCH 048/199] delete old line --- desc/optimize/_constraint_wrappers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 4f8f86ee9c..7695a671b4 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import desc_config, jax, jit, jnp +from desc.backend import jit, jnp from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -290,8 +290,6 @@ def hess(self, x_reduced, constants=None): def _jac(self, x_reduced, constants=None, op="scaled"): x = self.recover(x_reduced) v = self._unfixed_idx_mat - if desc_config["num_device"] != 1: - v = jax.device_put(v, desc_config["sharding"]) df = getattr(self._objective, "jvp_" + op)(v.T, x, constants) return df.T From 27d0c73e9a1521dabd36573697fdf7d8c24ed7ed Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 10 Feb 2025 19:17:19 -0500 Subject: [PATCH 049/199] add testing cell --- docs/notebooks/tutorials/multi_device.ipynb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 107f3de1eb..46db36a0fb 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -69,6 +69,16 @@ "cons = get_fixed_boundary_constraints(eq)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(obj.compute_scaled_error(obj.x()).shape)\n", + "print(obj.jac_scaled_error(obj.x()).shape)" + ] + }, { "cell_type": "code", "execution_count": null, From 30155452e702833af6fd7da9b125773cb28a0286 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 10 Feb 2025 19:23:55 -0500 Subject: [PATCH 050/199] clean up --- desc/backend.py | 1 + desc/objectives/utils.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 66fe406b08..4af60679e0 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -33,6 +33,7 @@ jax_config.update("jax_enable_x64", True) if desc_config["num_device"] != 1: + # for now, these are not used. Delete them if they are not needed. mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) desc_config["sharding"] = jax.sharding.NamedSharding( mesh, jax.sharding.PartitionSpec("grid") diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index b47ac2afcf..7246a202b7 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -5,7 +5,7 @@ import numpy as np -from desc.backend import desc_config, jax, jit, jnp, put, softargmax +from desc.backend import jit, jnp, put, softargmax from desc.io import IOAble from desc.utils import Index, errorif, flatten_list, svd_inv_null, unique_list, warnif @@ -265,8 +265,6 @@ def __call__(self, x_reduced): """Recover the full state vector from the reduced optimization vector.""" dx = put(jnp.zeros(self.dim_x), self.unfixed_idx, self.Z @ x_reduced) x_full = self.D * (self.xp + dx) - if desc_config["num_device"] != 1: - x_full = jax.device_put(x_full, desc_config["sharding_replicated"]) return jnp.atleast_1d(jnp.squeeze(x_full)) From c8481e1586fe113f11e88c9f7efd6261b5976524 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 10 Feb 2025 19:31:26 -0500 Subject: [PATCH 051/199] move params to device for printing too --- desc/objectives/objective_funs.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 20c5f1106b..38b1d4d6c7 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -613,13 +613,26 @@ def print_value(self, x, x0=None, constants=None): if x0 is not None: params0 = self.unpack_state(x0) assert len(params0) == len(constants) == len(self.objectives) - for par, par0, obj, const in zip( - params, params0, self.objectives, constants - ): - obj.print_value(par, par0, constants=const) + if self._is_multi_device: + for par, par0, obj, const in zip( + params, params0, self.objectives, constants + ): + par = jax.device_put(par, jax.devices("gpu")[obj._device_id]) + par0 = jax.device_put(par0, jax.devices("gpu")[obj._device_id]) + obj.print_value(par, par0, constants=const) + else: + for par, par0, obj, const in zip( + params, params0, self.objectives, constants + ): + obj.print_value(par, par0, constants=const) else: - for par, obj, const in zip(params, self.objectives, constants): - obj.print_value(par, constants=const) + if self._is_multi_device: + for par, obj, const in zip(params, self.objectives, constants): + par = jax.device_put(par, jax.devices("gpu")[obj._device_id]) + obj.print_value(par, constants=const) + else: + for par, obj, const in zip(params, self.objectives, constants): + obj.print_value(par, constants=const) return None def unpack_state(self, x, per_objective=True): From a800fd4bbb1cba426113992f074967d580de766b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 10 Feb 2025 23:10:59 -0500 Subject: [PATCH 052/199] update notebook to plot grid --- docs/notebooks/tutorials/multi_device.ipynb | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 46db36a0fb..ae89542e17 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -94,21 +94,8 @@ "metadata": {}, "outputs": [], "source": [ - "eq = get(\"HELIOTRON\")\n", - "r_per_gpu = 2\n", - "num_device = 5\n", - "rhos = jnp.linspace(0.01, 1.0, r_per_gpu * num_device)\n", - "for i in range(num_device):\n", - " grid = LinearGrid(\n", - " rho=rhos[i * r_per_gpu : (i + 1) * r_per_gpu],\n", - " # kind of experimental way of set giving\n", - " # less grid points to inner part, but seems\n", - " # to make transforms way slower\n", - " M=int(eq.M_grid * i / num_device),\n", - " N=eq.N_grid,\n", - " NFP=eq.NFP,\n", - " )\n", - " plot_grid(grid)" + "for obji in obj.objectives:\n", + " plot_grid(obji.constants[\"transforms\"][\"grid\"])" ] } ], From 69161c2de84a65490901c645f552a18b34435790 Mon Sep 17 00:00:00 2001 From: Yigit Gunsur Elmacioglu Date: Tue, 11 Feb 2025 17:36:24 -0500 Subject: [PATCH 053/199] made it WORK! pass all params on given device, merge arrays on cpu or gpu depending on the size. for very big matrices we move the to cpu to be able call qr or svd. need to use blocked and for loop. --- desc/backend.py | 31 +++- desc/objectives/getters.py | 35 ++-- desc/objectives/objective_funs.py | 57 +++++-- docs/notebooks/tutorials/multi_device.ipynb | 173 ++++++++++++++++---- 4 files changed, 227 insertions(+), 69 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 4af60679e0..3f6c240377 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -445,22 +445,43 @@ def tangent_solve(g, y): x = jax.lax.custom_root(res, x0, solve, tangent_solve, has_aux=False) return x - def pconcat(arrays): + def pconcat(arrays, mode="concat"): """Concatenate arrays that live on different devices. Parameters ---------- arrays : list of jnp.ndarray Arrays to concatenate. + mode : str + "concat:, "hstack" or "vstack. Default is "concat" Returns ------- out : jnp.ndarray - Concatenated array that lives in the first device. + Concatenated array that lives on CPU. """ - return jnp.concatenate( - [jax.device_put(x, device=jax.devices("gpu")[0]) for x in arrays] - ) + # we will use either CPU or GPU[0] for the matrix decompositions, so the array + # of float64 should fit into single device + size = jnp.array([x.size for x in arrays]) + size = jnp.sum(size) + if size*8/(1024**3) > desc_config["avail_mems"][0]: + device = jax.devices("cpu")[0] + else: + device = jax.devices("gpu")[0] + + if mode == "concat": + out = jnp.concatenate( + [jax.device_put(x, device=device) for x in arrays] + ) + elif mode == "hstack": + out = jnp.hstack( + [jax.device_put(x, device=device) for x in arrays] + ) + elif mode == "vstack": + out = jnp.vstack( + [jax.device_put(x, device=device) for x in arrays] + ) + return out # we can't really test the numpy backend stuff in automated testing, so we ignore it diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 74393ea271..75338bc0d6 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -339,7 +339,7 @@ def maybe_add_self_consistency(thing, constraints): return constraints -def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): +def get_parallel_forcebalance(eq, num_device, grid=None, use_jit=True, check_device=True): """Get an ObjectiveFunction for parallel computing ForceBalance. Parameters @@ -365,6 +365,12 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): f"Number of devices in desc_config ({desc_config['num_device']}) " f"does not match the number of devices in input ({num_device})." ) + if grid is not None: + if len(grid) != num_device: + raise ValueError( + f"Number of grids and num_device must be the same! Got " + f"{len(grid)=} and {num_device=}." + ) if eq.L_grid % num_device == 0: k = eq.L_grid // num_device L = eq.L_grid @@ -375,17 +381,20 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): rhos = jnp.linspace(0.01, 1.0, L) objs = () for i in range(num_device): - grid = LinearGrid( - rho=rhos[i * k : (i + 1) * k], - # kind of experimental way of set giving - # less grid points to inner part, but seems - # to make transforms way slower - # M=int(eq.M_grid * i / num_device), # noqa: E800 - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - ) - obj = ForceBalance(eq, grid=grid, device_id=i) + if grid is None: + gridi = LinearGrid( + rho=rhos[i * k : (i + 1) * k], + # kind of experimental way of set giving + # less grid points to inner part, but seems + # to make transforms way slower + # M=int(eq.M_grid * i / num_device), # noqa: E800 + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + ) + else: + gridi = grid[i] + obj = ForceBalance(eq, grid=gridi, device_id=i) obj.build(use_jit=use_jit) obj = jax.device_put(obj, jax.devices("gpu")[i]) # if the eq is also distrubuted across GPUs, then some internal logic that @@ -393,6 +402,6 @@ def get_parallel_forcebalance(eq, num_device, use_jit=True, check_device=True): # to be the same manually obj._things[0] = eq objs += (obj,) - objective = ObjectiveFunction(objs) + objective = ObjectiveFunction(objs, deriv_mode="blocked") objective.build(use_jit=use_jit) return objective diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index ef50d86396..19cba2ccaf 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -188,6 +188,21 @@ def collect_docs( return doc_params +def jit_with_dynamic_device(method): + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + # Get the device using self.id + device = jax.devices("gpu")[self._device_id] + + # Compile the method with jax.jit for the specific device + jitted_method = jax.jit(method, device=device) + + # Call the jitted function + return jitted_method(self, *args, **kwargs) + + return wrapper + + class ObjectiveFunction(IOAble): """Objective function comprised of one or more Objectives. @@ -463,7 +478,7 @@ def compute_unscaled(self, x, constants=None): f = pconcat( [ obj.compute_unscaled( - *jax.device_put(par, jax.devices("gpu")[i]), constants=const + *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const ) for i, (par, obj, const) in enumerate( zip(params, self.objectives, constants) @@ -504,7 +519,7 @@ def compute_scaled(self, x, constants=None): f = pconcat( [ obj.compute_scaled( - *jax.device_put(par, jax.devices("gpu")[i]), constants=const + *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const ) for i, (par, obj, const) in enumerate( zip(params, self.objectives, constants) @@ -545,7 +560,7 @@ def compute_scaled_error(self, x, constants=None): f = pconcat( [ obj.compute_scaled_error( - *jax.device_put(par, jax.devices("gpu")[i]), constants=const + *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const ) for i, (par, obj, const) in enumerate( zip(params, self.objectives, constants) @@ -754,15 +769,23 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): # one by one, and assemble into big block matrix # if objective doesn't depend on a given thing, that part is set to 0. for k, (obj, const) in enumerate(zip(self.objectives, constants)): + print(f"This should run on GPU id:{obj._device_id}") # get the xs that go to that objective thing_idx = self._things_per_objective_idx[k] xi = [xs[i] for i in thing_idx] vi = [vs[i] for i in thing_idx] + if self._is_multi_device: + xi = jax.device_put(xi, jax.devices("gpu")[obj._device_id]) + vi = jax.device_put(vi, jax.devices("gpu")[obj._device_id]) Ji_ = getattr(obj, "jvp_" + op)(vi, xi, constants=const) J += [Ji_] # this is the transpose of the jvp when v is a matrix, for consistency with # jvp_batched - J = jnp.hstack(J) + if not self._is_multi_device: + J = jnp.hstack(J) + else: + J = pconcat(J, mode="hstack") + return J def _jvp_batched(self, v, x, constants=None, op="scaled"): @@ -1252,7 +1275,7 @@ def _maybe_array_to_params(self, *args): argsout += (arg,) return argsout - @jit + @jit_with_dynamic_device def compute_unscaled(self, *args, **kwargs): """Compute the raw value of the objective.""" args = self._maybe_array_to_params(*args) @@ -1261,7 +1284,7 @@ def compute_unscaled(self, *args, **kwargs): f = self._loss_function(f) return jnp.atleast_1d(f) - @jit + @jit_with_dynamic_device def compute_scaled(self, *args, **kwargs): """Compute and apply weighting and normalization.""" args = self._maybe_array_to_params(*args) @@ -1270,7 +1293,7 @@ def compute_scaled(self, *args, **kwargs): f = self._loss_function(f) return jnp.atleast_1d(self._scale(f, **kwargs)) - @jit + @jit_with_dynamic_device def compute_scaled_error(self, *args, **kwargs): """Compute and apply the target/bounds, weighting, and normalization.""" args = self._maybe_array_to_params(*args) @@ -1313,7 +1336,7 @@ def _scale(self, f, *args, **kwargs): f_norm = jnp.atleast_1d(f) / self.normalization # normalization return f_norm * w * self.weight - @jit + @jit_with_dynamic_device def compute_scalar(self, *args, **kwargs): """Compute the scalar form of the objective.""" if self.scalar: @@ -1322,19 +1345,19 @@ def compute_scalar(self, *args, **kwargs): f = jnp.sum(self.compute_scaled_error(*args, **kwargs) ** 2) / 2 return f.squeeze() - @jit + @jit_with_dynamic_device def grad(self, *args, **kwargs): """Compute gradient vector of self.compute_scalar wrt x.""" argnums = tuple(range(len(self.things))) return Derivative(self.compute_scalar, argnums, mode="grad")(*args, **kwargs) - @jit + @jit_with_dynamic_device def hess(self, *args, **kwargs): """Compute Hessian matrix of self.compute_scalar wrt x.""" argnums = tuple(range(len(self.things))) return Derivative(self.compute_scalar, argnums, mode="hess")(*args, **kwargs) - @jit + @jit_with_dynamic_device def jac_scaled(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_scaled wrt x.""" argnums = tuple(range(len(self.things))) @@ -1345,7 +1368,7 @@ def jac_scaled(self, *args, **kwargs): chunk_size=self._jac_chunk_size, )(*args, **kwargs) - @jit + @jit_with_dynamic_device def jac_scaled_error(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_scaled_error wrt x.""" argnums = tuple(range(len(self.things))) @@ -1356,7 +1379,7 @@ def jac_scaled_error(self, *args, **kwargs): chunk_size=self._jac_chunk_size, )(*args, **kwargs) - @jit + @jit_with_dynamic_device def jac_unscaled(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_unscaled wrt x.""" argnums = tuple(range(len(self.things))) @@ -1390,7 +1413,7 @@ def _jvp(self, v, x, constants=None, op="scaled"): # sum over different things. return jnp.sum(jnp.asarray(Jv), axis=0).T - @jit + @jit_with_dynamic_device def jvp_scaled(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_scaled. @@ -1406,7 +1429,7 @@ def jvp_scaled(self, v, x, constants=None): """ return self._jvp(v, x, constants, "scaled") - @jit + @jit_with_dynamic_device def jvp_scaled_error(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_scaled_error. @@ -1422,7 +1445,7 @@ def jvp_scaled_error(self, v, x, constants=None): """ return self._jvp(v, x, constants, "scaled_error") - @jit + @jit_with_dynamic_device def jvp_unscaled(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_unscaled. @@ -1703,3 +1726,5 @@ def __call__(self, things): assert len(flat) == self.length unique, _ = unique_list(flat) return unique + + diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 46db36a0fb..cb5440e64a 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -26,21 +26,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "num_device = 1\n", - "# from desc import set_device\n", + "num_device = 2\n", + "from desc import set_device\n", "\n", - "# set_device(\"gpu\", num_device=num_device)" + "set_device(\"gpu\", num_device=num_device)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DESC version 0.13.0+1130.gc8481e158.dirty,using JAX backend, jax version=0.4.38, jaxlib version=0.4.38, dtype=float64\n", + "Using 2 devices:\n", + "\t Device 0: NVIDIA A100-PCIE-40GB (id=0) with 40.00 GB available memory\n", + "\t Device 1: NVIDIA A100-PCIE-40GB (id=1) with 40.00 GB available memory\n" + ] + } + ], "source": [ "from desc.examples import get\n", "from desc.objectives import *\n", @@ -61,9 +72,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Precomputing transforms\n", + "Precomputing transforms\n" + ] + } + ], "source": [ "obj = get_parallel_forcebalance(eq, num_device=num_device, check_device=False)\n", "cons = get_fixed_boundary_constraints(eq)" @@ -71,9 +91,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(34632,)\n", + "(34632, 1977)\n" + ] + } + ], "source": [ "print(obj.compute_scaled_error(obj.x()).shape)\n", "print(obj.jac_scaled_error(obj.x()).shape)" @@ -81,42 +110,116 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 8, + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Building objective: lcfs R\n", + "Building objective: lcfs Z\n", + "Building objective: fixed Psi\n", + "Building objective: fixed pressure\n", + "Building objective: fixed iota\n", + "Building objective: fixed sheet current\n", + "Building objective: self_consistency R\n", + "Building objective: self_consistency Z\n", + "Building objective: lambda gauge\n", + "Building objective: axis R self consistency\n", + "Building objective: axis Z self consistency\n", + "Timer: Objective build = 1.99 sec\n", + "Timer: Linear constraint projection build = 5.61 sec\n", + "Number of parameters: 1593\n", + "Number of objectives: 34632\n", + "Timer: Initializing the optimization = 7.73 sec\n", + "\n", + "Starting optimization\n", + "Using method: lsq-exact\n", + " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", + " 0 1 3.654e-07 1.803e-04 \n", + " 1 6 2.102e-07 1.552e-07 1.244e-03 5.327e-05 \n", + " 2 7 2.097e-07 5.059e-10 1.883e-03 6.156e-05 \n", + "Warning: Maximum number of iterations has been exceeded.\n", + " Current function value: 2.097e-07\n", + " Total delta_x: 2.644e-03\n", + " Iterations: 2\n", + " Function evaluations: 7\n", + " Jacobian evaluations: 3\n", + "Timer: Solution time = 23.4 sec\n", + "Timer: Avg time per step = 7.83 sec\n", + "==============================================================================================================\n", + " Start --> End\n", + "Total (sum of squares): 3.654e-07 --> 2.097e-07, \n", + "Maximum absolute Force error: 1.378e+02 --> 2.537e+02 (N)\n", + "Minimum absolute Force error: 1.059e-10 --> 1.060e-10 (N)\n", + "Average absolute Force error: 2.610e+01 --> 1.938e+01 (N)\n", + "Maximum absolute Force error: 1.108e-05 --> 2.040e-05 (normalized)\n", + "Minimum absolute Force error: 8.517e-18 --> 8.529e-18 (normalized)\n", + "Average absolute Force error: 2.099e-06 --> 1.558e-06 (normalized)\n", + "Maximum absolute Force error: 8.201e+03 --> 6.247e+03 (N)\n", + "Minimum absolute Force error: 1.635e-12 --> 2.050e-12 (N)\n", + "Average absolute Force error: 8.007e+01 --> 7.093e+01 (N)\n", + "Maximum absolute Force error: 6.596e-04 --> 5.024e-04 (normalized)\n", + "Minimum absolute Force error: 1.315e-19 --> 1.649e-19 (normalized)\n", + "Average absolute Force error: 6.440e-06 --> 5.705e-06 (normalized)\n", + "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", + "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", + "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", + "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", + "Fixed iota profile error: 0.000e+00 --> 0.000e+00 (dimensionless)\n", + "Fixed sheet current error: 0.000e+00 --> 0.000e+00 (~)\n", + "==============================================================================================================\n" + ] + } + ], "source": [ - "eq.solve(objective=obj, constraints=cons, maxiter=2, ftol=0, gtol=0, xtol=0, verbose=3)" + "eq.solve(objective=obj, constraints=cons, maxiter=2, ftol=0, gtol=0, xtol=0, verbose=3);" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "eq = get(\"HELIOTRON\")\n", - "r_per_gpu = 2\n", - "num_device = 5\n", - "rhos = jnp.linspace(0.01, 1.0, r_per_gpu * num_device)\n", - "for i in range(num_device):\n", - " grid = LinearGrid(\n", - " rho=rhos[i * r_per_gpu : (i + 1) * r_per_gpu],\n", - " # kind of experimental way of set giving\n", - " # less grid points to inner part, but seems\n", - " # to make transforms way slower\n", - " M=int(eq.M_grid * i / num_device),\n", - " N=eq.N_grid,\n", - " NFP=eq.NFP,\n", - " )\n", - " plot_grid(grid)" + "for obji in obj.objectives:\n", + " plot_grid(obji.constants[\"transforms\"][\"grid\"])" ] } ], "metadata": { "kernelspec": { - "display_name": "desc-env", + "display_name": "desc-env [~/.conda/envs/desc-env/]", "language": "python", - "name": "python3" + "name": "conda_desc-env" }, "language_info": { "codemirror_mode": { @@ -128,9 +231,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.11.6" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } From 23f66124a557050290c74dcf149d90f8537c18df Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 11 Feb 2025 20:52:13 -0500 Subject: [PATCH 054/199] fix formatting after cluster --- desc/__init__.py | 2 +- desc/backend.py | 18 +++++--------- desc/objectives/getters.py | 10 +++++--- desc/objectives/objective_funs.py | 27 ++++++++++++++------- docs/notebooks/tutorials/multi_device.ipynb | 10 ++++---- 5 files changed, 36 insertions(+), 31 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 86a919d84d..7005b3af2b 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -61,7 +61,7 @@ def __getattr__(name): config = {"device": None, "avail_mem": None, "kind": None, "num_device": None} -def set_device(kind="cpu", gpuid=None, num_device=1): +def set_device(kind="cpu", gpuid=None, num_device=1): # noqa : C901 """Sets the device to use for computation. If kind==``'gpu'`` and a gpuid is specified, uses the specified GPU. If diff --git a/desc/backend.py b/desc/backend.py index 3f6c240377..39f4a095e8 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -460,27 +460,21 @@ def pconcat(arrays, mode="concat"): out : jnp.ndarray Concatenated array that lives on CPU. """ - # we will use either CPU or GPU[0] for the matrix decompositions, so the array - # of float64 should fit into single device + # we will use either CPU or GPU[0] for the matrix decompositions, so the + # array of float64 should fit into single device size = jnp.array([x.size for x in arrays]) size = jnp.sum(size) - if size*8/(1024**3) > desc_config["avail_mems"][0]: + if size * 8 / (1024**3) > desc_config["avail_mems"][0]: device = jax.devices("cpu")[0] else: device = jax.devices("gpu")[0] if mode == "concat": - out = jnp.concatenate( - [jax.device_put(x, device=device) for x in arrays] - ) + out = jnp.concatenate([jax.device_put(x, device=device) for x in arrays]) elif mode == "hstack": - out = jnp.hstack( - [jax.device_put(x, device=device) for x in arrays] - ) + out = jnp.hstack([jax.device_put(x, device=device) for x in arrays]) elif mode == "vstack": - out = jnp.vstack( - [jax.device_put(x, device=device) for x in arrays] - ) + out = jnp.vstack([jax.device_put(x, device=device) for x in arrays]) return out diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 75338bc0d6..3c8853c8af 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -339,7 +339,9 @@ def maybe_add_self_consistency(thing, constraints): return constraints -def get_parallel_forcebalance(eq, num_device, grid=None, use_jit=True, check_device=True): +def get_parallel_forcebalance( + eq, num_device, grid=None, use_jit=True, check_device=True +): """Get an ObjectiveFunction for parallel computing ForceBalance. Parameters @@ -397,9 +399,9 @@ def get_parallel_forcebalance(eq, num_device, grid=None, use_jit=True, check_dev obj = ForceBalance(eq, grid=gridi, device_id=i) obj.build(use_jit=use_jit) obj = jax.device_put(obj, jax.devices("gpu")[i]) - # if the eq is also distrubuted across GPUs, then some internal logic that - # checks if the things are different will fail, so we need to set the eq - # to be the same manually + # if the eq is also distrubuted across GPUs, then some internal logic + # that checks if the things are different will fail, so we need to + # set the eq to be the same manually obj._things[0] = eq objs += (obj,) objective = ObjectiveFunction(objs, deriv_mode="blocked") diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 19cba2ccaf..9f324c6524 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -189,17 +189,25 @@ def collect_docs( def jit_with_dynamic_device(method): + """Just-in-time compile a decorator with a dynamic device. + + Decorates a method of a class with a dynamic device, allowing the method to be + compiled with jax.jit for the specific device. This is needed since + @functools.partial(jax.jit, device=jax.devices("gpu")[self._device_id]) is not + allowed in a class definition. + """ + @functools.wraps(method) def wrapper(self, *args, **kwargs): # Get the device using self.id device = jax.devices("gpu")[self._device_id] - + # Compile the method with jax.jit for the specific device jitted_method = jax.jit(method, device=device) - + # Call the jitted function return jitted_method(self, *args, **kwargs) - + return wrapper @@ -478,7 +486,8 @@ def compute_unscaled(self, x, constants=None): f = pconcat( [ obj.compute_unscaled( - *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const + *jax.device_put(par, jax.devices("gpu")[obj._device_id]), + constants=const, ) for i, (par, obj, const) in enumerate( zip(params, self.objectives, constants) @@ -519,7 +528,8 @@ def compute_scaled(self, x, constants=None): f = pconcat( [ obj.compute_scaled( - *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const + *jax.device_put(par, jax.devices("gpu")[obj._device_id]), + constants=const, ) for i, (par, obj, const) in enumerate( zip(params, self.objectives, constants) @@ -560,7 +570,8 @@ def compute_scaled_error(self, x, constants=None): f = pconcat( [ obj.compute_scaled_error( - *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const + *jax.device_put(par, jax.devices("gpu")[obj._device_id]), + constants=const, ) for i, (par, obj, const) in enumerate( zip(params, self.objectives, constants) @@ -785,7 +796,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): J = jnp.hstack(J) else: J = pconcat(J, mode="hstack") - + return J def _jvp_batched(self, v, x, constants=None, op="scaled"): @@ -1726,5 +1737,3 @@ def __call__(self, things): assert len(flat) == self.length unique, _ = unique_list(flat) return unique - - diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index cb5440e64a..67f2fe1f58 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -110,7 +110,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": { "editable": true, "slideshow": { @@ -180,7 +180,7 @@ } ], "source": [ - "eq.solve(objective=obj, constraints=cons, maxiter=2, ftol=0, gtol=0, xtol=0, verbose=3);" + "eq.solve(objective=obj, constraints=cons, maxiter=2, ftol=0, gtol=0, xtol=0, verbose=3)" ] }, { @@ -217,9 +217,9 @@ ], "metadata": { "kernelspec": { - "display_name": "desc-env [~/.conda/envs/desc-env/]", + "display_name": "desc-env", "language": "python", - "name": "conda_desc-env" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -231,7 +231,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.12.7" } }, "nbformat": 4, From 1e1dfebdfe9cbcc5312787c480c0469524165e6d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 11 Feb 2025 21:04:12 -0500 Subject: [PATCH 055/199] fix some problems for testing and docs --- desc/objectives/objective_funs.py | 7 +++++-- docs/index.rst | 1 + docs/notebooks/tutorials/multi_device.ipynb | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 9f324c6524..4de6e0a050 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -199,8 +199,11 @@ def jit_with_dynamic_device(method): @functools.wraps(method) def wrapper(self, *args, **kwargs): - # Get the device using self.id - device = jax.devices("gpu")[self._device_id] + # Get the device using self.id or default to CPU + if desc_config["device"] == "gpu": + device = jax.devices("gpu")[self._device_id] + else: + device = None # Compile the method with jax.jit for the specific device jitted_method = jax.jit(method, device=device) diff --git a/docs/index.rst b/docs/index.rst index 1c26bdeb58..1569e1668e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ installation notebooks/tutorials/use_outputs.ipynb performance_tips + notebooks/tutorials/multi_device.ipynb .. toctree:: diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 67f2fe1f58..c44bc260af 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -26,14 +26,14 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "num_device = 2\n", - "from desc import set_device\n", + "# from desc import set_device\n", "\n", - "set_device(\"gpu\", num_device=num_device)" + "# set_device(\"gpu\", num_device=num_device)" ] }, { From fd7638b52b40f6e8db8f7fccf586ff79f76042dc Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 11 Feb 2025 21:33:18 -0500 Subject: [PATCH 056/199] ignore multidevice for notebook tests, add additional warnings for gpu, move jit_with_device to backend --- .github/workflows/notebook_tests.yml | 2 +- desc/backend.py | 29 ++++++++++ desc/objectives/getters.py | 5 ++ desc/objectives/objective_funs.py | 87 ++++++++-------------------- 4 files changed, 59 insertions(+), 64 deletions(-) diff --git a/.github/workflows/notebook_tests.yml b/.github/workflows/notebook_tests.yml index a437d1bbb7..3bde1e62d5 100644 --- a/.github/workflows/notebook_tests.yml +++ b/.github/workflows/notebook_tests.yml @@ -93,7 +93,7 @@ jobs: export PYTHONPATH=$(pwd) pytest -v --nbmake "./docs/notebooks" \ --nbmake-timeout=2000 \ - --ignore=./docs/notebooks/zernike_eval.ipynb \ + --ignore=./docs/notebooks/zernike_eval.ipynb ./docs/notebooks/tutorials/multi_device.ipynb \ --splits 3 \ --group ${{ matrix.group }} \ --splitting-algorithm least_duration diff --git a/desc/backend.py b/desc/backend.py index 1fcf4b3579..07393f641e 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -475,6 +475,35 @@ def pconcat(arrays, mode="concat"): out = jnp.vstack([jax.device_put(x, device=device) for x in arrays]) return out + def jit_with_device(method): + """Decorator to Just-in-time compile a class method with a dynamic device. + + Decorates a method of a class with a dynamic device, allowing the method to be + compiled with jax.jit for the specific device. This is needed since + @functools.partial(jax.jit, device=jax.devices("gpu")[self._device_id]) is not + allowed in a class definition. + + Parameters + ---------- + method : callable + Class method to decorate. If DESC is running on GPU, the class should have + a device_id attribute. + """ + + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + # Get the device using self.id or default to CPU + if desc_config["device"] == "gpu" and hasattr(self, "_device_id"): + device = jax.devices("gpu")[self._device_id] + else: + device = None + + # Compile the method with jax.jit for the specific device + wrapped = jax.jit(method, device=device) + return wrapped(self, *args, **kwargs) + + return wrapper + # we can't really test the numpy backend stuff in automated testing, so we ignore it # for coverage purposes diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 6be7132b0f..ff664816cf 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -369,6 +369,11 @@ def get_parallel_forcebalance( from desc.backend import desc_config, jax, jnp from desc.grid import LinearGrid + if desc_config["device"] != "gpu": + raise ValueError( + "Parallel computing is only supported on GPU. " + "Please use DESC with GPU device." + ) if desc_config["num_device"] != num_device and check_device: raise ValueError( f"Number of devices in desc_config ({desc_config['num_device']}) " diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 4de6e0a050..d18f021854 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -10,6 +10,7 @@ execute_on_cpu, jax, jit, + jit_with_device, jnp, pconcat, tree_flatten, @@ -188,32 +189,6 @@ def collect_docs( return doc_params -def jit_with_dynamic_device(method): - """Just-in-time compile a decorator with a dynamic device. - - Decorates a method of a class with a dynamic device, allowing the method to be - compiled with jax.jit for the specific device. This is needed since - @functools.partial(jax.jit, device=jax.devices("gpu")[self._device_id]) is not - allowed in a class definition. - """ - - @functools.wraps(method) - def wrapper(self, *args, **kwargs): - # Get the device using self.id or default to CPU - if desc_config["device"] == "gpu": - device = jax.devices("gpu")[self._device_id] - else: - device = None - - # Compile the method with jax.jit for the specific device - jitted_method = jax.jit(method, device=device) - - # Call the jitted function - return jitted_method(self, *args, **kwargs) - - return wrapper - - class ObjectiveFunction(IOAble): """Objective function comprised of one or more Objectives. @@ -492,9 +467,7 @@ def compute_unscaled(self, x, constants=None): *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const, ) - for i, (par, obj, const) in enumerate( - zip(params, self.objectives, constants) - ) + for par, obj, const in zip(params, self.objectives, constants) ] ) return f @@ -534,9 +507,7 @@ def compute_scaled(self, x, constants=None): *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const, ) - for i, (par, obj, const) in enumerate( - zip(params, self.objectives, constants) - ) + for par, obj, const in zip(params, self.objectives, constants) ] ) return f @@ -576,9 +547,7 @@ def compute_scaled_error(self, x, constants=None): *jax.device_put(par, jax.devices("gpu")[obj._device_id]), constants=const, ) - for i, (par, obj, const) in enumerate( - zip(params, self.objectives, constants) - ) + for par, obj, const in zip(params, self.objectives, constants) ] ) return f @@ -642,26 +611,18 @@ def print_value(self, x, x0=None, constants=None): if x0 is not None: params0 = self.unpack_state(x0) assert len(params0) == len(constants) == len(self.objectives) - if self._is_multi_device: - for par, par0, obj, const in zip( - params, params0, self.objectives, constants - ): + for par, par0, obj, const in zip( + params, params0, self.objectives, constants + ): + if self._is_multi_device: par = jax.device_put(par, jax.devices("gpu")[obj._device_id]) par0 = jax.device_put(par0, jax.devices("gpu")[obj._device_id]) - obj.print_value(par, par0, constants=const) - else: - for par, par0, obj, const in zip( - params, params0, self.objectives, constants - ): - obj.print_value(par, par0, constants=const) + obj.print_value(par, par0, constants=const) else: - if self._is_multi_device: - for par, obj, const in zip(params, self.objectives, constants): + for par, obj, const in zip(params, self.objectives, constants): + if self._is_multi_device: par = jax.device_put(par, jax.devices("gpu")[obj._device_id]) - obj.print_value(par, constants=const) - else: - for par, obj, const in zip(params, self.objectives, constants): - obj.print_value(par, constants=const) + obj.print_value(par, constants=const) return None def unpack_state(self, x, per_objective=True): @@ -1289,7 +1250,7 @@ def _maybe_array_to_params(self, *args): argsout += (arg,) return argsout - @jit_with_dynamic_device + @jit_with_device def compute_unscaled(self, *args, **kwargs): """Compute the raw value of the objective.""" args = self._maybe_array_to_params(*args) @@ -1298,7 +1259,7 @@ def compute_unscaled(self, *args, **kwargs): f = self._loss_function(f) return jnp.atleast_1d(f) - @jit_with_dynamic_device + @jit_with_device def compute_scaled(self, *args, **kwargs): """Compute and apply weighting and normalization.""" args = self._maybe_array_to_params(*args) @@ -1307,7 +1268,7 @@ def compute_scaled(self, *args, **kwargs): f = self._loss_function(f) return jnp.atleast_1d(self._scale(f, **kwargs)) - @jit_with_dynamic_device + @jit_with_device def compute_scaled_error(self, *args, **kwargs): """Compute and apply the target/bounds, weighting, and normalization.""" args = self._maybe_array_to_params(*args) @@ -1350,7 +1311,7 @@ def _scale(self, f, *args, **kwargs): f_norm = jnp.atleast_1d(f) / self.normalization # normalization return f_norm * w * self.weight - @jit_with_dynamic_device + @jit_with_device def compute_scalar(self, *args, **kwargs): """Compute the scalar form of the objective.""" if self.scalar: @@ -1359,19 +1320,19 @@ def compute_scalar(self, *args, **kwargs): f = jnp.sum(self.compute_scaled_error(*args, **kwargs) ** 2) / 2 return f.squeeze() - @jit_with_dynamic_device + @jit_with_device def grad(self, *args, **kwargs): """Compute gradient vector of self.compute_scalar wrt x.""" argnums = tuple(range(len(self.things))) return Derivative(self.compute_scalar, argnums, mode="grad")(*args, **kwargs) - @jit_with_dynamic_device + @jit_with_device def hess(self, *args, **kwargs): """Compute Hessian matrix of self.compute_scalar wrt x.""" argnums = tuple(range(len(self.things))) return Derivative(self.compute_scalar, argnums, mode="hess")(*args, **kwargs) - @jit_with_dynamic_device + @jit_with_device def jac_scaled(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_scaled wrt x.""" argnums = tuple(range(len(self.things))) @@ -1382,7 +1343,7 @@ def jac_scaled(self, *args, **kwargs): chunk_size=self._jac_chunk_size, )(*args, **kwargs) - @jit_with_dynamic_device + @jit_with_device def jac_scaled_error(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_scaled_error wrt x.""" argnums = tuple(range(len(self.things))) @@ -1393,7 +1354,7 @@ def jac_scaled_error(self, *args, **kwargs): chunk_size=self._jac_chunk_size, )(*args, **kwargs) - @jit_with_dynamic_device + @jit_with_device def jac_unscaled(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_unscaled wrt x.""" argnums = tuple(range(len(self.things))) @@ -1427,7 +1388,7 @@ def _jvp(self, v, x, constants=None, op="scaled"): # sum over different things. return jnp.sum(jnp.asarray(Jv), axis=0).T - @jit_with_dynamic_device + @jit_with_device def jvp_scaled(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_scaled. @@ -1443,7 +1404,7 @@ def jvp_scaled(self, v, x, constants=None): """ return self._jvp(v, x, constants, "scaled") - @jit_with_dynamic_device + @jit_with_device def jvp_scaled_error(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_scaled_error. @@ -1459,7 +1420,7 @@ def jvp_scaled_error(self, v, x, constants=None): """ return self._jvp(v, x, constants, "scaled_error") - @jit_with_dynamic_device + @jit_with_device def jvp_unscaled(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_unscaled. From 0a77b1abedcf639c032b24f53f6e7dee6d07a55d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 12 Feb 2025 14:29:37 -0500 Subject: [PATCH 057/199] add changelog, fix notebook tests --- .github/workflows/notebook_tests.yml | 11 ++++++----- CHANGELOG.md | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/notebook_tests.yml b/.github/workflows/notebook_tests.yml index 3bde1e62d5..b7e7055a0c 100644 --- a/.github/workflows/notebook_tests.yml +++ b/.github/workflows/notebook_tests.yml @@ -92,8 +92,9 @@ jobs: source .venv-${{ env.version }}/bin/activate export PYTHONPATH=$(pwd) pytest -v --nbmake "./docs/notebooks" \ - --nbmake-timeout=2000 \ - --ignore=./docs/notebooks/zernike_eval.ipynb ./docs/notebooks/tutorials/multi_device.ipynb \ - --splits 3 \ - --group ${{ matrix.group }} \ - --splitting-algorithm least_duration + --nbmake-timeout=2000 \ + --ignore=./docs/notebooks/zernike_eval.ipynb \ + --ignore=./docs/notebooks/tutorials/multi_device.ipynb \ + --splits 3 \ + --group ${{ matrix.group }} \ + --splitting-algorithm least_duration diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0aa35490..fe9c8a3e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,8 +24,9 @@ New Features - Adds a new function ``desc.coils.initialize_helical_coils`` for creating an initial guess for stage 2 helical coil optimization. - Adds ``desc.vmec_utils.make_boozmn_output `` for writing boozmn.nc style output files for compatibility with other codes which expect such files from the Booz_Xform code. -- Renames compute quantity ``sqrt(g)_B`` to ``sqrt(g)_Boozer_DESC`` to more accurately reflect what the quantiy is (the jacobian from (rho,theta_B,zeta_B) to (rho,theta,zeta)), and adds a new function to compute ``sqrt(g)_Boozer`` which is the jacobian from (rho,theta_B,zeta_B) to (R,phi,Z). -- Allows specification of Nyquist spectrum maximum modenumbers when using ``VMECIO.save`` to save a DESC .h5 file as a VMEC-format wout file +- Renames compute quantity ``sqrt(g)_B`` to ``sqrt(g)_Boozer_DESC`` to more accurately reflect what the quantity is (the jacobian from (rho,theta_B,zeta_B) to (rho,theta,zeta)), and adds a new function to compute ``sqrt(g)_Boozer`` which is the jacobian from (rho,theta_B,zeta_B) to (R,phi,Z). +- Allows specification of Nyquist spectrum maximum mode-numbers when using ``VMECIO.save`` to save a DESC .h5 file as a VMEC-format wout file +- Adds initial support for multiple GPU optimization. This allows to compute derivatives on multiple GPU, and allows more memory intense objectives. Note that: at this phase, the multi-device support is for memory, not speed. Bug Fixes From 306ae44e130df16be441ccf26c519aafa83358ec Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 12 Feb 2025 17:13:05 -0500 Subject: [PATCH 058/199] add warning for deriv_mode blocked and moving array to CPU --- desc/backend.py | 5 +++++ desc/objectives/objective_funs.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/desc/backend.py b/desc/backend.py index 07393f641e..7c22a48e66 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -463,6 +463,11 @@ def pconcat(arrays, mode="concat"): size = jnp.array([x.size for x in arrays]) size = jnp.sum(size) if size * 8 / (1024**3) > desc_config["avail_mems"][0]: + warnings.warn( + "The total size of the arrays exceeds the available memory of the " + "GPU[id=0]. Moving the array to CPU. This may cause performance " + "degredation." + ) device = jax.devices("cpu")[0] else: device = jax.devices("gpu")[0] diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index d18f021854..a830f1d895 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -364,6 +364,12 @@ def build(self, use_jit=None, use_jit_wrapper=True, verbose=1): # noqa: C901 else: self._deriv_mode = "blocked" + if self._is_multi_device and self._deriv_mode != "blocked": + raise ValueError( + "When using multiple GPUs, the deriv_mode must be set to 'blocked'. " + "When you are creating the ObjectiveFunction, set deriv_mode='blocked'." + ) + if self._jac_chunk_size == "auto": # Heuristic estimates of fwd mode Jacobian memory usage, # slightly conservative, based on using ForceBalance as the objective @@ -744,12 +750,16 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): # one by one, and assemble into big block matrix # if objective doesn't depend on a given thing, that part is set to 0. for k, (obj, const) in enumerate(zip(self.objectives, constants)): - print(f"This should run on GPU id:{obj._device_id}") + # TODO: this is for debugging purposes, must be deleted before merging! + if self._is_multi_device: + print(f"This should run on GPU id:{obj._device_id}") # get the xs that go to that objective thing_idx = self._things_per_objective_idx[k] xi = [xs[i] for i in thing_idx] vi = [vs[i] for i in thing_idx] if self._is_multi_device: + # inputs to jitted functions must live on the same device. Need to + # put xi and vi on the same device as the objective xi = jax.device_put(xi, jax.devices("gpu")[obj._device_id]) vi = jax.device_put(vi, jax.devices("gpu")[obj._device_id]) Ji_ = getattr(obj, "jvp_" + op)(vi, xi, constants=const) From f5dd1fae00bdd695bed57d7d5cb896e0c7e24171 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 12 Feb 2025 17:16:57 -0500 Subject: [PATCH 059/199] add option to suppress cpu warning --- desc/backend.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 7c22a48e66..a7815e40b8 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -463,11 +463,14 @@ def pconcat(arrays, mode="concat"): size = jnp.array([x.size for x in arrays]) size = jnp.sum(size) if size * 8 / (1024**3) > desc_config["avail_mems"][0]: - warnings.warn( - "The total size of the arrays exceeds the available memory of the " - "GPU[id=0]. Moving the array to CPU. This may cause performance " - "degredation." - ) + if getattr(desc_config, "suppress_cpu_warning", False): + warnings.warn( + "The total size of the arrays exceeds the available memory of the " + "GPU[id=0]. Moving the array to CPU. This may cause performance " + "degredation. To suppress this warning, use " + "`from desc import config as desc_config` \n" + "`desc_config['suppress_cpu_warning'] = True`" + ) device = jax.devices("cpu")[0] else: device = jax.devices("gpu")[0] From 3326426e5f6edb19dace381abf2bcd45c85bf1e8 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 12 Feb 2025 17:17:49 -0500 Subject: [PATCH 060/199] make upper case --- desc/backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index a7815e40b8..7148b2896c 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -463,13 +463,13 @@ def pconcat(arrays, mode="concat"): size = jnp.array([x.size for x in arrays]) size = jnp.sum(size) if size * 8 / (1024**3) > desc_config["avail_mems"][0]: - if getattr(desc_config, "suppress_cpu_warning", False): + if getattr(desc_config, "SUPPRESS_CPU_WARNING", False): warnings.warn( "The total size of the arrays exceeds the available memory of the " "GPU[id=0]. Moving the array to CPU. This may cause performance " "degredation. To suppress this warning, use " "`from desc import config as desc_config` \n" - "`desc_config['suppress_cpu_warning'] = True`" + "`desc_config['SUPPRESS_CPU_WARNING'] = True`" ) device = jax.devices("cpu")[0] else: From fef9a90ae7b5e58c02649e258a17b0465063d2c7 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 12 Feb 2025 18:13:18 -0500 Subject: [PATCH 061/199] clean up set_device --- desc/__init__.py | 173 +++++++++--------------------- desc/backend.py | 3 +- desc/objectives/objective_funs.py | 26 +++-- 3 files changed, 71 insertions(+), 131 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 7005b3af2b..d731607979 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -61,21 +61,28 @@ def __getattr__(name): config = {"device": None, "avail_mem": None, "kind": None, "num_device": None} -def set_device(kind="cpu", gpuid=None, num_device=1): # noqa : C901 +def set_device(kind="cpu", gpuid=None, num_device=1): """Sets the device to use for computation. If kind==``'gpu'`` and a gpuid is specified, uses the specified GPU. If gpuid==``None`` or a wrong GPU id is given, checks available GPUs and selects the one with the most available memory. Respects environment variable CUDA_VISIBLE_DEVICES for selecting from multiple - available GPUs + available GPUs. + + Notes + ----- + This function must be called before importing anything else from DESC or JAX, + otherwise it will have no effect. Parameters ---------- kind : {``'cpu'``, ``'gpu'``} whether to use CPU or GPU. + gpuid : int, optional + GPU id to use. Default is None. Supported only when num_device is 1. num_device : int - number of devices to use. If None, uses only one device. + number of devices to use. Default is 1. """ config["kind"] = kind @@ -89,8 +96,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa : C901 config["avail_mem"] = cpu_mem config["num_device"] = 1 - if kind == "gpu" and num_device == 1: - # Set CUDA_DEVICE_ORDER so the IDs assigned by CUDA match those from nvidia-smi + elif kind == "gpu": os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" import nvgpu @@ -103,137 +109,64 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa : C901 set_device(kind="cpu") return - maxmem = 0 - selected_gpu = None gpu_ids = [dev["index"] for dev in devices] if "CUDA_VISIBLE_DEVICES" in os.environ: cuda_ids = [ s for s in re.findall(r"\b\d+\b", os.environ["CUDA_VISIBLE_DEVICES"]) ] - # check that the visible devices actually exist and are gpus gpu_ids = [i for i in cuda_ids if i in gpu_ids] if len(gpu_ids) == 0: - # cuda visible devices = '' -> don't use any gpu - warnings.warn( - colored( - ( - "CUDA_VISIBLE_DEVICES={} ".format( - os.environ["CUDA_VISIBLE_DEVICES"] - ) - + "did not match any physical GPU " - + "(id={}), falling back to CPU".format( - [dev["index"] for dev in devices] - ) - ), - "yellow", - ) - ) - set_device(kind="cpu") - return - devices = [dev for dev in devices if dev["index"] in gpu_ids] - - if gpuid is not None and (str(gpuid) in gpu_ids): - selected_gpu = [dev for dev in devices if dev["index"] == str(gpuid)][0] - else: - for dev in devices: - mem = dev["mem_total"] - dev["mem_used"] - if mem > maxmem: - maxmem = mem - selected_gpu = dev - config["device"] = selected_gpu["type"] + " (id={})".format( - selected_gpu["index"] - ) - if gpuid is not None and not (str(gpuid) in gpu_ids): warnings.warn( colored( - "Specified gpuid {} not found, falling back to ".format(str(gpuid)) - + config["device"], + f"CUDA_VISIBLE_DEVICES={os.environ['CUDA_VISIBLE_DEVICES']} did " + "not match any physical GPU " + f"(id={[dev['index'] for dev in devices]}), falling back to CPU", "yellow", ) ) - config["avail_mem"] = ( - selected_gpu["mem_total"] - selected_gpu["mem_used"] - ) / 1024 # in GB - config["num_device"] = 1 - os.environ["CUDA_VISIBLE_DEVICES"] = str(selected_gpu["index"]) - - # TODO: merge the "gpu" and "num_device" cases in single if block - if kind == "gpu" and num_device > 1: - import nvgpu - - try: - devices = nvgpu.gpu_info() - except FileNotFoundError: - devices = [] - if len(devices) == 0: - warnings.warn(colored("No GPU found, falling back to CPU", "yellow")) set_device(kind="cpu") return - gpu_ids = [dev["index"] for dev in devices] - if "CUDA_VISIBLE_DEVICES" in os.environ: - cuda_ids = [ - s for s in re.findall(r"\b\d+\b", os.environ["CUDA_VISIBLE_DEVICES"]) - ] - # check that the visible devices actually exist and are gpus - gpu_ids = [i for i in cuda_ids if i in gpu_ids] - if len(gpu_ids) == 0: - # cuda visible devices = '' -> don't use any gpu - warnings.warn( - colored( - ( - "CUDA_VISIBLE_DEVICES={} ".format( - os.environ["CUDA_VISIBLE_DEVICES"] - ) - + "did not match any physical GPU " - + "(id={}), falling back to CPU".format( - [dev["index"] for dev in devices] + devices = [dev for dev in devices if dev["index"] in gpu_ids] + memories = {dev["index"]: dev["mem_total"] - dev["mem_used"] for dev in devices} + + if num_device == 1: + if gpuid is not None: + if str(gpuid) in gpu_ids: + selected_gpu = next( + dev for dev in devices if dev["index"] == str(gpuid) + ) + else: + warnings.warn( + colored( + f"Specified gpuid {gpuid} not found, selecting GPU with " + "most memory", + "yellow", ) - ), - "yellow", + ) + else: + selected_gpu = max( + devices, key=lambda dev: dev["mem_total"] - dev["mem_used"] ) - ) - set_device(kind="cpu") - return + devices = [selected_gpu] - devices = [dev for dev in devices if dev["index"] in gpu_ids] - memories = {} - for dev in devices: - mem = dev["mem_total"] - dev["mem_used"] - memories[dev["index"]] = mem - - if num_device > len(devices): - warnings.warn( - colored( - "Requested {} GPUs, but only {} available".format( - num_device, len(devices) - ), - "yellow", - ) - ) - return - elif num_device < len(devices): - config["device"] = "gpu" - config["devices"] = [ - dev["type"] + " (id={})".format(dev["index"]) - for dev in devices[:num_device] - ] - config["avail_mems"] = [ - memories[dev["index"]] / 1024 for dev in devices[:num_device] - ] # in GB - config["num_device"] = num_device - # make the other gpu's invisible - visible_devices = "0" - for i in range(1, num_device): - visible_devices += f",{i}" - os.environ["CUDA_VISIBLE_DEVICES"] = visible_devices else: - config["device"] = "gpu" - config["devices"] = [ - dev["type"] + " (id={})".format(dev["index"]) for dev in devices - ] - config["avail_mems"] = [ - memories[dev["index"]] / 1024 for dev in devices - ] # in GB - config["num_device"] = num_device - # by default all gpus are already visible + if num_device > len(devices): + raise ValueError( + f"Requested {num_device} GPUs, but only {len(devices)} available" + ) + if gpuid is not None: + # TODO: implement multiple GPU selection + raise ValueError("Cannot specify 'gpuid' when requesting multiple GPUs") + + config["avail_mems"] = [ + memories[dev["index"]] / 1024 for dev in devices[:num_device] + ] # in GB + config["devices"] = [ + f"{dev['type']} (id={dev['index']})" for dev in devices[:num_device] + ] + os.environ["CUDA_VISIBLE_DEVICES"] = ",".join( + str(dev["index"]) for dev in devices[:num_device] + ) + config["device_type"] = "gpu" + config["num_device"] = num_device diff --git a/desc/backend.py b/desc/backend.py index 7148b2896c..b1063d9d6c 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -456,7 +456,8 @@ def pconcat(arrays, mode="concat"): Returns ------- out : jnp.ndarray - Concatenated array that lives on CPU. + Concatenated array that lives on GPU[id=0]. If thre is not enough memory + the array will be stored on CPU. """ # we will use either CPU or GPU[0] for the matrix decompositions, so the # array of float64 should fit into single device diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index a830f1d895..192840e5e6 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -373,20 +373,26 @@ def build(self, use_jit=None, use_jit_wrapper=True, verbose=1): # noqa: C901 if self._jac_chunk_size == "auto": # Heuristic estimates of fwd mode Jacobian memory usage, # slightly conservative, based on using ForceBalance as the objective - estimated_memory_usage = 2.4e-7 * self.dim_f * self.dim_x + 1 # in GB - mem_avail = ( - desc_config.get("avail_mem") - if desc_config.get("avail_mem") is not None - else sum(desc_config["avail_mems"]) - ) - max_chunk_size = round( - (mem_avail / estimated_memory_usage - 0.22) / 0.85 * self.dim_x - ) + if self._deriv_mode == "batched": + estimated_memory_usage = 2.4e-7 * self.dim_f * self.dim_x + 1 # in GB + mem_avail = desc_config["avail_mems"][0] # in GB + max_chunk_size = round( + (mem_avail / estimated_memory_usage - 0.22) / 0.85 * self.dim_x + ) self._jac_chunk_size = max([1, max_chunk_size]) if self._deriv_mode == "blocked": for obj in self.objectives: if obj._jac_chunk_size is None: - obj._jac_chunk_size = self._jac_chunk_size + estimated_memory_usage = ( + 2.4e-7 * obj.dim_f * obj.dim_x + 1 + ) # in GB + mem_avail = desc_config["avail_mems"][obj._device_id] # in GB + max_chunk_size = round( + (mem_avail / estimated_memory_usage - 0.22) + / 0.85 + * obj.dim_x + ) + obj._jac_chunk_size = max([1, max_chunk_size]) if not use_jit_wrapper: self._unjit() From 7e09142acde83e6e57d35b3cc72c959edcb9aae9 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 12 Feb 2025 18:43:51 -0500 Subject: [PATCH 062/199] nuch of clean up --- desc/__init__.py | 10 ++++------ desc/backend.py | 30 +++++++++++++++++------------- desc/objectives/getters.py | 10 ++++------ desc/objectives/objective_funs.py | 26 +++++++++++++------------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index d731607979..c68e0ce56f 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -86,15 +86,15 @@ def set_device(kind="cpu", gpuid=None, num_device=1): """ config["kind"] = kind + config["num_device"] = num_device if kind == "cpu": os.environ["JAX_PLATFORMS"] = "cpu" os.environ["CUDA_VISIBLE_DEVICES"] = "" import psutil cpu_mem = psutil.virtual_memory().available / 1024**3 # RAM in GB - config["device"] = "CPU" - config["avail_mem"] = cpu_mem - config["num_device"] = 1 + config["devices"] = ["CPU"] + config["avail_mems"] = [cpu_mem] elif kind == "gpu": os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" @@ -157,7 +157,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): ) if gpuid is not None: # TODO: implement multiple GPU selection - raise ValueError("Cannot specify 'gpuid' when requesting multiple GPUs") + raise ValueError("Cannot specify `gpuid` when requesting multiple GPUs") config["avail_mems"] = [ memories[dev["index"]] / 1024 for dev in devices[:num_device] @@ -168,5 +168,3 @@ def set_device(kind="cpu", gpuid=None, num_device=1): os.environ["CUDA_VISIBLE_DEVICES"] = ",".join( str(dev["index"]) for dev in devices[:num_device] ) - config["device_type"] = "gpu" - config["num_device"] = num_device diff --git a/desc/backend.py b/desc/backend.py index b1063d9d6c..ef0d3c292d 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -69,19 +69,12 @@ ) ) -if desc_config["num_device"] == 1: +print(f"Using {desc_config['num_device']} device:") +for i, dev in enumerate(desc_config["devices"]): print( - "Using device: {}, with {:.2f} GB available memory".format( - desc_config.get("device"), desc_config.get("avail_mem") - ) + f"\t Device {i}: {dev} with {desc_config['avail_mems'][i]:.2f} " + "GB available memory" ) -else: - print(f"Using {desc_config['num_device']} devices:") - for i, dev in enumerate(desc_config["devices"]): - print( - f"\t Device {i}: {dev} with {desc_config['avail_mems'][i]:.2f} " - "GB available memory" - ) if use_jax: # noqa: C901 from jax import custom_jvp, jit, vmap @@ -443,7 +436,7 @@ def tangent_solve(g, y): x = jax.lax.custom_root(res, x0, solve, tangent_solve, has_aux=False) return x - def pconcat(arrays, mode="concat"): + def pconcat(arrays, mode="concat"): # pragma: no cover """Concatenate arrays that live on different devices. Parameters @@ -502,7 +495,7 @@ def jit_with_device(method): @functools.wraps(method) def wrapper(self, *args, **kwargs): # Get the device using self.id or default to CPU - if desc_config["device"] == "gpu" and hasattr(self, "_device_id"): + if desc_config["kind"] == "gpu" and hasattr(self, "_device_id"): device = jax.devices("gpu")[self._device_id] else: device = None @@ -518,6 +511,7 @@ def wrapper(self, *args, **kwargs): # for coverage purposes else: # pragma: no cover jit = lambda func, *args, **kwargs: func + jit_with_device = jit execute_on_cpu = lambda func: func import scipy.optimize from numpy.fft import ifft, irfft, irfft2, rfft, rfft2 # noqa: F401 @@ -970,3 +964,13 @@ def take( else: out = np.take(a, indices, axis, out, mode) return out + + def pconcat(arrays, mode="concat"): + """Numpy implementation of desc.backend.pconcat.""" + if mode == "concat": + out = np.concatenate(arrays) + elif mode == "hstack": + out = np.hstack(arrays) + elif mode == "vstack": + out = np.vstack(arrays) + return out diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index ff664816cf..662283dbea 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -348,7 +348,7 @@ def add_if_multiple(constraints, cls): def get_parallel_forcebalance( eq, num_device, grid=None, use_jit=True, check_device=True -): +): # pragma: no cover """Get an ObjectiveFunction for parallel computing ForceBalance. Parameters @@ -361,15 +361,13 @@ def get_parallel_forcebalance( Returns ------- obj : ObjectiveFunction - An objective function with force balance objectives. Each objective is - computed on a separate device. The objective function is built with - `use_jit_wrapper=False` to make it compatible with JAX parallel computing. - Each objective will have a grid with same number of flux surfaces. + A built objective function with force balance objectives. Each objective is + computed on a separate device. """ from desc.backend import desc_config, jax, jnp from desc.grid import LinearGrid - if desc_config["device"] != "gpu": + if desc_config["kind"] != "gpu": raise ValueError( "Parallel computing is only supported on GPU. " "Please use DESC with GPU device." diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 192840e5e6..263dd6698a 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -297,24 +297,24 @@ def _unjit(self): pass @execute_on_cpu - def build(self, use_jit=None, use_jit_wrapper=True, verbose=1): # noqa: C901 + def build(self, use_jit=None, verbose=1): # noqa: C901 """Build the objective. Parameters ---------- use_jit : bool, optional - Whether to just-in-time compile the objective and derivatives. - use_jit_wrapper : bool, optional - Whether to use the jit wrapper for the objective. If multiple GPUs are - used, this will be set to False. + Whether to just-in-time compile the objective and derivatives. If using + multiple GPUs, instead of jitting the ObjectiveFunction, the sub-objectives + will be jitted individually, independent of the value of `use_jit`. verbose : int, optional Level of output. """ if use_jit is not None: self._use_jit = use_jit - if use_jit is False: - use_jit_wrapper = False + # use_jit_wrapper is used to determine if we jit the ObjectiveFunction + # methods. If we are using multiple GPUs, we don't want to jit them. + use_jit_wrapper = use_jit if self._is_multi_device: use_jit_wrapper = False @@ -472,7 +472,7 @@ def compute_unscaled(self, x, constants=None): for par, obj, const in zip(params, self.objectives, constants) ] ) - else: + else: # pragma: no cover f = pconcat( [ obj.compute_unscaled( @@ -512,7 +512,7 @@ def compute_scaled(self, x, constants=None): for par, obj, const in zip(params, self.objectives, constants) ] ) - else: + else: # pragma: no cover f = pconcat( [ obj.compute_scaled( @@ -552,7 +552,7 @@ def compute_scaled_error(self, x, constants=None): for par, obj, const in zip(params, self.objectives, constants) ] ) - else: + else: # pragma: no cover f = pconcat( [ obj.compute_scaled_error( @@ -626,11 +626,11 @@ def print_value(self, x, x0=None, constants=None): for par, par0, obj, const in zip( params, params0, self.objectives, constants ): - if self._is_multi_device: + if self._is_multi_device: # pragma: no cover par = jax.device_put(par, jax.devices("gpu")[obj._device_id]) par0 = jax.device_put(par0, jax.devices("gpu")[obj._device_id]) obj.print_value(par, par0, constants=const) - else: + else: # pragma: no cover for par, obj, const in zip(params, self.objectives, constants): if self._is_multi_device: par = jax.device_put(par, jax.devices("gpu")[obj._device_id]) @@ -763,7 +763,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): thing_idx = self._things_per_objective_idx[k] xi = [xs[i] for i in thing_idx] vi = [vs[i] for i in thing_idx] - if self._is_multi_device: + if self._is_multi_device: # pragma: no cover # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective xi = jax.device_put(xi, jax.devices("gpu")[obj._device_id]) From c32d7b4989afdb92857257a39708ddef89ec46ec Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 12 Feb 2025 23:28:46 -0500 Subject: [PATCH 063/199] clean up, fix issues --- desc/__init__.py | 34 +++++++++++++++++++++++++++---- desc/backend.py | 17 ++++++++++------ desc/objectives/objective_funs.py | 16 +++++++-------- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index c68e0ce56f..7a69d4661f 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -2,10 +2,13 @@ import importlib import os +import platform import re +import subprocess import warnings import colorama +import psutil from termcolor import colored from ._version import get_versions @@ -61,6 +64,23 @@ def __getattr__(name): config = {"device": None, "avail_mem": None, "kind": None, "num_device": None} +def _get_processor_name(): + """Get the processor name of the current system.""" + if platform.system() == "Windows": + return platform.processor() + elif platform.system() == "Darwin": + os.environ["PATH"] = os.environ["PATH"] + os.pathsep + "/usr/sbin" + command = "sysctl -n machdep.cpu.brand_string" + return subprocess.check_output(command).strip() + elif platform.system() == "Linux": + command = "cat /proc/cpuinfo" + all_info = subprocess.check_output(command, shell=True).decode().strip() + for line in all_info.split("\n"): + if "model name" in line: + return re.sub(".*model name.*:", "", line, 1) + return "" + + def set_device(kind="cpu", gpuid=None, num_device=1): """Sets the device to use for computation. @@ -85,15 +105,21 @@ def set_device(kind="cpu", gpuid=None, num_device=1): number of devices to use. Default is 1. """ + if kind == "cpu" and num_device > 1: + # TODO: implement multi-CPU support + raise ValueError("Cannot request multiple CPUs") + config["kind"] = kind config["num_device"] = num_device + + cpu_mem = psutil.virtual_memory().available / 1024**3 # RAM in GB + cpu_info = _get_processor_name() + config["cpu_info"] = f"{cpu_info} CPU" + config["cpu_mem"] = cpu_mem if kind == "cpu": os.environ["JAX_PLATFORMS"] = "cpu" os.environ["CUDA_VISIBLE_DEVICES"] = "" - import psutil - - cpu_mem = psutil.virtual_memory().available / 1024**3 # RAM in GB - config["devices"] = ["CPU"] + config["devices"] = [f"{cpu_info} CPU"] config["avail_mems"] = [cpu_mem] elif kind == "gpu": diff --git a/desc/backend.py b/desc/backend.py index ef0d3c292d..003a070921 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -69,12 +69,17 @@ ) ) -print(f"Using {desc_config['num_device']} device:") -for i, dev in enumerate(desc_config["devices"]): - print( - f"\t Device {i}: {dev} with {desc_config['avail_mems'][i]:.2f} " - "GB available memory" - ) +print( + f"CPU Info: {desc_config['cpu_info']} with {desc_config['cpu_mem']:.2f} " + "GB available memory" +) +if desc_config["kind"] == "gpu": + print(f"Using {desc_config['num_device']} device:") + for i, dev in enumerate(desc_config["devices"]): + print( + f"\t Device {i}: {dev} with {desc_config['avail_mems'][i]:.2f} " + "GB available memory" + ) if use_jax: # noqa: C901 from jax import custom_jvp, jit, vmap diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 263dd6698a..6fd5dac066 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -310,6 +310,7 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 Level of output. """ + use_jit_wrapper = True if use_jit is not None: self._use_jit = use_jit # use_jit_wrapper is used to determine if we jit the ObjectiveFunction @@ -373,24 +374,23 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 if self._jac_chunk_size == "auto": # Heuristic estimates of fwd mode Jacobian memory usage, # slightly conservative, based on using ForceBalance as the objective - if self._deriv_mode == "batched": - estimated_memory_usage = 2.4e-7 * self.dim_f * self.dim_x + 1 # in GB - mem_avail = desc_config["avail_mems"][0] # in GB - max_chunk_size = round( - (mem_avail / estimated_memory_usage - 0.22) / 0.85 * self.dim_x - ) + estimated_memory_usage = 2.4e-7 * self.dim_f * self.dim_x + 1 # in GB + mem_avail = desc_config["avail_mems"][0] # in GB + max_chunk_size = round( + (mem_avail / estimated_memory_usage - 0.22) / 0.85 * self.dim_x + ) self._jac_chunk_size = max([1, max_chunk_size]) if self._deriv_mode == "blocked": for obj in self.objectives: if obj._jac_chunk_size is None: estimated_memory_usage = ( - 2.4e-7 * obj.dim_f * obj.dim_x + 1 + 2.4e-7 * obj.dim_f * obj.things[0].dim_x + 1 ) # in GB mem_avail = desc_config["avail_mems"][obj._device_id] # in GB max_chunk_size = round( (mem_avail / estimated_memory_usage - 0.22) / 0.85 - * obj.dim_x + * obj.things[0].dim_x ) obj._jac_chunk_size = max([1, max_chunk_size]) From e2c0f7767909db35d99aae158d2c598faf424588 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 12 Feb 2025 23:51:21 -0500 Subject: [PATCH 064/199] fix set_device config['device'] problem --- desc/__init__.py | 3 ++- desc/backend.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 7a69d4661f..4aa8da27e6 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -61,7 +61,7 @@ def __getattr__(name): BANNER = colored(_BANNER, "magenta") -config = {"device": None, "avail_mem": None, "kind": None, "num_device": None} +config = {"devices": None, "avail_mem": None, "kind": None, "num_device": None} def _get_processor_name(): @@ -116,6 +116,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): cpu_info = _get_processor_name() config["cpu_info"] = f"{cpu_info} CPU" config["cpu_mem"] = cpu_mem + if kind == "cpu": os.environ["JAX_PLATFORMS"] = "cpu" os.environ["CUDA_VISIBLE_DEVICES"] = "" diff --git a/desc/backend.py b/desc/backend.py index 003a070921..e5bb0227f5 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -21,7 +21,7 @@ ) ) else: - if desc_config.get("device") is None: + if desc_config.get("devices") is None: set_device("cpu") try: with warnings.catch_warnings(): From 20526338e69ba1dfe8290eb61359362c9fca6b7a Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 00:36:39 -0500 Subject: [PATCH 065/199] update notebook and add device_id to all objectives --- desc/objectives/_bootstrap.py | 2 + desc/objectives/_coils.py | 13 + desc/objectives/_equilibrium.py | 10 + desc/objectives/_fast_ion.py | 1 + desc/objectives/_free_boundary.py | 4 + desc/objectives/_generic.py | 2 + desc/objectives/_geometry.py | 18 ++ desc/objectives/_neoclassical.py | 1 + desc/objectives/_omnigenity.py | 10 + desc/objectives/_power_balance.py | 4 + desc/objectives/_profiles.py | 8 + desc/objectives/_stability.py | 4 + desc/objectives/objective_funs.py | 7 + docs/notebooks/tutorials/multi_device.ipynb | 300 ++++++++++++++++---- 14 files changed, 325 insertions(+), 59 deletions(-) diff --git a/desc/objectives/_bootstrap.py b/desc/objectives/_bootstrap.py index 9f6850cb24..ca3fbe998f 100644 --- a/desc/objectives/_bootstrap.py +++ b/desc/objectives/_bootstrap.py @@ -64,6 +64,7 @@ def __init__( helicity=(1, 0), name="Bootstrap current self-consistency (Redl)", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -82,6 +83,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index 1a8051a529..8da36de2b9 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -49,6 +49,7 @@ def __init__( grid=None, name=None, jac_chunk_size=None, + device_id=0, ): self._grid = grid self._data_keys = data_keys @@ -64,6 +65,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): # noqa:C901 @@ -226,6 +228,7 @@ def __init__( grid=None, name="coil length", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 2 * np.pi @@ -330,6 +333,7 @@ def __init__( grid=None, name="coil curvature", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: bounds = (0, 1) @@ -429,6 +433,7 @@ def __init__( grid=None, name="coil torsion", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -528,6 +533,7 @@ def __init__( grid=None, name="coil current length", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -636,6 +642,7 @@ def __init__( grid=None, name="coil integrated curvature", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 2 * np.pi @@ -784,6 +791,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -965,6 +973,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -1288,6 +1297,7 @@ def __init__( vacuum=False, name="Quadratic flux", jac_chunk_size=None, + device_id=0, ): from desc.geometry import FourierRZToroidalSurface @@ -1491,6 +1501,7 @@ def __init__( name="Surface Quadratic Flux", field_fixed=False, jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -1700,6 +1711,7 @@ def __init__( field_fixed=False, eq_fixed=False, jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 1.0 if not hasattr(eq, "Psi") else eq.Psi @@ -1730,6 +1742,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_equilibrium.py b/desc/objectives/_equilibrium.py index 5f1b27b149..cd2036ca72 100644 --- a/desc/objectives/_equilibrium.py +++ b/desc/objectives/_equilibrium.py @@ -218,6 +218,7 @@ def __init__( grid=None, name="force-anisotropic", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -233,6 +234,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -355,6 +357,7 @@ def __init__( grid=None, name="radial force", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -370,6 +373,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -492,6 +496,7 @@ def __init__( grid=None, name="helical force", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -507,6 +512,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -630,6 +636,7 @@ def __init__( gamma=0, name="energy", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -646,6 +653,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -773,6 +781,7 @@ def __init__( grid=None, name="current density", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -788,6 +797,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_fast_ion.py b/desc/objectives/_fast_ion.py index 3dfff7aebf..9f13b9ffcb 100644 --- a/desc/objectives/_fast_ion.py +++ b/desc/objectives/_fast_ion.py @@ -207,6 +207,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_free_boundary.py b/desc/objectives/_free_boundary.py index d83895a092..1afafed87e 100644 --- a/desc/objectives/_free_boundary.py +++ b/desc/objectives/_free_boundary.py @@ -106,6 +106,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -459,6 +460,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -867,6 +869,7 @@ def __init__( deriv_mode="auto", name="NESTOR Boundary", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -887,6 +890,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_generic.py b/desc/objectives/_generic.py index 6889d2897f..58b08db31b 100644 --- a/desc/objectives/_generic.py +++ b/desc/objectives/_generic.py @@ -80,6 +80,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) self._p = _parse_parameterization(thing) self._scalar = not bool(data_index[self._p][self.f]["dim"]) @@ -197,6 +198,7 @@ def __init__( normalize_target=False, name="custom linear", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index c7393d9bf6..36839bee51 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -54,6 +54,7 @@ def __init__( grid=None, name="aspect ratio", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 2 @@ -69,6 +70,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -197,6 +199,7 @@ def __init__( grid=None, name="elongation", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 1 @@ -212,6 +215,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -338,6 +342,7 @@ def __init__( grid=None, name="volume", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 1 @@ -353,6 +358,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -525,6 +531,7 @@ def __init__( name="plasma-vessel distance", use_signed_distance=False, jac_chunk_size=None, + device_id=0, **kwargs, ): if target is None and bounds is None: @@ -565,6 +572,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -852,6 +860,7 @@ def __init__( grid=None, name="mean curvature", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: bounds = (-np.inf, 0) @@ -867,6 +876,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -992,6 +1002,7 @@ def __init__( grid=None, name="principal-curvature", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 1 @@ -1007,6 +1018,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -1127,6 +1139,7 @@ def __init__( grid=None, name="B-scale-length", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: bounds = (1, np.inf) @@ -1142,6 +1155,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -1259,6 +1273,7 @@ def __init__( grid=None, name="coordinate goodness", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -1275,6 +1290,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -1398,6 +1414,7 @@ def __init__( deriv_mode="auto", name="mirror ratio", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0.2 @@ -1413,6 +1430,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index ad571c24b9..d02ffed2cc 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -193,6 +193,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_omnigenity.py b/desc/objectives/_omnigenity.py index 2efca8e060..c897aff737 100644 --- a/desc/objectives/_omnigenity.py +++ b/desc/objectives/_omnigenity.py @@ -57,6 +57,7 @@ def __init__( N_booz=None, name="QS Boozer", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -75,6 +76,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) self._print_value_fmt = "Quasi-symmetry ({},{}) Boozer error: ".format( @@ -247,6 +249,7 @@ def __init__( helicity=(1, 0), name="QS two-term", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -263,6 +266,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) self._print_value_fmt = "Quasi-symmetry ({},{}) two-term error: ".format( @@ -410,6 +414,7 @@ def __init__( grid=None, name="QS triple product", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -425,6 +430,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -573,6 +579,7 @@ def __init__( field_fixed=False, name="omnigenity", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -605,6 +612,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -887,6 +895,7 @@ def __init__( grid=None, name="Isodynamicity", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -902,6 +911,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_power_balance.py b/desc/objectives/_power_balance.py index bc291cb8ae..2f8594e5ec 100644 --- a/desc/objectives/_power_balance.py +++ b/desc/objectives/_power_balance.py @@ -56,6 +56,7 @@ def __init__( grid=None, name="fusion power", jac_chunk_size=None, + device_id=0, ): errorif( fuel not in ["DT"], ValueError, f"fuel must be one of ['DT'], got {fuel}." @@ -75,6 +76,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -223,6 +225,7 @@ def __init__( grid=None, name="heating power", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -240,6 +243,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_profiles.py b/desc/objectives/_profiles.py index 0740c6142d..29756b6454 100644 --- a/desc/objectives/_profiles.py +++ b/desc/objectives/_profiles.py @@ -62,6 +62,7 @@ def __init__( grid=None, name="pressure", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -77,6 +78,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -197,6 +199,7 @@ def __init__( grid=None, name="rotational transform", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -212,6 +215,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -345,6 +349,7 @@ def __init__( grid=None, name="shear", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: bounds = (-np.inf, 0) @@ -360,6 +365,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -485,6 +491,7 @@ def __init__( grid=None, name="toroidal current", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: target = 0 @@ -500,6 +507,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_stability.py b/desc/objectives/_stability.py index 3ef4debc82..e0b441b33d 100644 --- a/desc/objectives/_stability.py +++ b/desc/objectives/_stability.py @@ -73,6 +73,7 @@ def __init__( grid=None, name="Mercier Stability", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: bounds = (0, np.inf) @@ -88,6 +89,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -231,6 +233,7 @@ def __init__( grid=None, name="Magnetic Well", jac_chunk_size=None, + device_id=0, ): if target is None and bounds is None: bounds = (0, np.inf) @@ -246,6 +249,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 6fd5dac066..e5ea0c7785 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -96,6 +96,12 @@ option will yield a larger chunk size than may be needed. It is recommended to manually choose a chunk_size if an OOM error is experienced in this case. """ +doc_device_id = """ + device_id : int, optional + Device ID to run the objective on. Defaults to 0. If different objectives + are on different devices, the ObjectiveFunction will run each sub-objective + on the device specified in the sub-objective. +""" docs = { "target": doc_target, "bounds": doc_bounds, @@ -106,6 +112,7 @@ "deriv_mode": doc_deriv_mode, "name": doc_name, "jac_chunk_size": doc_jac_chunk_size, + "device_id": doc_device_id, } diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index c44bc260af..b0b5e2f40d 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -26,14 +26,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "num_device = 2\n", - "# from desc import set_device\n", + "num_device = 3\n", + "from desc import set_device\n", "\n", - "# set_device(\"gpu\", num_device=num_device)" + "set_device(\"gpu\", num_device=num_device)" ] }, { @@ -45,10 +45,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version 0.13.0+1130.gc8481e158.dirty,using JAX backend, jax version=0.4.38, jaxlib version=0.4.38, dtype=float64\n", - "Using 2 devices:\n", - "\t Device 0: NVIDIA A100-PCIE-40GB (id=0) with 40.00 GB available memory\n", - "\t Device 1: NVIDIA A100-PCIE-40GB (id=1) with 40.00 GB available memory\n" + "DESC version 0.13.0+1523.ge2c0f7767.dirty,using JAX backend, jax version=0.4.38, jaxlib version=0.4.38, dtype=float64\n", + "CPU Info: AMD EPYC 7453 28-Core Processor CPU with 978.07 GB available memory\n", + "Using 3 device:\n", + "\t Device 0: NVIDIA A100-SXM4-40GB (id=0) with 40.00 GB available memory\n", + "\t Device 1: NVIDIA A100-SXM4-40GB (id=1) with 40.00 GB available memory\n", + "\t Device 2: NVIDIA A100-SXM4-40GB (id=2) with 40.00 GB available memory\n" ] } ], @@ -79,6 +81,7 @@ "name": "stdout", "output_type": "stream", "text": [ + "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n" ] @@ -92,25 +95,6 @@ { "cell_type": "code", "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(34632,)\n", - "(34632, 1977)\n" - ] - } - ], - "source": [ - "print(obj.compute_scaled_error(obj.x()).shape)\n", - "print(obj.jac_scaled_error(obj.x()).shape)" - ] - }, - { - "cell_type": "code", - "execution_count": null, "metadata": { "editable": true, "slideshow": { @@ -134,41 +118,52 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.99 sec\n", - "Timer: Linear constraint projection build = 5.61 sec\n", + "Timer: Objective build = 1.74 sec\n", + "Timer: Linear constraint projection build = 7.48 sec\n", "Number of parameters: 1593\n", "Number of objectives: 34632\n", - "Timer: Initializing the optimization = 7.73 sec\n", + "Timer: Initializing the optimization = 9.34 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", + "This should run on GPU id:0\n", + "This should run on GPU id:1\n", + "This should run on GPU id:2\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 3.654e-07 1.803e-04 \n", - " 1 6 2.102e-07 1.552e-07 1.244e-03 5.327e-05 \n", - " 2 7 2.097e-07 5.059e-10 1.883e-03 6.156e-05 \n", + " 0 1 5.161e-07 1.557e-04 \n", + "This should run on GPU id:0\n", + "This should run on GPU id:1\n", + "This should run on GPU id:2\n", + " 1 5 2.629e-07 2.531e-07 3.371e-03 1.961e-04 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 2.097e-07\n", - " Total delta_x: 2.644e-03\n", - " Iterations: 2\n", - " Function evaluations: 7\n", - " Jacobian evaluations: 3\n", - "Timer: Solution time = 23.4 sec\n", - "Timer: Avg time per step = 7.83 sec\n", + " Current function value: 2.629e-07\n", + " Total delta_x: 3.371e-03\n", + " Iterations: 1\n", + " Function evaluations: 5\n", + " Jacobian evaluations: 2\n", + "Timer: Solution time = 28.5 sec\n", + "Timer: Avg time per step = 14.2 sec\n", "==============================================================================================================\n", " Start --> End\n", - "Total (sum of squares): 3.654e-07 --> 2.097e-07, \n", - "Maximum absolute Force error: 1.378e+02 --> 2.537e+02 (N)\n", + "Total (sum of squares): 5.161e-07 --> 2.629e-07, \n", + "Maximum absolute Force error: 1.378e+02 --> 2.419e+02 (N)\n", "Minimum absolute Force error: 1.059e-10 --> 1.060e-10 (N)\n", - "Average absolute Force error: 2.610e+01 --> 1.938e+01 (N)\n", - "Maximum absolute Force error: 1.108e-05 --> 2.040e-05 (normalized)\n", - "Minimum absolute Force error: 8.517e-18 --> 8.529e-18 (normalized)\n", - "Average absolute Force error: 2.099e-06 --> 1.558e-06 (normalized)\n", - "Maximum absolute Force error: 8.201e+03 --> 6.247e+03 (N)\n", - "Minimum absolute Force error: 1.635e-12 --> 2.050e-12 (N)\n", - "Average absolute Force error: 8.007e+01 --> 7.093e+01 (N)\n", - "Maximum absolute Force error: 6.596e-04 --> 5.024e-04 (normalized)\n", - "Minimum absolute Force error: 1.315e-19 --> 1.649e-19 (normalized)\n", - "Average absolute Force error: 6.440e-06 --> 5.705e-06 (normalized)\n", + "Average absolute Force error: 2.932e+01 --> 1.966e+01 (N)\n", + "Maximum absolute Force error: 1.108e-05 --> 1.946e-05 (normalized)\n", + "Minimum absolute Force error: 8.517e-18 --> 8.524e-18 (normalized)\n", + "Average absolute Force error: 2.358e-06 --> 1.581e-06 (normalized)\n", + "Maximum absolute Force error: 2.276e+02 --> 3.279e+02 (N)\n", + "Minimum absolute Force error: 1.271e-10 --> 1.282e-10 (N)\n", + "Average absolute Force error: 3.359e+01 --> 2.870e+01 (N)\n", + "Maximum absolute Force error: 1.831e-05 --> 2.637e-05 (normalized)\n", + "Minimum absolute Force error: 1.022e-17 --> 1.031e-17 (normalized)\n", + "Average absolute Force error: 2.702e-06 --> 2.308e-06 (normalized)\n", + "Maximum absolute Force error: 8.201e+03 --> 6.277e+03 (N)\n", + "Minimum absolute Force error: 1.635e-12 --> 3.597e-12 (N)\n", + "Average absolute Force error: 8.964e+01 --> 7.837e+01 (N)\n", + "Maximum absolute Force error: 6.596e-04 --> 5.048e-04 (normalized)\n", + "Minimum absolute Force error: 1.315e-19 --> 2.893e-19 (normalized)\n", + "Average absolute Force error: 7.209e-06 --> 6.303e-06 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", @@ -180,17 +175,17 @@ } ], "source": [ - "eq.solve(objective=obj, constraints=cons, maxiter=2, ftol=0, gtol=0, xtol=0, verbose=3)" + "eq.solve(objective=obj, constraints=cons, maxiter=1, ftol=0, gtol=0, xtol=0, verbose=3);" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -200,7 +195,17 @@ }, { "data": { - "image/png": "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", + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAQgAAAEYCAYAAACgIGhkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAALEwAACxMBAJqcGAAAZ5BJREFUeJztnXl8U2X2/z9Juu97032jlJbu+wpVNgUFAdfhq6IO4Ki4jSK4IM6oKIz+YBQVcRlcxmEUFAX3pXTf6UJpS+m+70mbpG22+/ujk5iGJL1J7m1TvO/Xy5elzT33SXLv557nnPOch0UQBAEGBgYGDbDnewAMDAymCyMQDAwMWmEEgoGBQSuMQDAwMGiFEQgGBgatMALBwMCgFUYgGBgYtMIIBAMDg1YYgTAB3nnnHbi4uODNN9/E4OCgQTbkcjkee+yxWX83G2+99Rbs7e2vGMett96Ku+++GxcvXjRqXDKZDJ988glOnjyJI0eOYCHW6f31r3/FM888AwCQSCS4/fbb53lE9GEyAlFaWors7Gykp6ejuLh4xt8SExMhk8loH0Nvby/++te/4u9//zv27duHO+64A3v27NF5jKax5efnIz4+Hjk5OaTOm5CQgOzsbDz00ENwd3fXe9wjIyM4dOgQzp07p/N3ZEhKSsL111+P9vZ25e8qKysxPj6OZ599FhEREUaN6/vvv0dkZCQ2b94MLpeL8+fP6zU+derr6/Hyyy9r/TcdhIeHIz4+HgBgbm6Ozz77jNbzzScmIxDJyclKgUhNTZ3xt7KyMnA4HFrPPzk5ifXr1+PRRx/Fc889h3379uH48eNoamrSeZymsWVmZiI6Opr0uUtLS5GcnGzQuAHAxcUFjz/+OBwcHHT+jgzt7e3IyspCR0cHAIAgCAgEAgwMDCA0NNTocdnb2+P555+HQCBAT08PgoKC9LKpzm+//YbY2Fit/6aDoqIipKWlKf/NYrFoPd98YjbfA5iNr7/+Gg8//DBycnLw22+/Yc+ePdixYwfa29vR0tKCM2fOKC/AvXv3QiqVgsPhwN7eHrt27YJAIMBtt92GZcuWobGxEX/605+wcuVKfPjhh9izZw/uv/9+NDc3o7i4GLGxsfDz81Oe28LCAl988QUAXPH6xsZGPPvss8qxBQYG4uGHH4ZEIkFwcDC6urpIv8fS0lLcfffd1H5wRuDn54e2tjYA0zdDUFAQPDw8KLG9bNkyfPjhh1i6dCmef/55ODs7X/GawsJCnD59GlFRUbC2tsbw8DC2b9+Ozz77DGKxGF1dXXB3d4efnx/ee+897NixA729vaiqqprxby8vL3z33Xeor6+HhYUFYmJicObMGfB4PPB4PDz44INgs9n4/PPPsXz5cgBAXV0dnnvuOeVYhoeHsXbtWpSUlCh/193dDW9vbzQ3N+PMmTPw9vaGl5eXVjuqY9i8eTO8vLyueC/h4eFXjG3ZsmWUfObGYPICsX79erz++usAgHvuuQfHjx9Heno6XnjhBTz44IP46aefsHnzZvzwww8oLi7Gjz/+CADIzs7G6tWrsXjxYjz22GNYuXIlRkZGsGbNGqxcuVJpKzk5Gfv27cPBgweVT01g+kn62WefoaCgAG+88cYVry8vL0diYqJybGfPnkVTUxO+++47AMDp06dJv8eKigq88cYbyn/L5XKw2dPOXV1dHX766SeNx919990abzBj8fPzQ15eHsRiMVgsFqqqqmZ4OMaMqbe3F+np6cjMzMTevXuxatWqGaIMTHstUqkUS5YsQWJiIq655hosX74c33//PY4fP45XXnkFwcHBWLlyJY4cOYIdO3YAALy8vGb8u729HS+99BLy8vLw66+/wsnJCXZ2dkp71tbWyM/PBwD4+PggJSXliu/N1dVV+ZAAAB6PBycnJwBAX18fXF1dMTU1pfy7uh31MQgEAjQ2Nl7xXtzd3a8Ymylg8gKhicWLFwMA3N3dMT4+DgCoqamBSCTCK6+8AmD6Ih8cHERoaChycnJQVFQEc3PzK4Jv4eHhAICQkJAZT4mAgADs3r0bgYGBEAgEV7w+MTFxhp26uroZLnhwcDCp9zI2NgYASi9oamoKv/76K66//noAwNKlS7F06VJStoyFz+fDyckJfn5+6OjoQHFxMbKysrB3794Z0z5jxnTs2DHs2bMH5ubmCAoKwokTJ/DEE0/MeE1GRgb+/ve/Iz4+HiMjIxCLxfjkk09w4403ApiOiTz00EPo6+sDl8tVHqf+76+++gqLFi3CmTNnYGtri6ioKDz77LPYtWsXLC0tAUxPB/fv34+UlBTw+XyNN6aqgBUXFyMlJUU5zkOHDuH999+Hg4ODRjvqYwgNDcVzzz13xXuxs7NDeXn5jLGZAiYtELW1tRgYGLji95rmfDExMSgqKsLu3bsBAL/++isWLVqE9957Dz09Pfjggw8gFovxzjvvaLS1bt06vPTSS2hra0NgYCCA6SeZegBS23wzIiICv/76q/LfLS0tpN5jWVnZDLH55JNPsHHjRuW/dT2t77rrLri4uJA6DxnKy8uRkJAAZ2dntLS0wM7ODiwWC6WlpXj44YcpGRNBEBCLxTA3N0d0dDT6+/tBEATa29uVn7viicxms/H111/jzjvvxIULF7BkyRKIxWKMjY2hsrISPB4PycnJKC0txdKlS5WxHMW/rayssGHDBtx4440YGxtDc3MzpqamZtyAIpFIeTN/++23WLt2LfLy8pCVlaV8TU9PD7y9vQEAJSUlWLFiBXJychAfHw8Wi4Xa2lrExcVptKM+htbWVoyOjl7xXjIzM68YmylgMgJRXl6O3NxciMVivPjiiwCA1tZWxMfHo729He+88w5WrFiB9vZ2fPDBB9i6dStyc3NRW1uLdevWYfXq1SgtLcWePXtgb2+P0dFRvPLKK1izZg2++OILPPnkk3BxcQGfz8fJkyfh4OCA9vZ2vPnmm3jqqafg7u6OM2fO4NVXX4WzszNkMhkuX76MrVu3IigoCD/99NMVrz979qxybPv378d3332HP//5z/Dz8wNBEPj444+RkJAAe3t7REdH49///jciIyOV77m0tBSHDh2Cubk5jh07hrq6OjQ2NuK+++5TvobM01ogEODYsWOor6/H66+/ju3btwPAFb+zs7PTOA5gOvOye/du7Ny5E3fddRcyMjIQHx+Pt956C+fPn0deXh42bdpEekzaxvXwww/jyJEj8PLyAovFwpYtW9DT04OVK1fi8uXLymvBwsICp0+fRk9PD/bs2YOysjL8+OOPuHjxIhYvXoy+vj4EBwejoqICISEhsLW1hbe394x/33bbbTh8+DDMzMzA5/ORlpaGhISEGWOsq6tTzvXt7OzQ1tY2473xeDxs3rwZRUVFAKY9w8LCQtx3332QSqXw8PDA5OSkVjtRUVEzxnDHHXfgrrvuuuK9dHR0XDE2k4BgoJ2ioiLihhtumO9hmMw4NPHLL78ofz5w4MCMfzPMHybjQVzN+Pj44Msvv5zvYcw6jrKyMlRVVSEhIUGZ558rhEIhAKC5uRmffvopZVkTBuNgEcQCLGVjoIWysjK0tLTg1ltvndPcPkEQmJycNJnIPcPvMALBoGRychIsFsvkAmUM8wcjEAxK1L0G5tJgYGIQDEoYQWBQx2TWYjDMP3l5eXjvvffA4/HmeygMJgIjEAxKsrKyYGtrCysrq/keCoOJwAgEg5ITJ05gaGiI8SAYlDBBSgYGBq0wHgQDA4NWGIFgYGDQCiMQDAwMWmEEgoGBQSuMQDAwMGiFEQgGBgatMALBwMCgFUYgGBgYtMIs1rqKkcvlGBwcRG9vr/K/7u5udHV1oaenB3w+H1KpdMZ/BEGAzWbDzMwMZmZmEBMsiAk2Ary5WBzkD19fX2Wbd8V/Dg4OV/XeEH9kmErKBQ5BEOju7kZlZSVKS0tRVVWFnp4e9I4KwGIBlraOiAv1g4eHB7y8vODt7Q0/Pz/4+fnB1dUVFhYWsLS0hLm5OczMzFBdXY3o6GhIJBKIxWJ47/sekErgIB3Dmys90dXVhd7eXvT19WFgYACDg4MQCARgs9mws7NDUFAQkpKSkJiYiLi4ONjb28/3R8RgBIwHsYAgCAJdXV2oqKhAaWkpysrKlBuvREZGIiEhAXfccQdCQkLw12+bcLS4AztS/XFkUxTpc/D5fHA4HHA4HFhZWeHuzHAcLe7A3anR2DSLHR6PhwsXLqC4uBhHjx5FfX09JBLJ9FZ1iYlIS0lBXFyc3rt9McwfjECYMARBoL6+HqdPn8Yvv/yC7u5ueHh4ICoqCgkJCbjnnnsQEhKi3GRnvnFyckJmZiYyMzOVvxOLxaioqMB9h07gk+9fB7+7BY7mBMLDw3HDDTdg3bp1cHNzm8dRM+iCmWKYGBKJBPn5+fj0xBcozj8Hb29vXHfddVi/fj2Cg4NJi4HZrrOQyQlw2CxID6wjff6cnBxkZ2cbbUedB0/VKj2aw+vDUVJSgq+++gq//fYbLCwssGHDBmzYsAFhYWFMPMOEYDwIE4DH4+G7777Dl19+iXMlFXALCoc4MBFVJSWwtbU1yOaOVH/lDWkMVNhRFQfFdCcjIwMZGRkAgM7OTpw4cQIPPfQQ+vv7sWLFCmzcuBEZGRkwM2Mu0fmE8SDmCT6fj08//RQnTpwAn8/H8uXLsXnzZvxn0BHvlnTpHTugCnUPQtPNrS/6eCFCoRBfffUVvvnmG1RVVSE2NhZbt27FqlWraN/hneFKTGPy+geBIAgUFhbizjvvRHp6Ojo7O/Hee++hqqoKhw8fxrJly/DW5hjlU/vBU7UGn+vBU7Uw23XWKBsAcLS4AzI5gaPFHbO/WAs7Uv3BYbNIeSG2trbYsmUL/vOf/+DixYvYtm0bPv74Y0RFRWHfvn167ZrOYDyMQMwB4+PjeOONNxAbG4tXXnkFt9xyC2pra7F///4ZG/4qoOKmpMIGoN/NTTVsNhsrVqzAp59+iuLiYjg4OCB95VokZV2Ls2fPQi6Xz/mY/mgwAkEjLS0teOSRR5CcnIyenh788MMP+Prrr7F+/XqdwUYqbsr5vLHVoUKsHBwc8Pjjj+PGlz9FT+RmvHrkPcTGxuLIkSMzdl9noBZGIGigpKQEN9xwA/70pz8hKioK1dXV2L9//4yt6enmyKYoSqYqcz3F0IUiHnLTqmXI/fZL/Pjjj+jo6EBSUhIef/xx9Pf3G2Wf4UoYgaCQ+vp6pF27Bvc/+gR27dqF4uJi/PnPf4aFhYVedqiaHpjCzU1FkFOB+vvhcrl49dVXUV1djUWLFuHaa6/Fs88+i7GxMaPOw/A7jEBQQGdnJ7Zu3Yo1m25Hb9C1qM3ardwK3hCoeuKaws1NldgB2t+PhYUFHnjgAVRWVsLa2hrJycl4/fXXMTU1ZfQ5/+gwAmEEIyMj+Otf/4q1a9fi2muvxQ0vfoQu10ijb2yqpgfGYgoeCPB7RgYApAfWaRUrS0tLPPPMMygpKUFPTw/i4+Nx/PhxyGQyg8/9R4cRCAMQiUR46aWXkJGRAR8fH5w/fx533XUXZSlKwDQyGaYS6NT3fTg6OuIf//gHfv75Z+Tm5iIhIQFff/01s7WgATACoQcEQeDYsWOIj4/H5OQkKioq8Pjjj8+o9jOl9KIp3ODz6YV4eXnh/fffx+eff47jx48jMzMTFRUVBo/jjwhTx0qS9vZ23HPPPfD19UV+fr7WBUZUlTgr3GjFjWVIDMBYG6o3t6ExCGM/DyriIKGhoTh58iRKSkqw5Z4/I/uaa3H4wMuwtLQ0yN4fCcaDmAWCIPD222/j+uuvx6OPPoqPPvpI5+pDKuMH8z3NuNqCnCkpKWhevQ8F7Twsjk5gvAkSLCiBKCoqwhtvvAGJRDIn5+vo6MCqVatQWFiIoqIirF+/ntRxzDRjGlMJcgK/BzqXeDqifvEmrHzoBezYsQNPP/00xGKxUbZno6ysDMeOHUNlZSWt56GDBSUQiYmJsLS0RFVVFa0fOEEQOHr0KK677jrs3LkTH3/8MRwdHUkfr7iow91tjVoPMd/ZjPkOctJRQ1E/KIT0wDq8v3MziouLQRAEUlNTab95HRwcEBcXR+s56GBBCUReXh5YLBYmJydp+8A7OjqwevVq5Ofno6ioCBs2bNDbxpFNUZAeWIf6QeG8ZyLma4phatMLTe/FzMwM+/fvx7Fjx7B9+3Y888wztHgTUVFRuOmmmxZkn4sFJRDXXnsttm3bhqSkJFo+8A+PH0fK8hXwWrZZb69BE6YwRdB2PEEQkEgkEIlEEAgEGB8fx9jYGGQyGcbGxjA+Pg5rSOBqQYAN/RdFmdr0QpdYJSQkoLi4GDKZDKmpqWhoaDDqfOpYW1vDysoKLBZrwYnEgsxiWFlZUWpPJpNh165d+PpcKUY3vIR/j9viIwrszmcmQiwWg8/nw5s1jo0+coSxhlBWVobJyUll4ZC5uTnMzc3BYrHAZrPBYrEwNTWFpqYmEAQBzmgP7gsm4ChsQ07OqPIYKysr5X+Wlpaws7ODvb39jH4NppC9UEAmG2NmZoZXXnkFGzZswObNm3Hw4EGsXbvWqPMqWMj1FwuqYYwu9TX0bfD5fNx2220ICwuDJOsevFvajXB3W9QPCim5OKlo2TabDbFYDB6Pp/xPKBTC3Nwcjo6OePbnNgxPEeBL2Sh+7BpYWlrq7NKk2jBG/SZVeB2Tk5Mz/hMKhcr1D46OjnB0dMS754dwqKQf96YEGPQZUvG5Kcav7/fZ29uLTZs2YePGjXjyyScpeeofOnQIjz76qNF25poF5UEoRICqD/vSpUu45ZZb8PDDD+O+++4DALx1c6zy4jQm/6+AiroIdRsTExPo7+/HwMAAhEIhLCws4OjoCCcnJ3h7e8PW1lZ5Ufs2AWf/d6yh7esUsFgsWFhYwMLCQmNnaplMBj6fDz6fD8FQL/4WRYAtakF1tRyenp5wc3Mj1ULuwVO1kMkJsP733g1FPTBJFi8vL+Tk5ODee+/F//3f/+H99983ymvt6emZs8wb1SyoGARA3Yf9ww8/4KabbsKRI0eU4qCAytSgImAJwOCMxpsbI/FEqidGuttw/KvvUVlZCZlMhvDwcGRnZyMjIwORkZHw9fWFnZ3djCeeMZkQfeMIHA4HLi4uCAoKgtwtCHtqzNBmEwhfX18MDw8jPz8fJSUlaG9vx+TkpM7zAgCbzTJKoI35Hi0tLfHpp59i6dKluOaaa9DT02PwOAYGBlBfX4+2tjaDbcwXC04gjP2wCYLA66+/jr179+LHH3+c0aJdARU3tTr63mxyuRwDAwOorq5GTk4OpCM96BIR2FkiRUZGBkJCQmBvb0/K/TU0YGjoDaY6NXljUwxcXV2xdOlSZGdnY+nSpZBKpaioqEBeXh4aGxuvWJ5N1QIvKmIYTz/9NPbs2YOVK1eirKzMIBuxsbH44IMPEBgYaPA45osFJxDGfNhTU1PYunUriouLce7cOfj6+up8/VwsVVZHIBCgrq4O586dQ39/P7y9vbF8+XJMuAShaJiDO5MCaDs3Vej63Ozs7BASEoKMjAykpKTA1tYWDQ0NyM3NRVtbGx4+VU3JjU3ld7d+/XqcPHkSd9y1Fe9+8C+j7S0kFpxAGMrY2BhWrVqFkJAQ/Oc//yE1p9yR6g8WAJmcMNqL0OXqy+Vy9PT0oKCgADU1NXBycsLy5csRFRUFd3d3sNnseSmaotvzsLCwgK+vL5KTk5GcnAyJRIKgiTbcHyLD9zXtxgydclEMDw9H7/X7cPDIMTy6+5kFnZnQhz+EQIyOjmL16tW48847sXfvXtKbzxzZFAU2e9qFp+JJpH7DicViNDU14dy5cxgZGUFsbCzS09Ph4+OjcYyG3rBzOcUw1LW3srJCaGgoLlsHoXCIhd2xligoKEBPT49eNyPZ3hH6oLAZ7OWOtmt2oayiCrt37/5DiMRVLxCDg4NYtWoVHnjgAWzbtk3v46l8Eik8EiuWHP/vy3MoLCwEh8NBZmYmIiMjZ80yGDqWuZxiGOPaT4tLJ9LCA7HtplWIiYnB8PAwzp07h66uLlI3JJVTC3Wb9YNCSP6xAbnff42Ojg488sgjV71IXNUC0dfXh9WrV2PXrl246667DLJBZcDynxsicJMvgRej5fi+eQzLli1DcHAwzM3NSY9lLqcZhtxsxoiR+vns7OwQFRWFtLQ0jI6OIi8vD/39/RpvSsVTPtzdljIx1GaTw+Hg008/xdjYGHbs2HFVt9+/agViYGAAa9aswb333oubb77ZaHvGPJnkcjna29uRm5uLtAAnPF3LwaLgQIM23TVkHHOdxTAUbeeztLREVFQUEhMT0dXVhcLCQoyMjMx4jXrNAxVTC1022Ww23nzzTYyPj+P++++/aj2Jq1IgRkZGkJq9EtGbtuO6665DRUWF0SpvyApNgiDQ29uL3NxcCIVCZGRk4MmNWRC9cgMAwzwSQ25aU59ikI0b2NjYICEhAZGRkbh06RJKS0sxPj4OgNr3SMYbEYlEKC8vx5EjRyAWi/Hwww9flSKxoEqtycDn85GYkQ0krEerZzKkB9ahqakJPB4PCQkJBj21VSFbAiwUClFVVQVbW1uEhYXB2traIDuaMCQQSPYY1VJrQ8ZoyNgM/SyGh4dRX1+PikEp9pQIcE+yYWXd+o5HJBKhtLQUsbGxcHJyglwux5YtW+Dn54dXX311wS3I0sVV5UEIBAKsXbsWietuR6tnsvJpf6h2Ek5OTpR6EtqeVARBoKWlBWVlZQgPD0dsbOwV4kDGji7mapqh7xgNzWAY+lm4uroiIyMDP7eO4++RMvx6wbjUKJnxqIsDMD3d+OSTT9Dc3IwXXniBkjGYCleNQMjlctxxxx249dZb8dmrT13RjyE0NJQSkdAVtBQKhSgsLMTExASysrLg4uKi046hAUdD6jMMuQkV75XszW7o9MLQwqgHT9XC/Klv0Sazx+uXOHgy2hx1dXUGt7mfbaqjSRwUcDgcnDhxAoWFhfjXv/5l0PlNkatmivHMM89gaGgIR48eVf5O9eIDpi/g/WkOyPa1Mnq6oeqGSl5di9bWVnR0dCA6OlqnMGizoe80g4rVjppQnWLoy1xOLzQdSxCEQd8DmbHoEgdV+Hw+li1bhqNHjyI1NVWv85siV4UHoVDuI0eOzPi96hNQ8XTbUzRGiSeheCKne1njxY++wTfVHbN6DZpsGFqpaQot7dXR1+MAjHsf6seyWCwEBwcjKSkJ9fX1pL2J2YKSZMUBmF7ufuLECdx7773o7u7W+z2ZGgveg1BsWpOTkwNXV1etr1PvDUCFJzEwMIBPfyrC0ctsXBayDXqS0+UJGIoxHoS+GDu90HWsIhbU09ODpKQknaX1VHgO6pw9exYvvPACzp07pzEGtVBY0B5Ef38//u///g+ffPKJTnEAruwTaYwnQRAEmpub0dTUhA7rAFwWsg1uUGuKnsBcYUxtyWzHslgshISEIDw8HEVFRRgdHb3iNVR6DuqsW7cOfskrcOOtWxZ0+nPBCsTU1BQ2b96Mv/3tb4iJiSF9nGo9Q/ixS8jpmtRLJGQyGaqqqjA+Po60tDT8v02xRjWopWNp+UKByumFNtzc3JCcnIyamhp0dXXN+JuuQihjxEHBaZtM9AqleHrf3w063hRYkAJBEAR27NiBlStXYvPmzXoda4wnMTk5iaKiIjg6OiImJmbG1MRYT4CONQSmjiExC0MWY9na2iI9PR3d3d24ePGi8omu7TszVhxUPZNL8ffhm2/O4JtvvtHbjimwoFrOKTh8+DBEIhH27t1rsA1FilHhSexPcwBQoTUmwePxcP78eURGRsLd3f2KvxvboJaK1nRkUPSUnJqaUvaUlEgkkMvlIAgCExMTqKurUzaytbS0hKWl5YxGtcYWmxmDodsBmpubIzk5GQ0NDXjv9M94vGQKdyUFUBZz0DTG+kEhpP/YgN7eZKxcuRIhISGIiIgwyOZ8seCClOXl5dixYwcKCgoo6W6tGqCq37ZYY8Vlf38/GhoakJiYqHPFpSkFHKVSKfh8Png8Hvh8vrJrk5mZmbIbteKGNzc3V3a1rq6uRkxMDAiCgEwmw9TU1AwxmZycnO54zeHAyclJ2QuTbHcrQzG0Aa0msl79But95Hi1noOhl25Q/p4Kz0HbGEtKSrBjxw4UFxdT3pWdThaUQExNTSE1NRUffPABZZvmqEfD1cuye3p60NzcjJSUFFhYWJCyRWVXbLJIJBIMDAygv78fY2NjYLPZyptXcQOTefKTzWJIJBKl+PB4PAgEAmVPSi6XCxcXF0oFg+ou15gSYFckBzevyYa1tTUlnsNsY3z22WchFotx4MABg+zPBwtKIHbv3g0Oh4OXXnqJFvuKC0iRAuVyuWhvb0dKSgrpJdnA3HkSIpEIfX196Ovrg1QqhYeHBzw9PeHo6GjwNMCYNKdEIsHw8DD6+vowMjICJycncLlcuLu76/X5aYKKHpPq38vIyAhqamoQFRWF2tpag8RBUzGetjFKpVKkpaXhyJEjSE5ONug9zDULRiDKysqw+c77sO7vx/H2LfTscah6ARXf6onBwUGsWLFiVs9BHSo3fVFHLBajs7MTXV1dsLCwAJfLBZfLpSzXTlUdBEEQ4PF46Ovrw8DAACwtLREQEABPT885j2Ho8uz6+vpQVlaGxMREeHl56W1TLidAAKQfBlVVVcq+qAthqrEgBGJychKLoxMwtXInhu38aHsqK770m4MsEGU7AUt7Jyz3tTa4mIpKoRgZGUFraysEAgH8/Pzg6+urt3CRga5CqbGxMXR0dGBwcBBcLheBgYFzVkCkzaNTTCtCQkKU00iyY1LYBKDMhJD9jvfu3YuJiQkcPHhQ/zczxywIgdi1axcah6dw1nU17fP7vr4+fPhjCf5ex4aYYGsNXJLB2KkGQRDo7u5Ga2srrK2tERwcDGdnZ1qDgXRXUspkMvT09KC1tRW2trYICQkxeM5PFk1CrR5zUEw3UlNTdT7ZqYgzyWQypKen45///CdSUlKMem90Y/J1EKWlpcjJycGpd1+nbMdsbQwNDeHSpUvoswuAmGAbXEylwNDaCIIg0NfXh9zcXPB4PCQmJiIxMZHywN98wOFw4Ofnh6ysLAQGBqK+vh7l5eUQCASUn0tbzYSmgKSLiwuioqJQUlKic2MmKjpXcTgcvPvuu9i+fbvODYRMAZP2ICYnJ5Gamorjx48rqyX1CQrpg1AoRGlpKVJTU5VuJpkUKNWMjIygvr4eNjY2CAsLg42NDW3n0sRcrsVQMDg4iIaGBjg4OCAsLIyyubkmD262bEVPTw86OjqQkpKiFGO6rrnnn38eQqEQ//jHP4yyQycmLRBPPvkkrK2t8be//U3j3xUXAAvT27QZ+qVJpVIUFBQgOjoazs7Oyt/TscBLG4oNc4DpPRg07X05F8yHQAC/t+e7dOkSPD09ERoaSmofT01omwaQTWU2NDRAJpNh6dKlAOjLSimmGocPHzbZpeEmKxCXL1/GrbfeirKyshnbyquiuBBUg0X6foEEQaCsrAxeXl7w8/PT+Bo6PQnFwq/u7m5ERUXp3cOAauZLIBQoGvy2t7cjMjISbm5uetswxHNQRXFN/Ngtxb6iUVrjXlVVVdi+fTtKSkpMcvposjGIPXv24Nlnn9UqDsDvtfwPpAco5/qKeSfZRU+NjY2wtbXVKg6A8Qu8tDE+Po6CggJIJBJkZmbOuziYAmw2G0FBQUhOTsalS5dQW1sLqVSqlw312I++RVAsFgvx8fGwFA4i2EZOaadsdWJjYxESEoIvvviCcttUYJIeRGVlJR555BHk5eXpfaw+7mB3dzc6OztnzDfJ2jbGk1D1GtSnNfPNfHsQqhAEgfb2drS1tSEqKmrWJf2a0FccVOMNVpDAb6ITHdb+eH0TPbU3ANDW1oZV161F/YUag6dVdGGSHsSuXbvw4osvGnQs2fb0fD4fly9fRkJCAmnXTvXJdKh2Eq8U9uHY6V/18iQmJydRWFio9BpMSRxMDRaLhcDAQCQnJ6OxsXHGSkwyGFI+rboY7LVN8fi/NRnY6C4wuM+lLhTe7sHKcVgGRuOto8coP4exmJwH8csvv+C1117Dt99+a5Qd1ae96krJI5uiIJPJkJeXh8TERNjZ2Rllf5Mfgd3pXFKehGJF6NKlS+Hh4WHQeenGlDwIVQiCQENDA8bGxhAfHz9r6bahnoOmeENLSwuEQiGioqidYqheo3+OcsTp57ai+WLNnGeudGFSHgRBENizZw/2799vtC3Vp716r4X6+noEBAQYLA6q9rl+gaT6SXR3d6O6uhpJSUkmKw6mDIvFQnh4OHx9fVFYWAihUKj1tcZ4DpriDUFBQRgfH8fw8LCxb2NGjEz1Gn3nzkzcu+U2HD582OhzUIlJeRBffPEFTp48ic8++4xSu6pPB/mkAH+NssA9G1ZRGjXWtjkPQRBobGwEn88n9eSbb0zVg1BFV28OKj0HTXYzMzMNihOQWbshFAqRlJSEgoICk5l6mowHIZVK8be//Y0S70EdRbajeViA+4JkeLZCTHlKSdO+G3K5HOXl5ZDJZEhOTjZ5cVgoODk5IS0tDY2NjWhv/32zHKo9B1VsbGwQEBCA+vp6g8asOI9CHDRV19ra2uLPf/4zXn75ZYPOQQcmIxDvv/8+MjMzERgYSNs59qfY4ZcBNjbGBeidDiWDqkhIJBKUlZXBxcUFS5cuNckc90LGysoKaWlpynUd+oiDNjd/NgIDA/Waamg6zwPpATqF6OGHH8b3339vMi3zTWKKIZFIEBMTg9zcXIMKY8gwPDyMxsZGpKWlgcVi0dqzobGxEa2trQgLC0NQUBCltulmIUwxVJHJZCgpKcH4+DhSUlJIeQ7GfPezTTVU06QKr0Hf83zwwQcoKyvD22+/rdfY6MAkPIgT//0cbN8IPJ/bS4t9qVSK2tpaxMTEKJ/kqk8OKr0JmUyG0dFRuLi4YGhoyOhiKgbdKFri2draamxtrwljGgzb2NgoF5lpQjUgbuh57r77buVCvfnGJDyI0Kh4DC9/CGM2nlekJKmgsbERZmZmCAkJ0fh3qtZ0KGIObm5uCA4OpnRX8bliIXkQqtMKe3t7Zcl8QEAAreclCAKFhYWIjIyEo6MjAOrbDe7btw+2trZ48sknqRq2Qcy7QJSWluKBvz6FqrQnjHLLtDE1NYWioiIsW7ZM601KxZoOAKipqYGVlRUWL16s/J0piYRcLsfY2Bj4fD4EAoGyu7VYLFa+Znx8HPb29gCmU4uqzW0dHBzg6OgIGxubeY+paIo5yGQyFBUVYfHixbSnkkdHR3HyXDnuLxTTct2OjIwgKysLNTU1Opcb0M28C8Ttt9+OO++8E+vWTX+oVCtxbW0tnJ2d4evrO+trjVnW29bWhuHhYcTHx19x88yXSMjlcgwNDaG/vx+jo6OQy+XKm9ze3l7Z3drCwkI5ZlUPQi6Xz+hoPTY2Bh6PB5FIBEtLS7i5uYHL5dLe0VodXQHJqakpFBYWIikpyag6FzLsfv9rfNPNQqOATYvnu3XrVqxfvx6bNm2ixJ4hzKtA9Pf3Y82aNaisrLzixtFVCUkWkUiE8vJyZGVl6X0B6xPIGhoaQn19PdLT07Wq/VyJhEwmQ29vL3p7eyEQCODm5gZPT0+4uLiQyt+TnWJMTk5icHAQfX19yvP4+PjQ3vGKTLaCz+ejqqoK6enplKeWVR8iNhDDWdCFbrsgHNkUTel5gOmH286dO5GTk0O5bbLM68qQDz/8ELfccovGG0ZVFNQrIcmKRX19PZYsWWLQBUt2IxuRSITa2lqkpqbqdAVDQ0PR1NSEigrtm/MYg1AoRGtrKwYHB+Hp6YmwsDBan+xWVlbw8/ODn58fZDIZhoeH0draitraWvj7+8PPz4/yhUdkU5mOjo5YtGgRKioqSC/Emw31QqejxR2QHliHqioOPD2v3EiJCqKioiCRSNDc3Kw1fkY38+ZBEASBqKgonDt3btZVetpSR7o8Cz6fjwsXLiA9PZ22m0TRaEafPg5UexLj4+NoaGiAWCxGUFAQuFyuUXaNDVKKxWK0t7ejq6sLXl5eCAkJoeQpbkgRlHrjF2PQ1qR2YmICJSUlWL58OS3X2TvvvIOWlpZ520tj3gQiJycHb7zxBk6ePKnXcWTzzMXFxVi8eDGtPRZqamrg4OCgd3EXFSIxMTGBxsZGjI+PIzw8nLL6EaqyGHK5HG1tbWhvb0dAQAACAgIMDrYZuqkNQRAoLi7GokWLNG6XOBtkY1J1dXWws7OjJXsiEokQHx+P2traeanEnTeBuO2223DvvfdizZo1BtvQ9gX+PdsHly9fpnVzkqGhITQ1NSE1NdWgJ4ehIiGXy9HU1ITe3l6EhYWBy+VS+uSiOs0plUrR3NyM3t5eRERE6J1dMHbHK8UTnuwaCkMKncRiMQoKCrB8+XJa4kv33nsv1q1bp/dG1VQwLwIhFAqRnJyM2tpayj5Q1VqGp8LlgLM39m9KpMS2OlKpFPn5+Xrto6AJfUVibGwMVVVVyp6NdFyMdNVBTExMoLq6GtbW1oiIiCD1NKRiOzwAaG9vB4/HUzY+1oWhwfGLFy/CwcGBVLZMX+49/DlqT72LsnM/UW57NuYlMf/zzz8jMzOT0gtcUbXmbknAzpzAweJ+WtZbANMXQ1BQkNEbv2ha4KUJgiDQ1NSEqqoqxMTEICwsbN5rKvTF2toaKSkpcHZ2RkFBAYaGhnS+nipxAAB/f39MTExgcHBw1teqVj8qFvmRyZwFBQWhtbXVqHGqonrtHu+yQldH27y0yJ+Xq+zUqVPYuHEjpTYVX+aeRHt838vWmP2ggsHBQQiFQvj761+mq4nZREIqlaKsrAxisRiZmZnKyr2FCIvFgr+/P1JSUtDQ0IDW1laNHaKoFAfFeWNiYlBXV6dzzwsAeomCKtbW1rCyssLIyIgxQ1UKw9uF7cpr9/60QLiGROHXX381yrYhzLlAyOVyFBcXY+XKlZTblkqliHEicO7J6S+Y6vUWcrkcdXV1iI2NpXTer00kRCIRCgoK4OXlhaVLly44r0Eb1tbWSEtLw+joKGpqaq54z1SKg+o5g4KCcOnSJcpsqhMSEmKQF6F6bWpaFn5kUxRe2nk3Tp06Rf2gZ2HOr7jS0lJERkbS0pyzp6cHXl5eyhtJ9Wmg6k0YKhbt7e3w9PSkZU9JdZEYHR1FSUkJoqOjdXbcXqhwOBzExcXBzs4OxcXFkEgktImDAn9/fwwNDWFiYoJy2wDg7OwMgUAwo3RdG5pEQXWBl/qy8Ouvvx4FBQV69eSkgjkXiC+//BJr166lxXZHR4fWVJOmFnRvF7aTFgqpVIq2tjYsWrSI6mErUYhEUVERqqqqlHP2qxUWi4WQkBAEBASgsLAQJSUltImD4nxhYWFoaGigzb6fnx86Ozs1/n02UdAV97CwsEBoaCgqKytpGbs25lwgfvjhB1rSNWNjYzAzM9P6dFf94BVfCAGQjlG0tLTA39+f9ly0q6srBAKBck77R8DZ2Rli8XSXL1tbW1rP5enpCZFIhLGxMVrs+/r6orOzU/mkN0YU1Fm7di1Onz5Ny7i1MacC0dLSAkdHR1qeEJ2dnaQLVTRtuKMLsViM7u5uWrtdAdMiV1NTg6ysLLi6ulKyOY+po5hWJCUlITQ0FKWlpbS+ZxaLhSVLlhjcOm42LCwsUD8qQ8TfqREFVW6++WacPXuWlnFrY04F4uSXX2LYPZLytCNBEBgcHNS7CIfsl9TU1ISQkBBal92KxWJUVlYiPj4eNjY2pFOgCxn1mIOPjw+4XC5qamponWsrSvuNzTioouopfNQoQqKznBJRUMXNzQ1WVlZapzB0MKcC8eFnX6Cbm2hUoFATCpecjhtYIpFgcHCQ1kChotHMkiVLZmzaezWLhLaAZHBwMAiCoLSmQBNhYWG4fPmyUTa0TR+SFvsj1oWgRBTUWbVqFb7++mtKbJFhzgRCJBJBLBjDuI3HFTUKxopFX18fuFwuxSOepqOjA35+frQuYa6rq1P2VlDnahQJXdkKFouF6Oho9PT0kCpsMhQnJyeIxWKIRCK9jiMTU/jnphhkhHjiwJpgysd9880348yZM5Tb1cacCUR1dTUSY68MFFIhFv39/bQIBEEQ6OjooKwoShMDAwMQiUQIDQ3V+pqrSSTIpDI5HA4SExNx4cKFWQubjCE4OBgtLS2kXqupgEnX9IHL5aKvr4/yMUdERKCtrY1yu9qYs34Q5eXliI7+vanGkU1RM9wuXb0fdHWXmpqaAkEQsLS0pHzMg4ODcHFxoS1zIZFIUFdXp+y0rQu6+0nMBfrUOVhZWSE0NBQXLlxAXBw9G+dyuVw0NjZCJpPNOj1Vb0mouBa1TR08PT1RWVlJeR8HNpsNV1dX9Pb2wsvLi1LbGs9H+xn+R2lpKdLT0zX+TVMKUlUsLvQLtHoY/f398PT0pGXMbW1ttDZAvXDhAhYtWkQ6nbmQPQlDiqB8fHwgkUjQ399Py5jYbDY8PT3R2zt7N3Wy+1oosLa2hkwmo8UDioyMREVFBeV2NTFnHkRtbS2p5deaPAtVD0Ldw7AbbQXh7IMDi7VZNAxFL0a6inaGhoYgFov1Xv1HhycxMTEBoVAIuVwOiUSCgYEBmJubw97enpKKV0MrJBXxiKKiIri5udEShA4ICEBVVdWs34Mub0Ebnp6e6O/vp3yFZ0JCAsrKynDDDTdQalcTcyIQExMTIAhC712LtX0piunIeyXteDWawBPFvRDC8Iazmujr64O3t7dBx84GQRCor69HXFycQcFPY0SCIAiMjo5iYGAAPB4PExMTsLKygp2dHTgcDmQyGQYHByGRSDA+Pg6ZTAZ7e3s4OTnB29tb7zJzY8unrays4Ovri9bWVlqqWG1tbZXNeamepnp6eqK5uZlygcjMzMTjjz9OqU1tzIlAVFdXIywsjBJbqqJhhym0D/dgR2rAFZ6FIT0sVenr66OkVZk22w4ODkZ1XdZXJCQSCTo6OtDZ2QkHBwdwuVz4+/vD2tp6hkgNDAzMeN9yuRzj4+MYHR1FZWUlWCwWgoOD4enpOau4UbW2Ijg4GLm5uQgICKAlHqR40lMdjHZ0dMT4+DilNoHpFO1cBSrnRCDKy8sRGxtLud0HEtwhlTorA0GaPAhDGt5KpVJMTEzQ0jZdLpejsbERKSkpRtsiIxIEQaC5uRmdnZ3w9/dHRkaGXjcZm82Go6MjHB0dERgYCIFAgObmZjQ2NiIyMlJrP1EqF15xOBzlRkQRERFG2dIEl8tFQ0MD5QLBYrFgZmYGiURCqbCx2WyY2TjQGn9TMCcCUVpaiq1bt1Jul8fjKd039enIbBkSXQIxNDRkUA9DMvT29sLNzY2yFaG6REIgEKCqqgouLi5YtmwZJXN4Ozs7xMTEQCQSoaqqCvb29ggPD58Rq6BjVaafnx9yc3MRGhpKuRdhb28PoVBIKpuhL46OjuDxeAZfT9raKhJu/qioqKBt4aOCOcli1NTUIDU1lXK7Y2NjszZQ0ZYh0QWdhVetra0IDqa2gEY9u0EQBFpaWlBRUYGlS5ciIiKC8gvfxsYGaWlpsLOzQ35+vnJfTLqWbLPZbPj5+aG9vZ0ymwpYLBbc3NxI79qtD05OTnrvsamtGEv156XRsfg5r4jy8apDuwcxOTkJuVyud4ByNuRyOWQymV5RdjKRaEUQj0z/Qn3h8/mwsLCg/LMAfvckysvLYWNjA4lEgszMTFrXj7BYLAQFBcHDwwNlZWUICQlBc3Mzrf0cCgoKEBISQnllK5fLRU9PD+Vb9jk6OmpN02rzDtQrNDW95oGwQOzatYvSsWqCdoHo6uqiJRuguocklQiFQtja2tJSWk13XcWiRYuQn5+PsbExXHPNNXO2p6OtrS1iY2ORn5+PiIgI2lLD5ubmcHJyomUK6OLigosXL1JqE5ievtR3DSL1v2dnjY9pEgVtU2eZTIauri7Kx6sO7QJBhyoD0/EHOi5EuuwSBIHh4eEZ1aRUc+nSJTg4OMDa2hqVlZVzVnGpiEckJyejvr4eTk5OtO1H4ufnh46ODsoFQiGmxsQhtHkEvgIxLFlsnUKg+noynq4iJU03tAtEb28vLZFWPp9Pi2fC5/NpCVCOjIzAxcWFtkVfQ0NDGB4eVpZtz1VZtnrMwc7ODqWlpaT3odAXFxcX5XJwqj9LR0dH8Pn8WcWNzNQA+F0IXk9zQKiDABnhuoVA35odDodDeYZEnTnxIOioGRcIBLSkIXk8Hi0FOXQGPqVSKS5cuIDk5GTlTTMXazc0BSRtbGwQFBSEixcv0uItsVgsZWaA6nZ8ZAWCzNRA8bodqf64MdYO6+NYyoZDVC39dnV1paVSUxXa/c/u7m5a3oBYLKa88o0gCFrsAtMLv6jaHk8dxT4d6sFPOtdu6MpW+Pv7QygU0rZcm66VkmQzDqrZMG0rOlV/trKywtTUFOXjdXd3R09PD+V2VZkTD4Ku5dJUu5gikYiWnogTExOwtLSkxeXm8XgQCASIitL8VKLDk5gtlclisRAbG0vbprbu7u5obm6m1CYAODg4kOpVqe/UwNLSkpZNb7hcLqmFZsZAu0Bcam3H6v+04M6e6RtPkwum+jMZ90sul9Myl6crM0JX4BMALl++jCVLluj8PKgUCbJ1DtbW1nBzc0Nvby/lsSJzc3PIZDLI5XJKp04cDgcEQVAe37CysqJFIDw9PRe+B9E7MIRxjp3GNRKzrZ3Q9rMVpLDi8fHf4VrK5nPAdM0GHZ2k+Xw+LTtiTUxMYGJiglTGgAqR0LcIKjg4GJWVlbQEk+3t7SEQCGa06KMCc3NzSCQSWFhYUGaTrimGr68vrRsBAXMgEJYcNjgcjs4bXvVnXYuuFD+H2hHI9gCOkRQU1Z91Ccrk5CQtNzKPx6NlmqVYX0EWY0TCkApJGxsbcDgcWgLKikAl1QKheNpTKRBsNpuW/h2+vr747bffKLerCq0CMTk5CQcbyxlbp2ubu2laO6HtZ3cI0dg9SFpQVH+eTSDoSMlOTk7SshtXb2+v1iY82jBEJIwpn/b19UV3dzdlq3kV6KpQNAaFQFAtPAAon7oEBAQs7CnG0NCQ3gUzugJAip9bW1vBiucq00b6eBC6mJqaonyKoWjfTnXMRFEkY0gOXB+RMHZthYuLCy0XMV3zerrsWlhYUJ4h8/Pzo7WxL0CzQIjFYkpdNQVTU1PKqQAZQVH/WRuTk5OUpzjp+gzILFTTBRmRoGLhlY2Njd6do8lAp0AIhULK7VpaWlLelMbCwoL2akpa6yCkUikt6wHkcjktdgmCoLygiK7AJ4/HMzpeoqtOgqpVmSwWS3lzUImZmRmkUimlNgHQMlZgOkOy0PqIAnMgEHTk/ukos6ULOlqZAcZ7EAo0iQTVS7YVFYpUQtf3T1dAkcVi0bJbGN27fdM6xZBIJLQ96ReKQNDl7VBZg6863ZDJZJT3czA3N6flaU8HbDablpuOLoEwhPz8fExNTaGjowP33HOPztfSKhB0TTEWkkDQNVaqhUexB0VPTw+WLVtGaWEXn8+HVCqlvB6Caq8EmA7+dnd3z9oiUduCLW0/Z7C7sPP7dkQuCiT1erI/19bqvxvd119/jQMHDuDpp5+edQrMImaRtfPnz+Ps2bMgCAJcLhfbtm0jPZCqqio8++yzeOKJJ8iPngQTExMwNzenfPpCRyWlRCKBTCajPA4hEolgaWlJmUjI5XJl63tzc/Mrmtkaw+TkJNhsNuXBWjq+L5lMhqmpqVmb+lR08wECgOIjmuVnd0tgXAJMysm9nuzPf9t5L/gj+nXC+stf/oK3334bL7zwArZt26ZTuGe9w4KCgpCQkACJRIL169frNRAzMzOYmZkhOztbr+Nmo66uDh4eHpQvy87JyaF8rP39/RgZGUF4eDildisqKhAaGkpJvl4Rc8jKykJVVRV8fHzA4/EoW7vR0NAAR0dHylf10vF9jY6OoqOjY9aOYp/r6UFYsIbw1kUhro0MIPV6sj/bO+t/DyhEVSgUzl6GQMzC8PAwcfz4ceL555+f7aVXUF9fT9xwww16HzcbdXV1RH9/P+V2f/vtN8ptDg8PE1VVVZTbra+vJ7q7u422IxQKid9++40YHR0lCOL3z+DSpUtEaWkpIZPJjD5HeXk5wefzjbajDl3fV3V1NeV2z58/T4yMjFBuNyoqSu9jfv31V+K3334jjhw5MutrZ/UgrK2tcfvtt0MgEOitVGZmZrTkaekM+BALZKGOk5MTRkdHjZrX68pWULnAa3x8nPJSa7oW7InFYloasBA0pNABw7I511xzDQCQ8r5mHbG1tTUsLCwMaiFGV65asdcA1dARbacrr65Yi2AoZFKZVPSTUGSyqL456Eof01FNC0x/DlTHzOairoLWOggHBweDPI/ZIPNUVm0drr7hry67ExMTVA6VtgIZxWdgiCelT52DsSJBVb2GOnQVoNFRTauwS/V4R0ZGaOmqpgqtaU5nZ2dSDTjIoJpWcsEEuvv6YF0zfTPrsypUV8k1Xcty2Ww25UVjLBYLDg4OGB0d1cu7M6QIypjpRl9fHy09PoVCIS3bB0xOTtIyXjrqYdrb22nbP1YBrQKha36kbx5Z9Yb3sSaw2Rf4p45Vm9r6A+qCrs4/jo6OGBsbo7zTs7+/Pzo6OkjbNaZC0hCRkMvlGBgYoDyDA0yXmtPRLZ2OJz1d8bKOjg74+PjQYlsB7f0gCI45rP96EvdmLAagX98H1Z9Vb3gzyOAqmL3vg76LteiYYgC/xwuoFgg3NzfU1dWRWhBGRfm0viKh2PKAjuAcn8/H4sWLKbdLRwyCrs7TnZ2dC18gbJzdYTs5otcTfrYbniAInDvHQ3b29L+NaR0+Y6w2NrRtv9bS0kK5XRaLhYCAALS2turst0Dl2gqyIkH8b/u/pKQko86nzTYdq2QJgoBMJlswC/Z6e3u19iKlCtoFIizID0NiHm5bMb03J5kn/Gw3PF1l1g4ODrSU7yrsUp1CBaanGXl5efD19dXYcJeOvTLJiERraytcXFxoaZQzNjZGS+9Quhr70CUQ/f39WL16NeV2VaG97X1YcCD2JDle0QrcWDgcDuUpSTMzM8jlcsqzDmw2G7a2trRkdDgcDqKjo1FVVXXFXJeujXQB3dkNoVCIjo4OWmIPAH17jJBtLkw2K6aAToGgY88ZVWgXCB8fH3R3d1Nu19rampZ4gb29PcbHxym3S9deDsB01yYnJye0trYqf0enOCjQJBIEQaCqqgrR0dG07Q3a399P23aOZFKyqvEzMun0iYkJWjyTwcFB2rMYtAuEl5cXLb0D6ZoOGLJdOxk8PT1p+RwULFmyBJ2dnRgZGZkTcVCgLhKNjY207s05MTEBDodDS5cush6E6mY5qmKhTTjO1Xcg5Z1K0h4HWUQiES1TLVVoj0F4eXlhYGCAcrtOTk4YGBigfNcuJycndHV1Ub4Lt6WlJVgsFkQiES35ew6Hg+TkZBQVFYEgCCQkJNAuDgoUMYlz587Bzs4OiYmJtJ2Lrq0cCYIgPRVQj5/Nlpb/R4wU3aLfN+81dD8YTdDd9oB2gfDx8aFl9x8nJyc0NTVRbtfR0RF1dXWU2wV+35l6yZIltNgn/rfpCzA3Zbjq51X9j44LlyAIdHV1IS0tjXLbIpHIoCXuuoLtR4s78FCqDxzZ/WCzpbOm9/URjuHhYdq9B2AOBMLNzQ08Ho/yXZAUXYKpvhjNzMzA4XBomTf6+PggNzcXixcvpjyVpphWJCQkwMLCAmVlZfD390dgYCCtTxmJRILa2lqwWCxkZ2ejubmZtg2DFaXFdEwvqI5rKIRjYGAAAwNmkN4UqfybvvvBaHr99RbttGyOrA7tAsFiseDn54fm5maEhoZSatvW1hZCoZB0Pbpq9aYul47L5aK/v1/ZVp8qOBwOPDw80NvbS2mBi6aYQ0ZGBurr61FUVITY2FhapjX9/f24ePEiQkND4ePjAxaLReuu4i0tLbTsvA5MZ0Zm6yJlCOpxjdk8DrLC0TX8E9alUl9jog7tQUoASEpKQl5eHuV2yQQUVYNF6h+2NujMOAQHB6O5uZmy8lttAUkzMzNERUUhLCwMpaWluHz5MmUrYAUCASoqKtDe3o60tDT4+vrO8FLo2FV8fHwcYrEYzs7OlNhTRSKRQCKR0CKifD6fVCxI287g2nYSb7h4ASnJV4lAJCcno6KignK7Tk5OGjMZ2kRB9QPWha2tLSYnJ2lZqm5tbQ0XFxdKUr9kshWurq7IzMwEi8VCfn4+amtrDcr+yOVy9PX1oaSkBNXV1fD19UVSUpLWoB7VIlFfX09b7GZgYICWtCkwXRNizI7x2oRDMtKDiIgICkeqGdqnGACQkJCA1157jXK7jo6OykCl6vRBXRS0VW/qwsPDA4ODg7REzENDQ1FUVARvb2+DXXB9UplmZmYICQlBUFAQBgYG0NjYqJyaOTo6wsnJCfb29srdraempiCRSMDn88Hj8cDn8zE1NQV3d3csWbKE9PJtqqYbIyMjIAgCrq6uBh0/G319fQgODqbcrqL5DNUxIB6PB1cHO1rWd6gza9NaKiAIAuHh4bh48aLRc1L1VaCBohZ02gTireIuyOTEjPy0MemjkZERtLW1IT4+3qjxaqOxsRHm5uYGXZhU1DkQBAGBQKAUAaFQCJlMhpGREbi6usLMzAwODg5wcnKCo6OjUZWATU1NBve4JAgChYWFiIqKomW/TJlMhtzcXGRnZ1N+I3d1dUEoFFK+L+nZs2dx+vRpvPvuu5Ta1cSceBDGBiq1eQcAcIsvC13dHdiRGmiQp6ANZ2dn1NTU0LYSLyQkBPn5+fDy8tIrW0Lljlf29vawt7efUUuSk5NDeRrRGE+io6MDdnZ2tIgDMO09eHp60pLp6evrozwwDwDFxcVITk6m3K4m5iQGAUwHKgsKCvQ6RhFLeKuwXWMcYUeqPypG2fjTYhtK13kA0zeQj48Purq6KLGnjpmZGSIjIzWuodDGXFZIUo0hMYmJiQm0trZi6dKltI2rvb2d8qI4YDpmMz4+Touw1dbW0lqMpsqcCURycjLKy8s1/k1bPbvCW2ABSlFQD9o07l2HJU70bJemaMhC1yzMzc0Ntra26OjQnVUBFrY4KNBHJBRrOiIjI2nZvhGAcvEcmSCivgu0hoeH4erqSotn0tzcTKtoqjJnAhEfHz9jFyBtmQZNWYe/pAdo9Q5YLBZcXFwwMjJC+ZgtLS1hZ2dHi20FERERaG1t1blA7GoQBwVkRaK1tRW2trZwc3OjbSytra0ICgoi9VrFdflWYTspoaBrxSmfz4eVldWcBCiBORQIPz8/dPb2w/KJL3WmH1V/Jjtt8PT0pL1ugS7MzMwQHx+PiooKiMXiK/5+NYmDgtlEYnBwED09PbQ+JSUSCYaGhkjfxIrrkgVoXJClCkEQGBoaokXczpw5g4yMDMrtamPOBILFYiExLRM+wxeuEAVtuV6yuLu7Y2hoiJZxOzs7QyKR0NLLQYGDgwPCwsKuuGGuRnFQoE0khEIhLly4gMTERNqWiwNAW1sb/P39SU8BFNflX9IDNK7kVBULxT4gdLTaO3PmDDZu3Ei5XW3MmUAAwPY7b4d1W5le3gEZOBwOrK2taenjAABhYWGor6+nxbYCLy8vuLi44MKFCyAI4qoWBwXqIiEWi1FeXo64uDhaGqwokEgk6OrqMqiUXluVo6pYvPtLFZ7K6aN8ebdcLsf58+evTg8CAJYtWwZ5XxPeuIl619HX1xednZ16HUM28OTm5gapVEpLnwhVFi9eDIIgUFNTg5KSkqtaHBQoRKKsrAzFxcUICwuj/T03NTUhODjYaA9Fs1j4gSMaRfEQdE5DDCE3NxexsbG0BW01MacCYWFhgYiICJSUlFBuW7HASp9sBtm1GcB0MPHixYvGDHFWWCwWFi1ahO7ubmWB0h+BgIAAZfk3XSXPCiYnJzEwMAA/Pz9K7SrE4oVl3rCwsYeYYOuchhjCqVOn5nR6AcyxQADAxo0b8fnnn1Nul8PhwN3dnVSwUvFFhbvbklqbAUyXdVtYWNDS/EaBSCRCWVkZ0tLSwGazldONq5mJiQkUFxcjOjoaXl5elC7w0kRjYyMty+0VtLe3447lsbNOQwwRi9zcXFx33XW0jFsbcy4Qa9euRU5ODi22AwIC0N7ervFvmtKq9YNCveIg4eHhaGhooOWmVY05ODs7Izo6GhYWFigpKaFlH1JTYHR0FMXFxYiIiACXy6VlFagqAoEAY2NjtDV6FYvFEAgEM1ackolZkBGLpqYmODs7z7lXOecC4eLiAhsbG7S1tVFu297eXrnGADB+Vac6tra2cHV1ndEclgo0BSRZLBbCwsLg7++PgoICWrMo80FnZydqamqQnJw8Ix1Il0goYjsRERG0NdBpb2/XmRkxRixOnDgx59MLYI4Wa6nzj3/8AzKZDE899RTltvedKkZP/wDMPYOUHzxVC7iA6cU9eXl5SEpKMmoZrwIy2Qo+n4/z588jODgYfn5+tHaIysnJIbUtvKFIJBJcvHgRU1NTiI+P1xpwM2aBlyZaW1shEAho22hGLpfj3LlzyMrK0juIqGmtETCzenjZsmX46KOPKG9iNBtz7kEAwIYNG/DDDz/QYvvl4iGEOxD4uKzdoKKr2dC1D4W+kE1lOjo6IiMjA6OjoygpKaGl3f9cMDg4iIKCAjg7OyMpKUnnjUSlJyESidDe3k5qnw5DA4m9vb3w8PAwKMOgybNQLci6/9NCtPUNzbk4APMkEKGhoRgdHaW0ma3iiw1zt0POABvPJjtSvoBLgYuLCxwdHY2aauhb52Bubo6YmBgEBwejuLgYbW1tc9qY1himpqZQXV2Ny5cvIyUlhXSBEhUioVjTERUVRerm1SezpXqOlpYW0mXbutBUkJX3zefYvPEmo20bwrwIBABs3boVb731llE2tAUeT+68HklOUlqDe+Hh4ejo6IBQKNT7WGOKoDw8PJCZmYmJiQnk5uaip6fHZDMdEokEDQ0NKCoqgqurK1JTU/VuBGysSLS1tcHBwYF0sxlVr5OsN9Hf3w9bW1tKW9apPtzk9efw2IP3U2ZbH+ZNIO688058+eWXen/pZAKPZmZmCAwMpHUNhWKqUVlZCZlMRvo4Kiokzc3NER4ejtTUVAwODiI/Px+9vb0mIxQSiQSXL19Gfn4+LC0tsWzZsiv6VuqDoSLB5/P13gJQ9cYkEzwkCAKNjY20tcMrLy+HP9cd/v76BdSpYt4EwsXFBZGRkfjxxx9nfS0ZUVCfTgQEBKCvrw9TU1O0vgdfX1/S8Qiqy6etrKwQExOD+Ph4DA0N4dy5c2hqasLk5KTRtg2Bx+OhuroaBQUFYLFYyMrKQlBQECVBRn1FYmpqCufPn0dCQoLBFZPaMg2qdHV1wdXVlZaGtwBw5MgR3H///HgPwDxlMRScO3cOhw4dwpdffqnzdWa7zhqUjeju7sbw8LBB+weQbZEPANXV1bC1tdXZkn0u1lZIJBJ0d3ejs7MTHA4HXC4XXC5Xr4tXnywGQRAYHR1FX18fBgYGYGNjg8DAQLi7u9OWaSGT3ZDL5SgqKkJoaChllZmK6yHc3Rb1g0LsSPXHPzdEIC8vD+np6bTs1SESiZRtEuZqebc68yoQBEEgNjYW33///RXFK+q9Jw1JURIEgby8PCQkJOidklQVJemBdTpfq7ggFy1aBE9Pzyv+Ph8Lr0QiEfr6+tDf369sF+/k5AQnJyedKw11CYSika2ij+XY2BgcHR3B5XINjuAbgi6RIAgC1dXVsLe3R0hICOXnVr0uXkpzRGXnKNx8AykPhAPAoUOHMDAwgJdffply22SZV4EAgLfeegutra04ePAgAMyYRgAgdYPqYmBgAJ2dnUhISJj1tcaI0tTUFIqKipCQkDBjSzRTWJWpWGim6FCtWPVqYWEBKysrWFlZwcLCAiwWC5cvX8aiRYsgk8kwNTWFyclJZZdrMzMzODo6KjthOzg40L43pDa0iURraytGR0cRFxdHy9gU18gDqb4ImWzDU1VsSME26hrVhFwuR0JCAs6ePUv7Dt66mHeBEAqFiIiORe/Gg9iWHjKjzRxbJb5gDIWFhVi6dOmsZar6eA2aGBsbQ2VlJVJSUmBtbW0S4qANgiAgFosxOTmJyclJSCQSyOVyNDQ0IDw8HGw2G5aWlrCysoKlpeW8ubi6UBeJnp4etLa2IjU1ldZeEgDQ0NCAL+uH8VwRj7KNeFX59ttvcfz4cZw4ccJoW8Yw7wIBAPf8ZSeK+Fa47JtNWcWjKjweDxcuXEBGRobGp4qm+aUx7fJramoQExOD6upqkxQHXdBdSUk1CpHw9fXF5cuXkZqaSruYKXYWy8rKUnoviocLMLMC0lCuu+46/O1vf5uz7tXamLcshir7dj8BYcX32J7iS0txk5OTE1xcXNDS0qLx74Yu3tKEi4sLFi1ahIKCAkRERCwocViIhIaGgsPh4Pz580hKSqJdHBQxjujo6BlTG31a0s1GY2MjeDzevIsDYCICERAQgMz4SGTLGmk7R1hYGLq6ujQu5DJ08ZYmRCIRLl++jMjISFy8eHHBlkUvFHp7eyEQCBAUFITa2lpSKVBj+jK0tLTAycnpij1CZ2tJpw/PPfccdu/erffY6MAkphjAdHBp48aNqKiooG3+uOdUOewE3eixC8LR4k6j4g2aUI85KKYbiYmJpHcgn28W0hSjs7MT7e3tSElJgbm5OekFXoamzRVTi8zMTFLXqCFB78rKSjz44IMoLCyctwCwKibhQQBAUFAQMjMzcfToUdrOcbC4H/V8Fno62in1GgDN2QoXFxfExcWhvLyc1kYzfzQIgsDFixfR09OjFAeAfDEVmQIoTeesqqpCdHQ06QeYpqrM2drm79mzB6+++qpJiANgQh4EMJ2SzM7ORmVlJaVNS1WDkE1DArybZoaNKzJmpCONYbZsxdTUFMrLy8HlchEcHGwyX74mTN2DkEgkqKyshIODA5YsWaLxs9RnqTjZAHVzczOmpqYM3lFbcR65nAABzen7X375BQcPHsT3339v0DnowKQEAgD27t0Lc3NzPPfcc5TZVE9fzpbVIIPiC38k1Qs3uozNmq2Qy+WoqakBQRB6PYXmGlMWCIWLv2jRIvj4+Oh8rb79JHSluPWdWuhC27TjjZuWIjMzE++8845Blb90YXICMT4+jqSkJJSUlBjVXmu2+V9DQwPkcrnBTwSzXWfhai7H7gg57lm3nFS2giAItLa2oqenB/Hx8bTV7xuDqQpEX18f6uvrERcXRzozRIUnIZVKlbuLqwcmjUVVlG6zasbA+XP46ZuTlJ7DWExOIADg8OHD6OjowGuvvab3sWQrMQmCQGlpKXx9fWd9Gqnb3pHqDytI4DvRiX5rH7yySb+NVIeHh1FbW4vAwEAEBASY1JTD1ARCIpGgtrYWEokEsbGxsLS01Ot4YzwJyatrUV5eDk9PT1pWUyqup+3JPji950/I//EsJT0lqMQkBWJqagoJCQn46aef9G4wqviCyVRiSiQSFBYWIiYmhtRTSWGbaw18tsLOqCIoqVSK+vp6CAQCxMTEmIw3YUoC0d/fj4sXLyI0NBQ+Pj4GC6khnsSOVH8Es0ZxsZcHK24QLWstFLz11lsor6rBB+++Q9s5DMUkBQIAPv74Y/z666/48MMPZ32tMWsohEIhSktLkZaWNmtg9MFTtTh1vh0HEji4MTuNkiKooaEhXLhwwWS8CVMQCIlEggsXLkAsFiMmJoaSgLW+nkRPTw+O/lCOF+tYYLGpX2uhYHJyEvHx8cjJyaF9TxBDMJk0pzpbtmzBhQsXUFpaOutrVVNV+lZi2traIjIyEuXl5VobvygKa6wgwWcr7CgTB2B6167MzEyMj4+joKAAw8PDlNhdiMjlcrS2tiI/Px/u7u5ITk42SBw0FULp00+Cz+fj0qVL4Dn6g8VmI9zdlrLdsdR5/vnncdttt5mkOAAmLBBsNhvHjh3D/fffr3HXa8CwDXA04e7uDh8fH1RXV2ts/HK0uAOu5nL4TnTSsrbCzMwMUVFRiImJQXNzM4qLizE2NkbpOUwZgiDQ3d2N3NxcTE1NITMz06gOVNpqG8iIhGqjmcObpjfAqR8UGlQRORvl5eX46aefsGfPHkrtUonJCgQAxMbG4sYbb8Qzzzyj8e9UrqEIDAwEh8PB5cuXlb9TCFCalzV2R8jRb+1D69oKe3t7JCcnIywsDLW1taioqIBIJKLtfPMNQRAYGBhAXl4ehoeHkZaWhiVLlhi9nkJRCKXpya9LJGQyGcrLyxERETGjRsaQPpWzIRaLsX37drz//vu0NJuhCpONQSiQSCRIS0vDkSNHkJKSQkkjGW3I5XKUlZXBw8MDQUFBBqUyqYIgCAwODqKhoQG2trYIDg6mPM2mibmIQcjlcuXSbBsbGyxZsoSSPUbU0VXboB6TkMvlKC0tBZfL1dle3tiWAAp27doFCwsLvPjiiwbbmAtMXiCA6ZZut/zpTrRe/3dIMV2oQuUaClVkMhn+9c0v+M/lKUxybLHJY9ygVCagX9s6bRAEgZGREbS0tGBqagr+/v7w9vamrXsTnQIhEonQ0dGB3t5eeHp6IigoSO8u1/owW5WkQiTi4uJQWVkJNzc3BAcHG2WTDKWlpbj//vtRXFxs0t4DsEAEAgAefeoZ/HKxC3Vht1DWSEYbNrvPYG+EDK6WwC1rDPccqHraKJiYmEBHRwd6enrg4uICLy8vuLm5UboRLdUCMTU1hf7+fvT09EAmkykFbi4rSXV9D5cuXUJbWxuCgoIQGhpKiU1diMVipKam4oMPPkBsbCzp4+aLuWkiSAEHX9yHsJhEbF2xFh88ehst51A8HdK8bGDFGYeFpSXGxsb0FghNTxkqsLa2RlhYGBYvXozBwUH09fWhrq4OdnZ24HK58PT0NIkn0vj4uLIfJkEQ8PT0xNKlSylb+6Ivqis2VZHL5eDz+bC3twePx4NcLicttgqbijgH2QfWM888gxtuuGFBiAOwgDwIAKipqcHdd9+NkpISWm4E9ZiDnZ0dysrK4OXlpde2Z1R7DrogCALj4+Po7e3FwMAA2Gy2sjmtk5MTbGxs9MoG6OtByGQyjI2NKRvZ8ng8WFtbKwWLqkV3VEzXVG38c0MEysvL4ebmhpCQEIP3AtXnuy4pKcEDDzyAoqIikxByMiwogQCAF154AWNjYwaVYWtitvJpRWTb1dUVISEhpG42Ki5mQ5mamlLeqDweDyKRCBYWFkqxUDSpVfSaVL8ZNAmEVCpV9q5UNLIdHx8Hn88HADg4OCgb2To6OtIyfaBCdBU2bM2Bn9a7gsvlzihtNkQkyAbNxWIxUlJS8OGHHy4Y7wFYgAIhkUiQnZ2NJ554gpLt0MmUT5NdiTmfwqCLqakp8Pl8iEQi5Y2uuNnVv36BQHBFcxsOh6MUFIW42NnZ0SYGmqDKg/jqfDuejgROtBOIWnRlu3pjdhXXJWJbt27F4sWL8fTTTxs09vliwQkEML2yb8WKFThx4gQiIyMNsqEaJxgaE8xaPq26EjMxMVGj62zMU84UxIUgCOTk5OCaa66Zl/OTwZjPaXBwEBcuXMCOXCGaxrVnwgwVCW0ZjoMHD6K0tBT//e9/572UXl9MulBKG1wuFx999BG2bNmCkZERg2woiqyGxgSkyqdZLBaCg4OxePFiFBUVgcfjzfj7g6dqIf/fClJDgpKG9i+kEhaLZfIXsDG7bzc2NiItLQ2rogK0FlIBhu8FqijzV628/Pbbb/H555/j+PHjJv/ZamJBCgQAJCQkYPfu3bjlllv02jxXtTybaw0cSODoVT7t4eGBpKQkVFdXo7u7W/n7o8Udyk5BhngAVLfAu1rRVSWpCblcjurqavB4POWCPE03sjrG7CquGGOc1Rjue/ARnDx50mRW6+rLghUIALjjjjuQlJSEnTt3kj5GX89BE3Z2dkhPT0dnZyde//IczHedMXg9iEKwAFDe7t/UMaRsmczNrUCx25m9vT3i4uKuiJfMJjbGeBIjz2ah599/x+cffwg/Pz/Sx5oaC1ogAODll19Ge3s73n77bZ2vM9ZzUMfc3BwpKSmo7OZj71IZRsYFBt3gpjC1mC+Mee+zeVy9vb0oLCxEaGio1uwTXZ6EXC7Hrbfeir27HkdmZqZ+b8zEWPACwWaz8dlnn+Hdd99FTk6O1tdR4Tmow2Kx4OgdhH+3c7A/noOWlhaNq0F1YejUgqpFQ/OJMdOqI5uilMVKqp+BWCxGRUUFuru7kZ6eTmoZ9Wzj0FcknnzySYSFhWHH9j+Tf0MmyoIXCGA6D//FF1/ggQceQFtbm/L36pvjUOE5qNsGgPq9N2DLjasxMTGBwsJCCIVC0jYMjchfDZ6HtpucLOqfQW9vLwoKCuDl5YXExETS7ekUngQAraJLViQ+/PBD1NTUUFanM99cFQIBACEhIXjzzTexceNGDA4OAph5AR28LoTSZi/qFyeHw8HSpUsRHh6OsrIyUt4EnS72fGCIV0PFZ/Bgqs8Mr8HQ3bBnG8tsIvHtt9/i8OHD+O9//0vbYrq55qoRCAC49tpr8fzzz+P666/HyMiI8gJ6JNWLsl22Z2tS4+LigqysLKU3MT4+rtWOoWlRQzwPQ25efY8x5GY3Ruje3BiJjkfjkMTuxSM/9+DDDku9m9pqGouuDIk2kfj555/x1FNP4ezZs3OyLH+uuKoEAgBuuukmPPb4XxGXeS1kU0KM7buG1L4VZCHTpEbVm6iursb58+ev2KPTmLSoITfiXBxjyM1u6DRjcHAQ+fn5GBwcxMOlMhQMsoyebpHNkKiLRF5eHh599FGcOXOGdIf0hcJVJxAAsOVPd+Dpxx/Bjy8/iLy8PErbxOlzE7i4uCAjIwNeXtMeTF1dnbJ9njFPTkOOnYtjDL3Z9REiHo+HoqIitLW1IS4uDjExMbgjMQAAIJcTlARt9fEk/v3vf2PHjh04ffo0AgICjD63qXFVCgQA7PjzPXjiwW144YUXKOmXYGi9AovFApfLxbJly2Bvb48T3/6CWw5/AzbkBqVFDZ1ezFUZtzHTDF03pFAoRHl5OS5evIjw8HAkJSUp14wc2RQFDpsF4n/nNxaynsTAwABefPFFnDp1CiEhIUaf1xS5agUCAB64/35s374dq1atMrgkW4GxWQMWiwV/f39sK5JhSkZg0WQbWlpaIJVKaR+HoWOfq5iCrhtSJBKhpqYGFRUV8Pf3R3p6ukZvkI6grS6bOTk52LFjB7755hssWbKEsnOaGle1QADTq+gee+wxrF69Wpnd0AeqOmcruC8lAN/2cdBsFQipVIq8vDzU1dWRbk47V9MLY44zlN/P54ehoSGUlpYqW8FlZWXprGkgk6rUF202f/jhB+zcuRNnzpzRqwvVQuTqyMXMwu233w4LCwusXr0aX331lV5zRfWgpDHMaFjyP1d/0aJF6O3tRWVlJSwsLBAQEAAPDw+NlX+msOJzNtT3KNGHQzcugS9rDGbCVnyeN4g7rknQO3ZkzPnJ2FyBJrzwwgv47rvv4OvrS4l9U+aq9yAUbNq0CQcPHsT111+PvLw80sdR+RTV5LKz2Wz4+PggMzMTixcvRl9fH3JyctDY2HiFVzGXUwVDj9P381I05T1//jwKCgpQ3jGCl+rY2Fk0aVBgWd/FXPrYTO7+HnteeBk//PDDH0IcgD+QQADAypUrcfr0aezcuRNHjx6d9fVUP7Fnu3mcnJwQExODrKwsWFtbo6qqCrm5uWhoaACPx8OOVL85nSrQlbaUyWTo6+tDVVUVcnJy0NbWBj8/Pyxfvhxcv0AIZGyDBVmfxVxkeW1tKG7u+BhOEwOoLskHl8s12uZCYUEJxPfff49vvvnGKBuhoaE4d+4cvvrqKzz00EM6l4pTWc6sj9iYmZkpA3JpaWmwt7fHZ79VIGyyFYfSrLE3w0OvJe5zjabPbXJyEu3t7SgpKVFulOPn54fs7GzEx8fDzc0NLBbL6PJrBVR5fl1dXcjOzkZiYiLOnvyP3j023333XRw9ehRDQ0NGjWO+WFAdpRobG9HQ0AAul4uamhokJCQgPj7eIFsymQy7d+/G+fPn8cUXX8xwZ6nY+0AdY3sqmu06C0Iux1InFv59UyCGhoZgZWU1o0GttotXn3Or9qQ0dMwPnqrBmeoObIt1wYZF0x2j2Ww2PD09weVyr2hpZ8x4dY/DOA+woKAA27Ztw2uvvYbrr7/eoDEoCqlsbGxQVVVl1DU7HyyoICWfz8fU1BTYbDYcHBwQFxdnsC0Oh4ODBw/i448/xrJly3DixAmEh4cDoCcwaWwLfMVTNSvCX9lmTyQSgcfjzdhYx9raWtk8ViEa2tq+kz2nruPkcjkEAgF4PJ6ys/UtLjJExVrhp45hTBFm2Htjql7b6Rk6XnWMCVh+8MEHOHToEL788kuEhYUZPIampialt2fsNTsfLCgPQsHk5CRYLJZRdfeqlJWV4e6778b+/fuxYcMGSmMPVDwNyY6HIAhMTEwob1Y+n4+mvlEMCcVwtbVEmJfzFV2tLSwslK3mWCwWysvLkZiYCIIgcODXJnxzsR83R7jizljPGQ1vFRWhLBYLtra2M0TJ3Nzc6PdNdZt7sjbkcjkef/xxXLx4Ef/9738pq8Cl+pqdKxakQNBBT08P1q6/CTKfSGT+3068fQs1Sk/FhW7MzaY41pIDjDx/7RXt6yUSCQiCAEEQkMvl6Ovrg5eXF1gsFt4p7oBUDoxLWTiwIXqGuCiERRtRB3NwoV+ASE871D6ZPafvWR2y30FnZye2bt2KqKgovPbaa3O6+5epsqCClKpPOvX/jMXb2xulhfm4ZpE7zjx7FyoqKoy2SZUnQsW6jftSAmBjYwMXFxd4e3sjKCgI4eHhiI6ORkxMDGJjYxEfHw8bGxvExcUhNjYWHI8g/KuNAzffQAQEBMDT0xOOjo6wtLSc9TOvHxTO+P9cvmd1yASb33nnHaxZswaPPfYYDh06RJk40HnNzgULSiAUT7r/9//+n/JnxX9UYGFhgX++9ipOf3Yc27Ztw549e/QuhVaFiizIQiiO0oSxNzhV2QzFWADNi7k6OzuxZs0aFBQUoKCgADfccINR51KH7muWbhaUQADTUwGJRELrOeLj41FcXAwWi4XU1FRUVlYaZIeKp6CxImPM8cYcS8UNTlWaWdtirnfeeQerV6/GQw89hI8//pi2Pg5zcc3SxYITiIGBAdTX189oLUcHFhYWePnll3H06FHcd999ePrpp0l7E1R1qjZ2rw3FcXO5rFwVY29wKqcZqrYUXkN+fj4KCwtx4403Gm1fF3N1zdLBghOI2NhYfPDBB3ptpmsMCQkJKC4uBkEQpL0Jqp58xu61Md+Y0jRDUWE5VPQ1krOuwf1/+Qs++eSTOen+NNfXLJUsOIGYDywtLbF//3688847uO+++7Bjxw6dK0OpevLN9xTFWKEzpWlGUVERsrOzMdpYibrKUmy86Saj7P1RYARCDxITE1FWVobExEQsW7YMu3fvhkAgmPEaqoKKppQBmc8Yiq4AIxkaGhpw00034amnnsIrr7yCH78+CRcXF4PG8keEEQg9MTMzw7Zt21BZWQlnZ2ckJSXhwIEDysIhKqcX850BUbjl8yVQijEY0i2qu7sbW7duxZ/+9Cds374d586dQ2pqqkFj+CPDCISBWFtb46mnnkJRURGGhoYQHx+PO556VVlWu9CnF4DpbM6jz2fB4/Hw6KOPYs2aNVi1ahXKy8uxdu3aBVN3YGowAmEkTk5OOHDgAH788UdMdNTB78wzCBqtXfDTC4AaL4YKG2S6RU1OTuKFF15AamoqQkJCUFlZiS1btlDSj/SPDPPpUYS3tze++uwj/PzNSYQNFCM9PR3/+te/DCq0onKZuTFQ4cXQXRE5NDSEZ555BnFxcSAIAmVlZdi5cycsLCyMPh8DIxCUs3jxYnz3zWkcP34cZWVliI6OxnPPPYfh4WHSNqi6qUwhC0F1RaTiczl//jy2bNmC7OxseHh4oKSkBPv27YO9vb1R52CYCSMQNBEaGoojR46gqKgILi4uWL58ObZs2aJXuztjMYU4BlU2AOD1dYtxM6sW3+67D3/Z+Shuv/121NTU4JFHHoGDg4NRthk0wwgEzTg6OuKxxx5DdXU17rzzThw8eBBxcXHYv3+/1lb8ppDBUGAK04za2lrs2LEDMTExsOB14rv/foTi/HO48cYbmRgDzTCf7hzB4XBw3XXX4euvv8Z3330HAFi2bBnWrVuHN998c0bhlak8+eeTqqoqPPPMM0hOTsbDDz+Ma6+9FtXV1fjo6JGreh8KU4PpBzGPEASB6upqfPXVVzh79izMzMzgEZGMYutI3Hxt6rxkQlRbzgHU9GUgY0MqleKHH37AV199haKiIvj5+WHTpk244YYb4OXlZdB5GYxnQbWcu9pgsViIjY1FbGws9u3bh56eHnzx5Vfo+uzf+PLbf0D6QzY2bdqEFStWzNt28lS0f9NmY2RkBCdPnsS3336L+vp6pKWlYdOmTTh8+DBsbGyMHToDBTAehIkiFArx888/49SpUygpKUFISAiioqKQnJyMrKwsuLu76zze0Ce/ugdBVSzjgZPV+CKnAtdYD8BlvB01NTUQiUS47rrrsHHjRiQmJjLxBBOEEYgFgFwuR0NDA8rLy1FWVoby8nLw+XwEBAQgOjoaycnJyMzMhKenp/KY+ZxiyOVy1NfXo6CgABUVFbhw4QJauvvh4+eP9NQUrFqWjsTERGbqsABgBGKBIpPJcOnSJZSXlyv/Gx0dhY+PD7y8vDAMG7SKrREXFoSd1ycrW8bN9pQm40GIxWJ0dHSgo6MDXV1d6OrqQl9fH/r7+9HZ2YnR0VEEBQUhOTkZSUlJSEhI+ENtNnM1wQjEVYRcLkdrayu6u7vR29uLlvYO1Ld0QMIfQk9PD0ZHRyGXy6eDoR4esLe3B4fDgbm5OTgcDszMzCAQCGBrawupVAqpVIquUSEGx0SwlIogE40BAMzNzeHh4QFvb294e3vD19cX3t7e8PLyQmBgoM5NdhkWFoxA/AGZmJhAX18f+Hy+UggU/8lkMpiZmSn/Mzc3h5mZGdzd3eHh4cF0ev6DwQgEAwODVpiwMQMDg1YYgWBgYNAKIxAMDAxaYQSCgYFBK4xAMDAwaIURCAYGBq0wAsHAwKAVRiAYGBi0wggEA4DpnafeeOONBbvJLAM9MALBAGB61zBLS0tUVVXh2LFjBu9oznB1wQgEAwAgLy8PLBYLk5OTcHBwQFxc3HwPicEEYNZiMMxgcnISLBYLlpaW8z0UBhOAEQgGBgatMD0pGQBA596VzDPkjwsTg2AAMC0Cubm5OHbsGEZHR0EQhPI/hj8ujEAwKMnKyoKtrS2srKzmeygMJgIjEAxKTpw4gaGhIfB4vPkeCoOJwAQpGRgYtMJ4EAwMDFphBIKBgUErjEAwMDBohREIBgYGrTACwcDAoBVGIBgYGLTCCAQDA4NWGIFgYGDQyv8HrpBpr4N2PxkAAAAASUVORK5CYII=", "text/plain": [ "
" ] @@ -213,13 +218,190 @@ "for obji in obj.objectives:\n", " plot_grid(obji.constants[\"transforms\"][\"grid\"])" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using other Objectives\n", + "Above we used the convenience function for force balance objective, but we can also other objectives with this approach. There are some extra steps you need to apply though." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "eq = get(\"HELIOTRON\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Precomputing transforms\n", + "Timer: Precomputing transforms = 103 ms\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 104 ms\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 124 ms\n", + "Timer: Objective build = 12.9 ms\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "from desc.backend import jax\n", + "from desc.optimize import Optimizer\n", + "\n", + "grid1 = LinearGrid(\n", + " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2, 0.4]), sym=True\n", + ")\n", + "grid2 = LinearGrid(\n", + " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.6, 0.8, 1.0]), sym=True\n", + ")\n", + "\n", + "obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", + "obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", + "obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2)\n", + "\n", + "objs = [obj1, obj2, obj3]\n", + "for obji in objs:\n", + " obji.build(verbose=3)\n", + " obji = jax.device_put(obji, jax.devices(\"gpu\")[obji._device_id])\n", + " obji.things[0] = eq\n", + "\n", + "objective = ObjectiveFunction(objs)\n", + "objective.build(verbose=3)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "k = 1\n", + "R_modes = np.vstack(\n", + " (\n", + " [0, 0, 0],\n", + " eq.surface.R_basis.modes[\n", + " np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :\n", + " ],\n", + " )\n", + ")\n", + "Z_modes = eq.surface.Z_basis.modes[\n", + " np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :\n", + "]\n", + "constraints = (\n", + " FixBoundaryR(eq=eq, modes=R_modes),\n", + " FixBoundaryZ(eq=eq, modes=Z_modes),\n", + " FixPressure(eq=eq),\n", + " FixPsi(eq=eq),\n", + ")\n", + "# TODO: implement for proximal\n", + "optimizer = Optimizer(\"lsq-exact\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Building objective: lcfs R\n", + "Building objective: lcfs Z\n", + "Building objective: fixed pressure\n", + "Building objective: fixed Psi\n", + "Building objective: self_consistency R\n", + "Building objective: self_consistency Z\n", + "Building objective: lambda gauge\n", + "Building objective: axis R self consistency\n", + "Building objective: axis Z self consistency\n", + "Timer: Objective build = 367 ms\n", + "Timer: Linear constraint projection build = 2.84 sec\n", + "Number of parameters: 1614\n", + "Number of objectives: 1236\n", + "Timer: Initializing the optimization = 3.32 sec\n", + "\n", + "Starting optimization\n", + "Using method: lsq-exact\n", + "This should run on GPU id:0\n", + "This should run on GPU id:1\n", + "This should run on GPU id:2\n", + " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", + " 0 1 9.547e+04 3.162e+02 \n", + "This should run on GPU id:0\n", + "This should run on GPU id:1\n", + "This should run on GPU id:2\n", + " 1 6 4.883e+04 4.664e+04 5.955e+00 1.431e+02 \n", + "Warning: Maximum number of function evaluations has been exceeded.\n", + " Current function value: 4.883e+04\n", + " Total delta_x: 5.955e+00\n", + " Iterations: 1\n", + " Function evaluations: 6\n", + " Jacobian evaluations: 2\n", + "Timer: Solution time = 20.9 sec\n", + "Timer: Avg time per step = 10.4 sec\n", + "==============================================================================================================\n", + " Start --> End\n", + "Total (sum of squares): 9.547e+04 --> 4.883e+04, \n", + "Maximum absolute Quasi-symmetry (1,19) two-term error: 5.074e-01 --> 1.932e-01 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,19) two-term error: 9.317e-06 --> 1.989e-05 (T^3)\n", + "Average absolute Quasi-symmetry (1,19) two-term error: 1.363e-01 --> 2.698e-02 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,19) two-term error: 8.055e+00 --> 3.067e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.479e-04 --> 3.158e-04 (normalized)\n", + "Average absolute Quasi-symmetry (1,19) two-term error: 2.165e+00 --> 4.283e-01 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,19) two-term error: 1.686e+00 --> 1.997e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.764e-04 --> 9.557e-05 (T^3)\n", + "Average absolute Quasi-symmetry (1,19) two-term error: 4.168e-01 --> 2.352e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,19) two-term error: 2.676e+01 --> 3.170e+01 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,19) two-term error: 2.800e-03 --> 1.517e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,19) two-term error: 6.616e+00 --> 3.733e+00 (normalized)\n", + "Aspect ratio: 1.048e+01 --> 1.004e+01 (dimensionless)\n", + "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", + "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", + "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", + "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", + "==============================================================================================================\n" + ] + } + ], + "source": [ + "eq.optimize(\n", + " objective=objective,\n", + " constraints=constraints,\n", + " optimizer=optimizer,\n", + " maxiter=1,\n", + " verbose=3,\n", + " options={\n", + " \"initial_trust_ratio\": 1.0,\n", + " },\n", + ");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "desc-env", + "display_name": "desc-env [~/.conda/envs/desc-env/]", "language": "python", - "name": "python3" + "name": "conda_desc-env" }, "language_info": { "codemirror_mode": { @@ -231,7 +413,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.11.6" } }, "nbformat": 4, From 0d4cc4770e2956e37364303132bd349ff1e67f21 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 00:51:49 -0500 Subject: [PATCH 066/199] fix missing docs --- desc/objectives/_coils.py | 17 +++++++++++++++++ desc/objectives/_fast_ion.py | 1 + desc/objectives/_free_boundary.py | 2 ++ desc/objectives/_generic.py | 4 ++++ desc/objectives/_neoclassical.py | 1 + desc/objectives/_stability.py | 2 ++ 6 files changed, 27 insertions(+) diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index 8da36de2b9..a868b34ebd 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -246,6 +246,7 @@ def __init__( grid=grid, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -351,6 +352,7 @@ def __init__( grid=grid, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -451,6 +453,7 @@ def __init__( grid=grid, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -550,6 +553,7 @@ def __init__( grid=grid, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -659,6 +663,7 @@ def __init__( grid=grid, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -766,6 +771,7 @@ def __init__( use_softmin=False, softmin_alpha=1.0, dist_chunk_size=None, + device_id=0, ): from desc.coils import CoilSet @@ -944,6 +950,7 @@ def __init__( use_softmin=False, softmin_alpha=1.0, dist_chunk_size=None, + device_id=0, ): if target is None and bounds is None: bounds = (1, np.inf) @@ -1159,6 +1166,7 @@ def __init__( deriv_mode="auto", grid=None, name="coil arclength variance", + device_id=0, ): if target is None and bounds is None: target = 0 @@ -1175,6 +1183,7 @@ def __init__( deriv_mode=deriv_mode, grid=grid, name=name, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -1326,6 +1335,7 @@ def __init__( normalize_target=normalize_target, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -1523,6 +1533,7 @@ def __init__( normalize_target=normalize_target, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -1971,6 +1982,7 @@ def __init__( deriv_mode="auto", jac_chunk_size=None, name="linking current", + device_id=0, ): if target is None and bounds is None: target = 0 @@ -1991,6 +2003,7 @@ def __init__( deriv_mode=deriv_mode, jac_chunk_size=jac_chunk_size, name=name, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -2159,6 +2172,7 @@ def __init__( deriv_mode="auto", jac_chunk_size=None, name="coil-coil linking number", + device_id=0, ): from desc.coils import CoilSet @@ -2181,6 +2195,7 @@ def __init__( deriv_mode=deriv_mode, jac_chunk_size=jac_chunk_size, name=name, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): @@ -2302,6 +2317,7 @@ def __init__( deriv_mode="auto", source_grid=None, name="surface-current-regularization", + device_id=0, ): from desc.magnetic_fields import ( CurrentPotentialField, @@ -2329,6 +2345,7 @@ def __init__( loss_function=loss_function, deriv_mode=deriv_mode, name=name, + device_id=device_id, ) def build(self, use_jit=True, verbose=1): diff --git a/desc/objectives/_fast_ion.py b/desc/objectives/_fast_ion.py index 9f13b9ffcb..21f7eba847 100644 --- a/desc/objectives/_fast_ion.py +++ b/desc/objectives/_fast_ion.py @@ -171,6 +171,7 @@ def __init__( surf_batch_size=1, spline=False, Nemov=True, + device_id=0, ): if target is None and bounds is None: target = 0.0 diff --git a/desc/objectives/_free_boundary.py b/desc/objectives/_free_boundary.py index 1afafed87e..67d50d4242 100644 --- a/desc/objectives/_free_boundary.py +++ b/desc/objectives/_free_boundary.py @@ -81,6 +81,7 @@ def __init__( field_fixed=False, name="Vacuum boundary error", jac_chunk_size=None, + device_id=0, **kwargs, ): eval_grid = parse_argname_change(eval_grid, kwargs, "grid", "eval_grid") @@ -426,6 +427,7 @@ def __init__( chunk_size=1, name="Boundary error", jac_chunk_size=None, + device_id=0, **kwargs, ): if target is None and bounds is None: diff --git a/desc/objectives/_generic.py b/desc/objectives/_generic.py index 58b08db31b..3e5bd0109d 100644 --- a/desc/objectives/_generic.py +++ b/desc/objectives/_generic.py @@ -56,6 +56,7 @@ def __init__( name="generic", jac_chunk_size=None, compute_kwargs=None, + device_id=0, **kwargs, ): errorif( @@ -212,6 +213,7 @@ def __init__( normalize_target=normalize_target, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) def build(self, use_jit=False, verbose=1): @@ -337,6 +339,7 @@ def __init__( name="custom", jac_chunk_size=None, compute_kwargs=None, + device_id=0, **kwargs, ): errorif( @@ -361,6 +364,7 @@ def __init__( deriv_mode=deriv_mode, name=name, jac_chunk_size=jac_chunk_size, + device_id=device_id, ) self._p = _parse_parameterization(thing) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index d02ffed2cc..6e5783cb27 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -158,6 +158,7 @@ def __init__( pitch_batch_size=None, surf_batch_size=1, spline=False, + device_id=0, ): if target is None and bounds is None: target = 0.0 diff --git a/desc/objectives/_stability.py b/desc/objectives/_stability.py index e0b441b33d..dcce2b7051 100644 --- a/desc/objectives/_stability.py +++ b/desc/objectives/_stability.py @@ -417,6 +417,7 @@ def __init__( w0=1.0, w1=10.0, name="ideal ballooning lambda", + device_id=0, ): if target is None and bounds is None: target = 0 @@ -440,6 +441,7 @@ def __init__( loss_function=loss_function, deriv_mode=deriv_mode, name=name, + device_id=device_id, ) errorif( From 12ba4db8b3aae17e1c15d73600611685d1089eb2 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 02:05:20 -0500 Subject: [PATCH 067/199] initial test for proximal --- desc/optimize/_constraint_wrappers.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 7695a671b4..2d521ee121 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1104,7 +1104,24 @@ def __getattr__(self, name): # define these helper functions that are stateless so we can safely jit them -@functools.partial(jit, static_argnames=["op"]) +def jit_if_not_parallel(func): + """Jit a function if not in parallel mode.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + obj = args[0] + if getattr(obj, "_is_multi_device", False): + # Apply jit if jittable + jitted_func = functools.partial(jit, static_argnames=["op"])(func) + return jitted_func(*args, **kwargs) + else: + # Run normally if not jittable + return func(*args, **kwargs) + + return wrapper + + +@jit_if_not_parallel def _proximal_jvp_f_pure(constraint, xf, constants, dc, unfixed_idx, Z, D, dxdc, op): Fx = getattr(constraint, "jac_" + op)(xf, constants) Fx_reduced = Fx @ jnp.diag(D)[:, unfixed_idx] @ Z @@ -1118,7 +1135,7 @@ def _proximal_jvp_f_pure(constraint, xf, constants, dc, unfixed_idx, Z, D, dxdc, return Fxh_inv @ Fc -@functools.partial(jit, static_argnames=["op"]) +@jit_if_not_parallel def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): out = [] for k, (obj, const) in enumerate(zip(objective.objectives, objective.constants)): From f4468047def8e492aa3c85008f4131e0db5960c6 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 13:34:08 -0500 Subject: [PATCH 068/199] add obj._device attr for cleaner device_put --- desc/backend.py | 20 ++++++++++---------- desc/objectives/getters.py | 2 +- desc/objectives/objective_funs.py | 20 +++++++++----------- desc/optimize/_constraint_wrappers.py | 7 ++++++- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index e5bb0227f5..4665f291ce 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -461,8 +461,14 @@ def pconcat(arrays, mode="concat"): # pragma: no cover # array of float64 should fit into single device size = jnp.array([x.size for x in arrays]) size = jnp.sum(size) - if size * 8 / (1024**3) > desc_config["avail_mems"][0]: - if getattr(desc_config, "SUPPRESS_CPU_WARNING", False): + if ( + size * 8 / (1024**3) > desc_config["avail_mems"][0] + or desc_config["kind"] == "cpu" + ): + if ( + getattr(desc_config, "SUPPRESS_CPU_WARNING", False) + and desc_config["kind"] == "gpu" + ): warnings.warn( "The total size of the arrays exceeds the available memory of the " "GPU[id=0]. Moving the array to CPU. This may cause performance " @@ -487,7 +493,7 @@ def jit_with_device(method): Decorates a method of a class with a dynamic device, allowing the method to be compiled with jax.jit for the specific device. This is needed since - @functools.partial(jax.jit, device=jax.devices("gpu")[self._device_id]) is not + @functools.partial(jax.jit, device=obj._device) is not allowed in a class definition. Parameters @@ -499,14 +505,8 @@ def jit_with_device(method): @functools.wraps(method) def wrapper(self, *args, **kwargs): - # Get the device using self.id or default to CPU - if desc_config["kind"] == "gpu" and hasattr(self, "_device_id"): - device = jax.devices("gpu")[self._device_id] - else: - device = None - # Compile the method with jax.jit for the specific device - wrapped = jax.jit(method, device=device) + wrapped = jax.jit(method, device=self._device) return wrapped(self, *args, **kwargs) return wrapper diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index 662283dbea..f476de9648 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -408,7 +408,7 @@ def get_parallel_forcebalance( gridi = grid[i] obj = ForceBalance(eq, grid=gridi, device_id=i) obj.build(use_jit=use_jit) - obj = jax.device_put(obj, jax.devices("gpu")[i]) + obj = jax.device_put(obj, obj._device) # if the eq is also distrubuted across GPUs, then some internal logic # that checks if the things are different will fail, so we need to # set the eq to be the same manually diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index e5ea0c7785..3413acc9c4 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -483,8 +483,7 @@ def compute_unscaled(self, x, constants=None): f = pconcat( [ obj.compute_unscaled( - *jax.device_put(par, jax.devices("gpu")[obj._device_id]), - constants=const, + *jax.device_put(par, obj._device), constants=const ) for par, obj, const in zip(params, self.objectives, constants) ] @@ -523,8 +522,7 @@ def compute_scaled(self, x, constants=None): f = pconcat( [ obj.compute_scaled( - *jax.device_put(par, jax.devices("gpu")[obj._device_id]), - constants=const, + *jax.device_put(par, obj._device), constants=const ) for par, obj, const in zip(params, self.objectives, constants) ] @@ -563,8 +561,7 @@ def compute_scaled_error(self, x, constants=None): f = pconcat( [ obj.compute_scaled_error( - *jax.device_put(par, jax.devices("gpu")[obj._device_id]), - constants=const, + *jax.device_put(par, obj._device), constants=const ) for par, obj, const in zip(params, self.objectives, constants) ] @@ -634,13 +631,13 @@ def print_value(self, x, x0=None, constants=None): params, params0, self.objectives, constants ): if self._is_multi_device: # pragma: no cover - par = jax.device_put(par, jax.devices("gpu")[obj._device_id]) - par0 = jax.device_put(par0, jax.devices("gpu")[obj._device_id]) + par = jax.device_put(par, obj._device) + par0 = jax.device_put(par0, obj._device) obj.print_value(par, par0, constants=const) else: # pragma: no cover for par, obj, const in zip(params, self.objectives, constants): if self._is_multi_device: - par = jax.device_put(par, jax.devices("gpu")[obj._device_id]) + par = jax.device_put(par, obj._device) obj.print_value(par, constants=const) return None @@ -773,8 +770,8 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): if self._is_multi_device: # pragma: no cover # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective - xi = jax.device_put(xi, jax.devices("gpu")[obj._device_id]) - vi = jax.device_put(vi, jax.devices("gpu")[obj._device_id]) + xi = jax.device_put(xi, obj._device) + vi = jax.device_put(vi, obj._device) Ji_ = getattr(obj, "jvp_" + op)(vi, xi, constants=const) J += [Ji_] # this is the transpose of the jvp when v is a matrix, for consistency with @@ -1145,6 +1142,7 @@ def __init__( self._jac_chunk_size = jac_chunk_size self._device_id = device_id + self._device = jax.devices(desc_config["kind"])[device_id] self._target = target self._bounds = bounds diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 2d521ee121..3a2df245d6 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import jit, jnp +from desc.backend import jax, jit, jnp from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -1142,6 +1142,11 @@ def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): thing_idx = objective._things_per_objective_idx[k] xi = [xgs[i] for i in thing_idx] vi = [vgs[i] for i in thing_idx] + if objective._is_multi_device: # pragma: no cover + # inputs to jitted functions must live on the same device. Need to + # put xi and vi on the same device as the objective + xi = jax.device_put(xi, obj._device) + vi = jax.device_put(vi, obj._device) assert len(xi) > 0 assert len(vi) > 0 assert len(xi) == len(vi) From e5ed5cb300ddae0031a200bdac47d17a98e00e90 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 13:45:08 -0500 Subject: [PATCH 069/199] jit what you can, use pconcat --- desc/optimize/_constraint_wrappers.py | 30 +++++++++++++++++---------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 3a2df245d6..b33fbcf536 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import jax, jit, jnp +from desc.backend import jax, jit, jnp, pconcat from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -1124,15 +1124,20 @@ def wrapper(*args, **kwargs): @jit_if_not_parallel def _proximal_jvp_f_pure(constraint, xf, constants, dc, unfixed_idx, Z, D, dxdc, op): Fx = getattr(constraint, "jac_" + op)(xf, constants) - Fx_reduced = Fx @ jnp.diag(D)[:, unfixed_idx] @ Z - Fc = Fx @ (dxdc @ dc) - Fxh = Fx_reduced - cutoff = jnp.finfo(Fxh.dtype).eps * max(Fxh.shape) - uf, sf, vtf = jnp.linalg.svd(Fxh, full_matrices=False) - sf += sf[-1] # add a tiny bit of regularization - sfi = jnp.where(sf < cutoff * sf[0], 0, 1 / sf) - Fxh_inv = vtf.T @ (sfi[..., None] * uf.T) - return Fxh_inv @ Fc + + @jit + def fun(Fx, dxdc, dc, unfixed_idx, Z, D): + # F_reduced + Fxh = Fx @ jnp.diag(D)[:, unfixed_idx] @ Z + Fc = Fx @ (dxdc @ dc) + cutoff = jnp.finfo(Fxh.dtype).eps * max(Fxh.shape) + uf, sf, vtf = jnp.linalg.svd(Fxh, full_matrices=False) + sf += sf[-1] # add a tiny bit of regularization + sfi = jnp.where(sf < cutoff * sf[0], 0, 1 / sf) + Fxh_inv = vtf.T @ (sfi[..., None] * uf.T) + return Fxh_inv @ Fc + + return fun(Fx, dxdc, dc, unfixed_idx, Z, D) @jit_if_not_parallel @@ -1160,5 +1165,8 @@ def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): else: outi = getattr(obj, "jvp_" + op)([_vi for _vi in vi], xi, constants=const).T out.append(outi) - out = jnp.concatenate(out) + if objective._is_multi_device: # pragma: no cover + out = pconcat(out) + else: + out = jnp.concatenate(out) return out From 6dd7611314a869dc9d585bac7b7dc5ef7b96984f Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 14:05:34 -0500 Subject: [PATCH 070/199] fix device jit issue --- desc/objectives/objective_funs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 3413acc9c4..3c201911a7 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1114,6 +1114,8 @@ class _Objective(IOAble, ABC): "_normalization", "_deriv_mode", ] + # _device is of type jax.Device() which cannot be an argument to a jitted function. + _static_attrs = ["_device"] def __init__( self, From b3e961fccd0c95b9c444c038ef3eaf7fa9e0008c Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 14:33:07 -0500 Subject: [PATCH 071/199] make _device None for single device cases --- desc/objectives/objective_funs.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 3c201911a7..db0ed9df47 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1114,7 +1114,8 @@ class _Objective(IOAble, ABC): "_normalization", "_deriv_mode", ] - # _device is of type jax.Device() which cannot be an argument to a jitted function. + # _device is of type 'jaxlib.xla_extension.Device' which cannot be an argument + # to a jitted function. _static_attrs = ["_device"] def __init__( @@ -1144,7 +1145,15 @@ def __init__( self._jac_chunk_size = jac_chunk_size self._device_id = device_id - self._device = jax.devices(desc_config["kind"])[device_id] + # if device_id is not 0, this typically means we are using multiple devices and + # we won't jit the ObjectiveFunction methods. For single device, if we set + # _device to a jaxlib.xla_extension.Device type, jit will throw error expecting + # it to be static. So we set _device to None in that case which is simpler then + # making it static. + if device_id != 0: + self._device = jax.devices(desc_config["kind"])[device_id] + else: + self._device = None self._target = target self._bounds = bounds From bfb371ce84159b16951aff8f2b5db4fd10b5feaf Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 15:08:32 -0500 Subject: [PATCH 072/199] ok now it is fixed --- desc/optimize/_constraint_wrappers.py | 5 +- docs/notebooks/tutorials/multi_device.ipynb | 299 +++++++++++++++----- 2 files changed, 228 insertions(+), 76 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index b33fbcf536..5503a39e57 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1110,7 +1110,7 @@ def jit_if_not_parallel(func): @functools.wraps(func) def wrapper(*args, **kwargs): obj = args[0] - if getattr(obj, "_is_multi_device", False): + if not getattr(obj, "_is_multi_device", False): # Apply jit if jittable jitted_func = functools.partial(jit, static_argnames=["op"])(func) return jitted_func(*args, **kwargs) @@ -1144,6 +1144,9 @@ def fun(Fx, dxdc, dc, unfixed_idx, Z, D): def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): out = [] for k, (obj, const) in enumerate(zip(objective.objectives, objective.constants)): + # TODO: this is for debugging purposes, must be deleted before merging! + if objective._is_multi_device: + print(f"This should run on GPU id:{obj._device_id}") thing_idx = objective._things_per_objective_idx[k] xi = [xgs[i] for i in thing_idx] vi = [vgs[i] for i in thing_idx] diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index b0b5e2f40d..df1bc9d14c 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -8,7 +8,8 @@ } }, "source": [ - "# Multi-Gpu Equilibrium Solve" + "# How to use Multiple Devices\n", + "## Solving Equilibrium" ] }, { @@ -30,7 +31,7 @@ "metadata": {}, "outputs": [], "source": [ - "num_device = 3\n", + "num_device = 2\n", "from desc import set_device\n", "\n", "set_device(\"gpu\", num_device=num_device)" @@ -45,22 +46,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version 0.13.0+1523.ge2c0f7767.dirty,using JAX backend, jax version=0.4.38, jaxlib version=0.4.38, dtype=float64\n", - "CPU Info: AMD EPYC 7453 28-Core Processor CPU with 978.07 GB available memory\n", - "Using 3 device:\n", + "DESC version 0.13.0+1530.gb3e961fcc.dirty,using JAX backend, jax version=0.4.38, jaxlib version=0.4.38, dtype=float64\n", + "CPU Info: AMD EPYC 7453 28-Core Processor CPU with 968.37 GB available memory\n", + "Using 2 device:\n", "\t Device 0: NVIDIA A100-SXM4-40GB (id=0) with 40.00 GB available memory\n", - "\t Device 1: NVIDIA A100-SXM4-40GB (id=1) with 40.00 GB available memory\n", - "\t Device 2: NVIDIA A100-SXM4-40GB (id=2) with 40.00 GB available memory\n" + "\t Device 1: NVIDIA A100-SXM4-40GB (id=1) with 40.00 GB available memory\n" ] } ], "source": [ + "import numpy as np\n", + "\n", "from desc.examples import get\n", "from desc.objectives import *\n", "from desc.objectives.getters import *\n", "from desc.grid import LinearGrid\n", "from desc.backend import jnp\n", - "from desc.plotting import plot_grid" + "from desc.plotting import plot_grid\n", + "from desc.backend import jax\n", + "from desc.optimize import Optimizer" ] }, { @@ -81,7 +85,6 @@ "name": "stdout", "output_type": "stream", "text": [ - "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n" ] @@ -118,52 +121,44 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.74 sec\n", - "Timer: Linear constraint projection build = 7.48 sec\n", + "Timer: Objective build = 1.87 sec\n", + "Timer: Linear constraint projection build = 7.52 sec\n", "Number of parameters: 1593\n", "Number of objectives: 34632\n", - "Timer: Initializing the optimization = 9.34 sec\n", + "Timer: Initializing the optimization = 9.51 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", "This should run on GPU id:0\n", "This should run on GPU id:1\n", - "This should run on GPU id:2\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 5.161e-07 1.557e-04 \n", + " 0 1 3.654e-07 1.803e-04 \n", "This should run on GPU id:0\n", "This should run on GPU id:1\n", - "This should run on GPU id:2\n", - " 1 5 2.629e-07 2.531e-07 3.371e-03 1.961e-04 \n", - "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 2.629e-07\n", - " Total delta_x: 3.371e-03\n", + " 1 6 2.102e-07 1.552e-07 1.244e-03 5.327e-05 \n", + "Warning: Maximum number of function evaluations has been exceeded.\n", + " Current function value: 2.102e-07\n", + " Total delta_x: 1.244e-03\n", " Iterations: 1\n", - " Function evaluations: 5\n", + " Function evaluations: 6\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 28.5 sec\n", - "Timer: Avg time per step = 14.2 sec\n", + "Timer: Solution time = 27.0 sec\n", + "Timer: Avg time per step = 13.5 sec\n", "==============================================================================================================\n", " Start --> End\n", - "Total (sum of squares): 5.161e-07 --> 2.629e-07, \n", - "Maximum absolute Force error: 1.378e+02 --> 2.419e+02 (N)\n", - "Minimum absolute Force error: 1.059e-10 --> 1.060e-10 (N)\n", - "Average absolute Force error: 2.932e+01 --> 1.966e+01 (N)\n", - "Maximum absolute Force error: 1.108e-05 --> 1.946e-05 (normalized)\n", - "Minimum absolute Force error: 8.517e-18 --> 8.524e-18 (normalized)\n", - "Average absolute Force error: 2.358e-06 --> 1.581e-06 (normalized)\n", - "Maximum absolute Force error: 2.276e+02 --> 3.279e+02 (N)\n", - "Minimum absolute Force error: 1.271e-10 --> 1.282e-10 (N)\n", - "Average absolute Force error: 3.359e+01 --> 2.870e+01 (N)\n", - "Maximum absolute Force error: 1.831e-05 --> 2.637e-05 (normalized)\n", - "Minimum absolute Force error: 1.022e-17 --> 1.031e-17 (normalized)\n", - "Average absolute Force error: 2.702e-06 --> 2.308e-06 (normalized)\n", - "Maximum absolute Force error: 8.201e+03 --> 6.277e+03 (N)\n", - "Minimum absolute Force error: 1.635e-12 --> 3.597e-12 (N)\n", - "Average absolute Force error: 8.964e+01 --> 7.837e+01 (N)\n", - "Maximum absolute Force error: 6.596e-04 --> 5.048e-04 (normalized)\n", - "Minimum absolute Force error: 1.315e-19 --> 2.893e-19 (normalized)\n", - "Average absolute Force error: 7.209e-06 --> 6.303e-06 (normalized)\n", + "Total (sum of squares): 3.654e-07 --> 2.102e-07, \n", + "Maximum absolute Force error: 1.378e+02 --> 2.528e+02 (N)\n", + "Minimum absolute Force error: 1.059e-10 --> 1.059e-10 (N)\n", + "Average absolute Force error: 2.610e+01 --> 1.893e+01 (N)\n", + "Maximum absolute Force error: 1.108e-05 --> 2.033e-05 (normalized)\n", + "Minimum absolute Force error: 8.517e-18 --> 8.519e-18 (normalized)\n", + "Average absolute Force error: 2.099e-06 --> 1.522e-06 (normalized)\n", + "Maximum absolute Force error: 8.201e+03 --> 6.285e+03 (N)\n", + "Minimum absolute Force error: 1.635e-12 --> 2.274e-12 (N)\n", + "Average absolute Force error: 8.007e+01 --> 7.106e+01 (N)\n", + "Maximum absolute Force error: 6.596e-04 --> 5.055e-04 (normalized)\n", + "Minimum absolute Force error: 1.315e-19 --> 1.829e-19 (normalized)\n", + "Average absolute Force error: 6.440e-06 --> 5.715e-06 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", @@ -185,17 +180,7 @@ "outputs": [ { "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -205,7 +190,7 @@ }, { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -223,7 +208,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Using other Objectives\n", + "## Using other Objectives\n", "Above we used the convenience function for force balance objective, but we can also other objectives with this approach. There are some extra steps you need to apply though." ] }, @@ -238,7 +223,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -246,20 +231,16 @@ "output_type": "stream", "text": [ "Precomputing transforms\n", - "Timer: Precomputing transforms = 103 ms\n", + "Timer: Precomputing transforms = 1.22 sec\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 104 ms\n", + "Timer: Precomputing transforms = 1.21 sec\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 124 ms\n", - "Timer: Objective build = 12.9 ms\n" + "Timer: Precomputing transforms = 1.23 sec\n", + "Timer: Objective build = 14.1 ms\n" ] } ], "source": [ - "import numpy as np\n", - "from desc.backend import jax\n", - "from desc.optimize import Optimizer\n", - "\n", "grid1 = LinearGrid(\n", " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2, 0.4]), sym=True\n", ")\n", @@ -269,12 +250,12 @@ "\n", "obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", "obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", - "obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2)\n", + "obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=1)\n", "\n", "objs = [obj1, obj2, obj3]\n", "for obji in objs:\n", " obji.build(verbose=3)\n", - " obji = jax.device_put(obji, jax.devices(\"gpu\")[obji._device_id])\n", + " obji = jax.device_put(obji, obji._device)\n", " obji.things[0] = eq\n", "\n", "objective = ObjectiveFunction(objs)\n", @@ -283,7 +264,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -305,13 +286,12 @@ " FixPressure(eq=eq),\n", " FixPsi(eq=eq),\n", ")\n", - "# TODO: implement for proximal\n", "optimizer = Optimizer(\"lsq-exact\")" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -327,22 +307,22 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 367 ms\n", - "Timer: Linear constraint projection build = 2.84 sec\n", + "Timer: Objective build = 385 ms\n", + "Timer: Linear constraint projection build = 3.30 sec\n", "Number of parameters: 1614\n", "Number of objectives: 1236\n", - "Timer: Initializing the optimization = 3.32 sec\n", + "Timer: Initializing the optimization = 3.80 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", "This should run on GPU id:0\n", "This should run on GPU id:1\n", - "This should run on GPU id:2\n", + "This should run on GPU id:1\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 9.547e+04 3.162e+02 \n", "This should run on GPU id:0\n", "This should run on GPU id:1\n", - "This should run on GPU id:2\n", + "This should run on GPU id:1\n", " 1 6 4.883e+04 4.664e+04 5.955e+00 1.431e+02 \n", "Warning: Maximum number of function evaluations has been exceeded.\n", " Current function value: 4.883e+04\n", @@ -350,8 +330,8 @@ " Iterations: 1\n", " Function evaluations: 6\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 20.9 sec\n", - "Timer: Avg time per step = 10.4 sec\n", + "Timer: Solution time = 26.0 sec\n", + "Timer: Avg time per step = 13.0 sec\n", "==============================================================================================================\n", " Start --> End\n", "Total (sum of squares): 9.547e+04 --> 4.883e+04, \n", @@ -389,6 +369,175 @@ ");" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimization using Proximal Method" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "eq = get(\"precise_QA\")\n", + "eq.change_resolution(12,12,12,24,24,24)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Precomputing transforms\n", + "Timer: Precomputing transforms = 114 ms\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 118 ms\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 86.1 ms\n", + "Timer: Objective build = 10.5 ms\n" + ] + } + ], + "source": [ + "grid1 = LinearGrid(\n", + " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.2, 0.5, 4), sym=True\n", + ")\n", + "grid2 = LinearGrid(\n", + " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.6, 1.0, 6), sym=True\n", + ")\n", + "\n", + "obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", + "obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=0)\n", + "obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0)\n", + "\n", + "objs = [obj1, obj2, obj3]\n", + "for obji in objs:\n", + " obji.build(verbose=3)\n", + " obji = jax.device_put(obji, obji._device)\n", + " obji.things[0] = eq\n", + "\n", + "objective = ObjectiveFunction(objs)\n", + "objective.build(verbose=3)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "k = 12\n", + "R_modes = np.vstack(\n", + " (\n", + " [0, 0, 0],\n", + " eq.surface.R_basis.modes[\n", + " np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :\n", + " ],\n", + " )\n", + ")\n", + "Z_modes = eq.surface.Z_basis.modes[\n", + " np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :\n", + "]\n", + "constraints = (\n", + " ForceBalance(eq=eq),\n", + " FixBoundaryR(eq=eq, modes=R_modes),\n", + " FixBoundaryZ(eq=eq, modes=Z_modes),\n", + " FixPressure(eq=eq),\n", + " FixPsi(eq=eq),\n", + " FixCurrent(eq=eq),\n", + ")\n", + "optimizer = Optimizer(\"proximal-lsq-exact\")" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Building objective: force\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 133 ms\n", + "Timer: Objective build = 161 ms\n", + "Timer: Proximal projection build = 2.08 sec\n", + "Building objective: lcfs R\n", + "Building objective: lcfs Z\n", + "Building objective: fixed pressure\n", + "Building objective: fixed Psi\n", + "Building objective: fixed current\n", + "Timer: Objective build = 139 ms\n", + "Timer: Linear constraint projection build = 2.18 sec\n", + "Number of parameters: 624\n", + "Number of objectives: 12251\n", + "Timer: Initializing the optimization = 4.67 sec\n", + "\n", + "Starting optimization\n", + "Using method: proximal-lsq-exact\n", + " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", + " 0 1 2.003e+04 1.949e+02 \n", + " 1 5 1.551e+04 4.515e+03 2.594e-02 1.685e+02 \n", + "Warning: Maximum number of iterations has been exceeded.\n", + " Current function value: 1.551e+04\n", + " Total delta_x: 2.594e-02\n", + " Iterations: 1\n", + " Function evaluations: 5\n", + " Jacobian evaluations: 2\n", + "Timer: Solution time = 1.89 min\n", + "Timer: Avg time per step = 56.9 sec\n", + "==============================================================================================================\n", + " Start --> End\n", + "Total (sum of squares): 2.003e+04 --> 1.551e+04, \n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.058e-01 --> 2.625e-01 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.547e-05 --> 1.156e-05 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 5.258e-02 --> 7.294e-02 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.155e-01 --> 2.864e-01 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.688e-05 --> 1.261e-05 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 5.737e-02 --> 7.959e-02 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 2.241e-01 --> 5.706e-01 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.994e-05 --> 2.975e-05 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 9.189e-02 --> 1.198e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 2.446e-01 --> 6.226e-01 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.176e-05 --> 3.246e-05 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.003e-01 --> 1.307e-01 (normalized)\n", + "Aspect ratio: 6.000e+00 --> 6.242e+00 (dimensionless)\n", + "Maximum absolute Force error: 1.757e+01 --> 4.893e+01 (N)\n", + "Minimum absolute Force error: 1.160e-05 --> 1.148e-04 (N)\n", + "Average absolute Force error: 3.140e-01 --> 6.310e-01 (N)\n", + "Maximum absolute Force error: 1.256e-05 --> 3.498e-05 (normalized)\n", + "Minimum absolute Force error: 8.291e-12 --> 8.205e-11 (normalized)\n", + "Average absolute Force error: 2.244e-07 --> 4.511e-07 (normalized)\n", + "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", + "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", + "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", + "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", + "Fixed current profile error: 0.000e+00 --> 0.000e+00 (A)\n", + "==============================================================================================================\n" + ] + } + ], + "source": [ + "eq.optimize(\n", + " objective=objective,\n", + " constraints=constraints,\n", + " optimizer=optimizer,\n", + " maxiter=1,\n", + " verbose=3,\n", + " options={\n", + " \"initial_trust_ratio\": 1.0,\n", + " },\n", + ");" + ] + }, { "cell_type": "code", "execution_count": null, From 62e827ec8bdaabd935a3f4f1285b03c3c8fd0dff Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 13 Feb 2025 20:16:24 -0500 Subject: [PATCH 073/199] implement multicpu, add a test, need to make it work tho --- desc/__init__.py | 56 ++- desc/backend.py | 17 +- desc/objectives/getters.py | 15 +- desc/objectives/objective_funs.py | 28 +- docs/notebooks/tutorials/multi_device.ipynb | 445 +++++++++++++------- setup.cfg | 1 + tests/test_multidevice.py | 63 +++ 7 files changed, 446 insertions(+), 179 deletions(-) create mode 100644 tests/test_multidevice.py diff --git a/desc/__init__.py b/desc/__init__.py index 4aa8da27e6..37428255e3 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -81,7 +81,39 @@ def _get_processor_name(): return "" -def set_device(kind="cpu", gpuid=None, num_device=1): +def _set_cpu_count(n): + """Set the number of CPUs visible to JAX. + + By default, JAX sees the whole CPU as a single device, regardless of the number of + cores or threads. It then uses multiple cores and threads for lower level + parallelism within individual operations. + + Alternatively, you can force JAX to expose a given number of "virtual" CPUs that + can then be used manually for higher level parallelism (as in at the level of + multiple objective functions.) + + This function is mainly for testing on CI purposes of the parallelism in DESC. + + Parameters + ---------- + n : int + Number of virtual CPUs for high level parallelism. + + Notes + ----- + This function must be called before importing anything else from DESC or JAX, + and before calling ``desc.set_device``, otherwise it will have no effect. + """ + xla_flags = os.getenv("XLA_FLAGS", "") + xla_flags = re.sub( + r"--xla_force_host_platform_device_count=\S+", "", xla_flags + ).split() + os.environ["XLA_FLAGS"] = " ".join( + [f"--xla_force_host_platform_device_count={n}"] + xla_flags + ) + + +def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 """Sets the device to use for computation. If kind==``'gpu'`` and a gpuid is specified, uses the specified GPU. If @@ -105,10 +137,6 @@ def set_device(kind="cpu", gpuid=None, num_device=1): number of devices to use. Default is 1. """ - if kind == "cpu" and num_device > 1: - # TODO: implement multi-CPU support - raise ValueError("Cannot request multiple CPUs") - config["kind"] = kind config["num_device"] = num_device @@ -120,8 +148,22 @@ def set_device(kind="cpu", gpuid=None, num_device=1): if kind == "cpu": os.environ["JAX_PLATFORMS"] = "cpu" os.environ["CUDA_VISIBLE_DEVICES"] = "" - config["devices"] = [f"{cpu_info} CPU"] - config["avail_mems"] = [cpu_mem] + if num_device == 1: + config["devices"] = [f"{cpu_info} CPU"] + config["avail_mems"] = [cpu_mem] + else: + try: + import jax + + jax_cpu = jax.devices("cpu") + assert len(jax_cpu) == num_device + config["devices"] = [f"{dev}" for dev in jax_cpu] + config["avail_mems"] = [cpu_mem for _ in range(num_device)] + except ModuleNotFoundError: + raise ValueError( + "JAX not installed. Please install JAX to use multiple CPUs." + "Alternatively, set num_device=1 to use a single CPU." + ) elif kind == "gpu": os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" diff --git a/desc/backend.py b/desc/backend.py index 4665f291ce..96e04a6b2d 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -69,10 +69,19 @@ ) ) -print( - f"CPU Info: {desc_config['cpu_info']} with {desc_config['cpu_mem']:.2f} " - "GB available memory" -) +if desc_config["num_device"] == 1: + print( + f"CPU Info: {desc_config['cpu_info']} with {desc_config['cpu_mem']:.2f} " + "GB available memory" + ) +elif desc_config["kind"] == "cpu": + print(f"Using {desc_config['num_device']} CPUs:") + for i, dev in enumerate(desc_config["devices"]): + print( + f"\t CPU {i}: {dev} with {desc_config['avail_mems'][i]:.2f} " + "GB available memory" + ) + if desc_config["kind"] == "gpu": print(f"Using {desc_config['num_device']} device:") for i, dev in enumerate(desc_config["devices"]): diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index f476de9648..fba28f78a3 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -346,9 +346,7 @@ def add_if_multiple(constraints, cls): return constraints -def get_parallel_forcebalance( - eq, num_device, grid=None, use_jit=True, check_device=True -): # pragma: no cover +def get_parallel_forcebalance(eq, num_device, grid=None, use_jit=True): """Get an ObjectiveFunction for parallel computing ForceBalance. Parameters @@ -367,15 +365,10 @@ def get_parallel_forcebalance( from desc.backend import desc_config, jax, jnp from desc.grid import LinearGrid - if desc_config["kind"] != "gpu": - raise ValueError( - "Parallel computing is only supported on GPU. " - "Please use DESC with GPU device." - ) - if desc_config["num_device"] != num_device and check_device: + if desc_config["num_device"] < num_device: raise ValueError( f"Number of devices in desc_config ({desc_config['num_device']}) " - f"does not match the number of devices in input ({num_device})." + f"is less than the number of devices in input ({num_device})." ) if grid is not None: if len(grid) != num_device: @@ -414,6 +407,6 @@ def get_parallel_forcebalance( # set the eq to be the same manually obj._things[0] = eq objs += (obj,) - objective = ObjectiveFunction(objs, deriv_mode="blocked") + objective = ObjectiveFunction(objs) objective.build(use_jit=use_jit) return objective diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index db0ed9df47..d8560210a7 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -349,7 +349,7 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 errorif( isposint(self._jac_chunk_size) and self._deriv_mode in ["auto", "blocked"], ValueError, - "'jac_chunk_size' was passed into ObjectiveFunction, but the " + "\n'jac_chunk_size' was passed into ObjectiveFunction, but the \n" "ObjectiveFunction is not using 'batched' deriv_mode", ) sub_obj_jac_chunk_sizes_are_ints = [ @@ -358,11 +358,11 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 errorif( any(sub_obj_jac_chunk_sizes_are_ints) and self._deriv_mode == "batched", ValueError, - "'jac_chunk_size' was passed into one or more sub-objectives, but the" - " ObjectiveFunction is using 'batched' deriv_mode, so sub-objective " - "'jac_chunk_size' will be ignored in favor of the ObjectiveFunction's " - f"'jac_chunk_size' of {self._jac_chunk_size}." - " Specify 'blocked' deriv_mode if each sub-objective is desired to have a " + "\n'jac_chunk_size' was passed into one or more sub-objectives, but the\n" + " ObjectiveFunction is using 'batched' deriv_mode, so sub-objective \n" + "'jac_chunk_size' will be ignored in favor of the ObjectiveFunction's \n" + f"'jac_chunk_size' of {self._jac_chunk_size}.\n" + " Specify 'blocked' deriv_mode if each sub-objective is desired to have a\n" "different 'jac_chunk_size' for its Jacobian computation.", ) @@ -372,11 +372,15 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 else: self._deriv_mode = "blocked" - if self._is_multi_device and self._deriv_mode != "blocked": - raise ValueError( - "When using multiple GPUs, the deriv_mode must be set to 'blocked'. " - "When you are creating the ObjectiveFunction, set deriv_mode='blocked'." - ) + warnif( + self._is_multi_device and self._deriv_mode != "blocked", + UserWarning, + "\nWhen using multiple devices, the ObjectiveFunction will run each \n" + "sub-objective on the device specified in the sub-objective. \n" + "Setting the deriv_mode to 'blocked' to ensure that each sub-objective\n" + "runs on the correct device.", + ) + self._deriv_mode != "blocked" if self._jac_chunk_size == "auto": # Heuristic estimates of fwd mode Jacobian memory usage, @@ -762,7 +766,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): for k, (obj, const) in enumerate(zip(self.objectives, constants)): # TODO: this is for debugging purposes, must be deleted before merging! if self._is_multi_device: - print(f"This should run on GPU id:{obj._device_id}") + print(f"This should run on device id:{obj._device_id}") # get the xs that go to that objective thing_idx = self._things_per_objective_idx[k] xi = [xs[i] for i in thing_idx] diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index df1bc9d14c..b8ce359801 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -31,10 +31,11 @@ "metadata": {}, "outputs": [], "source": [ - "num_device = 2\n", - "from desc import set_device\n", + "num_device = 4\n", + "from desc import set_device, _set_cpu_count\n", "\n", - "set_device(\"gpu\", num_device=num_device)" + "_set_cpu_count(num_device)\n", + "set_device(\"cpu\", num_device=num_device)" ] }, { @@ -46,11 +47,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version 0.13.0+1530.gb3e961fcc.dirty,using JAX backend, jax version=0.4.38, jaxlib version=0.4.38, dtype=float64\n", - "CPU Info: AMD EPYC 7453 28-Core Processor CPU with 968.37 GB available memory\n", - "Using 2 device:\n", - "\t Device 0: NVIDIA A100-SXM4-40GB (id=0) with 40.00 GB available memory\n", - "\t Device 1: NVIDIA A100-SXM4-40GB (id=1) with 40.00 GB available memory\n" + "DESC version 0.13.0+1539.gb6b43370b.dirty,using JAX backend, jax version=0.4.38, jaxlib version=0.4.38, dtype=float64\n", + "Using 4 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 6.26 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 6.26 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 6.26 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 6.26 GB available memory\n" ] } ], @@ -71,9 +73,19 @@ "cell_type": "code", "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/yigit/Codes/DESC/desc/utils.py:560: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n" + ] + } + ], "source": [ - "eq = get(\"HELIOTRON\")" + "eq = get(\"HELIOTRON\")\n", + "eq.change_resolution(3, 3, 3, 6, 6, 6)" ] }, { @@ -85,14 +97,40 @@ "name": "stdout", "output_type": "stream", "text": [ + "Precomputing transforms\n", + "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n" ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/yigit/Codes/DESC/desc/utils.py:560: UserWarning: \n", + "When using multiple devices, the ObjectiveFunction will run each \n", + "sub-objective on the device specified in the sub-objective. \n", + "Setting the deriv_mode to 'blocked' to ensure that each sub-objective \n", + "runs on the correct device.\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n", + "TFRT_CPU_1\n", + "TFRT_CPU_2\n", + "TFRT_CPU_3\n" + ] } ], "source": [ - "obj = get_parallel_forcebalance(eq, num_device=num_device, check_device=False)\n", - "cons = get_fixed_boundary_constraints(eq)" + "obj = get_parallel_forcebalance(eq, num_device=num_device)\n", + "cons = get_fixed_boundary_constraints(eq)\n", + "for obji in obj.objectives:\n", + " print(obji._device)" ] }, { @@ -121,44 +159,52 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.87 sec\n", - "Timer: Linear constraint projection build = 7.52 sec\n", - "Number of parameters: 1593\n", - "Number of objectives: 34632\n", - "Timer: Initializing the optimization = 9.51 sec\n", + "Timer: Objective build = 1.53 sec\n", + "Timer: Linear constraint projection build = 3.80 sec\n", + "Number of parameters: 76\n", + "Number of objectives: 2704\n", + "Timer: Initializing the optimization = 5.36 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", - "This should run on GPU id:0\n", - "This should run on GPU id:1\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 3.654e-07 1.803e-04 \n", - "This should run on GPU id:0\n", - "This should run on GPU id:1\n", - " 1 6 2.102e-07 1.552e-07 1.244e-03 5.327e-05 \n", - "Warning: Maximum number of function evaluations has been exceeded.\n", - " Current function value: 2.102e-07\n", - " Total delta_x: 1.244e-03\n", + " 0 1 8.573e+06 4.135e+03 \n", + " 1 2 7.294e+05 7.844e+06 5.352e-01 6.490e+02 \n", + "Warning: Maximum number of iterations has been exceeded.\n", + " Current function value: 7.294e+05\n", + " Total delta_x: 5.352e-01\n", " Iterations: 1\n", - " Function evaluations: 6\n", + " Function evaluations: 2\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 27.0 sec\n", - "Timer: Avg time per step = 13.5 sec\n", + "Timer: Solution time = 13.4 sec\n", + "Timer: Avg time per step = 6.73 sec\n", "==============================================================================================================\n", " Start --> End\n", - "Total (sum of squares): 3.654e-07 --> 2.102e-07, \n", - "Maximum absolute Force error: 1.378e+02 --> 2.528e+02 (N)\n", - "Minimum absolute Force error: 1.059e-10 --> 1.059e-10 (N)\n", - "Average absolute Force error: 2.610e+01 --> 1.893e+01 (N)\n", - "Maximum absolute Force error: 1.108e-05 --> 2.033e-05 (normalized)\n", - "Minimum absolute Force error: 8.517e-18 --> 8.519e-18 (normalized)\n", - "Average absolute Force error: 2.099e-06 --> 1.522e-06 (normalized)\n", - "Maximum absolute Force error: 8.201e+03 --> 6.285e+03 (N)\n", - "Minimum absolute Force error: 1.635e-12 --> 2.274e-12 (N)\n", - "Average absolute Force error: 8.007e+01 --> 7.106e+01 (N)\n", - "Maximum absolute Force error: 6.596e-04 --> 5.055e-04 (normalized)\n", - "Minimum absolute Force error: 1.315e-19 --> 1.829e-19 (normalized)\n", - "Average absolute Force error: 6.440e-06 --> 5.715e-06 (normalized)\n", + "Total (sum of squares): 2.844e+10 --> 7.294e+05, \n", + "Maximum absolute Force error: 2.650e+05 --> 1.299e+05 (N)\n", + "Minimum absolute Force error: 1.534e-10 --> 1.681e-10 (N)\n", + "Average absolute Force error: 9.802e+04 --> 4.428e+04 (N)\n", + "Maximum absolute Force error: 2.131e-02 --> 1.045e-02 (normalized)\n", + "Minimum absolute Force error: 1.234e-17 --> 1.352e-17 (normalized)\n", + "Average absolute Force error: 7.883e-03 --> 3.561e-03 (normalized)\n", + "Maximum absolute Force error: 4.785e+05 --> 3.814e+05 (N)\n", + "Minimum absolute Force error: 1.889e-10 --> 1.945e-10 (N)\n", + "Average absolute Force error: 1.791e+05 --> 1.171e+05 (N)\n", + "Maximum absolute Force error: 3.848e-02 --> 3.067e-02 (normalized)\n", + "Minimum absolute Force error: 1.519e-17 --> 1.565e-17 (normalized)\n", + "Average absolute Force error: 1.441e-02 --> 9.422e-03 (normalized)\n", + "Maximum absolute Force error: 8.926e+06 --> 2.599e+06 (N)\n", + "Minimum absolute Force error: 9.420e-11 --> 1.601e-10 (N)\n", + "Average absolute Force error: 4.594e+05 --> 1.831e+05 (N)\n", + "Maximum absolute Force error: 7.178e-01 --> 2.091e-01 (normalized)\n", + "Minimum absolute Force error: 7.576e-18 --> 1.287e-17 (normalized)\n", + "Average absolute Force error: 3.695e-02 --> 1.473e-02 (normalized)\n", + "Maximum absolute Force error: 6.431e+12 --> 2.805e+10 (N)\n", + "Minimum absolute Force error: 7.111e-13 --> 5.213e-11 (N)\n", + "Average absolute Force error: 4.122e+09 --> 3.074e+07 (N)\n", + "Maximum absolute Force error: 5.172e+05 --> 2.256e+03 (normalized)\n", + "Minimum absolute Force error: 5.719e-20 --> 4.193e-18 (normalized)\n", + "Average absolute Force error: 3.315e+02 --> 2.472e+00 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", @@ -167,10 +213,33 @@ "Fixed sheet current error: 0.000e+00 --> 0.000e+00 (~)\n", "==============================================================================================================\n" ] + }, + { + "data": { + "text/plain": [ + "(Equilibrium at 0x7e5726b83f50 (L=3, M=3, N=3, NFP=19, sym=True, spectral_indexing=fringe),\n", + " message: Maximum number of iterations has been exceeded.\n", + " success: False\n", + " fun: [-9.316e-05 -9.293e-05 ... 2.140e-02 -4.778e-03]\n", + " x: [-2.477e-02 -1.206e-01 ... 7.442e-03 1.768e-01]\n", + " nit: 1\n", + " cost: 729415.2017973666\n", + " v: [ 1.000e+00 1.000e+00 ... 1.000e+00 1.000e+00]\n", + " optimality: 649.0096310587396\n", + " nfev: 2\n", + " njev: 2\n", + " allx: [Array([-3.392e-05, 8.921e-06, ..., 0.000e+00, 0.000e+00], dtype=float64), Array([ 2.416e-05, 1.501e-03, ..., 0.000e+00, 0.000e+00], dtype=float64)]\n", + " alltr: [Array( 1.402e+06, dtype=float64), np.float64(1401525.8219770438)]\n", + " history: [[{'R_lmn': Array([-3.392e-05, 8.921e-06, ..., 0.000e+00, 1.850e-05], dtype=float64), 'Z_lmn': Array([ 9.011e-06, 1.167e-05, ..., -3.697e-05, 1.686e-05], dtype=float64), 'L_lmn': Array([-6.194e-07, -1.567e-05, ..., -9.721e-06, -1.466e-05], dtype=float64), 'p_l': Array([ 1.800e+04, -3.600e+04, ..., 0.000e+00, 0.000e+00], dtype=float64), 'i_l': Array([ 1.000e+00, 1.500e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'c_l': Array([], shape=(0,), dtype=float64), 'Psi': Array([ 1.000e+00], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.039e+01, 1.019e-01, 1.330e-03, 1.737e-05], dtype=float64), 'Za_n': Array([ 1.802e-05, 1.335e-03, 9.939e-02], dtype=float64), 'Rb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'Zb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}], [{'R_lmn': Array([ 2.416e-05, 1.501e-03, ..., 0.000e+00, 1.858e-03], dtype=float64), 'Z_lmn': Array([-7.446e-04, -3.249e-04, ..., 1.482e-03, 1.084e-03], dtype=float64), 'L_lmn': Array([-2.244e-04, -7.899e-04, ..., 9.491e-04, -7.183e-04], dtype=float64), 'p_l': Array([ 1.800e+04, -3.600e+04, ..., 0.000e+00, 0.000e+00], dtype=float64), 'i_l': Array([ 1.000e+00, 1.500e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'c_l': Array([], shape=(0,), dtype=float64), 'Psi': Array([ 1.000e+00], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.031e+01, 8.864e-02, -1.262e-02, -5.261e-04], dtype=float64), 'Za_n': Array([-1.489e-03, 2.919e-03, 1.679e-01], dtype=float64), 'Rb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'Zb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}]])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "eq.solve(objective=obj, constraints=cons, maxiter=1, ftol=0, gtol=0, xtol=0, verbose=3);" + "eq.solve(objective=obj, constraints=cons, maxiter=1, ftol=0, gtol=0, xtol=0, verbose=3)" ] }, { @@ -180,9 +249,29 @@ "outputs": [ { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWAAAAF2CAYAAABQ7kLKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAOxQAADsUBR2zs/wAAgfdJREFUeJztvXdc2+X6//9KIBBmwg6jhE3ZpVAoo9BltdqldZxq3VaPx1P3cZ3Rn/boUc9Re9Rq3ePoqaPaqR5bW0oLHUChlL0JM+wAgSRk3L8/+k0+tAXKSN53Au/n48GjZeR9XQnklSvXfQ0OIYSAhYWFhYVxuLQdYGFhYZmrsALMwsLCQglWgFlYWFgowQowCwsLCyVYAWZhYWGhBCvALCwsLJRgBZiFhYWFEqwAs7CwsFCCFWAWFhYWSrACzMLCwkIJVoBZWFhYKDHrBLikpARLly4Fh8PB/Pnz8eabb17xM1u3bsXWrVspeDc+HR0dePTRR5GamoqlS5ciNTUVK1aswDvvvIP+/v6r3n6i+/S///0PCxYsAIfDMbbbLCwsM4HMUgCQzz77bMzvDQ0NkaGhIWYdmoCmpibi5+dH3nnnHaLT6Qxf//HHHwmPxyO//PLLVa9xtfuUlZVFpvPrbmxsJI8//jgBQO677z7y5ptvTvkaE9HW1kZuvPHGad22qamJPPXUU4TP55O33377iu8PDg6SuLg4snjx4hn7rVaryfbt28nbb79N3n77bXL27NkZXe9qlJWVkcLCwnE/p8HPP/9MoqKiyEcffXTJ12699VaKXlk2c1KAzY21a9eSdevWjfm9++67j/z2228ztjFdASaEkGPHjhFPT88Z+3A57777Ltm6dSsRi8XTvsapU6fI7bffTv74xz9e8b3vvvuOhIeHk+zs7Bl4eZFHH33U8Hv4+9//Tn7/+9/P+JoTceedd5K9e/eO+zktEhISSGVlpeFzmUxGpFIpRY8smzknwO+88w4Ri8UkMzPT8LUPPviAhIeHE7FYTN5++22ycuVKEhgYSL744gvDz0ilUnLbbbeRtLQ0kpaWRrZu3UqGh4cN3//3v/9Nli1bRlasWEEWL15M3nvvvXGvf+211xJHR0fy2Wefkd7eXsLlcsnHH3884f2Z6Bpj3afq6mqSmZlJEhMTydq1a8lbb701bQH+61//arIop6GhYUYC/M4775ADBw6Q66677pKvnzt3jpw8eZLY2dkRlUo1Ix+bm5tJeHi44fP+/n7S09Mzo2teDbFYTPr6+sb9nAY9PT3E39+fqg+zDWuK2Q8q/PGPf0R3dzeOHz9u+NqDDz4IGxsbPPjggwgLC8ORI0ewe/du/P73v8cdd9wBDoeD9evXIyEhAd988w20Wi1uuukmPPvss3j77bcBAFqtFj/99BPs7OygUqkQHR2NuLg4pKamGq7/0EMPwdfXF//73/+wd+9e8Pl81NTUQKfTYd68eRP6PdE17rnnnkvuk0ajwZo1a7B+/Xq8/vrr0Gg0uPnmm6f9mGVlZeGOO+4Y9/tSqRQ7duyY8BoZGRm4/vrrp+3DeKjVasyfPx+1tbWGr6lUKkgkEoyMjCAlJQU2NjYz8vfw4cPw8vLCd999h6GhIZSUlGDbtm0T3r69vR3ffvstCgsLsX79esjlcuTm5mLHjh2wt7eHQqHAtm3bEBERAYVCgX379uHw4cP45ZdfcPDgQXC5XLz77rtwcnJCVVWV4fN169YhNjYWGo0Gr7/+OlxdXQEA7u7uuPnmm1FcXIycnBx8/PHHiImJQXd3N8LCwvDss8/i22+/xalTp/Dwww+jtrYW5eXlWLt2LZYvX36J76tXr8b1119/xZlCVlYWli5dCgCQy+X4/PPPcfToUXzwwQfQarVXvf54PgMY9/EY7/5c7fdnMdB+BTAVmCAFsW3btkuiRUII+eyzz4hAIDB8XlFRQQCQtrY2cvbsWQLgkrdee/bsIXZ2doac7bFjx8h1111H0tLSSGZmJhEIBOT111+/5PrOzs5X+KK/9q+//nrJ19955x2SmZlJwsPDyVNPPTXhNS6/Tzk5OQQAqampMXz/u+++m1YEPDQ0RGxsbC6578ZkJhGwSqUiO3fuJCMjI4TP5xONRkMIuXhfR0ZGyEMPPUS2b98+Yx9ffvll4ubmRmQyGSGEkA8//JBs2bJlwtt8/PHHRKvVEg8PD9LY2EgIIeS5554jH3zwASGEkFtuuYV8++23hBBCvv32W3L99ddfctvHH3983M8JuZiS0P99NzU1kX/961+EEEJ++eUXolKpiLOzM6msrCQymYz09vaSDz/8kKjVauLs7EzOnDlDCCHk4MGD5A9/+MMVvh8+fJjU1tZe8fWHH37Y8K7wiy++IEqlksTExJCqqqpJXX88nyd6PMa7P7OFORcBT4RQKDT8n8/nA7gYTTU2NgIAtmzZAi73YuGIUqmEp6cnurq6MDQ0hNWrV+O9997DfffdBwBYunQphoaGLrm+i4vLFTZDQ0PB5XIhkUgu+fof//hH/PGPf8TSpUvR3d094TUup6WlBQDg6elp+Jq7u/tVbzcWubm5cHNzQ3h4+CVfr6qquuJrTJOXl4ekpCTweDyIRCJIJBIoFAqEhoaCx+MhKysLn3766YztODk5ITg4GAKBAADg7++PP//5z/jwww/Hvc3NN9+M8vJyREdHQywWAwCam5vh5OSEc+fOITs7G9988w0AoKysDMuWLTPc9sSJE7jpppvG/byiogL79u3Dddddhy+++ALW1tZ4/PHHAQDXXXcdTpw4gZSUlEt+P7feeiuKioqwYMECJCcnAwDOnz8/5u/wmmuuGfM+HT16FC+88AIAYMOGDaipqQGPx0NYWBi8vLwmvP5EPk/0eIx3f2YLc0qAm5qa0NTUNOXbBQQEAAC++eYb+Pj4GL7e2dkJT09P7NmzByqVCjfeeKPheyMjI5O6touLC2644Qb88MMP2LJly5R9Gwt9OqOzsxPOzs4AcImIT4Xjx49fIg4ADG8v9U8IWimIoqIiPPLIIwCAkJAQVFVVob+/H7/73e/Q1taG1tZWJCUlXXG7qfobFxeHL7/80vA9DocDjUYz4e0FAgF2796NzMxMAAAhBNnZ2di6dSuOHz+OzMxMw4t5dnY23njjDchkMgiFQuTk5OCtt96CUqmEWq2+4vPi4mIkJCTg9ttvH9P2sWPHDKmC0f5kZWVd8rvcu3cv9u/fj76+vqu+sDc3NwMAfHx8oFar4ezsjE8//RT33nuvwe+Jrj+Rz1d7PMa6P7OFOSXA9fX1l+R+J0tiYiKSkpKwa9cuvPTSSwAu5sOef/55nDlzxiBEJ06cwPr169HY2Iji4mKsWrVqUtffuXMnUlJS8Prrr+Ppp582/CGWlpaiubkZQUFBU/I3OTkZEREReP/99/HGG29Ao9Hgiy++mNI19GRlZRmieuBi5P/Xv/4Vn3/+ueFrIpEIr7766rSuPxPUarXhsQoODsYHH3yA//73vwAu+p2WlgYej3fF7abqb2pqKuRyOQYGBuDs7Izy8nLcdttthu//8ssvCAoKuiJCy83Nxfr16wEAe/bsQXJyMpKTk1FaWgqRSAQAaGhoQGlpKWJjY/Hll19i/fr14PP5cHV1xc6dO6/4/O6770ZoaKjhHRpw8Xeyf/9+g09ZWVl47bXXrrgfWVlZePbZZwEA1dXVsLOzg7OzM77//nvcf//9hp87evQo5s2bh7CwsEvuS2ZmJn766SfExcXB398f3333HYqLi/HFF1/gsccem/D6CxYsGNdnoVA47uNx//33j3t/ZgW0cyDGpqKigmzcuJEAIImJiWTjxo2Gj4yMDOLm5kbEYjERCATk5ptvJoQQ8s0335Dw8HBia2tLbrvtNtLe3k6Sk5MJAJKcnEwqKyuJVColmzZtIikpKWTZsmVk/fr1pKmpyWB3x44dxN/fn6xcuZJs2bKFxMbGErFYTHbs2HHJ9TMzM0lRUdEVfkulUvLII4+QRYsWkYyMDBIXF0dSU1PJzp07yeDg4ITX0FdBjL5PNTU1JDMzkyxcuJCsWrWKbN++nQAgmZmZRCKRkF27dpG4uLhxH8f8/Hzy9NNPEy6XSx544AGyfft28sQTT5CgoKAx84bT4ZNPPiGbNm0itra25MknnyRHjx41fG8i/86dO0fuuusuEhQURL788ktCCCFvv/02OXLkCNHpdGT79u0kJiaGLFq0iHz99ddG8TU3N5c89thj5LXXXiOPPvooGRwcNHzvuuuuI4888sgVtwkLCyPvvvsu+eSTT8hTTz1lqJqRy+XkgQceILt37ya7d+8md911F3njjTdIa2sr0Wg05JZbbiGffPIJycnJueJzPa+99hrZuXMn+eKLL8hHH310SQ14bGwsUavVV/izYMEColAoCCGEdHR0kE2bNpEdO3ZcUT++du3aK+qqKyoqyB133HFJDfDNN99MPvzwQ1JSUjKp64/n80SPx0T3ZzbAIYTdijwXuffee+Hu7o5//vOftF0ZE3P373K++uorbN682fB5a2sr1q9fj4KCAopesZg7cyoFwXKRY8eOobKyEkeOHKHtyphM1r/s7Gzk5uZCp9NhyZIlhnwr00gkkksOPAEgJyfHcBjFwjIebAQ8B1EoFLCysrqiPtZcmKx/AwMDKCoqglwuxw033MCQd1eSk5OD9PR0w+d5eXnYunUrHBwcsGPHDsTGxlLzjcW8YQWYxWIpLS3F4OAgsrKyDOVRLCyWBCvALBbL4OAgnJycJlVGxcJijrACzMLCwkKJWTcPmIWFhcVSYKsgWCyWsQbMs2/oWCwJNgJmsVh27twJQgiefvppkIujVWm7xMIyJVgBZrFY/vCHP6Czs9Mw7IaFxdJgBZjFYiGEICsrC3fffTdtV1hYpgUrwCwWy44dO5Cbm4tdu3bRdoWFZVqwZWgsLCwslGAjYBYWFhZKsALMwsLCQglWgFlYWFgowQowCwsLCyVYAWZhYWGhBCvALCwsLJRgBZiFhYWFEqwAs7CwsFCCFWAWFhYWSrACzMLCwkIJVoBZWFhYKMEKMAsLCwsl2I0YLGbB8PAw2tvb0dbWhvb2dsP/29ra0NHRAbVaDY1GA7VaDbVaDa1WC41GA41GAysrK/B4PMO/1tbWsLa2ho2NDTw8PODj4wMfHx94e3vD29vb8H9HR0fad5tljsNOQ2MxORqNBuXl5SgrKzOIaltbG1pbW9He3o6Ojg7DhmN3d3e4u7vDw8MDnp6e8Pb2hpeXF/h8PqytrcHj8Qwia2NjAy6Xi4qKCgQGBhoEeWRkBBqNBiqVCp2dnWhvb0dnZye6urrQ1dWF7u5uDAwMwMHBAV5eXvD29oavr69BqH18fBAREYHo6GjY2NjQfvhYZjGsALMYFbVajbKyMhQWFqKgoAD5+fkoKSmBvb09wsLC4OnpeYno+fn5QSwWw9/fH87OzlO2p9VqcejQIaxZswZWVlaTvt3Q0BAaGxvR3NyM5uZmw4uBVCpFV1cXamtrIZPJEBUVhUWLFiExMREJCQmIjo6Gra3tlP1kYRkLVoBZps3IyAjKyspw7tw5g9iWlpbCyckJERERiImJwaJFi5Ceno7g4GBwucY/cpiuAE+GhoYG5OTkIC8vD6WlpSgvL0dfXx8iIyORkJCARYsWISEhAbGxsawos0wLVoBZJs3Q0BCOHDmC//3vfwZREggEV4htUFCQScR2LEwpwGMhkUgMonzhwgVUVFSgp6cHERERWLRoEa699lpce+21EAgEJveFxfJhBZhlQlpbW3Ho0CHs3bsXx48fR1BQEDIzM5Gamor09HQEBgZS9Y9pAR6L5uZm5OTk4PTp0zh+/DgqKyuRnp6OG2+8EWvXrkVAQAAVv1jMH1aAWS6BEILz58/jwIED2LdvH8rLyw2R3e9+9zuEhobSdvESzEGAL6exsRHfffcdfv75Z+Tl5SEwMBAbNmzAunXrsGjRIsbeHbCYP6wAs0ClUiErKwsHDhzA/v37oVAosGTJEqxduxY333wzhEIhbRfHxRwFeDRyuRx79+7FgQMHkJ2dDQBYu3Yt1q9fj5UrV8Le3p6yhyw0YQV4jqJUKvHjjz9iz549OHz4MDw9PbFixQrceOONWLVqFaytLaNE3NwFeDRarRbHjx/HDz/8gKNHj6KpqQkrVqzAxo0bccstt7B1yXMQVoDnGKWlpfjwww/x5ZdfwtfXFxs2bMCtt96KuLg42q5NC0sS4MuprKzEt99+i3379qG2thabNm3Cgw8+iISEBHA4HNrusTAAm4yaAwwNDeHTTz9FUlISUlNT0dnZiYMHD6KsrAwvv/yyxYqvpTN//nxs27YNRUVFyMrKwsjICK655hrExcXhvffeQ39/P20XWUwMK8CzmOrqavzxj3+Et7c33n77bfzud79DS0sLvvnmGyxZsoS2eyyjSExMxOeff47W1lY8/PDD+OKLL+Dj44P77rsPJSUltN1jMRGsAM8yCCE4fPgwVq9ejQULFqCrqwtHjhzB+fPn8eSTT06r24yFOezt7fHwww/j7NmzOHXqFDQaDVJTU5GZmYn9+/dDq9XSdpHFiLACPEsYGhrCrl27MH/+fNx9992IjY1FQ0MDvv32WyQnJ9N2j2UaxMXF4csvv4REIsHSpUuxdetWhISE4K233mLTE7MEVoAtHKVSiTfeeAPz5s3Dhx9+iKeffhoSiQSvvfYavLy8aLvHYgRcXV3x4osvoqGhAdu3b8d3332HefPmYfv27RgaGqLtHssMYAXYQtFqtfj8888RGhqKL7/8El9++SUKCwuxZcsWdoLXLMXKygqbN2/G6dOnceDAAfz0008IDAzEzp07oVarabvHMg1YAbYwCCE4cOAAoqOjsX37drz88ssoKirCmjVraLvGwiBLly7FmTNn8P777+O9995DeHg4du/eDZ1OR9s1linACrAFkZOTg7S0NDz00EPYsmULqqqqcNddd7GtrXOYjRs3oqSkBM8++yyee+45xMfH49dffwVb3m8ZsM9cC6CkpAQ33HAD1q5di2XLlqG2thZPPvmkxXSrsZgWLpeLhx56CNXV1bjtttuwefNmLFu2DHl5ebRdY7kKrACbMY2Njdi8eTNSUlLg7++PmpoavPzyy3BwcKDtGosZYmtrixdeeAF1dXVITEzEypUrcdNNN6Gqqoq2ayzjwAqwGdLb24vHHnsMUVFR0Gq1KC0txfvvvw93d3farrFYAM7OzvjXv/6F6upqCIVCLFy4EA888ADa29tpu8ZyGawAmxn79+9HREQEKioqcObMGezevZudJ8syLUQiET799FOcP38e3d3diIyMxFdffcXmh80IVoDNhN7eXtx+++24//778eqrr+Lw4cOIiYmh7RbLLCA0NBT79u3Dxx9/jD/96U9Yt24dGw2bCXNGgAsKCnDixAm89dZbtF25ggMHDiAiIgIymQwlJSW49957abvEMgvZuHEjysrKYGdnh8jISHz99ddsNEyZOSPAlZWVSEpKQn19PW1XDPT29uKOO+7Afffdh3/84x/4+eef4e3tTdstllmMq6srvvvuO3z44Yd46qmnsH79ekilUtpuITs7G6+88gr+/ve/GwbXzwXm1DzgQ4cOgc/ng8fjITc3FzqdDkuWLEFmZibjvhw8eBAPPPAAFi5ciE8//ZQV3mliyfOAadPT04OHHnoIx44dw7vvvotNmzZRm0M8MDCAoqIiyOVy3HDDDVR8oMGciYBffvllrFmzBv/+978RHx+PtLQ0xMfHMy6+fX192Lx5M+655x68/PLL+OWXX1jxZaGCm5sb9uzZg127duHJJ5/Ehg0b0NHRQcWXpqYm2NjYoLi4mIp9WswZAV68eDFyc3OxcOFCar/sQ4cOISIiAl1dXbhw4QIeeOABRu2zsIzFrbfeitLSUlhbWyMiIgK7d+9mPDcsFouRkpKChx9+mFG7tJlTKQg9g4ODcHJyQl9fH1xcXBix94c//AE///wzXn31VWzZssXkNucKbArCuHz77bd47LHHsHjxYnzyySdwc3Oj7dKsZk4KMJPU19djzZo18PT0xFdffQU/Pz/aLpklhBCoVCoolUrDx+Wfa7Va6HQ6EEIMHzqdDiMjI7CzswOHwwGHwwGXy4WVlRVsbW3B5/Mv+dB/zdbWlp2hMQ7d3d245557UFpaikOHDiE6Opq2S7OWOSPAVztcMMXDkJWVhY0bN2LTpk14++2353yERgiBUqmETCaDTCbDwMAAFAqFQWw5HM6YYqn/sLa2vkRkORwOdDodsrOzsWTJEnA4HIMwazSaMUVc/7lOp7vk+s7OzhAKhRAKhQYxn8vodDr85S9/wbvvvouvvvoK69atM6m9sR7vuSBNc2aay+hf5p49eyCXy3HPPfeYzNZ7772H5557Dv/617/w0EMPmcSOOUMIgUKhgEwmQ39/v+FftVoNgUAAgUAALy8v2NvbG0SQx+NNWfj0K3oEAsGkX+AIIVCr1QZRVigUGBgYQF1dHfr7+8Hlcg1iLBAIIBQKYW9vP6dEmcvl4pVXXkFMTAzuvPNOPPPMM3jhhRdM9hgQQkz+vDRH5owA6zl//jxiY2Nx6tQpk1x/ZGQEW7duxf79+/HTTz8hIyPDJHbMDZ1Oh76+PnR2dhoiXI1GYxAwHx8fREZGwsnJifpbfw6HAxsbG9jY2FyxI48QgsHBQcOLRn19vUGU9ffF09MTbm5u1O8HE2zatAnh4eG48cYbUVxcjM8//xz29vZGt2Pq56W5MucEuLOz0xDtGJuuri7cdNNNGBwcxNmzZyEWi41uw5xQq9Xo6upCe3s7Ojs7YWVlBS8vL/j6+iIqKgqOjo4WJ1IcDgfOzs5wdnbGvHnzAFwUZblcDplMhr6+PhQXF0OlUsHLywsikQienp6zegvJwoULUVBQgPXr1yMtLQ0HDhwwPDbGwpTPS3NmzgnwqlWrsHv3bgwODhr1uhcuXMCaNWuQmJiIX3/91SRRgjkwPDwMqVQKqVSKnp4eODs7QyQSISQkBM7OzrPybTqHw4GTkxOcnJwwb948gyBLpVI0NDSgqKgIrq6uEIlE8PLygqOjI22XjY6HhwdOnDiBLVu2ICEhAfv27UNqaqrRrm+q56W5M2cO4UzJjz/+iHvuuQdPPPEEtm3bZnFR30QQQtDf34/29nZIpVIMDQ3B3d3dIDZ2dnZU/TOHMjSVSoWOjg5IpVJ0dnbCzs4OIpEI3t7ecHFxmXUvSm+99Rb++te/4u2338Z9991H2x2LZs5FwMZEp9Nh+/bteOutt/DJJ5/glltuoe2S0RgZGUFLSwskEgmUSiVEIhHmz58PDw8PdhPHZdja2sLf3x/+/v7QarXo7u6GVCpFXl4erK2tIRaL4e/vD1tbW9quGoUnnngCkZGR2Lx5My5cuIB//etf7N/ENGEj4GmiUCiwefNmFBUV4ccff8SCBQtouzRjCCHo7e1FY2Mj2tvb4erqioCAAIhEIrON6s0hAh4PnU6Hzs5OSCQSdHV1wcvLC2KxGB4eHrMiKq6trcW6devg4+ODH374AQKBgLZLFgcrwNNgaGgIa9asgUKhwKFDhyx+U4VWq0VLSwvq6+uhUqkMEZslrD4yZwEejUKhQFNTEyQSCbhcLoKCgjBv3jzweDzars0IuVyOG2+8Eb29vThy5AhcXV1pu2RRsAI8RQYGBrB69WpYW1vj559/tgiRGg+FQoGGhgZIJBI4ODggKCgIPj4+ZhvtjoWlCLAeQgikUinq6+shk8ng7++PoKAgi/47GhkZwcaNG9HU1ITffvsNHh4etF2yGFgBngJ9fX1YtWoVBAKBYbSlJTI8PIzKykq0tbXB29sbQUFBjMzEMAWWJsCj6e/vR319PVpbW+Hl5YWIiAiLraDQaDS47bbbUF5ejqysLIhEItouWQSsAE+S7u5urFy5Ej4+Pti3b59F1n2qVCpUV1dDIpFg3rx5CAsLo17FMFMsWYD1qFQq1NTUoLGxEX5+fggPD7fI34tWq8XmzZtRUFCArKwsdu7JJLCc95oU6ezsREZGBgIDA3HgwAGLE1+NRoOqqir89ttvUCqVWLp0KeLi4izyST4bsbW1RXR0NJYvXw5CCI4ePYqysjKMjIzQdm1KWFlZ4euvv0Z6ejrS0tIgkUhou2T2sLUjV6GnpwfLli2DWCzGyy+/bFHlNjqdDo2NjaiuroazszPS0tIgFAppu8UyDvb29oiPj0dwcDAqKirw22+/ITQ0FEFBQRYT3XO5XPzlL3+BXC5HZmYmcnNz4evrS9sts4WNgCdAJpPhmmuuQUhICL755hs0NzejqqqKtltXhRCClpYWHD16FM3NzUhISEBqaiorvhaCs7MzkpOTsXjxYnR0dOC3336DRCKBTqej7dpVaWhoQHV1NT788EMsXboUS5cuNYudc+YKmwMeh8HBQaxcuRKurq44ePAgrK2tIZfLkZubi4CAAISHh9N2cUy6u7tRWloKjUaDiIgI+Pj4zIqa0/GYDTngiSCEoLOzE+Xl5dDpdIiOjoaXlxdtt8akoaEBlZWVSE1NhUAggE6nw+23344LFy7gxIkTFl+uaQpYAR6D4eFhXHvttbCxscHPP/98SQeTuYqwRqNBeXk5WlpaEBERAbFYbFHlZNNltguwHv27mrKyMnh6eiI6OtqsziIuF189Wq0Wt9xyC+rr65GVlWWx1TamYvY/Q6eIUqnE2rVrAQA//fTTFe2jjo6OSEtLQ2Njo9mkI7q7u5GVlYWhoSEsW7YMgYGBc0J85xIcDgfz5s3DsmXLoNPpkJWVZTZv7ccTX+Diwdx3330HPz8/rFq1CgMDA5S8NE/YZ+koCCF48MEH0d/fj//973/j1vmaiwhrNBpcuHABeXl5CAsLw+LFi9nKhlmOra0tEhMTERMTg6KiIhQWFkKtVlPzZyLx1WNtbY0ff/wRTk5O2LRpk2GIPgsrwJfw5ptvIisrCwcPHrxqZxJtEb486hWLxbM618tyKT4+Pli+fDm0Wi2OHTtGZZ38ZMRXj42NDfbu3Yuamhr85S9/YchD88dyaqpMzP/+9z+89NJLOHz4MLy9vSd1G70I5+bmAgAjOWGNRoOKigo0NzcjKioK/v7+rPDOUWxtbbFo0SK0traisLAQXl5eiImJYWS+xFTEV49AIMCBAweQmpqK2NhYbNq0ycRemj9sBAygqqoKv/vd7/DOO+8gOTl5SrdlMhLu6enB8ePHIZfL2aiXxYCvry+j0fB0xFfP/Pnz8eWXX+LBBx9EQUGBiTy0HOa8AMtkMqxZswYPPPAA7rrrrmldw9QiTAhBXV0dzpw5g9DQUDbXy3IF+mg4OjoahYWFqKysNMlW4ZmIr541a9bghRdewLp169De3m5kDy2LOV2GptVqsXr1alhZWeGnn36aceWAKUrUtFotiouL0dPTg+Tk5CuWSM515koZ2lQYGhrC2bNn4ejoiIULFxqte9MY4juaO+64AzU1NThx4oTFDraaKXM6Av7Tn/6ElpYWfP/990Yp2zJ2JKxUKpGbmwuFQoGMjAxWfFkmhYODA5YsWQJCCE6ePInh4eEZX9PY4gsAn376KQDgwQcfNEm0bgnMWQH+/PPP8Z///Af79+836ghAY4mwTCZDdnY2BAIBUlJSZs06GxZm4PF4SEpKgkgkQnZ2Nrq7u6d9LVOIL3AxbbJ//34cO3YMb775ptGua0nMySqIM2fOYOvWrdizZw9CQ0ONfv2ZVke0tLSguLgYkZGRCAwMNLp/LHMDDoeDiIgIODk54ezZs4iKikJAQMCUrmEq8dXj7e2NH374AatWrUJUVBSuu+46o9swZ+ZcBNzS0oL169fjxRdfxLXXXmsyO9OJhAkhKC8vR0lJCZKTk1nxZTEKfn5+SEtLQ1VVFYqLiyc91MfU4qsnOTkZ77zzDn73u9+ZTXcpU8ypQziFQoG0tDTExcXhs88+Y8TmZA/m1Go1CgsLMTw8jKSkJIteUTNdNBoNFAoFlEollEolVCqV4f/6D7VaDZ1OB0IICCEGMbGysgKHwwGHw4GNjQ34fD5sbW3B5/MNH/rP7ezsLH4X23RQKpXIy8uDlZUVEhMTJ0xrMSW+o3nqqadw4MAB5Ofnz5nJfXNKgJ944gnk5OTg9OnTjM71vZoIq1QqnDp1Cg4ODkY9tTZn1Go1+vv70d/fD5lMBplMBrlcPq546j94PB64XC64XC44HA50Oh2OHDmCFStWgMvlQqfTYWRkZFwRV6lUUKlUcHBwgFAohEAggFAohFAonBOirK+q6e3tRWpqKuzt7a/4GRriC1ycX71q1Sp4e3vjP//5D2N2aTJnBDg3NxerV69GXl4e5s+fz7j98URYqVTi1KlTcHV1RVxc3KxsrCCEYHBwEF1dXejr64NMJsPQ0JBBBEcL4VRFcDplaGq1GgMDAwbh14u/vb29wR9PT084OzvP2t9HRUUFWlpakJaWdsm7LVriq6elpQWxsbH44osvDEOxZjOzP9TCxfGSd999N5577jkq4guMfTCnUCiQm5sLT09PxMTEzKonu06nQ09PD6RSKTo6OqBSqeDp6QkXFxcEBARAIBBQizh5PB7c3Nzg5uZm+JpGo7kkGq+trYWVlRW8vLzg7e0NNze3WVNnrD+c43K5yMnJQWpqKpycnKiLL3AxX/3aa69hy5YtqKiomPXjK+dEBPz444/jzJkzOHXqFPUxjfpI2MfHB1KpFD4+PoiMjJwV4qtWq9HR0QGpVIrOzk7weDyIRCKIRCK4ubmZ5LE3VSMGIQS9vb2QSqWQSqVQKBTw9PSESCSCl5fXrCkLrKmpQV1dHcRiMRobG6mK72iuvfZaeHh44KuvvqLtikmZ9QKck5OD66+/Hvn5+WYzQL2rqwunTp2Cm5sb0tLSLFp89RsbJBIJOjo6IBAIDKLr5ORk8vvGVCecXC43iHFfXx/c3d0hFoshEomov6jPlPz8fLS1tWHRokXw8fGh7Q4AoLW1FbGxsfj8889ndSpiVqcg9KmH559/3mzEV6FQ4Pz58wgICIBUKkV1dbXZ+DYVlEolJBIJJBIJOBwOxGIxYmJiZu2MCkdHR4SEhCAkJAQqlQotLS2oqKjAhQsX4O/vj4CAgDEPtMydhoYGdHd3Izg4GBcuXICzs7NRG5Omi6+vL1577TU88MADqKiogKurK22XTMKsjoAfe+wx5OXlITc31yyiFKVSiZycHIhEIkRFRWFoaMgs1xtNRF9fH+rr69He3g4vLy8EBATA3d2dWhRPcxaEPk0hkUjQ1tYGT09PBAUFwc3NzSLe1Vye862urkZDQwPS09PNpgxy9erVcHV1xddff03bFZMwawX45MmTuOGGG8wm9aBSqZCbmwt3d/dLDtzMdcfc5XR2dqKqqgqDg4MICAhAYGCgWUS75jKMR/+OoKGhAXw+H+Hh4RCJRGYrxOMduFVWVqK5uRlpaWlmEdHrUxGfffYZ1q1bR9sdozMrBXhoaAgxMTF46KGH8Oyzz9J2BxqNBjk5ORAIBFiwYMEVT0pzFuG+vj6Ul5djcHAQYWFh8Pf3N6s6ZXMRYD06nQ4tLS2orKwEn89HZGSk2W0DnqjaQd+N2d7ejoyMDLNY/PnJJ5/ghRdemJWpiFkpwI8++igKCgqQk5NDPfVACMG5c+egVquxePHicSMicxNhuVyOiooKdHZ2IjQ0FEFBQWYlvHrMTYD1aLVaNDY2orq6Gi4uLoiIiDCL6oLJlJrp/2ZHRkawePFi6s8hALj++ushFArx3//+l7YrRoX+I2tkTpw4gc8//xxffPGFWfzh1NTUQCaTITExccK3o7R3zOnRHxIeP34cdnZ2uOaaaxAWFmaW4mvOWFlZITg4GCtXroRAIEBOTg7OnTuHoaEhaj5Nts6Xw+EgPj4eIyMjKC8vZ9DD8fnkk09w+PBh7N+/n7YrRoW+QhkRhUKBe+65B3/5y19MMuVsqrS3t6O2thbJycmTajqgKcI6nQ6VlZU4evQoCCFYvnw5oqOjzeItqCXD4/EQERGBFStWgMfjISsrC6WlpdBoNIz6MdUmCysrKyQnJ6OlpQUSiYQBDyfG29sb//znP7Flyxb09fXRdsdozCoBfueddyAQCPD000/TdgUDAwMoLCxEYmIinJycJn07GiKsnz3c1dWFjIwMxMfHm8UBzGyCz+cjNjYWy5Ytw8DAAI4fP46enh5GbE+3w83Ozg5JSUkoLS1Fb2+vCT2cHPfeey+io6Px6quv0nbFaMyaHLBMJkNAQAC++eYb6jNFR0ZGkJ2djcDAQISEhEzrGkzkhHU6Haqrq1FXV4fw8HAEBweb7an9eJhrDngiCCGQSCQoKyuDv78/IiIiTJbiMUZ7cVNTE8rLy5GZmUm98qWgoABLly5FVVUVfH19qfpiDGZNBPzqq69iwYIF1MVXp9MhPz8fbm5uCA4OnvZ1TB0J9/f3Izs7G52dncjIyEBISIjFia+lwuFwEBAQgGXLlmFwcNBk0bCxZjv4+/vDz88PZ8+eZTx1cjmJiYm45pprsG3bNqp+GItZEQG3tbUhLCwMR48enfJaeWNz4cIFyGQypKWlGSUiM3YkPBui3tFYYgQ8mtHRsFgsRkREhFHuh7EH6xBCcObMGfB4PCQkJFD9m6mpqcGCBQtQWFhoFhVDM2FWRMAvvvgiVqxYQV18Gxsb0d7ejqSkJKOJgTEjYYVCgZMnT7JRrxkxOhrW54blcvmMrmmKqWYcDgeJiYmQyWSoqakxyjWnS2hoKG677Ta88MILVP0wBhYvwNXV1fjqq6/w+uuvU/Wjv78fZWVlSEpKMvqKbWOIcG9vL7Kzs+Hm5ob09PQpHQyymB57e3ukpKTAz88PJ06cQGdn57SuY8qRkjweD8nJyaipqZnRkk9j8Morr+DIkSPIz8+n6sdMsXgBfuGFF3DLLbdQfSui0+lQVFSEsLAwk80vnYkINzU14fTp04iMjER0dLRZ1EezXAmHw0F4eDgWLFiA/Px81NXVTWldOxPzfJ2cnBAdHY3z589TzQeLRCJs2bIFzzzzjEWvtLfoZ2JBQQEOHz6Mf/zjH1T9qKmpAYfDmdGh22SYqggTQlBaWory8nKkpKTA39/fpP6xGAcfHx+kp6ejrq4O58+fh1arveptmBym7u/vDwcHB1RUVJjUztXYtm0bSktLceTIEap+zASLFuBnnnkG999/P7y9van50N/fj9raWixcuJCRyHKyIqxWq3HmzBl0d3cjMzNz1vXQz3YEAgEyMzMxNDSEU6dOQalUjvuzTG+y4HA4WLBgAZqbm6mmIpydnfHEE0/gT3/606Q3PZsbFivAR44cwYULF/D//X//HzUfRqcemMypXk2Eh4aGkJ2dDR6Ph/T0dOq1myzTw9bWFqmpqXB2dsaJEycwMDBwxc/QWiNkZ2eHqKgoFBUVUU1FPPXUU5DJZPjuu++o+TATLFKAdTodnnnmGTz66KNUB5zU1NSAy+VOu9liJownwoODg8jJyYGfnx8SEhLYGQ4WDpfLRVxcHIKDg5GbmwuZTGb4Hu0dbv7+/nB0dKQ6L8LW1hbPP/88XnjhBYyMjFDzY7pYpAB///336O7upjpqUp96iI+Pp1bKdbkIDwwMIDc3F0FBQZg/fz5bYjaLCA4ORkREBE6dOoXe3l7q4gv8XyqipaWFaipiy5Yt4PP5+Oijj6j5MF0sToB1Oh3+9re/4ZlnnqG2GFGn06GwsJDx1MNY6EW4vr4eJ06cQGhoqFkMImIxPgEBAYiJicGpU6dQXl5uFgs0zSEVYWVlhRdffBHbt2+3uCjY4gT4yJEjGBgYwEMPPUTNh+rqalhZWVFJPYyFTqcDIQQcDod6qyiLaRn9+51MdQQT+Pv7w8nJiWoqYuPGjRAIBNizZw81H6aDxQnwW2+9hbvuuovamMSBgQHU1dVRTT2MRi6X49SpUwgPD0dmZib1ecIspkOfdliyZAliYmJw5syZS3LCtOBwOIiLi0NLSwtjE94uh8vl4oEHHsCbb75Jxf50sSgBrqqqwokTJ/Dkk09S86G8vBxBQUHUUw/Axa3P+pxvcHCw2Qx1NzWEEGi1WqjVasNbTq1Wa9EF+Vfj8pyvv78/IiMjcfr06TGrI5jGzs4O4eHhKC0tpfZ7+MMf/oD6+nqcOXOGiv3pYFHDeB555BF0dXVRKznp7u5Gfn4+Vq5cOakB66ZEo9Hg5MmT8PT0RFRU1CXfM7f1RlNFp9NBLpdDJpNhaGgISqUSSqUSKpXK8O942NjYgM/ng8/nw9bWFnw+Hw4ODhAKhXBycrLILsCJDtxqa2vR0NCAjIwMamcierRaLY4dO4aoqCj4+PhQ8eHhhx9Gb28vvv32Wyr2p4rFCLBMJoOfnx+OHz+OxMRExu0TQnDy5En4+vqavONtMr7k5+eDEIKkpKQxUyGWJMJKpRKdnZ2QyWSQyWQYGBiAlZUVBAIBnJycrhBUPp8PKysrcDgc6HQ6/PTTT1i9ejUAGER6tGDrxVytVsPZ2RlCoRBCoRCenp5mXyN9tWoHQgiKioqgUCiQkpJC/QWmubkZ1dXVWLZsGRVf6urqEBMTg5qaGouYF2wxRaKffvopYmJiqIgvAEilUiiVSgQEBFCxP5qqqirI5XIsWbJk3Dy0Ph2Rm5sLAGYlwoQQDA4Oor29HR0dHejv74erqytcXFwQEhICoVAIOzu7SeXY9fGDlZUVrKysYGNjM2Z6iBACpVIJmUyG/v5+tLW14cKFC3BycoJIJIJIJIJAIDCLvL6eyZSa6fOvubm5KCkpQVxcHMNeXoqfnx9qa2vR3NwMsVjMuP3g4GAsXboUO3fuxCuvvMK4/aliEREwIQRhYWH485//jHvuuYeK/aysLISEhFCfp9Da2ooLFy4gIyMDDg4OV/15c4qE5XI5JBIJ2traoFar4eXlBZFIBE9Pz2mndGYyD1ij0aCrqwtSqRQdHR3gcDjw8fGBWCyGs7PztPwxFlOt81UqlcjOzkZYWBgCAwMZ8HB8pFIpiouLsXLlSiozmn/99VfceeedaG1tpZ4qvBoWEQGfPHkS/f39uP3226nYb2pqAgDMmzePin09MpkM58+fR1JS0qTEF6AfCet0OkilUjQ0NKCvrw++vr6Ij4+Hq6sr9bfL1tbW8Pb2hre3Nwgh6OvrQ3NzM06ePAknJycEBgbC19eXcT+n02TB5/ORnJyM3NxcODk5wd3d3cRejo+XlxccHBxQX19PpSb9mmuugbOzMw4ePIibbrqJcftTwSJOJHbt2oWNGzdSKT3TarWoqqpCZGQk1benSqUSeXl5iIyMhIeHx5RuS6M6QqvVoq6uDr/99hsqKirg4+ODa6+9FvHx8XB3d6cuvpfD4XDg6uqKuLg4XHvttRCLxairq8Phw4dRXV3NWH31TDrchEKhYZTl0NCQiTy8OhwOB5GRkaipqYFarWbcPpfLxaZNm7Br1y7GbU8Vs09B9Pb2wtfXF4WFhYiIiGDcfm1tLdrb25Genk5NgAkhOHXqFBwcHBAXFzdtP5ha9Nnc3IzKykrY29sjPDwcHh4eJnvsTLmSiBCCnp4eVFdXY2BgAGFhYQgICDDZi4ex2osrKirQ0dGBjIwMqi90Z8+ehZOTEyIjIxm3LZVKERgYiMrKSiq56MliXmHIGHz11VdYsGABFfFVq9Worq6mHv02NDRApVIhJiZmRn6YMhImhKC9vR1ZWVmoq6tDXFwc0tPT4enpaVYHW1OBw+HA3d0dqampSEhIQHNzM44ePYrm5maj17oac7bD/PnzweVyUV1dbSTvpkdERATq6+uhUCgYty0SibBs2TJ88sknjNueCmYtwIQQ7Nq1C/feey8V+7W1tXBzc4ObmxsV+8DF0ZIVFRWIj483SoRnChHWR9YlJSUIDQ3FsmXLIBKJLFZ4x8LDwwMZGRmIiopCVVUVsrOz0d/fb5RrG3uwDofDQXx8POrq6ozm43RwdnaGj48PtReChx56CB9//LFZt+ebtQCfO3cOUqkUd911F+O2NRoNGhoaEBYWxrhtPfoaz8DAQKOuOjKWCBNCUFdXZ9g1t2LFCvj7+88q4R2Nvkpi+fLl8PX1RU5ODqqqqmY0DNxUU82cnJwQHh6OwsJCqsPKw8LC0NzcPGHzjKlYu3YteDwefvvtN8ZtTxazFuC9e/di2bJlRl9yORlaWlrg6Ohosh1vk6G+vh4jIyMmydfOVITlcjlycnIgkUiQlpZmtHXqlgCXy0VoaCgyMjIglUrHHZZ+NUw9UjI4OBjW1tZU29IdHR3h7u4OiUTCuG0ul4tVq1Zh3759jNueLGYtwPv27cP69esZt0sIQX19PdWON7lcjsrKSqOlHsZiuiIskUiQnZ0Nd3d3ZGZmQigUmsQ/c8fJyQlLliyBr68vTp48idra2knnhpmY56tPRdTX11Md2hMUFISGhgYqkfjNN9+MAwcOmO2cELMV4MbGRtTX12PDhg2M2+7u7oZaraa2a44QgvPnzxs99TAWUxFhnU6HCxcuoLKyEikpKXMq6h0PfTS8ZMkSNDY2orCw8KpjIpkcpu7o6Ijw8HAUFRVRS0V4eHiAx+Ohvb2dcdsrVqyASqVCUVER47Yng9kK8MGDB5GUlESlI6m+vh6BgYHUSngaGxtNlnoYi8mI8MjICE6fPg2ZTMYu+RwDZ2dnZGRkQKVSIScnZ9yTfxqbLPSpiJqaGkbsXQ6Hw0FQUBDq6uoYt21tbY0lS5Zg//79jNueDGYrwPv27cP111/PuF2FQoGuri5qtYMajQZVVVWIiYlhNLqcSIQHBgaQnZ0NOzs7pKWlUcnJWwI2NjZYvHgxXF1dkZ2djb6+vku+T2uNEIfDQUxMDGpra6kchgEXZ0QMDg5SqcpYt26d2eaBzVKABwYGkJOTg9tuu41x2xKJBCKRiNpov7q6Ojg7O0+5280YjCXCvb29yMnJQWBgoEnz0bMFLpeLmJgYw/62zs5OAPQXaAqFQnh5eVErCbO2tsa8efOoHMZt3LgRVVVVaGlpYdz21TBLAf71118NXUdMQghBU1MTtehXpVKhtraWSueQntEiXFRUhNOnTyMmJgYhISGztrzMFIjFYiQkJCA/Px/FxcXUF2gCFxsjJBIJtTZlsViMlpYWxlcpCQQCLFq0CAcPHmTU7mQwSwHet28fVq5cybjdzs5OcLlcaoNMqqur4eXlRb2qwNHREZGRkWhqaoKnpyf1IUSWikgkwrx589DY2IiwsDDqCzQdHBzg7++PyspKKvYFAgEcHBzQ1tbGuO3Vq1dj7969jNu9GmYnwBqNBj/99BNuvfVWxm1LJBJqjQTDw8OQSCSYP38+47Yvp7e3FxcuXEB0dDR6e3tn9XojU9LQ0IDW1lbExsaisrLSkI6gSVhYGKRSKbUOuYCAACppiFtvvRUnTpyAXC5n3PZEmJ0Anzp1Cvb29khOTmbUrlqtRkdHB7V5v5WVlZg3bx4cHR2p2Ncjl8tx5swZxMTEIDg4eE7smDMFo3O+gYGBhnQE7SWafD4fQUFBqKiooGLf19cXMpkMw8PDjNoNCQlBUFAQDh8+zKjdq2F2Arx//34q60w6OjogFAqpnPAPDAygvb2d+sB0tVqNs2fPIjg42PBCNFcWfRqTsQ7cRCIRIiMjkZeXB6VSSdW/kJAQ9PX1obu7m3Hb1tbW8PDwgFQqZdz2ypUrza4awiwFmEbzhVQqhUgkYtwuANTU1CAgIIBqeRchBOfOnYOzs/MV8y9YEZ48E1U7BAYGwsvLC/n5+YwfRI2Gx+MhNDSUWl2wSCSiIsA333wzfvrpJ6qP/eWYlQBXVVVBKpVi7dq1jNrV6XTo6OiAl5cXo3aBi4PW29vbERQUxLjt0ZSXl0OpVCI+Pn7MHDgrwldnMqVm+pGiFy5coNoeKxaL0dvbi8HBQcZte3l5oaenh/Fh7enp6eDxeGa1tt6sBDgrKwuLFi1iPBLs6emBra3tmMscTU1DQwNEIhHV7bytra1obm5GUlISrK3H31LFivD4TLbOl8vlYtGiReju7kZjYyNzDl4Gj8fDvHnzUF9fz7htPp8PgUDA+KEkl8tFamoqsrKyGLU7EWYlwPn5+VS2uurTD0xXP2i1WjQ2NlKNfpVKJYqLi7Fw4ULY29tf9edZEb6SqTZZ2NraIjExEeXl5VRP5QMDA9Hc3IyRkRHGbdNKQyxcuBB5eXmM2x0PsxLggoICJCUlMWqTEAKpVEol/dDe3g47OztqIy8JISguLoavry88PT0nfTtWhP+P6Xa4ubi4ICgoCEVFRdRSEU5OTnBzc0NzczPjtkUiETo6OhgfEJSSkoJz584xanMizEaAVSoVKioqsGTJEkbtDg4OQq1WU9l60djYiICAAGodZi0tLejv759W5x0rwjNvLw4LC4NaraaSBtAjFoshkUgYfxFwcnICj8dDb28vo3YXL16Mzs5OdHV1MWp3PMxGgEtKSuDq6sp415U++mW67E0ul0Mmk8HX15dRu3oUCgVKSkoQHx8PHo83rWvMZRE2xmwHKysrLFy4EJWVldRSESKRCCMjI1cMDjI1HA6HShrCwcEBwcHBZhMFm40Anzt3jsriTVrpB4lEAl9f32mL30wpLS2Fr6/vjIf+0Fp5r1KpoFAoDHMNVCoVY+VFxhysIxQKERQUhOLiYiqpCC6XS21IjpeXF6RSKeP3OzIy0mwEePwjb4bJz89HbGwsozZVKhX6+/sZF2BCCFpbW7Fw4UJG7erp6+tDV1eX0eZt6EU4NzcXAIzaUKLRaNDf3w+ZTGb4V186ZWVlZXjncuTIEYMvQqEQAoHA8K8xX+RMMdUsNDQUR48eRWdnJ5VgYN68eTh58iTi4uIYfSfo7u4OlUoFuVzOaAVSfHy82RzEmY0AFxQU4Omnn2bUZnd3N4RCIeNR6MDAALRaLZWh5oQQlJeXIyQkBDY2Nka7rjFFWKPRoK2tDc3Nzejp6YGdnR2EQiGEQiH8/PwMvzMOhwOtVotDhw7hhhtuACHEINIymQyNjY0YGhqCi4sL/P39Z/yOw1QjJa2trREeHo7y8nJ4enoyfibg5OQEW1tb9PT0MDoGlcvlwsPDA93d3YwKcHJyMj744APG7E2EWQiwSqVCeXk50tPTGbUrk8moVCDQyjsDQFdXFwYHB00ya2OmIjw8PIz6+npIJBI4OTnB398fixYtmtQLBYfDgbW1Ndzd3S+ZZqdWq9He3o7m5maUlpZi3rx5CA4OnvLMDVPP8/X390dtbS1aW1vh5+dn9OtPxOh8LNNzqIVCIePzMVJTU9HR0YHu7m5qkw/1mEUOuLS0FEKhkPH5vzKZjMqIQFptz/roNzw8fMKGi5kwnZzwyMgIioqKcOzYMajVaixZsgQZGRkICAiYcZTO4/Hg7++PtLQ0LF26FABw/PhxFBQUTHomAxPD1LlcLiIjI1FRUUFldxutfCwNAXZ0dERQUJBZ5IHNQoDPnTvH+BBy/dtVpmfvKhQKDAwMUNl40d7eDo1GY/KB81MRYalUiqysLGg0GqxYsQLx8fEm2wPo6OiIuLg4XHPNNbCyskJWVhZaWlomFB0mN1l4e3vDxsYGTU1NJrUzFm5ublCr1Yy3JgsEAgwODjI+nyEiIoIVYD0FBQWIiYlh1Obw8DB0Oh3j4x87Ojrg7u5Opfqhrq4OwcHBjKQ+ribCarUahYWFKCoqQkxMDBYtWsRYO7atrS3i4+OxcOFClJWVIT8/f8xdaUyvEeJwOAgJCUFdXR3jkSiXyzVEwUxia2sLPp+PgYEBRu3Gx8cjPz+fUZtjYRYCnJ+fz3gHnEwmg1AoZPzAg1bZm756gMk66/FEuKenB8eOHYNWq8Xy5cvh4+MzY1tnJX145WgNzkgmX8/q5eWF5cuXg8fj4dixY+jo6DB8j9YON29vb2i1WiqNAjQEGLgYBTOdhli8eDEKCgoYtTkW1AV4ZGQEZWVlVA7gmM7/ajQadHd3U8n/NjY2wt/f32S53/G4XIQ7Ojpw5swZREREYNGiRVddfnq6cXLCuuKD0/jbr9VIezd3SiLM4/EQHx+PBQsWoKCgAK2trVQXaHK5XAQEBKChoYFRu8BFAe7v72d8XjGNPHBaWhra29upzEQeDXUBlkgk4PP5CAwMZNQujfxvX18f7OzsJjX0xpio1Wq0tLRQWzaqF+G6ujrk5eUhISFhUptHTjf2IX1n7qSElcPhQKsj4HA4yKrtmbKP3t7eWLx4MYqKilBWVkZ1gaa/vz86OzsZF0IejwcXFxf09Ez98ZsJQqGQ8RVJjo6O8PHxoTYTWQ91AW5vb2f8QIoQQiUCplX21tbWBqFQSGXcph59zt3KymrST7asuu5JCyshBFZcDgghWBYy/lyPiSLqgYEBcDgccDgcqlPK+Hw+vLy8qBzG0YhGhUKhoTaeSTw8PNDe3s6ozcuZkwKsFwOmBYlW2VtbWxu1mRPAxcg/Pz8fiYmJyMjImHSJ2vIQ90kL67Hfp2D7teHI/WMaFovHfpGbKKLWpx3S09ORnJyM8+fPUx3Y4ufnR0UcaORjbW1tYWtry/hBnLu7OyvA7e3tjBdD9/f3QyAQMH4ARyPtQTPvDFyc26Cf8yESiaZUorZY7ILcP6ZdVVgBIMnfBc+vCJnwZ8aLqC/P+bq7uyMuLg6FhYWMb23Q4+HhgYGBASgUCkbt6tMBc6Ee2NPTkxXg1tbWKc2iNQYDAwMmqzUdD7VajeHhYcYj4M7OTjg5OVHbuFFRUQE7O7tLcvxTFeGrCetkGSuiHu/Azc/PD66urigpKZmx3enA4/Hg5uZ2SWUGEzg6OkKn0zG+tdjZ2ZnxGmRvb2+0trYyavNyqAtwW1sbvL29GbWpVCoZFySZTAZHR0fGqxBoLhvt7e2FRCIZc8+co6Mj7ANiUFJVi9/OnmfEn8sjai+dbMJqh9jYWHR0dFApzQLobI3gcDhUolE7OzvGDx19fHxYAW5ra2O8912lUl21/MnY0Mj/EkLQ0dFBRYC1Wi0KCwsRFRU1ZtXH6cY+LP24EI8XEDRJJIyK8PMrQq4qvsDF3GRcXByKi4upre3p6uqCRqNh1C6tPDDTAuzr68umINrb2xk/IFIqlYwv/qSR/9Wf6tM4+Kuuroadnd24pW/6fGzrMPC3UivIOloZmyc8lTpfHx8fuLq6oqKighHfRmNvbw8HBwfGt0bQiID5fP6Y3YimxN/fn9q7Gz1mIcCTqQk1JrQEmEbZG41uP41Gg4aGBkRFRY1re3Q+tl0BiObHMzLUfTpNFpGRkWhubmY8QgPoiKFAIGC8IoHP50OpVDJ6+BcQEICenh7G32GMhuo4Sv1gGianoBFCoFKpGBdghULBeAOGXoCZRiKRGAaij4c+H5tV24NlIW5YLHaB3MfZJEPd9VxNfE839iGrrhvLQ9wvOfRzcHCAl5cXGhoaGN/aIhQKGW+MsLOzw8jICLRaLaysrBixaWtrC51OB7VabdQ51RPh4eEBHo+Hjo4OamWaVCNgqVQKBwcHRkVC/zaHqV8ycLECQqvVzpm8c0NDA4KDg6/6s5dXOJhyvdFkxHeirrvg4GBIJBLGR0XSiIB5PB6srKwYjfi5XC7jeWAulwt3d3e0tbUxZvMKH6hZxsUDOKZrgJVKJWxtbRl9W65SqQx/1Eyh0+kwMDBApd1ao9FMu7TQFCI8mbTD1bruXFxcwOPx0NnZaRSfJouzszMUCgXjh4A0crI0DuJoN2NQFWAaTRg00g80cs6Dg4Pg8XiM221qasK8efNmNPLSmCI82Zzv1bruOBwO/P39GW8Ptra2hqOjI+OzEvQ5WaZtMi36tJsxqOaA29vbGW/CoCGGNGwODw/DwcGB8QO47u5uLFiwYMbXMcaOuakcuI2Vk74cLy8v1NTUgBDC6OPq4ODAeGMELQFm2ibteRBUBbi3t5fxHOVcEWB9qoVJ1Go1hoaGpnTANREzEeHpVDssFrtM6JuTkxN0Oh3jB6pzRQxp2HR1dWX8kHM0VFMQGo2G0cMwgE4TBg0xpJFq6e/vh6Oj4xXbPqYyVvJyppOOMNU8X31NNY0aWaaFiUY+1tbWlvEUBI/Ho1qGRl2AmTyYAmAYicgkcyXvPF7Z21TGSo7FVETY1MPUaXWJMS1MNPKxVlZWjFeZWFtbU+ly1ENVgEdGRhifjaDT6RjPi6rVasZ3wJlTs8lUxkqOx2REmIlNFjSGh9OIgHk8HuOT4DgcDhUBnrONGBqNhnEBZvoARW+TiUWYo6GRahkYGBhz59xkDrgmw+iccLNMgT3NXFR3DOAxP2B3dhHcVF0m32Th7OzMeFsyjQiYy+UyPpKSw+EwbnPOCzDTkSENMaQRddO4n1qtdtzf59UOuCaLfopadXE+ets5yG3l4DE/QNfbip81Iqw2gvhOdGBII2fI5XIZjwxpiCEN0TdWpD8wMIA333wTycnJaGxsxMMPPzyp201JgFtbW/Hvf//bcGro6uqKRx99dNqbdjUaDWxtbRldRaLVakEIYdSmTqebEzb1tkxtc3dJJw6VcLAthiBOcPEJ+2oZUDfUibiARty7aPqbn89K+rDig9PgcDjYfrgSx36fgiT/S0VYo9Ew+rgSQqBUKhm3qVAoGP+bZfp+9vb2oqysbMbX+fzzz5GQkIDVq1fj5ptvxh133DGpmeMcMoWXnJKSEgQFBSEnJwfd3d244447ZuT0/fffD51Ohw0bNszoOixzG0IAht9gsMwSXnrpJRQWFs448t66dSvuueceJCQk4JFHHsE999yDRYsWXfV2U4qAY2Ji8Nlnn2HDhg3YtWvXtJ3Vo+/UWrNmzYyvNVnOnTsHd3d3RjcEnz59GgEBAYwOns/OzkZUVBSjnYZHjx5FYmKiyWu7l+zMhatGhtv8CT6sAZ6OBL5vAr5v5mJdpAjf3Llw2tfOa+rD8l2nDW/BL4+AFQoFjh8/jtWrVxvjrkyKgYEBnD17Ftdccw1jNru7u1FWVobMzEzGbLa1tUEikSAlJYUxmyUlJUYZBja6umoqQ4ymnAPOzc3Fvffei6Kioqne9Ap4PB6jE5cAGPKiTNvkcDiz3qbelqlt/iHGAbqePvythIs2BQDosNwLGNERPLksZEb2UwLdkfWH9AkPDK2srBj/XXK5XEZtcjicOWHTWOdQUVFRaG9vx4IFC9Dc3IygoKBJ3W7KAvziiy8CAP71r39N9aZXGre2Zry8hsbhwlwpr7GxsTH577OhoQFuyk4U2InRONQCG+7F3+X/V8rBvxKs4KLsBDCzw76JDgyVSiXjzUM06uXnSrWQsSqxNm/ejB07dkCn02Hp0qWTHoI1Zcv6uZnGGKJOQyRonLTSOMWmUbqkb1Lw8fG54nvTaUW+nNF1vqsFAlwTLcYbx2oBtOFv18fi+mh3k84TBujMWKZR063T6RgXQxqib6wI2NnZGX/7298AADfccMOkb0e1EWOuFHvT6mRi+t3FeLNrZ9KKrGesJovFYhd8c9fFfO+9i+aZdJ6wHhqbTWgIsEqlYjzSp9UkxXQvwmioCrC1tTWjJSd6m0xH3XNJgPv7+694hzHTVuSpdLiZWoTnSgRMo32eRmMWjV6E0VAV4LkycYnWMBWmRd/R0RFarRYKheKSr8+kFXk67cWmEuGJpr2ZEloDpObC/BIag7JGQ1WAPT090d3dzajNuSLANGxyuVwIBAL09V2aYtC3Im+/Nhy5f0ybdA54JrMdpiPCpxv78MrRmnFTJDKZDPb29nNirsdcsdnZ2QmRSMSozdFQbUX28fFhBdhEODk5YXBwkPHDFJFIhNbW1iuWHE61FdkYg3WmMk9Yn6fmcDj46/+qxnyhaG1tZbSWW8/AwACcnJwYtTlXBLirq2vMQ2OmoBoBe3t7My7ANEf7MVl9wefzwePxMDg4yJhNAJg3bx46Ojpm9Bgbc6rZZCPhq+WpNRoNWltbp912P10UCgXUajUVAZ4Lc7O7u7upvKjqMQsBZvIgjs/nG1ZuM4V+3gWTh380h4d7eHhMe3eaKUZKTkaEr5anbm1thZOT06T6+41Jf38/nJ2dGX0XQwihcghHw+acFmAPDw8QQhjdyWRtbQ0rKytGo2Aul8tIk8Ll0JhdC1xc4V5fXz/lcj9TzvO9mghPlKcmhKCurg7BwcFG9Wky0Ki60A8oZzIa1Wg00Gg0jArwyMgIent7564AW1lZwcPDAxKJhDGbHA6HSk7WwcEBcrmcUZvj1eWaGnd3d9ja2qKlpWXCnxt96MXEMPXJiPDzK0KuyP12dHRAo9FQeaLSEGC5XA57e3tGa3KVSiV4PB6jHX9NTU3g8XhwcZn5mNTpQlWAgYuHNkyv+qYhwDTSAfoImOm6Zw6Hg/nz56O8vHzcdxqjmzNe2nMSJWUVJh+mDky9OkKj0aCkpATz58+n0hnW19fHuADLZDIqzSZM53+bmprg5eXFePPHaKgLsLe391UjJWND4yCORjrAzs4Ojo6O6OrqYtQucPGF1dPTEyUlJWN+X3/odY2XFr8TE9Ta+DP2pJ+KCJeVlcHJyYnxwzfg4qxaLpfLeN6ZRtRNI//b0tJCNf0AmIEA+/r6MpoDBui26TI9h0IkEkEqlTJqU090dDR6enrQ1tZ2xfeWh7hjlZcWm8QEL5ZykRruO8YVjI8+7VHao76qCHd1daG1tRVxcXFUoiSpVAqRSMS47f7+/jnR7ccKMC4KMNMCYW9vj+HhYUZtOjk5Qa1WMy78IpEIHR0djAs/cHE6WlxcHIqLi694x+Glk+H34dbodg7G1/emG2Vd0dW4fCbFRCKs0Whw/vx5REdHw87OzuS+jYVegJlEo9FALpcznoIYHh5m/HFua2u7ol6daagLsLe3Nzo7Oxm1SSMfq38rSSMPDOCK7jSmEIlE8PLyQlFRkaEqQn/glrkkDU+uimZEfIGxa33HSkcQQlBcXEwt9QBcPAhTKBSMDtQHLka/dnZ2jA/ioZH26OjooNqEAZiJADOdoxQIBBgaGmJ8EhuNPDCHw6GahgCA2NhYjIyMoKioCPX19VOqdrhae/BUGK/Wd7QIV1ZWorS0FP39/Vi4cCG1AxqpVApPT0/G5wDTEEJCCJUpc11dXWwKgkY7Mo/Hg729PQYGBhi1S6sszM/PD83NzYyP4dRjbW2NxYsXG9bcpKSkTFp8JzvG8qzk6kI9Ua2vo6MjUlNTUVtbi+bmZqSkpDAeBeohhKCpqQl+fn6M26aR/x0cHASXy4W9vT2jdmm3IQNmIMC+vr7o6uoyFH8zBQ0xdHFxQV9fH+P5WDc3N1hZWTGe6hlNa2srtFot7OzsUFdXN6kXg6mMsVzxwelJCfV4tb6EEDQ0NMDGxgYcDofx0sjR9PX1YWRkhPH8LyEEvb29jAuwXvSZfrfBpiBwMUfo6OiIwsJCRu3SyAM7OTmBy+Wit7eXUbscDgdisRiNjY2M2tWjz/mmpaUhIyMDQ0NDOHPmzBVjKy9nKmMsZzJvWKVSIT8/Hz09PcjIyMCSJUtMOtT9ajQ2NmLevHmM1x3L5XKoVCq4uU1+XKgxoFF33NjYiIGBAZNtTpks1AWYw+Fg4cKFOHXqFKN2aUTANPOxYrEY3d3djHfjXd7hZmNjg9TUVDg6OiIrKwsSiWTcdwRTGWM53XnDbW1tyMrKgrW1NdLT08Hn8xnZrDEeSqUSbW1tCAwMZNQu8H95Z6aFn0baIycnB5GRkVRnAQOUx1HqWbRoEc6dO8eoTaFQiKGhIcan8ItEIpSWliIqKooxm8DFkjA/Pz/U19cjNjaWEZvjtRdbW1sjNjYWPj4+KCoqQnt7O+Li4sYsQ5rsGMtjv0/B8XrZuJuML0elUqGkpATd3d1YsGDBFW/3pzLK0pg0NjbC09OT8XwocFGAjbGifSoQQqgc/OXl5WHRokWM2hwL6hEwACQmJqKsrIxRm/qDOKarEtzd3aFQKBiPRIGLQ3Kampqu+tbfGExmtoO7uzuWLVsGe3t7ZGVloba2dtqVKUn+Y+d2L0ej0aChoQFZWVngcrlYvnz5uLlWpiPhkZER1NfXIyQkxOS2LkelUkEmk8HLy4tRu3K5nMoBXElJCRITExm1ORZmEQEnJCSgurqa8WhUnwdmMudlZWUFT09PdHR0wNHRkTG7wMUctK+vL6qqqrBgwQKT2ZnKYB19NKz3q6qqCmKxGGKx2KgzcIeGhtDU1ITGxkY4OTlh4cKF8PT0vOrtmIyEa2pq4ObmBldXV5PZGI+Ojg64uLhQqf8VCASMH8CVl5cjISGBUZtjYRYCLBaLYWtri3PnziE5OZkxu7TKwkQiEZqbm6mMN5w/fz6OHj2K4OBgkwz5nu5UMzc3N6SmpqK/vx/19fXIzs6Gs7MzfHx8IBQKIRAIprQKSKPRYGBgAH19fWhvb4dMJoO3tzeSk5OnLHBMiLBCoUBDQwMyMjKMfu3JQKPrDqCT/5VIJOjr60NMTAyjdsfCLAR49EEckwLs4uLC6ChMPV5eXiguLsbIyAjjEYednR0CAwNRUVGBpKQko17bGCMlBQIB4uPjERMTg7a2NnR2dqKxsRFDQ0NwdHQ0iLGNjc0lTQotLS3QaDTo7++HTCaDXC6HnZ0dhEIh/Pz8kJycPKNdbqYW4aqqKvj6+jI+eAcAtFotOjs7ERkZybjt3t5exlMuOTk5iIiIoH4AB5iJAAN0DuJcXV2hUqkwODjI6MoXW1tbCIVCSKVS+Pv7M2ZXT2hoKH777Tf09vYa7e2usef5Wltbw9/f3/D4qNVqg7j29/dDrVZDq9Ua6onb2tpgY2MDZ2dn+Pn5QSgUGv3FzVQiPDg4iJaWFqxYscIo15sqXV1dhsl5TKJSqdDf3w8PDw9G7ebl5ZlF/hcwIwFOTEzE4cOHGbXJ5XLh6ekJqVTK+M4tf39/NDU1URFgGxsbhIeHo7i4GJmZmTMuO2JimDqPx4O7u/sVsxG0Wi0OHTqEpKQkRtp2jS3C+rkTQUFB1Ib+SCQSKjMvpFIpXF1dGd8yXVJSgltvvZVRm+NhFlUQwMWDuMrKSsaHh9Oqy/X19YVMJmN8aaaeoKAgWFtbz/h0nwnxNTeMWR1RX1+PkZERag0BSqUSnZ2dVAIBWnnniooKsziAA8xIgAMCAmBjY4Pz588zatfLywsymYzxAe3W1tbw8/OjkoMGLubd4+PjUV9fP+2DyLkovnqMIcJyuRyVlZWIj49nfOiOHv1WCKZn8Wq1WnR1dTEuwC0tLeju7jaLAzjAjARYfxCnf2vHFDY2NnBxcUFHRwejdoGLLzrNzc2MbmgejaOjI+bPn4/CwsIp+zCXxVfPTESYEIKioiIEBgZS20lGCIFEIoFYLGbcdnd3N+zt7eHg4MCo3ZMnTyIiIoLxF5zxMBsBBugcxAH00hBCoRAODg6Mr2QaTVBQEHg8HiorKyd9G1Z8/4/pirC+6YTmLAKpVAoOhzOpemhT2KaRfjCnAzjAzAQ4KSmJ8RQEcFGAOzs7qUSiQUFBqK+vp7KxArj4ziMhIQFNTU2TeiFgxfdKpirCnZ2dqK6uRmJiIrXUA3Ax/xwUFMR4EwQhhJoAM91rcDXMSoBXrlyJyspKxvOijo6OsLOzQ0/P1KZoGQMfHx+MjIwwPhN5NPb29li0aBGKi4snzAez4js+kxVhuVyOgoICLFy4kErNrx59SR+N6of+/n4QQhhPvQwMDCA/Px+rV69m1O5EmJUACwQCpKWl4bvvvmPcNq00BJfLRWBgIGpqahi3PRp3d3dERUXh7NmzY+6tY8X36lxNhNVqNc6ePYuQkBDqmxhqa2shFosZLwEDLqYfaKyD//HHHxESEkKl4mM8zEqAAeDGG2/EL7/8wrhdvQDTSAUEBgaiv7+fyvr40QQEBEAkEiEvL++SdAwrvpNnPBHW6XQoKCiAQCBAaGgoRQ8vRqBSqZTK0B+AXv734MGD2LBhA+N2J8LsBHjt2rU4c+YM49PCXF1dodPpqCyv5PF4CA8PR3l5ObVcsJ6YmBhYW1sjPz8fOp2OFd9pcLkI6yseRkZGsGDBAmp75vSUl5cjODiYSiWAXC6HXC5nvPtNq9UiOzsb69atY9Tu1TA7AQ4MDERAQAD27dvHqF0Oh4N58+ZRq8sNCAjAyMgI2tvbqdjXw+VykZSUBI1Gg+zsbFRUVLDiOw30ItzQ0IDs7GwMDg4iJSWF0Wl/Y9Hd3Q2ZTEZlEBRwsevO19eX8cfh6NGjsLa2NpsGDD1mJ8AAsH79ehw4cIBxu2KxGK2trYxvSwYuCt/8+fNRUVFBbXmmHmtra4hEIgwODkIgEDA+I2C24ODgAFdXVwwMDMDDw4Pakk89hBCUl5cjLCyMSu5Xp9OhqamJSt3xjz/+iHXr1jG+7eNqmJc3/4/169fj+PHjjJeFOTo6wsXFhVpdrp+fH7hcLpqbm6nY19PQ0ICamhqkpaWBEIL8/HxqzSKWik6nQ2FhIYaHh5Geno6WlhZqO+b0SKVSKJVKxrde6Glvbwefz6fSeHLs2DGzSz8AZirASUlJIIQgOzubcdtisZhqe3BkZCQqKyupCd7onK+bmxsWL14MQghOnjzJyCaN2YBKpcLp06cxPDyM1NRUuLq6Utsxp0en06G8vBzz58+nVnus77pjOgdeUVFBddrcRJilAHO5XKxZswY//PAD47a9vb2hUCioHMYBgKenJxwcHFBXV8e47bEO3KytrZGcnAx3d3dkZ2czvtHZ0hgYGMCJEydgZ2eH1NRUQ9qB5qJPAGhubjacc9BgaGgIvb298PPzY9z2N998gxUrVlCbNjcRZinAwMU0xNGjRxm3a2VlBX9/f9TX1zNuG7gYBUdHR6OmpobRSpCJqh24XC6io6MRERGB06dPo6mpiTG/LIn29nacPHkSQUFBYw7YoSXCSqUSZWVliImJoVaBUV9fD19fXyp58F9//dXsys/0mK0AX3PNNWhsbKTSoBAUFIT29vYxGxKYQCgUIigoCEVFRYyUpU221EwsFiMlJQXl5eW4cOEC46NDzRX92/uioiIsWrQIwcHB4wod0yKsnzfs6+vLeOmXHrVajaamJiqVF93d3SgqKsKaNWsYtz0ZzFaAHRwcsGzZMuzevZtx23Z2dvDy8kJjYyPjtvWEh4dDo9GYPBKfap2vq6srMjMzIZfLcfz4cSrt2+aETCZDdnY2enp6kJGRMaVFn0yIcEtLC/r7+6msG9LT1NQEoVBIpfV6z549iI2NZXzb82QxWwEGgA0bNlDpigMuRsGNjY3UDsO4XC7i4+NRWVlpslTEdJss7OzskJKSgtDQUJw5cwYlJSVzLhrW6XSoqKhAbm4u/P39kZ6ePqVyPSZEWKFQoKSkBPHx8VTKzoCLEXhDQwOCgoKo2DfH7rfRmL0AFxUVoaGhgXHbrq6u4PP5VEdFmjIVMdMONw6HA7FYjGXLlmFwcBDHjx9HR0cH9U4+Juju7kZ2dja6u7uRmZk5YcphIkwpwuaQegBgaO+n0Xrc29uL48eP45ZbbmHc9mQxawH28vLC6tWr8c477zBum8PhICwsDFVVVVRrYPWpCGNWRRizvdje3t4QDZ8/fx65ubmztlKiv78fZ86cQX5+PsRi8ZSj3rEwlQg3NzdjcHAQUVFRRrvmVCGEoLKyEqGhoVQO/3bt2oX4+HiEhYUxbnuymLUAA8BDDz2E7777jooIent7g8/nU80F61MRVVVVRtkfZ4rZDvpoeMWKFfDy8sKZM2eQl5dHbd+dsRkaGsK5c+eQk5MDgUCAlStXGnWOrrFFWKFQoLS0FPHx8VRbn1taWqDT6ahNH/v666/x8MMPU7E9WcxegK+55hpwOBz8+OOPjNvWN0ZUV1dTaU/WIxQKERoairNnz87ID1MP1rG2tkZoaChWrlwJBwcHZGdno6CgAL29vRaZmujv70dhYSGysrLA4/GwYsUKREREmCSfaiwR1mq1yMvLg7+//xUbpJlEp9OhsrISERERVNp/T548idbWVmzcuJFx21PB7AXYysoKW7Zswccff0zFvru7O4RCIZXGiNGEhoZCKBSioKBgWmLG5FQzGxsbREVFGYT47NmzOHHiBCQSidkf1mm1WjQ3NyMnJwe5ubmwtbXF8uXLERsba/LpYTMVYf3UNRsbG6pVDwDQ2NgIGxsbanOPd+7cibvuustsdr+NB4dYQGjS0tKC0NBQ1NTUUOmk6e/vR05ODlauXAlbW1vG7evRaDTIycmBu7s7oqOjJ3072iMltVotWltbIZFIMDAwAG9vb4hEInh6es74LbJWq8WhQ4ewZs2aabfYajQadHd3QyqVoq2tDQ4ODhCLxfDz86PyFl4ulyM3NxcBAQFT2hlXXV2NpqYmZGZmUqt6AC7W/f72229ITEykcgA4MDAAPz8/nDp1akrPExpYhAADwA033IDo6Gi89tprVOyfO3cOPB4PsbGxVOzrUSgUyM7ORmRk5KRya7TF93IGBwfR1taG9vZ2DA4Owt3dHSKRCCKRaFqtotMVYKVSiY6ODkilUnR1dcHBwQEikQg+Pj5m8ThNVYSlUikKCwuxZMkSODk5MeDh+FRWVqKvrw8pKSlU7L/xxhv49ttvkZeXR8X+VLAYAf71119x7733Gt7aMM3Q0BCysrKwbNkyxldpX05vby9Onz6NlJQUuLq6jvtz5ia+l6NQKC4RQR6PB6FQaPgQCATg8/kTHnZNRoCVSqVhB5pMJkN/fz+USuUl4m9vb2+quzltJivCAwMDOHnyJBITE6k3HKhUKvz2229IS0uDUChk3L5Op0NkZCT++te/4o477mDc/lSxGAHW6XQIDw/Hs88+iwceeICKDxcuXIBarTaLoc4SiQQVFRXIzMwcM3I0d/G9HJ1Oh4GBgUuEcmBgAFwuF3w+H3w+H7a2tob/W1lZgcPhgBCCCxcuIDo6GoQQqFQqKJVKw4dKpYJGo4Gzs7NB1PVdWTQ3Ek+Wq4nwyMgIsrOzERgYSG3F0GhKSkqgUqmorX4/cOAAHnzwQTQ1NVGfvzwZLEaAAeC9997DJ598gnPnzlGxr1QqcfToUaSnp5uFqJWWlqK7u/uSqVuA5YnveOh0OigUCoOQjhZWnU5n+Ojo6ICPjw+srKwuEWm9aNvZ2VmE2I7HeCKs0Whw5swZ2NnZYeHChdRXHQ0PD+PYsWNYunQptSH+K1asQGZmJv72t79RsT9VLEqA5XI5fH19cejQISxZsoSKD5WVlejt7UVKSgr1P3hCCAoLCzE0NISUlBTweLxZI76TxRiHcJbA5SKs1Wpx9uxZcLlcLFq0yCzu+7lz52BtbY24uDgq9isqKpCQkIDGxsZJzeQwB8y+DG00jo6OeOCBB/Cvf/2Lmg8hISEYGhqivrUCuFinHB8fDz6fjzNnzqCurm5Oie9cYnSJWmVlJfLz8wHAbMRXKpWis7MT8+fPp+bDP//5T9xyyy0WI76AhQkwAGzduhVHjhyhJoDW1taIj49HaWmpWWyI4HK5SExMhFqtRllZGZKTk1nxnaU4OjoiJSUFNTU1GBgYQFJSklmI78jICM6fP4+4uDhqZZr9/f3Ys2cPnnjiCSr2p4vFCXBAQACuv/56bNu2jZoP7u7u8PPzw/nz582iw0sikUCpVMLV1RVlZWVUu/ZYTIdWq0VZWRmcnZ2h0+moNwfpKSkpgbu7O3x8fKj58Pe//x0JCQlYsGABNR+mg8UJMAC8/PLL+Pbbb6kMa9cTGRkJuVxOfTuEPueblpaG1NRU8Pl8nDp1CiMjI1T9YjEu+gM3QgjS0tKQnp5OdcecHn0JYUxMDDUfOjo68OGHH+LVV1+l5sN0sUgBDg8Px6ZNm/DMM89Q80GfiigrK6OWirj8wI3L5SIhIQGOjo7Izc3F8PAwFb9YjItSqcSpU6dgZWWF5ORkWFtbU98xB/xf6iE2NpZqh+if//xnLF++HMnJydR8mC4WKcAA8OKLL+LIkSNUu11opiLGq3bgcrlYuHAhvLy8DJsaWCwX/cYNgUBwRc6XtgibQ+qhpqYGu3fvxj/+8Q9qPswEixVgX19f/OEPf8Bzzz1H1Q8aqYirlZrpp7jFxMTgzJkzVMdpskyf1tZW5ObmIiwsDHFxcWNOFaMlwu3t7dRTDwDw3HPP4bbbbqNafTET6A0LNQLPP/88AgIC8Ouvv+Laa6+l4oM+FZGXlwdPT0+Tr76eSp2vn58fHBwckJeXh4GBAURHR1MZDcgyNfSDzBsbG5GUlHTVgTZ6Ec7NzQWAKQ3wmQ4jIyMoLi6mnno4d+4cfv31V1RWVlLzYaZY9LPRxcUFzz//PF544QXodDpqfri7u2PevHkmT0VMp8nCxcUFmZmZkMlkOH36NHs4Z+ao1Wrk5eWhvb0dGRkZk54mxmQkXFJSAg8PD6qpBwB49tln8fDDD1OZkGgsLFqAAeDRRx9FW1sbvvnmG6p+REREYHh42GSVGTPpcOPz+UhLSwOfz0d2djZkMplJfGSZGYODgzh58iQAYMmSJVMe+sSECDc2NqK7u5t66uG3335Dfn4+nn/+eap+zBSLF2B7e3u89NJLePHFF6nubrO2tkZSUhJqa2shlUqNem1jtBdbWVlh4cKFCAoKQm5uLiorK6m+a2D5PwghqK2txYkTJ+Dr64ukpKRpz/M1pQh3d3ejrKwMSUlJVAfd6HQ6PP/883juuecmnAZoCVi8AAPAvffeC0IIdu7cSdUPJycnJCYmorCwEAMDA0a5pjFnO3A4HAQHByMjIwOdnZ3Izs5Gf3+/UfxkmR5yuRw5OTlobm5GWloawsPDZzxjxBQiPDw8jPz8fMTFxcHFxcUo15wu33//PVpaWvDYY49R9cMYzAoBtra2xj/+8Q+8/vrrUCqVVH3x9PREWFgYzp49O+N8q6kG6zg5OWHJkiXw8/NDTk4OGw1TgBCCuro6ZGdnw93dHZmZmUadn2tMEdZoNDh79qxhSwhNtFot/va3v+HFF180yxnOU2VWCDAA3HTTTfD29jaLesDg4GC4ubkhPz9/2sJm6qlmHA4HoaGhhmj4xIkTbDTMEPqoVyKRIC0tzWSLK40hwvqJe/b29oiIiDCyh1Pn/fffh1arxX333UfbFaMwawSYw+Hg9ddfxzvvvGP0HOx0fImLi4NWq0VpaemUb8/kSEknJyekp6fD19cXOTk5KCsrYyslTIRGo0FVVZXJot6xmKkIV1VVQS6Xm8W84YGBAbz66qt49dVXqezqMwUWNQ94Mvzud7+DXC7HoUOHaLsCpVKJ7OxshIeHIyAgYFK3oTnPd3BwEOXl5ejp6UFoaCiCgoLMYtrWRFjCPGCdTofGxkZUV1fD2dkZUVFRjP9up7Pos62tDcXFxcjIyKC+hgsA7rrrLkilUvz666/UXwyMxex4GRnFzp07ERERgc8//xz33HMPVV/4fD6SkpJw6tQpODo6wt3dfcKfpz1M3cnJCcnJyejt7UV5eTnq6+sRHh4Of39/toFjGhBC0NLSgsrKStjY2CAhIYHKlmBg6s0a/f39KCoqQlJSklmI76FDh7B//36UlpbOGvEFZmEEDAD79u3Dli1bUFJSApFIRNsdtLS0oKSkZMJIgrb4Xg4hBJ2dnSgvL4dOp0NERAS8vb3N7o/fHCPg0Y+dVqtFZGSk2Tx2k4mElUolTpw4gZCQEAQFBTHs4ZX09/cjOjoa27Zto7YP0lTMSgEGgE2bNmFgYAA//fQTbVcAXFxl1NzcjPT09Cvalc1NfEdzeRQXEhICb29vs4mIzUmACSGQSqWoq6uDXC7H/PnzzfLdw0QirFKpkJubC3d3d8TExJjFi8bmzZvR1dWF//3vf2bhjzGZtQLc09ODiIgIvPbaa7j33ntpuwNCCMrLy9He3o709HTw+XwA5i2+o9HpdGhqakJ9fT3UajUCAwMREBBAffOsOQiwWq02PDYcDgeBgYEQi8VmfVA0lgiPjIzg1KlTEAgEWLBggVmI3YEDB3DnnXeirKyMegmcKZi1AgwA+/fvx/3334+SkhJ4e3vTdgeEEJSUlKCrqwvp6eloa2uzCPEdDSEE3d3dqK+vR1dXF7y9vSEWi+Hm5kblCUtLgAkh6Ovrg0QiQWtrK1xdXREUFAQvLy+zEK7JMFqEg4KCDGcV5lDxAFxMPURFReHFF1/E/fffT9sdkzCrBRgAbr/9dshkMvz888+0XQFw8YlbXFyMjo4OaLVapKWlWYz4Xs7w8DAkEgmamppgZWUFf39/eHt7w9HRkbEnMNMCPDQ0BKlUiqamJiiVSvj7+0MsFlNbwz5T9DXJXC4XLi4uSEhIMJuUyR133IGenh788ssvZvGCYArM9z2SkXj33XcRERGBTz/91CyKtzkcDpydndHc3Aw7OztDKsIS0Rfnh4eHo7OzE83NzaipqYGtrS1EIhFEIhFcXV3N5gk9HfSRrlQqhVQqxfDwMDw8PBAeHg6RSGTR9w0AbG1twefzMTg4CCcnJ7O5P/v378ehQ4dQVlY2a8UXmAMRMHAxj3TfffeZRSpCn/NNSUlBY2Mjenp6kJqaavI5wkyh0+nQ3d2Njo4OSKVSqNVqeHl5QSQSwdPTc9pDZsbDFBGwRqNBV1cXpFIpOjo6wOFwDC8o7u7u1A/7jMXIyAhOnz4Ne3t7hIeH4/Tp01OqEzYV+tTD9u3bzeL8xpTMCQEGLr6d6e3txS+//ELNh8sP3AghKC0tRUdHB1JTU2dFb/toCCEYHBw0RI8ymQyOjo4QCoUQCASGf2dyWDVTAdZqtejv70d/fz9kMhn6+/sxMDAAZ2dng+gKBIJZF4WpVCqcOnUKzs7OiI+PB5fLnVazhinQP1d//vnnWfe4X86cEeDe3l5ERkbi5ZdfppLQH6/aQV8d0draitTUVIvNJU6GkZERg8jJZDLIZDIoFAo4OjoaBNnBwQF8Ph98Ph+2trZXfQJORoAJIVCpVFAqlVCpVBgaGjL4MDg4CD6ff8mLglAopLrpwdQoFAqcPn0arq6uiIuLu+Qxpi3C+/fvx913342ysjL4+voybp9p5owAAxe7ae666y7k5uYyOljkaqVmhBBUV1ejrq4OixYtotYtRYORkZFLos/h4WGDUOp0OkOOUv9hY2MDDocDLpcLDodjWN8TFhYGDocDnU6HkZERqFQqKBQKqFQqqFQqADCIur29/SUR+GwW28vp7e1FXl4e/Pz8EBUVNeYLHC0Rbm5uRlJSEv7xj39Q72JlijklwMDFPXI//PAD8vLyTD4IBZhanW9bWxuKiooQERGBwMDAWf/2ayIIIVCr1VAqlZd8jIyMgBBi+NBqtWhuboZYLIaVlRU4HA5sbGzGFe65THNzMy5cuIDo6GiIxeIJf5ZpEVYqlUhJSUFCQgI++uijOfO7mnMCrNPpsG7dOiiVSvz6668mPVCZTpNFf38/zp49C09PT8TGxprNqbS5Yg6NGOaOPs3V3NyMRYsWwc3NbVK3Y0qEdTodbrnlFkilUmRlZVFv7mGSOffs5nK5+O9//4vW1lY8/vjjJrMz3Q43gUCAzMxMwx+//u0zC8t0UKvVOHv2LLq6upCRkTFp8QWYW/T50ksvIS8vD3v37p1T4gvMQQEGAGdnZxw6dAhff/01PvroI6Nff6btxba2tkhNTYWTkxO7Nohl2sjlcpw4cQJWVlZIT0+fVpWNqUX4hx9+wBtvvIGDBw/C09PT6Nc3d+akAAMXt1bs2bMHTz31lGETrTEw1mwHLpeLuLg4hISEICcnB21tbUbzkWX2o99y4ufnh8TExBmV+plKhEtKSvDAAw/g888/x4IFC4x2XUtizgowACxfvhyvvPIKbr31VjQ3N8/4esYerMPhcBAUFISkpCQUFxejsrIScyxlzzJF9Lvm8vPzER8fb5Qln4DxRbinpwfr16/HY489ho0bN874epbKnDuEuxxCCB588EEUFBTg9OnT024NNvVUs6GhIeTl5cHa2hrx8fGzul54KrCHcP+HQqHA+fPnMTQ0hKSkJDg7OxvdhjEO5jQaDZYvXw53d3fs2bNnTh80z917/v/gcDjYuXMnHBwccMcdd0xriSYTIyUdHByQkZEBd3d3ZGdno66ujo2GWQBcDCIkEgmysrLg4OCApUuXmkR8AeNEwo888ghkMhn+85//zGnxBVgBBgDY2Nhg7969yM/Px0svvTSl2zI5z9fKygoRERFITU2FRCJBbm4u5HK5SW2ymDcKhQJnz55FdXU1kpKSEBsba/I5xDMR4XfffRc//PADDh48aBarjmjDCvD/w8PDAwcPHsSbb76JH374YVK3oTVM3cXFBZmZmXB1dWWj4TnK6KjX3t4ey5Ytu+rOQWMyHRE+duwYnn/+eezdu/eqjSBzhVk/jnIqxMXF4bPPPsN9990HDw8PZGRkjPuztDdZWFlZGXaNFRUVob29HfHx8WxUMQdQKBQoLi7G4OAgkpKSGBXe0Uxl0ef58+dx22234Y033sCSJUuYctHsYSPgy9i4cSNef/11bNiwAWfOnBnzZ2iL72hGR8PHjx9HfX09Gw3PUgghaGpqohb1jsVkIuGysjJcd911eOKJJ/Dggw8y7KF5w0bAY/DQQw9BqVRizZo1OHz4MBYuXGj4njmJr57R0XBhYSGam5sRFRVF/cnJYjz6+vpQXl6O4eFhsxvYNFEkXFNTg1WrVuHBBx/ECy+8QMtFs4UV4HF47LHHoFQqcd111+Ho0aOIiYkxS/EdjYuLC5YuXYrGxkbk5+dDKBQiMjLSLH1lmRyDg4OoqKhAd3e3YU28OS77HEuEGxoasGLFCtx+++148cUXKXtonpjfb9KMePbZZ6FQKLBq1Sr897//hVKpNFvx1WNlZYXg4GD4+/ujrq4OOTk5EIlEmD9/PpsftiAUCgWqqqrQ0tKCwMBALFiwwOznJIwW4Y6ODtxzzz1Yv349Xn/99Tkz3WyqsAJ8FbZt2waFQoE77rgDBw4cMGvxHQ2Px8P8+fMRGBiIqqoqZGVlwd/fH2FhYRa9h262MzIygpqaGjQ0NMDX1xcrVqywqHVVjo6OCAgIwHXXXYeVK1fi7bffZsV3AlgBvgocDgevvvoquFwu1q5di8OHDyMmJoa2W5PG1tYWsbGxCA4ORmVlJY4ePYrg4GAEBwcbfT8by/TRarWor69HTU0N3NzckJGRYbJmClNSV1eH6667Dtdccw127drFiu9VYAV4EnA4HPzjH/+AnZ0dVqxYgV9++QUJCQm03ZoSDg4OSEhIQH9/P8rLy/Hbb78hJCQEYrHY7N/azmbUarVhm7SDgwMWL14MV1dX2m5Ni6qqKqxcuRI33XQTduzYwYrvJGAFeAr87W9/A5/Px7XXXouDBw8iJSWFtktTRiAQICUlBd3d3aitrUVVVRXmzZuHoKAgODk50XZvzjA0NISGhgZIJBIIhUIsWLAAnp6eFitapaWlWLVqFe688068+uqrFns/mIYV4CnyzDPPgM/n44YbbsDevXuRmZlJ26Vp4e7uDnd3d8jlctTX1yM7Oxuurq4ICgqCl5cX+wQyAYQQdHd3o76+Hl1dXfD19UV6errFnCuMR2FhIVavXo2HH34Y27ZtY/92pgArwNPg0UcfBZ/Px9q1a7Fr1y7cfvvttF2aNo6OjoiNjUVERAQkEglKS0tx4cIF+Pv7w9/ff1pDvFkuRalUoqmpCU1NTdDpdAgICMCCBQtmxTLQn376CXfddReefvppPP/887TdsThYAZ4mDz74ILy9vbF582aUlJTg5ZdftujJTjweDyEhIQgODkZ3dzckEgmOHTsGNzc3iMVieHl5zflxj1NBp9Ohq6sLEokEnZ2d8PT0RExMjEWnGS7n9ddfx/bt2/H+++9j8+bNtN2xSObMPOC9e/fi+PHj2LRpExYvXmy065aVlWHNmjWIjo7GN998M6tqbUdGRtDc3IympiYMDQ3Bw8MDIpEIXl5eZlPKZk7zgEdGRtDR0QGpVIrOzk7w+XzMmzcP/v7+ZvN4GYORkRHcd999OHr0KPbv34+kpKQZXe+jjz5CZWUlFAoF3njjDYsqu5spc0aAc3JykJ6ebpJr9/T04Oabb0Z3dzcOHDiAwMBAk9ihydDQEKRSKTo6OtDT0wOBQACRSASRSAQnJydqUR1tAZbL5ZBKpZBKpejr64OLi4vhRWo2HmpKpVKsX78eOp0O+/fvh4+Pz4yv2dnZCTc3Nxw/fhwrVqwwgpeWw5wR4PLycuh0OlRVVcHd3R25ubnQ6XRYsmSJUQ7S1Go1nnzySXz77bfYvXv3rP5DUqvV6OzsNAgyj8cziLGbmxujqRimBVin06Gvr88gukqlEp6engbRnc0lfXl5edi4cSMyMzPx8ccfGzWq37NnD6677jqcO3fO6M9Nc2bO5IDz8/Nx991346uvvjIMBZHL5Ub7BfN4PLzzzjuIiYnBhg0b8Morr2Dr1q1Guba5wePx4OvrC19fX+h0OvT29kIqlaK4uBhKpRJCoRBCoRACgQBCoRCOjo4WmfckhGBoaAj9/f2QyWSGD/0LTkxMDNzc3KinPpjgP//5Dx555BH89a9/xdNPP2303+eFCxdw8803Iz4+HoBxn5vmzJwR4KioKBQUFGDhwoVoamqCjY0NiouLccMNNxjVzoMPPoiIiAjceOONuHDhAt5//32zHJ5iLLhcrqGkLSoqCsPDwwahampqQklJCXQ63SWCbI6irBdbmUx2ieACMPjt7++P2NhYs/PdlOh0Ojz77LP46KOP8M033+D66683iZ2hoSEAMOlz0xyZMymI0QwODsLJycmQszMFEokEa9euhVAoxN69e+Hm5mYSO+YOIeQSUdaLm06ng0AggJ2dHfh8Pvh8PmxtbQ3/5/P5sLa2vqrQTTYFoVaroVQqoVKpoFQqDR8qlQoKhQL9/f0AcMkLhUAgmFNiezlyuRy33norqqurcejQIcyfP9/kNpl4bpoTc1KAmWJoaAh33nknzp8/j7179yIuLo62S2aBXpQHBgagUCguEUP9/0dGRmBlZXWJKOsFmcPhgMvlgsPhgBCC+vp6BAQEGK5NCIFGo7nkelqtFjwe7xKB14u+nZ0dnJ2d4eDgMGfF9nJqa2uxbt06+Pj44Pvvv58TYkiD2fve2AxwcHDAnj17sH37dmRkZGDbtm14/PHHLbpe2BhwOBw4ODhMWLKn0+muiFa1Wi10Op1BZAkh0Gq1AC7mpa2srAzizOVyrxDauZCrNQYff/wxnn32WWzevBlvvPGGyVNoV3vRm80x4pyJgGn/krOysnDPPfdALBbjs88+Q3BwsEntzRVol6HNJlpbW/HAAw+gqKgIH330EdauXcu4D3v27IFcLsc999zDuG0azJlQbHTU9P333+Ozzz675GumZtmyZSgrK0N0dDQWLlyIt956CzqdzuR2WVgmwyeffILY2Fi4ubmhvLycivieP38esbGxjNulyZwRYD00f8mOjo547733sHfvXuzYsQNLly5FXV0dFV9YWACgvb0dq1evxgsvvIDPPvsMX331FbVxmJ2dnbhw4cKcek7MOQE2h1/y8uXLUVpaiqioKCQkJGDHjh1sNMzCOJ9++iliYmLg4uKCiooKrFu3jqo/q1atglqtxuDgIFU/mGTO5IBHs3v3bpw9exY7duyg7QqOHj2Ke++9F0FBQfjss89mZRuzKWFzwFOnvb0d999/PwoKCvDRRx9h/fr1tF2as8y5CBgANm3aZBbiCwArVqxAWVkZwsPDER8fj7fffpuNhllMxmeffYbY2FgIBAJUVFSw4kuZOSnA5oaTkxM++OAD7NmzB//85z+xfPlyNDQ00HaLZRYhlUpxww03GLradu/ePWebg8wJVoDNiJUrV6KsrAyhoaGIjY3F1q1b0dvbS9stFgtGLpfj2Wefxfz58+Hk5ISKigps2LCBtlss/w9WgM0MZ2dnfPTRRzhx4gSqqqoQEhKCbdu2YXh4mLZrLBbEyMgIXn/9dYSEhCA3Nxc///wzvvnmGzbqNTNYATZT4uPjcfjwYfzwww/4+eefERoain//+9+Gzi8WlrHQ6XT4+OOPER4eji+++AKffPIJTp48idTUVNqusYwBK8BmzrJly5CXl4d33nkHO3fuREREBL7++mv2oI7lCvbv34+4uDi89NJLeOmll3DhwgXccMMN7HwLM4YVYAuAw+HgpptuQnl5OZ555hn86U9/wqJFi/Drr7/Sdo3FDDh58iTS0tJw33334f7770dNTQ3uvPNOtizPAmAF2IKwtrbGAw88gLq6Otx666247bbbDBEyy9yjtLQU119/Pa6//nqsXLkSjY2NePzxx2fFtuW5AivAFoidnR2effZZNDQ0ICkpCcuXL8eGDRtQVFRE2zUWBqisrMTtt9+O5ORkBAcHo66uDi+++OKs3EE322EF2IJxcXHBa6+9hqqqKnh7eyM9PR1LlizBd999x+aIZxk6nQ4HDhzAihUrsHDhQvB4PJSUlOCdd96Bp6cnbfdYpgkrwLMAX19fvP/++2hubsaaNWvw5JNPIiQkBH//+98Nmx5YLJOhoSG88cYbiIyMxJYtW5CRkYGGhgZ88cUXCAoKou0eywxhBXgW4erqimeffRaNjY14/fXX8csvv8Df3x/33nsvm56wMCoqKvD73/8e8+bNw9dff42//OUvaG5uxrZt2+Dl5UXbPRYjwQrwLMTa2ho333wzcnNzkZWVBS6XiyVLliAhIQFvv/22YQEii3mhVCrx4YcfIiUlBQkJCRgcHMShQ4dw7tw5bN68eVavvJ+rsAI8y1m4cCE++eQTtLe346GHHsLnn38OHx8fbN68GWfOnKHtHgsuzqi+//774evrix07duD2229Ha2srvv76a6SmprJ1vLMYVoDnCE5OTnjwwQdRWFiI7OxsODs7Y9WqVQgNDcVDDz2Ew4cPs112DKHT6XDixAls3boVkZGRSEtLg06nw08//YSysjJs3bqVXYI5R5iT84BZLqJQKHDs2DHs378fBw4cgEajQUZGBtauXYubbroJAoGAtotXxVLmAQ8NDRke5+PHj0OtVmPNmjXYsGEDrrnmGjg6OtJ2kYUCrACzALgYlRUWFmL//v3Yv38/ampqkJSUhGuvvRa33Xab2S4RNWcBbmlpwbfffouff/4Zp0+fxrx587BhwwasX78eycnJZucvC/OwAswyJk1NTTh48CD27duHEydOIDQ0FCtWrMAtt9yC1NRUcLnmkb0yJwHW6XQoKCjA999/jyNHjqC8vBwpKSnYsGED1q5di5CQEKr+sZgfrACzXJWBgQEcPnwY+/fvx08//QQej4fo6GjExsYiKSkJS5YsgZ+fHxXfaAqwVCrFyZMncfbsWVy4cAGlpaWQy+VYvXo11q9fj9WrV7O5XJYJYQWYZUpoNBoUFhaioKAA+fn5OHfuHCoqKuDm5oaIiAiDKKenp0MsFpvcH6YEuL29/RKxLS8vR0dHB8LCwpCQkIBFixYZ/mXLxVgmCyvALDNGqVSipKQE586dQ35+PgoKClBRUQEXFxdERkYiJiYGSUlJSE5OhlgsNqpAGVuANRoNJBIJCgoKLhHbzs5OhIeHIzEx0SC2cXFxsLe3N8K9YJmrsALMYhJUKhVKSkouiZYrKiowMjICV1dXuLu7w93dHZ6envD09ISPjw98fX3h5+cHsVgMf3//SYnbZAVYpVKhqakJEokEzc3NaG1tRVtbGzo6OtDV1YXu7m50d3ejp6cHVlZWV4htbGws7OzsjPkQsbCwAszCHIQQyGQytLe3o62tDe3t7Yb/6wWxvb0dUqkUCoUCQqEQ7u7ucHV1BY/Hg7W1NaysrGBtbW34P5fLxeDgIGxtbaHVaqHVaqHRaAwffX196OrqQl9fH/h8Pry8vODt7Q1vb2/4+fnBx8fH8Ln+/66urmzzAwsjsALMYnYQQjA4OGgQ6M7OTqjVamg0GsO/oz+srKwMAj36g8fjwcPDwyCwAoGAFVYWs4IVYBYWFhZKmEcxJwsLC8schBVgFhYWFkqwAszCwsJCCVaAWVhYWCjBCjALCwsLJVgBZmFhYaEEK8AsLCwslGAFmIWFhYUSrACzsLCwUIIVYBYWFhZKWNN2gIVlqhQUFGB4eBjnzp3DE088QdsdFpZpw0bALBZHZWUlkpKSUF9fT9sVFpYZwQ7jYbFIDh06BD6fDx6Ph9zcXOh0OixZsgSZmZm0XWNhmTRsCoLF4nj55Zfx5z//GWvXrsXXX38NAJDL5az4slgcbAqCxeJYvHgxcnNzsXDhQjQ1NcHGxgbFxcW03WJhmTJsCoLFohkcHISTkxP6+vrYDcQsFgcrwCwsLCyUYHPALBbH1dYKsTEFi6XA5oBZLA5CCAgh2LlzJwghePrppw1fY8WXxZJgBZjFYvnDH/6Azs5OiMVi2q6wsEwLVoBZLBZCCLKysnD33XfTdoWFZVqwAsxisezYsQO5ubnYtWsXbVdYWKYFWwXBwsLCQgk2AmZhYWGhBCvALCwsLJRgBZiFhYWFEqwAs7CwsFCCFWAWFhYWSrACzMLCwkIJVoBZWFhYKMEKMAsLCwslWAFmYWFhoQQrwCwsLCyUYAWYhYWFhRKsALOwsLBQ4v8Hlub1m7lFxWkAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWAAAAF2CAYAAABQ7kLKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAOxQAADsUBR2zs/wAAg21JREFUeJztnXdc2/X2/18JhA0JoxD2hrLLLqPQZbXa5b7VXnf1erXur+ter19vr171Xq+9at1a9erVarXT1dpSWmgps5S9CTPsAIEkZLx/f/BLvrQFykg+7wQ+z8eDRws0n3OSNK+cnPcZHEIIAQsLCwsL43BpO8DCwsKyWGEFmIWFhYUSrACzsLCwUIIVYBYWFhZKsALMwsLCQglWgFlYWFgowQowCwsLCyVYAWZhYWGhBCvALCwsLJRgBZiFhYWFEqwAs7CwsFBiwQlwWVkZVq5cCQ6Hg6VLl+Jf//rXZf9mx44d2LFjBwXvpqarqwuPPPIIUlNTsXLlSqSmpmLNmjV4++23MTg4eMXbT3effvnlFyxbtgwcDkffbrOwsMwHskABQPbs2TPp70ZGRsjIyAizDk1DS0sL8fLyIm+//TbRaDS6n//www+Ex+ORn3/++YrXuNJ9ysrKInN5upubm8ljjz1GAJB77rmH/Otf/5r1Naajo6ODXH/99XO6bUtLC3nyySeJlZUVeeutty77/fDwMImJiSHLly+ft99KpZLs3LmTvPXWW+Stt94i586dm9f1rkRFRQUpLi6e8nsa/PTTTyQiIoJ89NFHF/3slltuoeiVabMoBdjY2LhxI9m0adOkv7vnnnvIb7/9Nm8bcxVgQgg5ceIEcXV1nbcPl/LOO++QHTt2EF9f3zlf48yZM+S2224jDz/88GW/+/bbb0loaCjJzs6eh5fjPPLII7rn4W9/+xv5wx/+MO9rTsfvf/97sn///im/p0V8fDyprq7WfS+RSIhYLKbokWmz6AT47bffJr6+viQzM1P3sw8++ICEhoYSX19f8tZbb5G1a9cSf39/8vnnn+v+jVgsJrfeeitJS0sjaWlpZMeOHWR0dFT3+3//+99k1apVZM2aNWT58uXk3XffnfL6V199NbGzsyN79uwh/f39hMvlko8//nja+zPdNSa7T7W1tSQzM5MkJCSQjRs3kjfffHPOAvzCCy8YLMppamqalwC//fbb5NChQ+Saa6656OdFRUXk9OnTxNramigUinn52NraSkJDQ3XfDw4Okr6+vnld80r4+vqSgYGBKb+nQV9fH/Hx8aHqw0LDnGL2gwoPP/wwent7cfLkSd3P7r//flhYWOD+++9HSEgIjh07hq+//hp/+MMfcPvtt4PD4WDz5s2Ij4/HN998A7VajRtuuAHPPPMM3nrrLQCAWq3Gjz/+CGtraygUCkRGRiImJgapqam66z/wwAPw9PTEL7/8gv3798PKygp1dXXQaDTw9vae1u/prnHXXXdddJ9UKhU2bNiAzZs34/XXX4dKpcJNN90058csKysLt99++5S/F4vF2LVr17TXyMjIwLXXXjtnH6ZCqVRi6dKlqK+v1/1MoVBAJBJhbGwMKSkpsLCwmJe/R48ehZubG7799luMjIygrKwML7744rS37+zsxN69e1FcXIzNmzdDKpUiNzcXu3btgo2NDWQyGV588UWEhYVBJpPhwIEDOHr0KH7++WccPnwYXC4X77zzDuzt7VFTU6P7ftOmTYiOjoZKpcLrr78OJycnAICLiwtuuukmlJaWIicnBx9//DGioqLQ29uLkJAQPPPMM9i7dy/OnDmDBx98EPX19aisrMTGjRuxevXqi3xfv349rr322svOFLKysrBy5UoAgFQqxWeffYbjx4/jgw8+gFqtvuL1p/IZwJSPx1T350rPn8lA+x3AUGCaFMSLL754UbRICCF79uwhfD5f931VVRUBQDo6Osi5c+cIgIs+eu3bt49YW1vrcrYnTpwg11xzDUlLSyOZmZmEz+eT119//aLrOzg4XOaL9tq//vrrRT9/++23SWZmJgkNDSVPPvnktNe49D7l5OQQAKSurk73+2+//XZOEfDIyAixsLC46L7rk/lEwAqFguzevZuMjY0RKysrolKpCCHj93VsbIw88MADZOfOnfP28eWXXybOzs5EIpEQQgj58MMPyfbt26e9zccff0zUajVZsmQJaW5uJoQQ8uyzz5IPPviAEELIzTffTPbu3UsIIWTv3r3k2muvvei2jz322JTfEzKektD+/25paSH//Oc/CSGE/Pzzz0ShUBAHBwdSXV1NJBIJ6e/vJx9++CFRKpXEwcGB5OXlEUIIOXz4MPnjH/94me9Hjx4l9fX1l/38wQcf1H0q/Pzzz4lcLidRUVGkpqZmRtefyufpHo+p7s9C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", "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -190,9 +279,9 @@ }, { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -216,9 +305,19 @@ "cell_type": "code", "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/yigit/Codes/DESC/desc/utils.py:560: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n" + ] + } + ], "source": [ - "eq = get(\"HELIOTRON\")" + "eq = get(\"HELIOTRON\")\n", + "eq.change_resolution(3, 3, 3, 6, 6, 6)" ] }, { @@ -231,12 +330,12 @@ "output_type": "stream", "text": [ "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.22 sec\n", + "Timer: Precomputing transforms = 606 ms\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.21 sec\n", + "Timer: Precomputing transforms = 684 ms\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.23 sec\n", - "Timer: Objective build = 14.1 ms\n" + "Timer: Precomputing transforms = 681 ms\n", + "Timer: Objective build = 6.35 ms\n" ] } ], @@ -272,14 +371,10 @@ "R_modes = np.vstack(\n", " (\n", " [0, 0, 0],\n", - " eq.surface.R_basis.modes[\n", - " np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :\n", - " ],\n", + " eq.surface.R_basis.modes[np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :],\n", " )\n", ")\n", - "Z_modes = eq.surface.Z_basis.modes[\n", - " np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :\n", - "]\n", + "Z_modes = eq.surface.Z_basis.modes[np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :]\n", "constraints = (\n", " FixBoundaryR(eq=eq, modes=R_modes),\n", " FixBoundaryZ(eq=eq, modes=Z_modes),\n", @@ -307,11 +402,11 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 385 ms\n", - "Timer: Linear constraint projection build = 3.30 sec\n", - "Number of parameters: 1614\n", - "Number of objectives: 1236\n", - "Timer: Initializing the optimization = 3.80 sec\n", + "Timer: Objective build = 274 ms\n", + "Timer: Linear constraint projection build = 1.74 sec\n", + "Number of parameters: 97\n", + "Number of objectives: 456\n", + "Timer: Initializing the optimization = 2.04 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", @@ -319,41 +414,64 @@ "This should run on GPU id:1\n", "This should run on GPU id:1\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 9.547e+04 3.162e+02 \n", + " 0 1 1.561e+22 1.767e+11 \n", "This should run on GPU id:0\n", "This should run on GPU id:1\n", "This should run on GPU id:1\n", - " 1 6 4.883e+04 4.664e+04 5.955e+00 1.431e+02 \n", - "Warning: Maximum number of function evaluations has been exceeded.\n", - " Current function value: 4.883e+04\n", - " Total delta_x: 5.955e+00\n", + " 1 5 5.466e+21 1.014e+22 2.003e+01 1.046e+11 \n", + "Warning: Maximum number of iterations has been exceeded.\n", + " Current function value: 5.466e+21\n", + " Total delta_x: 2.003e+01\n", " Iterations: 1\n", - " Function evaluations: 6\n", + " Function evaluations: 5\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 26.0 sec\n", - "Timer: Avg time per step = 13.0 sec\n", + "Timer: Solution time = 8.54 sec\n", + "Timer: Avg time per step = 4.27 sec\n", "==============================================================================================================\n", " Start --> End\n", - "Total (sum of squares): 9.547e+04 --> 4.883e+04, \n", - "Maximum absolute Quasi-symmetry (1,19) two-term error: 5.074e-01 --> 1.932e-01 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,19) two-term error: 9.317e-06 --> 1.989e-05 (T^3)\n", - "Average absolute Quasi-symmetry (1,19) two-term error: 1.363e-01 --> 2.698e-02 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,19) two-term error: 8.055e+00 --> 3.067e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.479e-04 --> 3.158e-04 (normalized)\n", - "Average absolute Quasi-symmetry (1,19) two-term error: 2.165e+00 --> 4.283e-01 (normalized)\n", - "Maximum absolute Quasi-symmetry (1,19) two-term error: 1.686e+00 --> 1.997e+00 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.764e-04 --> 9.557e-05 (T^3)\n", - "Average absolute Quasi-symmetry (1,19) two-term error: 4.168e-01 --> 2.352e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,19) two-term error: 2.676e+01 --> 3.170e+01 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,19) two-term error: 2.800e-03 --> 1.517e-03 (normalized)\n", - "Average absolute Quasi-symmetry (1,19) two-term error: 6.616e+00 --> 3.733e+00 (normalized)\n", - "Aspect ratio: 1.048e+01 --> 1.004e+01 (dimensionless)\n", + "Total (sum of squares): 5.469e+26 --> 5.466e+21, \n", + "Maximum absolute Quasi-symmetry (1,19) two-term error: 5.015e-01 --> 1.664e-01 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.219e-03 --> 7.828e-04 (T^3)\n", + "Average absolute Quasi-symmetry (1,19) two-term error: 1.947e-01 --> 6.795e-02 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,19) two-term error: 7.961e+00 --> 2.641e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.935e-02 --> 1.243e-02 (normalized)\n", + "Average absolute Quasi-symmetry (1,19) two-term error: 3.091e+00 --> 1.079e+00 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,19) two-term error: 2.906e+12 --> 9.188e+09 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.348e-03 --> 2.568e-04 (T^3)\n", + "Average absolute Quasi-symmetry (1,19) two-term error: 3.194e+09 --> 1.010e+07 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,19) two-term error: 4.614e+13 --> 1.459e+11 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,19) two-term error: 2.141e-02 --> 4.077e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,19) two-term error: 5.070e+10 --> 1.603e+08 (normalized)\n", + "Aspect ratio: 1.053e+01 --> 8.876e+00 (dimensionless)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", "==============================================================================================================\n" ] + }, + { + "data": { + "text/plain": [ + "(Equilibrium at 0x7e56a13c7140 (L=3, M=3, N=3, NFP=19, sym=True, spectral_indexing=fringe),\n", + " message: Maximum number of iterations has been exceeded.\n", + " success: False\n", + " fun: [ 3.114e-01 9.780e-01 ... 6.524e+02 8.763e+01]\n", + " x: [-1.118e-01 5.238e-02 ... 1.617e+00 -2.211e-01]\n", + " nit: 1\n", + " cost: 5.465705943561737e+21\n", + " v: [ 1.000e+00 1.000e+00 ... 1.000e+00 1.000e+00]\n", + " optimality: 104553392458.05392\n", + " nfev: 5\n", + " njev: 2\n", + " allx: [Array([-3.392e-05, 8.921e-06, ..., 0.000e+00, 0.000e+00], dtype=float64), Array([-4.503e-05, -1.034e-03, ..., 0.000e+00, 0.000e+00], dtype=float64)]\n", + " alltr: [Array( 2.307e+16, dtype=float64), np.float64(5767465574622139.0), np.float64(1441866393655534.8), np.float64(360466598413883.75), np.float64(360466598413883.75)]\n", + " history: [[{'R_lmn': Array([-3.392e-05, 8.921e-06, ..., 0.000e+00, 1.850e-05], dtype=float64), 'Z_lmn': Array([ 9.011e-06, 1.167e-05, ..., -3.697e-05, 1.686e-05], dtype=float64), 'L_lmn': Array([-6.194e-07, -1.567e-05, ..., -9.721e-06, -1.466e-05], dtype=float64), 'p_l': Array([ 1.800e+04, -3.600e+04, ..., 0.000e+00, 0.000e+00], dtype=float64), 'i_l': Array([ 1.000e+00, 1.500e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'c_l': Array([], shape=(0,), dtype=float64), 'Psi': Array([ 1.000e+00], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.039e+01, 1.019e-01, 1.330e-03, 1.737e-05], dtype=float64), 'Za_n': Array([ 1.802e-05, 1.335e-03, 9.939e-02], dtype=float64), 'Rb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'Zb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}], [{'R_lmn': Array([-4.503e-05, -1.034e-03, ..., 0.000e+00, -1.036e-03], dtype=float64), 'Z_lmn': Array([-4.761e-04, -1.745e-03, ..., 8.102e-04, -2.369e-04], dtype=float64), 'L_lmn': Array([-2.543e-03, -3.826e-03, ..., 3.800e-04, 7.083e-04], dtype=float64), 'p_l': Array([ 1.800e+04, -3.600e+04, ..., 0.000e+00, 0.000e+00], dtype=float64), 'i_l': Array([ 1.776e+00, -3.323e+00, ..., 3.407e+00, 3.810e+00], dtype=float64), 'c_l': Array([], shape=(0,), dtype=float64), 'Psi': Array([ 1.000e+00], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.042e+01, 8.998e-02, -2.521e-03, 3.798e-04], dtype=float64), 'Za_n': Array([-9.521e-04, -1.837e-03, 7.433e-02], dtype=float64), 'Rb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'Zb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}]])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -366,7 +484,7 @@ " options={\n", " \"initial_trust_ratio\": 1.0,\n", " },\n", - ");" + ")" ] }, { @@ -378,17 +496,27 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/yigit/Codes/DESC/desc/utils.py:560: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n" + ] + } + ], "source": [ "eq = get(\"precise_QA\")\n", - "eq.change_resolution(12,12,12,24,24,24)" + "# eq.change_resolution(12, 12, 12, 24, 24, 24)\n", + "eq.change_resolution(3, 3, 3, 6, 6, 6)" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -396,12 +524,12 @@ "output_type": "stream", "text": [ "Precomputing transforms\n", - "Timer: Precomputing transforms = 114 ms\n", + "Timer: Precomputing transforms = 810 ms\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 118 ms\n", + "Timer: Precomputing transforms = 1.32 sec\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 86.1 ms\n", - "Timer: Objective build = 10.5 ms\n" + "Timer: Precomputing transforms = 407 ms\n", + "Timer: Objective build = 5.30 ms\n" ] } ], @@ -429,22 +557,18 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ - "k = 12\n", + "k = 1\n", "R_modes = np.vstack(\n", " (\n", " [0, 0, 0],\n", - " eq.surface.R_basis.modes[\n", - " np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :\n", - " ],\n", + " eq.surface.R_basis.modes[np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :],\n", " )\n", ")\n", - "Z_modes = eq.surface.Z_basis.modes[\n", - " np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :\n", - "]\n", + "Z_modes = eq.surface.Z_basis.modes[np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :]\n", "constraints = (\n", " ForceBalance(eq=eq),\n", " FixBoundaryR(eq=eq, modes=R_modes),\n", @@ -458,7 +582,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -467,62 +591,93 @@ "text": [ "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 133 ms\n", - "Timer: Objective build = 161 ms\n", - "Timer: Proximal projection build = 2.08 sec\n", + "Timer: Precomputing transforms = 863 ms\n", + "Timer: Objective build = 1.08 sec\n", + "Timer: Proximal projection build = 4.83 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 139 ms\n", - "Timer: Linear constraint projection build = 2.18 sec\n", - "Number of parameters: 624\n", - "Number of objectives: 12251\n", - "Timer: Initializing the optimization = 4.67 sec\n", + "Timer: Objective build = 232 ms\n", + "Timer: Linear constraint projection build = 1.11 sec\n", + "Number of parameters: 8\n", + "Number of objectives: 911\n", + "Timer: Initializing the optimization = 6.23 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 2.003e+04 1.949e+02 \n", - " 1 5 1.551e+04 4.515e+03 2.594e-02 1.685e+02 \n", + " 0 1 2.011e+04 1.952e+02 \n", + " 1 4 8.735e+03 1.138e+04 4.838e-02 1.104e+02 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 1.551e+04\n", - " Total delta_x: 2.594e-02\n", + " Current function value: 8.735e+03\n", + " Total delta_x: 4.838e-02\n", " Iterations: 1\n", - " Function evaluations: 5\n", + " Function evaluations: 4\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 1.89 min\n", - "Timer: Avg time per step = 56.9 sec\n", + "Timer: Solution time = 29.2 sec\n", + "Timer: Avg time per step = 14.6 sec\n", "==============================================================================================================\n", " Start --> End\n", - "Total (sum of squares): 2.003e+04 --> 1.551e+04, \n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.058e-01 --> 2.625e-01 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.547e-05 --> 1.156e-05 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 5.258e-02 --> 7.294e-02 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.155e-01 --> 2.864e-01 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.688e-05 --> 1.261e-05 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 5.737e-02 --> 7.959e-02 (normalized)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 2.241e-01 --> 5.706e-01 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.994e-05 --> 2.975e-05 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 9.189e-02 --> 1.198e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 2.446e-01 --> 6.226e-01 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.176e-05 --> 3.246e-05 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.003e-01 --> 1.307e-01 (normalized)\n", - "Aspect ratio: 6.000e+00 --> 6.242e+00 (dimensionless)\n", - "Maximum absolute Force error: 1.757e+01 --> 4.893e+01 (N)\n", - "Minimum absolute Force error: 1.160e-05 --> 1.148e-04 (N)\n", - "Average absolute Force error: 3.140e-01 --> 6.310e-01 (N)\n", - "Maximum absolute Force error: 1.256e-05 --> 3.498e-05 (normalized)\n", - "Minimum absolute Force error: 8.291e-12 --> 8.205e-11 (normalized)\n", - "Average absolute Force error: 2.244e-07 --> 4.511e-07 (normalized)\n", - "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", - "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", + "Total (sum of squares): 2.011e+04 --> 8.735e+03, \n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.813e-01 --> 6.254e-01 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.150e-04 --> 4.713e-03 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 5.169e-02 --> 2.630e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.978e-01 --> 6.824e-01 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.346e-04 --> 5.143e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 5.640e-02 --> 2.869e-01 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.161e+00 --> 9.141e-01 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.945e-03 --> 1.241e-03 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.051e-01 --> 2.811e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.267e+00 --> 9.974e-01 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.122e-03 --> 1.354e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.147e-01 --> 3.067e-01 (normalized)\n", + "Aspect ratio: 5.996e+00 --> 6.709e+00 (dimensionless)\n", + "Maximum absolute Force error: 1.345e+05 --> 1.302e+04 (N)\n", + "Minimum absolute Force error: 8.350e+00 --> 2.077e+00 (N)\n", + "Average absolute Force error: 5.462e+03 --> 1.001e+03 (N)\n", + "Maximum absolute Force error: 9.614e-02 --> 9.309e-03 (normalized)\n", + "Minimum absolute Force error: 5.969e-06 --> 1.485e-06 (normalized)\n", + "Average absolute Force error: 3.904e-03 --> 7.158e-04 (normalized)\n", + "R boundary error: 0.000e+00 --> 4.734e-19 (m)\n", + "Z boundary error: 0.000e+00 --> 3.478e-18 (m)\n", "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", "Fixed current profile error: 0.000e+00 --> 0.000e+00 (A)\n", "==============================================================================================================\n" ] + }, + { + "data": { + "text/plain": [ + "(Equilibrium at 0x7e5688ede8a0 (L=3, M=3, N=3, NFP=2, sym=True, spectral_indexing=ansi),\n", + " message: Maximum number of iterations has been exceeded.\n", + " success: False\n", + " fun: [-6.669e-02 -1.838e-01 ... 1.709e-01 -1.291e+02]\n", + " x: [-2.124e-01 1.388e-01 1.794e-01 -7.720e-02 -1.261e-01\n", + " 4.834e-02 -2.327e-01 -1.485e-01]\n", + " nit: 1\n", + " cost: 8735.080665954583\n", + " v: [ 1.000e+00 1.000e+00 1.000e+00 1.000e+00 1.000e+00\n", + " 1.000e+00 1.000e+00 1.000e+00]\n", + " optimality: 110.41872641325968\n", + " nfev: 4\n", + " njev: 2\n", + " allx: [Array([ 0.000e+00, 0.000e+00, ..., 1.082e-03, -2.543e-03], dtype=float64), Array([ 0.000e+00, 0.000e+00, ..., 1.082e-03, -2.543e-03], dtype=float64)]\n", + " alltr: [Array( 5.665e+02, dtype=float64), np.float64(130.5803471209196), np.float64(32.6450867802299), np.float64(65.29017356045979)]\n", + " history: [[{'R_lmn': Array([-3.535e-03, 1.627e-03, ..., 5.860e-04, 1.585e-04], dtype=float64), 'Z_lmn': Array([-9.096e-04, 1.867e-03, ..., -1.343e-04, 1.075e-03], dtype=float64), 'L_lmn': Array([-2.543e-03, -2.040e-04, ..., -1.109e-03, -1.629e-03], dtype=float64), 'p_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", + " 0.000e+00], dtype=float64), 'i_l': Array([], shape=(0,), dtype=float64), 'c_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", + " 0.000e+00], dtype=float64), 'Psi': Array([ 8.700e-02], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.020e+00, 1.971e-01, 2.315e-02, 2.547e-03], dtype=float64), 'Za_n': Array([-2.473e-03, -2.071e-02, -1.521e-01], dtype=float64), 'Rb_lmn': Array([ 2.268e-04, 1.531e-03, ..., 2.246e-03, 1.295e-04], dtype=float64), 'Zb_lmn': Array([ 4.367e-04, 9.219e-04, ..., 1.082e-03, -2.543e-03], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}], [{'R_lmn': Array([-3.535e-03, 1.627e-03, ..., 5.860e-04, 1.585e-04], dtype=float64), 'Z_lmn': Array([-9.096e-04, 1.867e-03, ..., -1.343e-04, 1.075e-03], dtype=float64), 'L_lmn': Array([-2.543e-03, -2.040e-04, ..., -1.109e-03, -1.629e-03], dtype=float64), 'p_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", + " 0.000e+00], dtype=float64), 'i_l': Array([], shape=(0,), dtype=float64), 'c_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", + " 0.000e+00], dtype=float64), 'Psi': Array([ 8.700e-02], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.020e+00, 1.971e-01, 2.315e-02, 2.547e-03], dtype=float64), 'Za_n': Array([-2.473e-03, -2.071e-02, -1.521e-01], dtype=float64), 'Rb_lmn': Array([ 2.268e-04, 1.531e-03, ..., 2.246e-03, 1.295e-04], dtype=float64), 'Zb_lmn': Array([ 4.367e-04, 9.219e-04, ..., 1.082e-03, -2.543e-03], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}], [{'R_lmn': Array([-3.074e-03, 1.531e-03, ..., -1.188e-04, 1.295e-04], dtype=float64), 'Z_lmn': Array([-6.459e-04, 9.544e-04, ..., -7.129e-05, 2.324e-04], dtype=float64), 'L_lmn': Array([ 1.664e-03, 5.507e-04, ..., -2.559e-03, 1.939e-03], dtype=float64), 'p_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", + " 0.000e+00], dtype=float64), 'i_l': Array([], shape=(0,), dtype=float64), 'c_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", + " 0.000e+00], dtype=float64), 'Psi': Array([ 8.700e-02], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.050e+00, 1.833e-01, 2.304e-02, 2.564e-03], dtype=float64), 'Za_n': Array([-1.729e-03, -1.924e-02, -1.507e-01], dtype=float64), 'Rb_lmn': Array([ 2.268e-04, 1.531e-03, ..., 2.246e-03, 1.295e-04], dtype=float64), 'Zb_lmn': Array([ 4.367e-04, 9.219e-04, ..., 1.082e-03, -2.543e-03], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}]])" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ @@ -535,7 +690,7 @@ " options={\n", " \"initial_trust_ratio\": 1.0,\n", " },\n", - ");" + ")" ] }, { @@ -548,9 +703,9 @@ ], "metadata": { "kernelspec": { - "display_name": "desc-env [~/.conda/envs/desc-env/]", + "display_name": "desc-env", "language": "python", - "name": "conda_desc-env" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -562,7 +717,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/setup.cfg b/setup.cfg index 93b478c506..b3ebfd05ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,6 +79,7 @@ per-file-ignores = desc/compute/data_index.py: E501 # need imports in weird order for selecting device before benchmarks tests/benchmarks/benchmark*.py: E402 + tests/test_multidevice.py: E402 # stop complaining about setting gpu before import other desc stuff desc/examples/precise_QA.py: E402 desc/examples/precise_QH.py: E402 diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py new file mode 100644 index 0000000000..d7ddc543b3 --- /dev/null +++ b/tests/test_multidevice.py @@ -0,0 +1,63 @@ +"""Tests for the multidevice capabilities.""" + +# This file has to run on a separate process because it changes the number of CPUs +from desc import _set_cpu_count, set_device + +num_device = 1 +_set_cpu_count(num_device) +set_device(kind="cpu", num_device=num_device) + +import numpy as np +import pytest + +from desc.backend import jax, jnp +from desc.examples import get +from desc.grid import Grid, LinearGrid +from desc.objectives import ForceBalance, ObjectiveFunction + + +@pytest.mark.xfail(reason="This test is not working right now") +@pytest.mark.unit +def test_multidevice_jac(): + """Test that the Jacobian is the same for a single and multi device.""" + eq = get("HELIOTRON") + eq.change_resolution(3, 3, 3, 6, 6, 6) + + # TODO: This doesn't work right now because grid order is not the same + grid1 = LinearGrid( + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2, 0.4]), sym=True + ) + grid2 = LinearGrid( + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.6, 0.8]), sym=True + ) + grid1 = Grid(grid1.nodes, weights=grid1.weights, spacing=grid1.spacing) + grid2 = Grid(grid2.nodes, weights=grid2.weights, spacing=grid2.spacing) + grid3 = Grid( + jnp.vstack([grid1.nodes, grid2.nodes]), + weights=jnp.hstack([grid1.weights, grid2.weights]), + spacing=jnp.hstack([grid1.spacing, grid2.spacing]), + ) + + objective1 = ForceBalance(eq, grid=grid1, device_id=0) + objective2 = ForceBalance(eq, grid=grid2, device_id=0) + objective3 = ForceBalance(eq, grid=grid3, device_id=0) + + for obj in [objective1, objective2, objective3]: + obj.build() + obj = jax.device_put(obj, device=obj._device) + obj.things[0] = eq + + obj1 = ObjectiveFunction([objective1, objective2]) + obj2 = ObjectiveFunction(objective3) + obj1.build(use_jit=False) + obj2.build(use_jit=False) + + np.testing.assert_allclose(obj1.x(eq), obj2.x(eq)) + + np.testing.assert_allclose( + grid3.nodes, jnp.vstack([grid1.nodes, grid2.nodes]), atol=1e-12, rtol=1e-12 + ) + err1 = objective1.jac_scaled_error(obj1.x(eq)) + err2 = objective2.jac_scaled_error(obj2.x(eq)) + + np.testing.assert_allclose(err1, err2) From 315b4ed8ebe71ac9d541daa71f19e33206812caa Mon Sep 17 00:00:00 2001 From: YigitElma Date: Fri, 14 Feb 2025 00:41:25 -0500 Subject: [PATCH 074/199] improve test --- desc/objectives/objective_funs.py | 3 +- tests/test_multidevice.py | 76 ++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index d8560210a7..1205e8d2ef 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -380,7 +380,8 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 "Setting the deriv_mode to 'blocked' to ensure that each sub-objective\n" "runs on the correct device.", ) - self._deriv_mode != "blocked" + if self._is_multi_device: + self._deriv_mode = "blocked" if self._jac_chunk_size == "auto": # Heuristic estimates of fwd mode Jacobian memory usage, diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index d7ddc543b3..dbfc57a4a9 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -10,54 +10,76 @@ import numpy as np import pytest -from desc.backend import jax, jnp +from desc.backend import jax from desc.examples import get -from desc.grid import Grid, LinearGrid +from desc.grid import LinearGrid from desc.objectives import ForceBalance, ObjectiveFunction -@pytest.mark.xfail(reason="This test is not working right now") +@pytest.mark.xfail(reason="We need to make a new action for these tests.") @pytest.mark.unit def test_multidevice_jac(): """Test that the Jacobian is the same for a single and multi device.""" eq = get("HELIOTRON") - eq.change_resolution(3, 3, 3, 6, 6, 6) + eq.change_resolution(6, 6, 3, 12, 12, 6) + eq1 = eq.copy() + eq2 = eq.copy() - # TODO: This doesn't work right now because grid order is not the same grid1 = LinearGrid( - M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2, 0.4]), sym=True + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2]), sym=True ) grid2 = LinearGrid( M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.6, 0.8]), sym=True ) - grid1 = Grid(grid1.nodes, weights=grid1.weights, spacing=grid1.spacing) - grid2 = Grid(grid2.nodes, weights=grid2.weights, spacing=grid2.spacing) - grid3 = Grid( - jnp.vstack([grid1.nodes, grid2.nodes]), - weights=jnp.hstack([grid1.weights, grid2.weights]), - spacing=jnp.hstack([grid1.spacing, grid2.spacing]), + grid3 = LinearGrid( + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2, 0.6]), sym=True + ) + grid4 = LinearGrid( + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.4, 0.8, 0.9]), sym=True ) - objective1 = ForceBalance(eq, grid=grid1, device_id=0) - objective2 = ForceBalance(eq, grid=grid2, device_id=0) - objective3 = ForceBalance(eq, grid=grid3, device_id=0) + objective1 = ForceBalance(eq1, grid=grid1, device_id=0) + objective2 = ForceBalance(eq1, grid=grid2, device_id=1) + objective3 = ForceBalance(eq2, grid=grid3, device_id=0) + objective4 = ForceBalance(eq2, grid=grid4, device_id=0) - for obj in [objective1, objective2, objective3]: + for obj in [objective1, objective2, objective3, objective4]: obj.build() obj = jax.device_put(obj, device=obj._device) - obj.things[0] = eq + objective1.things[0] = eq1 + objective2.things[0] = eq1 + objective3.things[0] = eq2 + objective4.things[0] = eq2 + # this one is multi-device, and grids have different sizes obj1 = ObjectiveFunction([objective1, objective2]) - obj2 = ObjectiveFunction(objective3) - obj1.build(use_jit=False) - obj2.build(use_jit=False) + # this one is single device, and grids have different sizes + obj2 = ObjectiveFunction([objective3, objective4]) + obj1.build() + obj2.build() - np.testing.assert_allclose(obj1.x(eq), obj2.x(eq)) + assert obj1._is_multi_device + assert not obj2._is_multi_device - np.testing.assert_allclose( - grid3.nodes, jnp.vstack([grid1.nodes, grid2.nodes]), atol=1e-12, rtol=1e-12 - ) - err1 = objective1.jac_scaled_error(obj1.x(eq)) - err2 = objective2.jac_scaled_error(obj2.x(eq)) + np.testing.assert_allclose(obj1.x(eq1), obj2.x(eq2)) + + # multi-device objective must be blocked + assert obj1._deriv_mode == "blocked" + assert obj2._deriv_mode == "batched" + + # creating grids like grid3 = [grid1, grid2] doesn't give the same + # node, spacing and weight ordering, so we can't compare the Jacobians + # or the objective values directly. Instead, we compare the objective + # values before and after a single iteration of the solver. This should + # always decrease the objective value. + error_init1 = obj1.compute_scalar(obj1.x(eq1)) + error_init2 = obj2.compute_scalar(obj2.x(eq2)) + + eq1.solve(objective=obj1, maxiter=1) + eq2.solve(objective=obj2, maxiter=1) + + error_final1 = obj1.compute_scalar(obj1.x(eq1)) + error_final2 = obj2.compute_scalar(obj2.x(eq2)) - np.testing.assert_allclose(err1, err2) + assert error_final1 < error_init1 + assert error_final2 < error_init2 From 865a2f870eab05af324b9db1f51c7ec734609129 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 25 Feb 2025 00:11:09 -0500 Subject: [PATCH 075/199] fix formatting --- desc/backend.py | 1 - 1 file changed, 1 deletion(-) diff --git a/desc/backend.py b/desc/backend.py index 7ed7445293..dcdc37e71d 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -66,7 +66,6 @@ def print_backend_info(): else: print(f"Using NumPy backend: version={np.__version__}, dtype={y.dtype}.") - if desc_config["num_device"] == 1: print( f"CPU Info: {desc_config['cpu_info']} with {desc_config['cpu_mem']:.2f} " From 3b4f847fd70b3219f80e865207d2fb6819ac93a0 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 25 Feb 2025 15:35:51 -0500 Subject: [PATCH 076/199] implement MPI to objective function,update tutorial --- desc/objectives/getters.py | 8 +- desc/objectives/objective_funs.py | 223 +++-- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 42 + .../tutorials/mpi-tutorials/mpi-proximal.py | 91 ++ docs/notebooks/tutorials/multi_device.ipynb | 829 +++++++----------- setup.cfg | 1 + 6 files changed, 632 insertions(+), 562 deletions(-) create mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py create mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index fba28f78a3..bd5e0e3ebd 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -346,7 +346,7 @@ def add_if_multiple(constraints, cls): return constraints -def get_parallel_forcebalance(eq, num_device, grid=None, use_jit=True): +def get_parallel_forcebalance(eq, num_device, mpi, grid=None, use_jit=True, verbose=1): """Get an ObjectiveFunction for parallel computing ForceBalance. Parameters @@ -400,13 +400,13 @@ def get_parallel_forcebalance(eq, num_device, grid=None, use_jit=True): else: gridi = grid[i] obj = ForceBalance(eq, grid=gridi, device_id=i) - obj.build(use_jit=use_jit) + obj.build(use_jit=use_jit, verbose=verbose) obj = jax.device_put(obj, obj._device) # if the eq is also distrubuted across GPUs, then some internal logic # that checks if the things are different will fail, so we need to # set the eq to be the same manually obj._things[0] = eq objs += (obj,) - objective = ObjectiveFunction(objs) - objective.build(use_jit=use_jit) + objective = ObjectiveFunction(objs, mpi=mpi, deriv_mode="blocked") + objective.build(use_jit=use_jit, verbose=verbose) return objective diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 1205e8d2ef..57ecde27df 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -234,6 +234,8 @@ class ObjectiveFunction(IOAble): accurately estimate the available device memory, so the "auto" chunk_size option will yield a larger chunk size than may be needed. It is recommended to manually choose a chunk_size if an OOM error is experienced in this case. + mpi : MPI object, optional + MPI communicator. """ @@ -246,6 +248,7 @@ def __init__( deriv_mode="auto", name="ObjectiveFunction", jac_chunk_size="auto", + mpi=None, ): if not isinstance(objectives, (tuple, list)): objectives = (objectives,) @@ -275,6 +278,85 @@ def __init__( self._name = name device_ids = [obj._device_id for obj in objectives] self._is_multi_device = len(set(device_ids)) > 1 + if mpi is not None: + self.mpi = mpi + self.comm = self.mpi.COMM_WORLD + self.rank = self.comm.Get_rank() + self.size = self.comm.Get_size() + self.running = True + assert all(device_ids == np.arange(self.size)) + + if self._is_multi_device and mpi is None: + raise ValueError( + "When using multiple devices, MPI communicator must be passed." + ) + + def __enter__(self): + # when entering the context manager, we start the worker loop + # this will allow the root rank to send messages to the workers + # to compute and to stop + self.worker_loop() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # this will be called when the context manager exits + # we send a stop message to the workers + if self.rank == 0: + message = ("STOP", None, None) + self.comm.bcast(message, root=0) + self.running = False + + def worker_loop(self): + """Worker loop for MPI parallelization. + + This function is called when the ObjectiveFunction is used as a context manager. + + with ObjectiveFunction(...) as obj: + if rank == 0: + eq.optimize(objective=obj) + + Worker processes will be in a loop waiting for messages from the root rank + during the context manager. The root rank will send messages to the workers + to compute the objective function and its derivatives, and to stop. The workers + will then broadcast the results back to the root rank. Once the context manager + exits, the loop will be terminated by the root rank. + + This way, we can still use MPI parallelization with the ObjectiveFunction, but + prevent execution of redundant calculations multiple times on different ranks. + + """ + if self.rank == 0: + return # Root rank won't enter worker loop + while self.running: + print(f"Rank {self.rank} waiting for message") + message = (None, None, None) + message = self.comm.bcast(message, root=0) + if message[0] == "STOP": + print(f"Rank {self.rank} STOPPING") + break + elif "jvp" in message[0]: + print(f"Rank {self.rank} computing {message[0]}") + # inputs to jitted functions must live on the same device. Need to + # put xi and vi on the same device as the objective + obj = self.objectives[self.rank] + const = self.constants[self.rank] + thing_idx = self._things_per_objective_idx[self.rank] + xi = [message[1][i] for i in thing_idx] + vi = [message[2][i] for i in thing_idx] + xi = jax.device_put(xi, obj._device) + vi = jax.device_put(vi, obj._device) + J_rank = getattr(obj, message[0])(vi, xi, constants=const) + J_rank = np.asarray(J_rank) + self.comm.gather(J_rank, root=0) + elif "compute" in message[0]: + print(f"Rank {self.rank} computing {message[0]}") + obj = self.objectives[self.rank] + const = self.constants[self.rank] + par = message[1][self.rank] + par = jax.device_put(par, obj._device) + f_rank = getattr(obj, message[0])(*par, constants=const) + f_rank = np.asarray(f_rank) + self.comm.gather(f_rank, root=0) def _unjit(self): """Remove jit compiled methods.""" @@ -485,14 +567,18 @@ def compute_unscaled(self, x, constants=None): ] ) else: # pragma: no cover - f = pconcat( - [ - obj.compute_unscaled( - *jax.device_put(par, obj._device), constants=const - ) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) + if self.rank == 0: + message = ("compute_unscaled", params, None) + self.comm.bcast(message, root=0) + + par = params[0] + obj = self.objectives[0] + const = self.constants[0] + f_rank = obj.compute_unscaled(*par, constants=const) + f_rank = np.asarray(f_rank) + print(f"Rank {self.rank} waiting to gather") + fs = self.comm.gather(f_rank, root=0) + f = pconcat(fs) return f @jit @@ -524,14 +610,18 @@ def compute_scaled(self, x, constants=None): ] ) else: # pragma: no cover - f = pconcat( - [ - obj.compute_scaled( - *jax.device_put(par, obj._device), constants=const - ) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) + if self.rank == 0: + message = ("compute_scaled", params, None) + self.comm.bcast(message, root=0) + + par = params[0] + obj = self.objectives[0] + const = self.constants[0] + f_rank = obj.compute_scaled(*par, constants=const) + f_rank = np.asarray(f_rank) + print(f"Rank {self.rank} waiting to gather") + fs = self.comm.gather(f_rank, root=0) + f = pconcat(fs) return f @jit @@ -563,14 +653,18 @@ def compute_scaled_error(self, x, constants=None): ] ) else: # pragma: no cover - f = pconcat( - [ - obj.compute_scaled_error( - *jax.device_put(par, obj._device), constants=const - ) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) + if self.rank == 0: + message = ("compute_scaled_error", params, None) + self.comm.bcast(message, root=0) + + par = params[0] + obj = self.objectives[0] + const = self.constants[0] + f_rank = obj.compute_scaled_error(*par, constants=const) + f_rank = np.asarray(f_rank) + print(f"Rank {self.rank} waiting to gather") + fs = self.comm.gather(f_rank, root=0) + f = pconcat(fs) return f @jit @@ -748,42 +842,63 @@ def jac_unscaled(self, x, constants=None): return self.jvp_unscaled(v, x, constants).T def _jvp_blocked(self, v, x, constants=None, op="scaled"): - v = ensure_tuple(v) - if len(v) > 1: - # using blocked for higher order derivatives is a pain, and only really - # is needed for perturbations. Just pass that to jvp_batched for now - return self._jvp_batched(v, x, constants, op) + if not self._is_multi_device: + v = ensure_tuple(v) + if len(v) > 1: + # using blocked for higher order derivatives is a pain, and only really + # is needed for perturbations. Just pass that to jvp_batched for now + return self._jvp_batched(v, x, constants, op) + + if constants is None: + constants = self.constants + xs_splits = np.cumsum([t.dim_x for t in self.things]) + xs = jnp.split(x, xs_splits) + vs = jnp.split(v[0], xs_splits, axis=-1) + J = [] + assert len(self.objectives) == len(self.constants) + # basic idea is we compute the jacobian of each objective wrt each thing + # one by one, and assemble into big block matrix + # if objective doesn't depend on a given thing, that part is set to 0. + for k, (obj, const) in enumerate(zip(self.objectives, constants)): + # get the xs that go to that objective + thing_idx = self._things_per_objective_idx[k] + xi = [xs[i] for i in thing_idx] + vi = [vs[i] for i in thing_idx] + Ji_ = getattr(obj, "jvp_" + op)(vi, xi, constants=const) + J += [Ji_] + else: + if self.rank == 0: + v = ensure_tuple(v) + if len(v) > 1: + # using blocked for higher order derivatives is a pain, and only + # really is needed for perturbations. Just pass that to + # jvp_batched for now + return self._jvp_batched(v, x, constants, op) + + if constants is None: + constants = self.constants + xs_splits = np.cumsum([t.dim_x for t in self.things]) + xs = jnp.split(x, xs_splits) + vs = jnp.split(v[0], xs_splits, axis=-1) + message = ("jvp_" + op, xs, vs) + self.comm.bcast(message, root=0) + + obj = self.objectives[0] + const = self.constants[0] + thing_idx = self._things_per_objective_idx[0] + xi = [xs[i] for i in thing_idx] + vi = [vs[i] for i in thing_idx] + J_rank = getattr(obj, "jvp_" + op)(vi, xi, constants=const) + J_rank = np.asarray(J_rank) + print(f"Rank {self.rank} waiting to gather") + J = self.comm.gather(J_rank, root=0) - if constants is None: - constants = self.constants - xs_splits = np.cumsum([t.dim_x for t in self.things]) - xs = jnp.split(x, xs_splits) - vs = jnp.split(v[0], xs_splits, axis=-1) - J = [] - assert len(self.objectives) == len(self.constants) - # basic idea is we compute the jacobian of each objective wrt each thing - # one by one, and assemble into big block matrix - # if objective doesn't depend on a given thing, that part is set to 0. - for k, (obj, const) in enumerate(zip(self.objectives, constants)): - # TODO: this is for debugging purposes, must be deleted before merging! - if self._is_multi_device: - print(f"This should run on device id:{obj._device_id}") - # get the xs that go to that objective - thing_idx = self._things_per_objective_idx[k] - xi = [xs[i] for i in thing_idx] - vi = [vs[i] for i in thing_idx] - if self._is_multi_device: # pragma: no cover - # inputs to jitted functions must live on the same device. Need to - # put xi and vi on the same device as the objective - xi = jax.device_put(xi, obj._device) - vi = jax.device_put(vi, obj._device) - Ji_ = getattr(obj, "jvp_" + op)(vi, xi, constants=const) - J += [Ji_] # this is the transpose of the jvp when v is a matrix, for consistency with # jvp_batched if not self._is_multi_device: J = jnp.hstack(J) else: + # this will handle the device placement of the J matrix J = pconcat(J, mode="hstack") return J diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py new file mode 100644 index 0000000000..4f6a7e788a --- /dev/null +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -0,0 +1,42 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath(".")) +sys.path.append(os.path.abspath("../../../")) + +from desc import _set_cpu_count, set_device + +num_device = 4 +_set_cpu_count(num_device) +set_device("cpu", num_device=num_device) + +import numpy as np +from mpi4py import MPI + +from desc.backend import jax +from desc.examples import get +from desc.grid import LinearGrid +from desc.objectives import ForceBalance, ObjectiveFunction +from desc.objectives.getters import ( + get_fixed_boundary_constraints, + get_parallel_forcebalance, +) + +if __name__ == "__main__": + rank = MPI.COMM_WORLD.Get_rank() + eq = get("HELIOTRON") + eq.change_resolution(6, 6, 6, 12, 12, 12) + + obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1) + cons = get_fixed_boundary_constraints(eq) + with obj as obj: + if rank == 0: + eq.solve( + objective=obj, + constraints=cons, + maxiter=1, + ftol=0, + gtol=0, + xtol=0, + verbose=3, + ) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py new file mode 100644 index 0000000000..b41202c2ee --- /dev/null +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -0,0 +1,91 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath(".")) +sys.path.append(os.path.abspath("../../../")) + +from desc import _set_cpu_count, set_device + +num_device = 3 +_set_cpu_count(num_device) +set_device("cpu", num_device=num_device) + +import numpy as np +from mpi4py import MPI + +from desc.backend import jax, jnp +from desc.examples import get +from desc.grid import LinearGrid +from desc.objectives import ( + AspectRatio, + FixBoundaryR, + FixBoundaryZ, + FixCurrent, + FixPressure, + FixPsi, + ForceBalance, + ObjectiveFunction, + QuasisymmetryTwoTerm, +) +from desc.optimize import Optimizer + +if __name__ == "__main__": + rank = MPI.COMM_WORLD.Get_rank() + + eq = get("precise_QA") + eq.change_resolution(3, 3, 3, 6, 6, 6) + + grid1 = LinearGrid( + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.2, 0.5, 4), sym=True + ) + grid2 = LinearGrid( + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.6, 1.0, 6), sym=True + ) + + obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) + obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) + obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2) + + objs = [obj1, obj2, obj3] + for obji in objs: + obji.build(verbose=3) + obji = jax.device_put(obji, obji._device) + obji.things[0] = eq + + objective = ObjectiveFunction(objs, mpi=MPI) + objective.build(verbose=3) + + k = 1 + R_modes = np.vstack( + ( + [0, 0, 0], + eq.surface.R_basis.modes[ + np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : + ], + ) + ) + Z_modes = eq.surface.Z_basis.modes[ + np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : + ] + constraints = ( + ForceBalance(eq=eq), + FixBoundaryR(eq=eq, modes=R_modes), + FixBoundaryZ(eq=eq, modes=Z_modes), + FixPressure(eq=eq), + FixPsi(eq=eq), + FixCurrent(eq=eq), + ) + optimizer = Optimizer("proximal-lsq-exact") + + with objective as objective: + if rank == 0: + eq.optimize( + objective=objective, + constraints=constraints, + optimizer=optimizer, + maxiter=1, + verbose=3, + options={ + "initial_trust_ratio": 1.0, + }, + ) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index b8ce359801..1216839820 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -9,6 +9,11 @@ }, "source": [ "# How to use Multiple Devices\n", + "\n", + "In this tutorial, we will see how to use multiple devices to run DESC. This will make the optimization problem scalable to computing clusters.\n", + "\n", + "This tutorials will not be able to run on a Jupyter Notebook, so we will provide the content of the script here but run an underlying python script to show the results.\n", + "\n", "## Solving Equilibrium" ] }, @@ -47,12 +52,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version 0.13.0+1539.gb6b43370b.dirty,using JAX backend, jax version=0.4.38, jaxlib version=0.4.38, dtype=float64\n", + "DESC version=0.13.0+1674.g865a2f870.dirty.\n", + "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 6.26 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 6.26 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 6.26 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 6.26 GB available memory\n" + "\t CPU 0: TFRT_CPU_0 with 7.72 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.72 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.72 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 7.72 GB available memory\n" ] } ], @@ -63,93 +69,91 @@ "from desc.objectives import *\n", "from desc.objectives.getters import *\n", "from desc.grid import LinearGrid\n", - "from desc.backend import jnp\n", + "from desc.backend import jnp, print_backend_info\n", "from desc.plotting import plot_grid\n", "from desc.backend import jax\n", - "from desc.optimize import Optimizer" + "from desc.optimize import Optimizer\n", + "\n", + "print_backend_info()" ] }, { - "cell_type": "code", - "execution_count": 4, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/yigit/Codes/DESC/desc/utils.py:560: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n" - ] - } - ], "source": [ - "eq = get(\"HELIOTRON\")\n", - "eq.change_resolution(3, 3, 3, 6, 6, 6)" + "```python\n", + "import sys\n", + "import os\n", + "\n", + "sys.path.insert(0, os.path.abspath(\".\"))\n", + "sys.path.append(os.path.abspath(\"../../../../\"))\n", + "\n", + "from desc import set_device, _set_cpu_count\n", + "num_device = 4\n", + "_set_cpu_count(num_device)\n", + "set_device(\"cpu\", num_device=num_device)\n", + "\n", + "from mpi4py import MPI\n", + "import numpy as np\n", + "\n", + "from desc.objectives import ForceBalance, ObjectiveFunction\n", + "from desc.objectives.getters import get_parallel_forcebalance, get_fixed_boundary_constraints\n", + "from desc.grid import LinearGrid\n", + "from desc.examples import get\n", + "\n", + "from desc.backend import jax\n", + "\n", + "if __name__ == \"__main__\":\n", + " rank = MPI.COMM_WORLD.Get_rank()\n", + " eq = get(\"HELIOTRON\")\n", + " eq.change_resolution(6,6,6,12,12,12)\n", + "\n", + " obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1)\n", + " cons = get_fixed_boundary_constraints(eq)\n", + " with obj as obj:\n", + " if rank == 0:\n", + " eq.solve(objective=obj, constraints=cons, maxiter=1, ftol=0, gtol=0, xtol=0, verbose=3)\n", + "```" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Precomputing transforms\n", - "Precomputing transforms\n", - "Precomputing transforms\n", - "Precomputing transforms\n" - ] - }, { "name": "stderr", "output_type": "stream", "text": [ - "/home/yigit/Codes/DESC/desc/utils.py:560: UserWarning: \n", - "When using multiple devices, the ObjectiveFunction will run each \n", - "sub-objective on the device specified in the sub-objective. \n", - "Setting the deriv_mode to 'blocked' to ensure that each sub-objective \n", - "runs on the correct device.\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n" + "/home/yigit/miniconda3/envs/mpi/lib/python3.12/pty.py:95: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n", + " pid, fd = os.forkpty()\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "None\n", - "TFRT_CPU_1\n", - "TFRT_CPU_2\n", - "TFRT_CPU_3\n" - ] - } - ], - "source": [ - "obj = get_parallel_forcebalance(eq, num_device=num_device)\n", - "cons = get_fixed_boundary_constraints(eq)\n", - "for obji in obj.objectives:\n", - " print(obji._device)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ + "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n", + "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Rank 1 waiting for message\n", + "Precomputing transforms\n", "Building objective: lcfs R\n", + "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n", + "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n", "Building objective: lcfs Z\n", + "Precomputing transforms\n", + "Precomputing transforms\n", "Building objective: fixed Psi\n", "Building objective: fixed pressure\n", "Building objective: fixed iota\n", @@ -159,138 +163,117 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.53 sec\n", - "Timer: Linear constraint projection build = 3.80 sec\n", - "Number of parameters: 76\n", - "Number of objectives: 2704\n", - "Timer: Initializing the optimization = 5.36 sec\n", + "Timer: Objective build = 1.49 sec\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Rank 2 waiting for message\n", + "Rank 3 waiting for message\n", + "Timer: LinearConstraintProjection build = 4.38 sec\n", + "Number of parameters: 609\n", + "Number of objectives: 15000\n", + "Timer: Initializing the optimization = 5.93 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", + "Rank 1 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 3 computing compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 2 waiting for message\n", + "Rank 1 waiting for message\n", + "Rank 3 waiting for message\n", + "Rank 1 computing jvp_scaled_error\n", + "Rank 2 computing jvp_scaled_error\n", + "Rank 3 computing jvp_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 waiting for message\n", + "Rank 2 waiting for message\n", + "Rank 3 waiting for message\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 8.573e+06 4.135e+03 \n", - " 1 2 7.294e+05 7.844e+06 5.352e-01 6.490e+02 \n", + " 0 1 5.928e+00 2.480e+00 \n", + "Rank 1 computing compute_scaled_error\n", + "Rank 3 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 waiting for message\n", + "Rank 2 waiting for message\n", + "Rank 3 waiting for message\n", + "Rank 1 computing jvp_scaled_error\n", + "Rank 2 computing jvp_scaled_error\n", + "Rank 3 computing jvp_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 waiting for message\n", + "Rank 2 waiting for message\n", + "Rank 3 waiting for message\n", + " 1 2 9.876e-01 4.941e+00 3.322e-01 6.448e-01 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 7.294e+05\n", - " Total delta_x: 5.352e-01\n", + " Current function value: 9.876e-01\n", + " Total delta_x: 3.322e-01\n", " Iterations: 1\n", " Function evaluations: 2\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 13.4 sec\n", - "Timer: Avg time per step = 6.73 sec\n", + "Timer: Solution time = 20.3 sec\n", + "Timer: Avg time per step = 10.1 sec\n", "==============================================================================================================\n", " Start --> End\n", - "Total (sum of squares): 2.844e+10 --> 7.294e+05, \n", - "Maximum absolute Force error: 2.650e+05 --> 1.299e+05 (N)\n", - "Minimum absolute Force error: 1.534e-10 --> 1.681e-10 (N)\n", - "Average absolute Force error: 9.802e+04 --> 4.428e+04 (N)\n", - "Maximum absolute Force error: 2.131e-02 --> 1.045e-02 (normalized)\n", - "Minimum absolute Force error: 1.234e-17 --> 1.352e-17 (normalized)\n", - "Average absolute Force error: 7.883e-03 --> 3.561e-03 (normalized)\n", - "Maximum absolute Force error: 4.785e+05 --> 3.814e+05 (N)\n", - "Minimum absolute Force error: 1.889e-10 --> 1.945e-10 (N)\n", - "Average absolute Force error: 1.791e+05 --> 1.171e+05 (N)\n", - "Maximum absolute Force error: 3.848e-02 --> 3.067e-02 (normalized)\n", - "Minimum absolute Force error: 1.519e-17 --> 1.565e-17 (normalized)\n", - "Average absolute Force error: 1.441e-02 --> 9.422e-03 (normalized)\n", - "Maximum absolute Force error: 8.926e+06 --> 2.599e+06 (N)\n", - "Minimum absolute Force error: 9.420e-11 --> 1.601e-10 (N)\n", - "Average absolute Force error: 4.594e+05 --> 1.831e+05 (N)\n", - "Maximum absolute Force error: 7.178e-01 --> 2.091e-01 (normalized)\n", - "Minimum absolute Force error: 7.576e-18 --> 1.287e-17 (normalized)\n", - "Average absolute Force error: 3.695e-02 --> 1.473e-02 (normalized)\n", - "Maximum absolute Force error: 6.431e+12 --> 2.805e+10 (N)\n", - "Minimum absolute Force error: 7.111e-13 --> 5.213e-11 (N)\n", - "Average absolute Force error: 4.122e+09 --> 3.074e+07 (N)\n", - "Maximum absolute Force error: 5.172e+05 --> 2.256e+03 (normalized)\n", - "Minimum absolute Force error: 5.719e-20 --> 4.193e-18 (normalized)\n", - "Average absolute Force error: 3.315e+02 --> 2.472e+00 (normalized)\n", + "Rank 1 computing compute_scaled_error\n", + "Rank 3 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 waiting for message\n", + "Rank 2 waiting for message\n", + "Rank 3 waiting for message\n", + "Rank 1 computing compute_scaled_error\n", + "Rank 3 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 waiting for message\n", + "Rank 2 waiting for message\n", + "Rank 3 waiting for message\n", + "Total (sum of squares): 7.355e+00 --> 9.876e-01, \n", + "Maximum absolute Force error: 1.077e+05 --> 7.858e+04 (N)\n", + "Minimum absolute Force error: 1.201e-10 --> 1.307e-10 (N)\n", + "Average absolute Force error: 3.587e+04 --> 1.527e+04 (N)\n", + "Maximum absolute Force error: 8.664e-03 --> 6.319e-03 (normalized)\n", + "Minimum absolute Force error: 9.659e-18 --> 1.052e-17 (normalized)\n", + "Average absolute Force error: 2.885e-03 --> 1.228e-03 (normalized)\n", + "Maximum absolute Force error: 4.334e+05 --> 2.316e+05 (N)\n", + "Minimum absolute Force error: 1.482e-10 --> 1.411e-10 (N)\n", + "Average absolute Force error: 7.335e+04 --> 3.237e+04 (N)\n", + "Maximum absolute Force error: 3.485e-02 --> 1.863e-02 (normalized)\n", + "Minimum absolute Force error: 1.192e-17 --> 1.135e-17 (normalized)\n", + "Average absolute Force error: 5.899e-03 --> 2.604e-03 (normalized)\n", + "Maximum absolute Force error: 1.057e+06 --> 9.148e+05 (N)\n", + "Minimum absolute Force error: 1.000e-10 --> 7.018e-11 (N)\n", + "Average absolute Force error: 1.205e+05 --> 5.606e+04 (N)\n", + "Maximum absolute Force error: 8.500e-02 --> 7.357e-02 (normalized)\n", + "Minimum absolute Force error: 8.043e-18 --> 5.644e-18 (normalized)\n", + "Average absolute Force error: 9.693e-03 --> 4.509e-03 (normalized)\n", + "Maximum absolute Force error: 5.498e+07 --> 5.636e+06 (N)\n", + "Minimum absolute Force error: 5.034e-13 --> 1.381e-11 (N)\n", + "Average absolute Force error: 3.746e+05 --> 1.295e+05 (N)\n", + "Maximum absolute Force error: 4.422e+00 --> 4.533e-01 (normalized)\n", + "Minimum absolute Force error: 4.048e-20 --> 1.110e-18 (normalized)\n", + "Average absolute Force error: 3.013e-02 --> 1.042e-02 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", "Fixed iota profile error: 0.000e+00 --> 0.000e+00 (dimensionless)\n", "Fixed sheet current error: 0.000e+00 --> 0.000e+00 (~)\n", - "==============================================================================================================\n" + "==============================================================================================================\n", + "Rank 1 STOPPING\n", + "Rank 2 STOPPING\n", + "Rank 3 STOPPING\n" ] - }, - { - "data": { - "text/plain": [ - "(Equilibrium at 0x7e5726b83f50 (L=3, M=3, N=3, NFP=19, sym=True, spectral_indexing=fringe),\n", - " message: Maximum number of iterations has been exceeded.\n", - " success: False\n", - " fun: [-9.316e-05 -9.293e-05 ... 2.140e-02 -4.778e-03]\n", - " x: [-2.477e-02 -1.206e-01 ... 7.442e-03 1.768e-01]\n", - " nit: 1\n", - " cost: 729415.2017973666\n", - " v: [ 1.000e+00 1.000e+00 ... 1.000e+00 1.000e+00]\n", - " optimality: 649.0096310587396\n", - " nfev: 2\n", - " njev: 2\n", - " allx: [Array([-3.392e-05, 8.921e-06, ..., 0.000e+00, 0.000e+00], dtype=float64), Array([ 2.416e-05, 1.501e-03, ..., 0.000e+00, 0.000e+00], dtype=float64)]\n", - " alltr: [Array( 1.402e+06, dtype=float64), np.float64(1401525.8219770438)]\n", - " history: [[{'R_lmn': Array([-3.392e-05, 8.921e-06, ..., 0.000e+00, 1.850e-05], dtype=float64), 'Z_lmn': Array([ 9.011e-06, 1.167e-05, ..., -3.697e-05, 1.686e-05], dtype=float64), 'L_lmn': Array([-6.194e-07, -1.567e-05, ..., -9.721e-06, -1.466e-05], dtype=float64), 'p_l': Array([ 1.800e+04, -3.600e+04, ..., 0.000e+00, 0.000e+00], dtype=float64), 'i_l': Array([ 1.000e+00, 1.500e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'c_l': Array([], shape=(0,), dtype=float64), 'Psi': Array([ 1.000e+00], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.039e+01, 1.019e-01, 1.330e-03, 1.737e-05], dtype=float64), 'Za_n': Array([ 1.802e-05, 1.335e-03, 9.939e-02], dtype=float64), 'Rb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'Zb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}], [{'R_lmn': Array([ 2.416e-05, 1.501e-03, ..., 0.000e+00, 1.858e-03], dtype=float64), 'Z_lmn': Array([-7.446e-04, -3.249e-04, ..., 1.482e-03, 1.084e-03], dtype=float64), 'L_lmn': Array([-2.244e-04, -7.899e-04, ..., 9.491e-04, -7.183e-04], dtype=float64), 'p_l': Array([ 1.800e+04, -3.600e+04, ..., 0.000e+00, 0.000e+00], dtype=float64), 'i_l': Array([ 1.000e+00, 1.500e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'c_l': Array([], shape=(0,), dtype=float64), 'Psi': Array([ 1.000e+00], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.031e+01, 8.864e-02, -1.262e-02, -5.261e-04], dtype=float64), 'Za_n': Array([-1.489e-03, 2.919e-03, 1.679e-01], dtype=float64), 'Rb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'Zb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}]])" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "eq.solve(objective=obj, constraints=cons, maxiter=1, ftol=0, gtol=0, xtol=0, verbose=3)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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Tp48idTUVNqusYwBK8BmzrJly5CXl4d33nkHO3fuREREBL7++mv2oI7lCvbv34+4uDi89NJLeOmll3DhwgXccMMN7HwLM4YVYAuAw+HgpptuQnl5OZ555hn86U9/wqJFi/Drr7/Sdo3FDDh58iTS0tJw33334f7770dNTQ3uvPNOtizPAmAF2IKwtrbGAw88gLq6Otx666247bbbDBEyy9yjtLQU119/Pa6//nqsXLkSjY2NePzxx2fFtuW5AivAFoidnR2effZZNDQ0ICkpCcuXL8eGDRtQVFRE2zUWBqisrMTtt9+O5ORkBAcHo66uDi+++OKs3EE322EF2IJxcXHBa6+9hqqqKnh7eyM9PR1LlizBd999x+aIZxk6nQ4HDhzAihUrsHDhQvB4PJSUlOCdd96Bp6cnbfdYpgkrwLMAX19fvP/++2hubsaaNWvw5JNPIiQkBH//+98Nmx5YLJOhoSG88cYbiIyMxJYtW5CRkYGGhgZ88cUXCAoKou0eywxhBXgW4erqimeffRaNjY14/fXX8csvv8Df3x/33nsvm56wMCoqKvD73/8e8+bNw9dff42//OUvaG5uxrZt2+Dl5UXbPRYjwQrwLMTa2ho333wzcnNzkZWVBS6XiyVLliAhIQFvv/22YQEii3mhVCrx4YcfIiUlBQkJCRgcHMShQ4dw7tw5bN68eVavvJ+rsAI8y1m4cCE++eQTtLe346GHHsLnn38OHx8fbN68GWfOnKHtHgsuzqi+//774evrix07duD2229Ha2srvv76a6SmprJ1vLMYVoDnCE5OTnjwwQdRWFiI7OxsODs7Y9WqVQgNDcVDDz2Ew4cPs112DKHT6XDixAls3boVkZGRSEtLg06nw08//YSysjJs3bqVXYI5R5iT84BZLqJQKHDs2DHs378fBw4cgEajQUZGBtauXYubbroJAoGAtotXxVLmAQ8NDRke5+PHj0OtVmPNmjXYsGEDrrnmGjg6OtJ2kYUCrACzALgYlRUWFmL//v3Yv38/ampqkJSUhGuvvRa33Xab2S4RNWcBbmlpwbfffouff/4Zp0+fxrx587BhwwasX78eycnJZucvC/OwAswyJk1NTTh48CD27duHEydOIDQ0FCtWrMAtt9yC1NRUcLnmkb0yJwHW6XQoKCjA999/jyNHjqC8vBwpKSnYsGED1q5di5CQEKr+sZgfrACzXJWBgQEcPnwY+/fvx08//QQej4fo6GjExsYiKSkJS5YsgZ+fHxXfaAqwVCrFyZMncfbsWVy4cAGlpaWQy+VYvXo11q9fj9WrV7O5XJYJYQWYZUpoNBoUFhaioKAA+fn5OHfuHCoqKuDm5oaIiAiDKKenp0MsFpvcH6YEuL29/RKxLS8vR0dHB8LCwpCQkIBFixYZ/mXLxVgmCyvALDNGqVSipKQE586dQ35+PgoKClBRUQEXFxdERkYiJiYGSUlJSE5OhlgsNqpAGVuANRoNJBIJCgoKLhHbzs5OhIeHIzEx0SC2cXFxsLe3N8K9YJmrsALMYhJUKhVKSkouiZYrKiowMjICV1dXuLu7w93dHZ6envD09ISPjw98fX3h5+cHsVgMf3//SYnbZAVYpVKhqakJEokEzc3NaG1tRVtbGzo6OtDV1YXu7m50d3ejp6cHVlZWV4htbGws7OzsjPkQsbCwAszCHIQQyGQytLe3o62tDe3t7Yb/6wWxvb0dUqkUCoUCQqEQ7u7ucHV1BY/Hg7W1NaysrGBtbW34P5fLxeDgIGxtbaHVaqHVaqHRaAwffX196OrqQl9fH/h8Pry8vODt7Q1vb2/4+fnBx8fH8Ln+/66urmzzAwsjsALMYnYQQjA4OGgQ6M7OTqjVamg0GsO/oz+srKwMAj36g8fjwcPDwyCwAoGAFVYWs4IVYBYWFhZKmEcxJwsLC8schBVgFhYWFkqwAszCwsJCCVaAWVhYWCjBCjALCwsLJVgBZmFhYaEEK8AsLCwslGAFmIWFhYUSrACzsLCwUIIVYBYWFhZKWNN2gIVlqhQUFGB4eBjnzp3DE088QdsdFpZpw0bALBZHZWUlkpKSUF9fT9sVFpYZwQ7jYbFIDh06BD6fDx6Ph9zcXOh0OixZsgSZmZm0XWNhmTRsCoLF4nj55Zfx5z//GWvXrsXXX38NAJDL5az4slgcbAqCxeJYvHgxcnNzsXDhQjQ1NcHGxgbFxcW03WJhmTJsCoLFohkcHISTkxP6+vrYDcQsFgcrwCwsLCyUYHPALBbH1dYKsTEFi6XA5oBZLA5CCAgh2LlzJwghePrppw1fY8WXxZJgBZjFYvnDH/6Azs5OiMVi2q6wsEwLVoBZLBZCCLKysnD33XfTdoWFZVqwAsxisezYsQO5ubnYtWsXbVdYWKYFWwXBwsLCQgk2AmZhYWGhBCvALCwsLJRgBZiFhYWFEqwAs7CwsFCCFWAWFhYWSrACzMLCwkIJVoBZWFhYKMEKMAsLCwslWAFmYWFhoQQrwCwsLCyUYAWYhYWFhRKsALOwsLBQ4v8Hlub1m7lFxWkAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWAAAAF2CAYAAABQ7kLKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAOxQAADsUBR2zs/wAAg21JREFUeJztnXdc2/X2/18JhA0JoxD2hrLLLqPQZbXa5b7VXnf1erXur+ter19vr171Xq+9at1a9erVarXT1dpSWmgps5S9CTPsAIEkZLx/f/BLvrQFykg+7wQ+z8eDRws0n3OSNK+cnPcZHEIIAQsLCwsL43BpO8DCwsKyWGEFmIWFhYUSrACzsLCwUIIVYBYWFhZKsALMwsLCQglWgFlYWFgowQowCwsLCyVYAWZhYWGhBCvALCwsLJRgBZiFhYWFEqwAs7CwsFBiwQlwWVkZVq5cCQ6Hg6VLl+Jf//rXZf9mx44d2LFjBwXvpqarqwuPPPIIUlNTsXLlSqSmpmLNmjV4++23MTg4eMXbT3effvnlFyxbtgwcDkffbrOwsMwHskABQPbs2TPp70ZGRsjIyAizDk1DS0sL8fLyIm+//TbRaDS6n//www+Ex+ORn3/++YrXuNJ9ysrKInN5upubm8ljjz1GAJB77rmH/Otf/5r1Naajo6ODXH/99XO6bUtLC3nyySeJlZUVeeutty77/fDwMImJiSHLly+ft99KpZLs3LmTvPXWW+Stt94i586dm9f1rkRFRQUpLi6e8nsa/PTTTyQiIoJ89NFHF/3slltuoeiVabMoBdjY2LhxI9m0adOkv7vnnnvIb7/9Nm8bcxVgQgg5ceIEcXV1nbcPl/LOO++QHTt2EF9f3zlf48yZM+S2224jDz/88GW/+/bbb0loaCjJzs6eh5fjPPLII7rn4W9/+xv5wx/+MO9rTsfvf/97sn///im/p0V8fDyprq7WfS+RSIhYLKbokWmz6AT47bffJr6+viQzM1P3sw8++ICEhoYSX19f8tZbb5G1a9cSf39/8vnnn+v+jVgsJrfeeitJS0sjaWlpZMeOHWR0dFT3+3//+99k1apVZM2aNWT58uXk3XffnfL6V199NbGzsyN79uwh/f39hMvlko8//nja+zPdNSa7T7W1tSQzM5MkJCSQjRs3kjfffHPOAvzCCy8YLMppamqalwC//fbb5NChQ+Saa6656OdFRUXk9OnTxNramigUinn52NraSkJDQ3XfDw4Okr6+vnld80r4+vqSgYGBKb+nQV9fH/Hx8aHqw0LDnGL2gwoPP/wwent7cfLkSd3P7r//flhYWOD+++9HSEgIjh07hq+//hp/+MMfcPvtt4PD4WDz5s2Ij4/HN998A7VajRtuuAHPPPMM3nrrLQCAWq3Gjz/+CGtraygUCkRGRiImJgapqam66z/wwAPw9PTEL7/8gv3798PKygp1dXXQaDTw9vae1u/prnHXXXdddJ9UKhU2bNiAzZs34/XXX4dKpcJNN90058csKysLt99++5S/F4vF2LVr17TXyMjIwLXXXjtnH6ZCqVRi6dKlqK+v1/1MoVBAJBJhbGwMKSkpsLCwmJe/R48ehZubG7799luMjIygrKwML7744rS37+zsxN69e1FcXIzNmzdDKpUiNzcXu3btgo2NDWQyGV588UWEhYVBJpPhwIEDOHr0KH7++WccPnwYXC4X77zzDuzt7VFTU6P7ftOmTYiOjoZKpcLrr78OJycnAICLiwtuuukmlJaWIicnBx9//DGioqLQ29uLkJAQPPPMM9i7dy/OnDmDBx98EPX19aisrMTGjRuxevXqi3xfv349rr322svOFLKysrBy5UoAgFQqxWeffYbjx4/jgw8+gFqtvuL1p/IZwJSPx1T350rPn8lA+x3AUGCaFMSLL754UbRICCF79uwhfD5f931VVRUBQDo6Osi5c+cIgIs+eu3bt49YW1vrcrYnTpwg11xzDUlLSyOZmZmEz+eT119//aLrOzg4XOaL9tq//vrrRT9/++23SWZmJgkNDSVPPvnktNe49D7l5OQQAKSurk73+2+//XZOEfDIyAixsLC46L7rk/lEwAqFguzevZuMjY0RKysrolKpCCHj93VsbIw88MADZOfOnfP28eWXXybOzs5EIpEQQgj58MMPyfbt26e9zccff0zUajVZsmQJaW5uJoQQ8uyzz5IPPviAEELIzTffTPbu3UsIIWTv3r3k2muvvei2jz322JTfEzKektD+/25paSH//Oc/CSGE/Pzzz0ShUBAHBwdSXV1NJBIJ6e/vJx9++CFRKpXEwcGB5OXlEUIIOXz4MPnjH/94me9Hjx4l9fX1l/38wQcf1H0q/Pzzz4lcLidRUVGkpqZmRtefyufpHo+p7s9C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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "for obji in obj.objectives:\n", - " plot_grid(obji.constants[\"transforms\"][\"grid\"])" + "!mpirun -n 4 python mpi-tutorials/mpi-eq-solve.py" ] }, { @@ -302,313 +285,186 @@ ] }, { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/yigit/Codes/DESC/desc/utils.py:560: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n" - ] - } - ], - "source": [ - "eq = get(\"HELIOTRON\")\n", - "eq.change_resolution(3, 3, 3, 6, 6, 6)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Precomputing transforms\n", - "Timer: Precomputing transforms = 606 ms\n", - "Precomputing transforms\n", - "Timer: Precomputing transforms = 684 ms\n", - "Precomputing transforms\n", - "Timer: Precomputing transforms = 681 ms\n", - "Timer: Objective build = 6.35 ms\n" - ] - } - ], "source": [ - "grid1 = LinearGrid(\n", - " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2, 0.4]), sym=True\n", - ")\n", - "grid2 = LinearGrid(\n", - " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.6, 0.8, 1.0]), sym=True\n", + "```python\n", + "import sys\n", + "import os\n", + "\n", + "sys.path.insert(0, os.path.abspath(\".\"))\n", + "sys.path.append(os.path.abspath(\"../../../\"))\n", + "\n", + "from desc import set_device, _set_cpu_count\n", + "num_device = 3\n", + "_set_cpu_count(num_device)\n", + "set_device(\"cpu\", num_device=num_device)\n", + "\n", + "from mpi4py import MPI\n", + "import numpy as np\n", + "\n", + "from desc.objectives import (\n", + " ForceBalance, \n", + " ObjectiveFunction, \n", + " QuasisymmetryTwoTerm, \n", + " AspectRatio, \n", + " FixBoundaryR, \n", + " FixBoundaryZ,\n", + " FixPressure,\n", + " FixPsi,\n", + " FixCurrent,\n", ")\n", + "from desc.grid import LinearGrid\n", + "from desc.examples import get\n", "\n", - "obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", - "obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", - "obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=1)\n", + "from desc.backend import jax, jnp\n", + "from desc.optimize import Optimizer\n", "\n", - "objs = [obj1, obj2, obj3]\n", - "for obji in objs:\n", - " obji.build(verbose=3)\n", - " obji = jax.device_put(obji, obji._device)\n", - " obji.things[0] = eq\n", + "if __name__ == \"__main__\":\n", + " rank = MPI.COMM_WORLD.Get_rank()\n", "\n", - "objective = ObjectiveFunction(objs)\n", - "objective.build(verbose=3)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "k = 1\n", - "R_modes = np.vstack(\n", - " (\n", - " [0, 0, 0],\n", - " eq.surface.R_basis.modes[np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :],\n", + " eq = get(\"precise_QA\")\n", + " eq.change_resolution(3, 3, 3, 6, 6, 6)\n", + "\n", + " grid1 = LinearGrid(\n", + " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.2, 0.5, 4), sym=True\n", + " )\n", + " grid2 = LinearGrid(\n", + " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.6, 1.0, 6), sym=True\n", " )\n", - ")\n", - "Z_modes = eq.surface.Z_basis.modes[np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :]\n", - "constraints = (\n", - " FixBoundaryR(eq=eq, modes=R_modes),\n", - " FixBoundaryZ(eq=eq, modes=Z_modes),\n", - " FixPressure(eq=eq),\n", - " FixPsi(eq=eq),\n", - ")\n", - "optimizer = Optimizer(\"lsq-exact\")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Building objective: lcfs R\n", - "Building objective: lcfs Z\n", - "Building objective: fixed pressure\n", - "Building objective: fixed Psi\n", - "Building objective: self_consistency R\n", - "Building objective: self_consistency Z\n", - "Building objective: lambda gauge\n", - "Building objective: axis R self consistency\n", - "Building objective: axis Z self consistency\n", - "Timer: Objective build = 274 ms\n", - "Timer: Linear constraint projection build = 1.74 sec\n", - "Number of parameters: 97\n", - "Number of objectives: 456\n", - "Timer: Initializing the optimization = 2.04 sec\n", - "\n", - "Starting optimization\n", - "Using method: lsq-exact\n", - "This should run on GPU id:0\n", - "This should run on GPU id:1\n", - "This should run on GPU id:1\n", - " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 1.561e+22 1.767e+11 \n", - "This should run on GPU id:0\n", - "This should run on GPU id:1\n", - "This should run on GPU id:1\n", - " 1 5 5.466e+21 1.014e+22 2.003e+01 1.046e+11 \n", - "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 5.466e+21\n", - " Total delta_x: 2.003e+01\n", - " Iterations: 1\n", - " Function evaluations: 5\n", - " Jacobian evaluations: 2\n", - "Timer: Solution time = 8.54 sec\n", - "Timer: Avg time per step = 4.27 sec\n", - "==============================================================================================================\n", - " Start --> End\n", - "Total (sum of squares): 5.469e+26 --> 5.466e+21, \n", - "Maximum absolute Quasi-symmetry (1,19) two-term error: 5.015e-01 --> 1.664e-01 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.219e-03 --> 7.828e-04 (T^3)\n", - "Average absolute Quasi-symmetry (1,19) two-term error: 1.947e-01 --> 6.795e-02 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,19) two-term error: 7.961e+00 --> 2.641e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.935e-02 --> 1.243e-02 (normalized)\n", - "Average absolute Quasi-symmetry (1,19) two-term error: 3.091e+00 --> 1.079e+00 (normalized)\n", - "Maximum absolute Quasi-symmetry (1,19) two-term error: 2.906e+12 --> 9.188e+09 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,19) two-term error: 1.348e-03 --> 2.568e-04 (T^3)\n", - "Average absolute Quasi-symmetry (1,19) two-term error: 3.194e+09 --> 1.010e+07 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,19) two-term error: 4.614e+13 --> 1.459e+11 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,19) two-term error: 2.141e-02 --> 4.077e-03 (normalized)\n", - "Average absolute Quasi-symmetry (1,19) two-term error: 5.070e+10 --> 1.603e+08 (normalized)\n", - "Aspect ratio: 1.053e+01 --> 8.876e+00 (dimensionless)\n", - "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", - "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", - "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", - "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", - "==============================================================================================================\n" - ] - }, - { - "data": { - "text/plain": [ - "(Equilibrium at 0x7e56a13c7140 (L=3, M=3, N=3, NFP=19, sym=True, spectral_indexing=fringe),\n", - " message: Maximum number of iterations has been exceeded.\n", - " success: False\n", - " fun: [ 3.114e-01 9.780e-01 ... 6.524e+02 8.763e+01]\n", - " x: [-1.118e-01 5.238e-02 ... 1.617e+00 -2.211e-01]\n", - " nit: 1\n", - " cost: 5.465705943561737e+21\n", - " v: [ 1.000e+00 1.000e+00 ... 1.000e+00 1.000e+00]\n", - " optimality: 104553392458.05392\n", - " nfev: 5\n", - " njev: 2\n", - " allx: [Array([-3.392e-05, 8.921e-06, ..., 0.000e+00, 0.000e+00], dtype=float64), Array([-4.503e-05, -1.034e-03, ..., 0.000e+00, 0.000e+00], dtype=float64)]\n", - " alltr: [Array( 2.307e+16, dtype=float64), np.float64(5767465574622139.0), np.float64(1441866393655534.8), np.float64(360466598413883.75), np.float64(360466598413883.75)]\n", - " history: [[{'R_lmn': Array([-3.392e-05, 8.921e-06, ..., 0.000e+00, 1.850e-05], dtype=float64), 'Z_lmn': Array([ 9.011e-06, 1.167e-05, ..., -3.697e-05, 1.686e-05], dtype=float64), 'L_lmn': Array([-6.194e-07, -1.567e-05, ..., -9.721e-06, -1.466e-05], dtype=float64), 'p_l': Array([ 1.800e+04, -3.600e+04, ..., 0.000e+00, 0.000e+00], dtype=float64), 'i_l': Array([ 1.000e+00, 1.500e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'c_l': Array([], shape=(0,), dtype=float64), 'Psi': Array([ 1.000e+00], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.039e+01, 1.019e-01, 1.330e-03, 1.737e-05], dtype=float64), 'Za_n': Array([ 1.802e-05, 1.335e-03, 9.939e-02], dtype=float64), 'Rb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'Zb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}], [{'R_lmn': Array([-4.503e-05, -1.034e-03, ..., 0.000e+00, -1.036e-03], dtype=float64), 'Z_lmn': Array([-4.761e-04, -1.745e-03, ..., 8.102e-04, -2.369e-04], dtype=float64), 'L_lmn': Array([-2.543e-03, -3.826e-03, ..., 3.800e-04, 7.083e-04], dtype=float64), 'p_l': Array([ 1.800e+04, -3.600e+04, ..., 0.000e+00, 0.000e+00], dtype=float64), 'i_l': Array([ 1.776e+00, -3.323e+00, ..., 3.407e+00, 3.810e+00], dtype=float64), 'c_l': Array([], shape=(0,), dtype=float64), 'Psi': Array([ 1.000e+00], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.042e+01, 8.998e-02, -2.521e-03, 3.798e-04], dtype=float64), 'Za_n': Array([-9.521e-04, -1.837e-03, 7.433e-02], dtype=float64), 'Rb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'Zb_lmn': Array([ 0.000e+00, 0.000e+00, ..., 0.000e+00, 0.000e+00], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}]])" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "eq.optimize(\n", - " objective=objective,\n", - " constraints=constraints,\n", - " optimizer=optimizer,\n", - " maxiter=1,\n", - " verbose=3,\n", - " options={\n", - " \"initial_trust_ratio\": 1.0,\n", - " },\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Optimization using Proximal Method" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/yigit/Codes/DESC/desc/utils.py:560: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n" - ] - } - ], - "source": [ - "eq = get(\"precise_QA\")\n", - "# eq.change_resolution(12, 12, 12, 24, 24, 24)\n", - "eq.change_resolution(3, 3, 3, 6, 6, 6)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Precomputing transforms\n", - "Timer: Precomputing transforms = 810 ms\n", - "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.32 sec\n", - "Precomputing transforms\n", - "Timer: Precomputing transforms = 407 ms\n", - "Timer: Objective build = 5.30 ms\n" - ] - } - ], - "source": [ - "grid1 = LinearGrid(\n", - " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.2, 0.5, 4), sym=True\n", - ")\n", - "grid2 = LinearGrid(\n", - " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.6, 1.0, 6), sym=True\n", - ")\n", "\n", - "obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", - "obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=0)\n", - "obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0)\n", + " obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", + " obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", + " obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2)\n", "\n", - "objs = [obj1, obj2, obj3]\n", - "for obji in objs:\n", - " obji.build(verbose=3)\n", - " obji = jax.device_put(obji, obji._device)\n", - " obji.things[0] = eq\n", + " objs = [obj1, obj2, obj3]\n", + " for obji in objs:\n", + " obji.build(verbose=3)\n", + " obji = jax.device_put(obji, obji._device)\n", + " obji.things[0] = eq\n", "\n", - "objective = ObjectiveFunction(objs)\n", - "objective.build(verbose=3)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "k = 1\n", - "R_modes = np.vstack(\n", - " (\n", - " [0, 0, 0],\n", - " eq.surface.R_basis.modes[np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :],\n", + " objective = ObjectiveFunction(objs, mpi=MPI)\n", + " objective.build(verbose=3)\n", + "\n", + " k = 1\n", + " R_modes = np.vstack(\n", + " (\n", + " [0, 0, 0],\n", + " eq.surface.R_basis.modes[np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :],\n", + " )\n", " )\n", - ")\n", - "Z_modes = eq.surface.Z_basis.modes[np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :]\n", - "constraints = (\n", - " ForceBalance(eq=eq),\n", - " FixBoundaryR(eq=eq, modes=R_modes),\n", - " FixBoundaryZ(eq=eq, modes=Z_modes),\n", - " FixPressure(eq=eq),\n", - " FixPsi(eq=eq),\n", - " FixCurrent(eq=eq),\n", - ")\n", - "optimizer = Optimizer(\"proximal-lsq-exact\")" + " Z_modes = eq.surface.Z_basis.modes[np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :]\n", + " constraints = (\n", + " ForceBalance(eq=eq),\n", + " FixBoundaryR(eq=eq, modes=R_modes),\n", + " FixBoundaryZ(eq=eq, modes=Z_modes),\n", + " FixPressure(eq=eq),\n", + " FixPsi(eq=eq),\n", + " FixCurrent(eq=eq),\n", + " )\n", + " optimizer = Optimizer(\"proximal-lsq-exact\")\n", + "\n", + " with objective as objective:\n", + " if rank == 0:\n", + " eq.optimize(\n", + " objective=objective,\n", + " constraints=constraints,\n", + " optimizer=optimizer,\n", + " maxiter=1,\n", + " verbose=3,\n", + " options={\n", + " \"initial_trust_ratio\": 1.0,\n", + " },\n", + " )\n", + "```" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n", + "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 799 ms\n", + "Timer: Precomputing transforms = 801 ms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 736 ms\n", + "Timer: Precomputing transforms = 741 ms\n", + "Precomputing transforms\n", + "Precomputing transforms\n", + "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", + " warnings.warn(colored(msg, \"yellow\"), err)\n", + "Timer: Precomputing transforms = 767 ms\n", + "Timer: Precomputing transforms = 767 ms\n", + "Timer: Objective build = 12.6 ms\n", + "Rank 1 waiting for message\n", + "Timer: Objective build = 13.3 ms\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 863 ms\n", - "Timer: Objective build = 1.08 sec\n", - "Timer: Proximal projection build = 4.83 sec\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 947 ms\n", + "Timer: Objective build = 1.49 sec\n", + "Timer: Precomputing transforms = 1.60 sec\n", + "Timer: Objective build = 15.1 ms\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 1.47 sec\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 1.49 sec\n", + "Timer: Objective build = 25.1 ms\n", + "Rank 2 waiting for message\n", + "Timer: Eq Update LinearConstraintProjection build = 3.55 sec\n", + "Timer: Proximal projection build = 6.11 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 232 ms\n", - "Timer: Linear constraint projection build = 1.11 sec\n", + "Timer: Objective build = 510 ms\n", + "Timer: LinearConstraintProjection build = 1.30 sec\n", "Number of parameters: 8\n", "Number of objectives: 911\n", - "Timer: Initializing the optimization = 6.23 sec\n", + "Timer: Initializing the optimization = 7.96 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", + "Rank 1 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 2 waiting for message\n", + "Rank 0 waiting to gather\n", + "Rank 1 waiting for message\n", + "This should run on GPU id:0\n", + "This should run on GPU id:1\n", + "This should run on GPU id:2\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 2.011e+04 1.952e+02 \n", + "Rank 1 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 2 waiting for message\n", + "Rank 0 waiting to gather\n", + "Rank 1 waiting for message\n", + "Rank 1 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 2 waiting for message\n", + "Rank 1 waiting for message\n", + "Rank 1 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 2 waiting for message\n", + "Rank 1 waiting for message\n", + "This should run on GPU id:0\n", + "This should run on GPU id:1\n", + "This should run on GPU id:2\n", " 1 4 8.735e+03 1.138e+04 4.838e-02 1.104e+02 \n", "Warning: Maximum number of iterations has been exceeded.\n", " Current function value: 8.735e+03\n", @@ -616,10 +472,20 @@ " Iterations: 1\n", " Function evaluations: 4\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 29.2 sec\n", - "Timer: Avg time per step = 14.6 sec\n", + "Timer: Solution time = 36.1 sec\n", + "Timer: Avg time per step = 18.0 sec\n", "==============================================================================================================\n", " Start --> End\n", + "Rank 1 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 2 waiting for message\n", + "Rank 1 waiting for message\n", + "Rank 1 computing compute_scaled_error\n", + "Rank 2 computing compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 2 waiting for message\n", + "Rank 1 waiting for message\n", "Total (sum of squares): 2.011e+04 --> 8.735e+03, \n", "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.813e-01 --> 6.254e-01 (T^3)\n", "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.150e-04 --> 4.713e-03 (T^3)\n", @@ -645,65 +511,20 @@ "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", "Fixed current profile error: 0.000e+00 --> 0.000e+00 (A)\n", - "==============================================================================================================\n" + "==============================================================================================================\n", + "Rank 1 STOPPING\n", + "Rank 2 STOPPING\n" ] - }, - { - "data": { - "text/plain": [ - "(Equilibrium at 0x7e5688ede8a0 (L=3, M=3, N=3, NFP=2, sym=True, spectral_indexing=ansi),\n", - " message: Maximum number of iterations has been exceeded.\n", - " success: False\n", - " fun: [-6.669e-02 -1.838e-01 ... 1.709e-01 -1.291e+02]\n", - " x: [-2.124e-01 1.388e-01 1.794e-01 -7.720e-02 -1.261e-01\n", - " 4.834e-02 -2.327e-01 -1.485e-01]\n", - " nit: 1\n", - " cost: 8735.080665954583\n", - " v: [ 1.000e+00 1.000e+00 1.000e+00 1.000e+00 1.000e+00\n", - " 1.000e+00 1.000e+00 1.000e+00]\n", - " optimality: 110.41872641325968\n", - " nfev: 4\n", - " njev: 2\n", - " allx: [Array([ 0.000e+00, 0.000e+00, ..., 1.082e-03, -2.543e-03], dtype=float64), Array([ 0.000e+00, 0.000e+00, ..., 1.082e-03, -2.543e-03], dtype=float64)]\n", - " alltr: [Array( 5.665e+02, dtype=float64), np.float64(130.5803471209196), np.float64(32.6450867802299), np.float64(65.29017356045979)]\n", - " history: [[{'R_lmn': Array([-3.535e-03, 1.627e-03, ..., 5.860e-04, 1.585e-04], dtype=float64), 'Z_lmn': Array([-9.096e-04, 1.867e-03, ..., -1.343e-04, 1.075e-03], dtype=float64), 'L_lmn': Array([-2.543e-03, -2.040e-04, ..., -1.109e-03, -1.629e-03], dtype=float64), 'p_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", - " 0.000e+00], dtype=float64), 'i_l': Array([], shape=(0,), dtype=float64), 'c_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", - " 0.000e+00], dtype=float64), 'Psi': Array([ 8.700e-02], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.020e+00, 1.971e-01, 2.315e-02, 2.547e-03], dtype=float64), 'Za_n': Array([-2.473e-03, -2.071e-02, -1.521e-01], dtype=float64), 'Rb_lmn': Array([ 2.268e-04, 1.531e-03, ..., 2.246e-03, 1.295e-04], dtype=float64), 'Zb_lmn': Array([ 4.367e-04, 9.219e-04, ..., 1.082e-03, -2.543e-03], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}], [{'R_lmn': Array([-3.535e-03, 1.627e-03, ..., 5.860e-04, 1.585e-04], dtype=float64), 'Z_lmn': Array([-9.096e-04, 1.867e-03, ..., -1.343e-04, 1.075e-03], dtype=float64), 'L_lmn': Array([-2.543e-03, -2.040e-04, ..., -1.109e-03, -1.629e-03], dtype=float64), 'p_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", - " 0.000e+00], dtype=float64), 'i_l': Array([], shape=(0,), dtype=float64), 'c_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", - " 0.000e+00], dtype=float64), 'Psi': Array([ 8.700e-02], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.020e+00, 1.971e-01, 2.315e-02, 2.547e-03], dtype=float64), 'Za_n': Array([-2.473e-03, -2.071e-02, -1.521e-01], dtype=float64), 'Rb_lmn': Array([ 2.268e-04, 1.531e-03, ..., 2.246e-03, 1.295e-04], dtype=float64), 'Zb_lmn': Array([ 4.367e-04, 9.219e-04, ..., 1.082e-03, -2.543e-03], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}], [{'R_lmn': Array([-3.074e-03, 1.531e-03, ..., -1.188e-04, 1.295e-04], dtype=float64), 'Z_lmn': Array([-6.459e-04, 9.544e-04, ..., -7.129e-05, 2.324e-04], dtype=float64), 'L_lmn': Array([ 1.664e-03, 5.507e-04, ..., -2.559e-03, 1.939e-03], dtype=float64), 'p_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", - " 0.000e+00], dtype=float64), 'i_l': Array([], shape=(0,), dtype=float64), 'c_l': Array([ 0.000e+00, 0.000e+00, 0.000e+00, 0.000e+00,\n", - " 0.000e+00], dtype=float64), 'Psi': Array([ 8.700e-02], dtype=float64), 'Te_l': Array([], shape=(0,), dtype=float64), 'ne_l': Array([], shape=(0,), dtype=float64), 'Ti_l': Array([], shape=(0,), dtype=float64), 'Zeff_l': Array([], shape=(0,), dtype=float64), 'a_lmn': Array([], shape=(0,), dtype=float64), 'Ra_n': Array([ 1.050e+00, 1.833e-01, 2.304e-02, 2.564e-03], dtype=float64), 'Za_n': Array([-1.729e-03, -1.924e-02, -1.507e-01], dtype=float64), 'Rb_lmn': Array([ 2.268e-04, 1.531e-03, ..., 2.246e-03, 1.295e-04], dtype=float64), 'Zb_lmn': Array([ 4.367e-04, 9.219e-04, ..., 1.082e-03, -2.543e-03], dtype=float64), 'I': Array([], shape=(0,), dtype=float64), 'G': Array([], shape=(0,), dtype=float64), 'Phi_mn': Array([], shape=(0,), dtype=float64)}]])" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "eq.optimize(\n", - " objective=objective,\n", - " constraints=constraints,\n", - " optimizer=optimizer,\n", - " maxiter=1,\n", - " verbose=3,\n", - " options={\n", - " \"initial_trust_ratio\": 1.0,\n", - " },\n", - ")" + "!mpirun -n 3 python mpi-tutorials/mpi-proximal.py" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "desc-env", + "display_name": "mpi", "language": "python", "name": "python3" }, @@ -717,7 +538,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/setup.cfg b/setup.cfg index b3ebfd05ea..45334c7b64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,6 +84,7 @@ per-file-ignores = desc/examples/precise_QA.py: E402 desc/examples/precise_QH.py: E402 desc/examples/reactor_QA.py: E402 + docs/notebooks/tutorials/mpi-tutorials/*.py: E402 max-line-length = 88 exclude = docs/* From 5444157e3bd0a815c6089443c746a54fb28fc4c4 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 25 Feb 2025 16:05:01 -0500 Subject: [PATCH 077/199] add comments --- desc/objectives/objective_funs.py | 29 +- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 21 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 19 ++ docs/notebooks/tutorials/multi_device.ipynb | 291 +++++++++--------- 4 files changed, 207 insertions(+), 153 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 57ecde27df..9f1a756f6d 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -235,7 +235,7 @@ class ObjectiveFunction(IOAble): option will yield a larger chunk size than may be needed. It is recommended to manually choose a chunk_size if an OOM error is experienced in this case. mpi : MPI object, optional - MPI communicator. + MPI communicator. Required when using multiple devices. """ @@ -284,7 +284,12 @@ def __init__( self.rank = self.comm.Get_rank() self.size = self.comm.Get_size() self.running = True - assert all(device_ids == np.arange(self.size)) + if not all(device_ids == np.arange(self.size)): + raise ValueError( + "When using multiple devices, device_id of the objectives " + "must be consecutive and must be the same as ranks. Got " + f"device_ids: {device_ids}, ranks: {np.arange(self.size)}" + ) if self._is_multi_device and mpi is None: raise ValueError( @@ -295,18 +300,21 @@ def __enter__(self): # when entering the context manager, we start the worker loop # this will allow the root rank to send messages to the workers # to compute and to stop - self.worker_loop() + self._worker_loop() return self def __exit__(self, exc_type, exc_val, exc_tb): # this will be called when the context manager exits # we send a stop message to the workers if self.rank == 0: + # only the root rank can send the stop message + # in general, the message contains 3 parts, but for the stop message + # we only need the first part message = ("STOP", None, None) self.comm.bcast(message, root=0) self.running = False - def worker_loop(self): + def _worker_loop(self): """Worker loop for MPI parallelization. This function is called when the ObjectiveFunction is used as a context manager. @@ -323,19 +331,24 @@ def worker_loop(self): This way, we can still use MPI parallelization with the ObjectiveFunction, but prevent execution of redundant calculations multiple times on different ranks. + This is very similar to the strategy used in Simsopt. """ if self.rank == 0: - return # Root rank won't enter worker loop + # Root rank won't enter worker loop + return while self.running: - print(f"Rank {self.rank} waiting for message") + # The message contains 3 parts, + # message[0] is the operation to be performed + # message[1] is the state vector (for compute and jvp's) + # message[2] is the output (for only jvp's) message = (None, None, None) message = self.comm.bcast(message, root=0) if message[0] == "STOP": print(f"Rank {self.rank} STOPPING") break elif "jvp" in message[0]: - print(f"Rank {self.rank} computing {message[0]}") + print(f"Rank {self.rank} : {message[0]}") # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective obj = self.objectives[self.rank] @@ -349,7 +362,7 @@ def worker_loop(self): J_rank = np.asarray(J_rank) self.comm.gather(J_rank, root=0) elif "compute" in message[0]: - print(f"Rank {self.rank} computing {message[0]}") + print(f"Rank {self.rank} : {message[0]}") obj = self.objectives[self.rank] const = self.constants[self.rank] par = message[1][self.rank] diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 4f6a7e788a..52ae2231ef 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -1,22 +1,21 @@ import os import sys +# Add the path to the parent directory to augment search for module sys.path.insert(0, os.path.abspath(".")) sys.path.append(os.path.abspath("../../../")) +# These will be used for diving the single CPU into multiple virtual CPUs +# such that JAX and XLA thinks there are multiple devices from desc import _set_cpu_count, set_device num_device = 4 _set_cpu_count(num_device) set_device("cpu", num_device=num_device) -import numpy as np from mpi4py import MPI -from desc.backend import jax from desc.examples import get -from desc.grid import LinearGrid -from desc.objectives import ForceBalance, ObjectiveFunction from desc.objectives.getters import ( get_fixed_boundary_constraints, get_parallel_forcebalance, @@ -27,9 +26,21 @@ eq = get("HELIOTRON") eq.change_resolution(6, 6, 6, 12, 12, 12) + # this will create a parallel objective function + # user can create their own parallel objective function as well which will be + # shown in the next example obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1) cons = get_fixed_boundary_constraints(eq) + + # Until this line, the code is performed on all ranks, so it might print some + # information multiple times. The following part will only be performed on the + # master rank + + # this context manager will put the workers in a loop to listen to the master + # to compute the objective function and its derivatives with obj as obj: + # apart from cost evaluation and derivatives, everything else will be only + # performed on the master rank if rank == 0: eq.solve( objective=obj, @@ -40,3 +51,5 @@ xtol=0, verbose=3, ) + + # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index b41202c2ee..ebc8ae726d 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -1,9 +1,12 @@ import os import sys +# Add the path to the parent directory to augment search for module sys.path.insert(0, os.path.abspath(".")) sys.path.append(os.path.abspath("../../../")) +# These will be used for diving the single CPU into multiple virtual CPUs +# such that JAX and XLA thinks there are multiple devices from desc import _set_cpu_count, set_device num_device = 3 @@ -35,6 +38,8 @@ eq = get("precise_QA") eq.change_resolution(3, 3, 3, 6, 6, 6) + # create two grids with different rho values, this will effectively separate + # the quasisymmetry objective into two parts grid1 = LinearGrid( M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.2, 0.5, 4), sym=True ) @@ -42,19 +47,23 @@ M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.6, 1.0, 6), sym=True ) + # when using parallel objectives, the user needs to supply the device_id obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2) objs = [obj1, obj2, obj3] + # this part will probably be automatized in the future for obji in objs: obji.build(verbose=3) obji = jax.device_put(obji, obji._device) obji.things[0] = eq + # Parallel objective function needs the MPI communicator objective = ObjectiveFunction(objs, mpi=MPI) objective.build(verbose=3) + # we will fix some modes as usual k = 1 R_modes = np.vstack( ( @@ -77,7 +86,15 @@ ) optimizer = Optimizer("proximal-lsq-exact") + # Until this line, the code is performed on all ranks, so it might print some + # information multiple times. The following part will only be performed on the + # master rank + + # this context manager will put the workers in a loop to listen to the master + # to compute the objective function and its derivatives with objective as objective: + # apart from cost evaluation and derivatives, everything else will be only + # performed on the master rank if rank == 0: eq.optimize( objective=objective, @@ -89,3 +106,5 @@ "initial_trust_ratio": 1.0, }, ) + + # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 1216839820..385a1bec9a 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -32,47 +32,42 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "num_device = 4\n", "from desc import set_device, _set_cpu_count\n", "\n", + "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# such that JAX and XLA thinks there are multiple devices\n", + "# Note that this is just to trick JAX. Since JAX can already use multiple core and threads\n", + "# for single CPU, this will not give a speedup. This is just to test the code\n", "_set_cpu_count(num_device)\n", "set_device(\"cpu\", num_device=num_device)" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.13.0+1674.g865a2f870.dirty.\n", + "DESC version=0.13.0+1675.g3b4f847fd.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.72 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.72 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.72 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 7.72 GB available memory\n" + "\t CPU 0: TFRT_CPU_0 with 7.15 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.15 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.15 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 7.15 GB available memory\n" ] } ], "source": [ - "import numpy as np\n", - "\n", - "from desc.examples import get\n", - "from desc.objectives import *\n", - "from desc.objectives.getters import *\n", - "from desc.grid import LinearGrid\n", - "from desc.backend import jnp, print_backend_info\n", - "from desc.plotting import plot_grid\n", - "from desc.backend import jax\n", - "from desc.optimize import Optimizer\n", + "from desc.backend import print_backend_info\n", "\n", "print_backend_info()" ] @@ -82,43 +77,68 @@ "metadata": {}, "source": [ "```python\n", - "import sys\n", "import os\n", + "import sys\n", "\n", + "# Add the path to the parent directory to augment search for module\n", "sys.path.insert(0, os.path.abspath(\".\"))\n", - "sys.path.append(os.path.abspath(\"../../../../\"))\n", + "sys.path.append(os.path.abspath(\"../../../\"))\n", + "\n", + "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# such that JAX and XLA thinks there are multiple devices\n", + "from desc import _set_cpu_count, set_device\n", "\n", - "from desc import set_device, _set_cpu_count\n", "num_device = 4\n", "_set_cpu_count(num_device)\n", "set_device(\"cpu\", num_device=num_device)\n", "\n", "from mpi4py import MPI\n", - "import numpy as np\n", "\n", - "from desc.objectives import ForceBalance, ObjectiveFunction\n", - "from desc.objectives.getters import get_parallel_forcebalance, get_fixed_boundary_constraints\n", - "from desc.grid import LinearGrid\n", "from desc.examples import get\n", - "\n", - "from desc.backend import jax\n", + "from desc.objectives.getters import (\n", + " get_fixed_boundary_constraints,\n", + " get_parallel_forcebalance,\n", + ")\n", "\n", "if __name__ == \"__main__\":\n", " rank = MPI.COMM_WORLD.Get_rank()\n", " eq = get(\"HELIOTRON\")\n", - " eq.change_resolution(6,6,6,12,12,12)\n", + " eq.change_resolution(6, 6, 6, 12, 12, 12)\n", "\n", + " # this will create a parallel objective function\n", + " # user can create their own parallel objective function as well which will be\n", + " # shown in the next example\n", " obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1)\n", " cons = get_fixed_boundary_constraints(eq)\n", + "\n", + " # Until this line, the code is performed on all ranks, so it might print some\n", + " # information multiple times. The following part will only be performed on the\n", + " # master rank\n", + "\n", + " # this context manager will put the workers in a loop to listen to the master\n", + " # to compute the objective function and its derivatives\n", " with obj as obj:\n", + " # apart from cost evaluation and derivatives, everything else will be only\n", + " # performed on the master rank\n", " if rank == 0:\n", - " eq.solve(objective=obj, constraints=cons, maxiter=1, ftol=0, gtol=0, xtol=0, verbose=3)\n", + " eq.solve(\n", + " objective=obj,\n", + " constraints=cons,\n", + " maxiter=1,\n", + " ftol=0,\n", + " gtol=0,\n", + " xtol=0,\n", + " verbose=3,\n", + " )\n", + "\n", + " # if you put a code here, it will be performed on all ranks\n", + "\n", "```" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -144,7 +164,6 @@ "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", - "Rank 1 waiting for message\n", "Precomputing transforms\n", "Building objective: lcfs R\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", @@ -152,10 +171,10 @@ "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", "Building objective: lcfs Z\n", - "Precomputing transforms\n", - "Precomputing transforms\n", "Building objective: fixed Psi\n", + "Precomputing transforms\n", "Building objective: fixed pressure\n", + "Precomputing transforms\n", "Building objective: fixed iota\n", "Building objective: fixed sheet current\n", "Building objective: self_consistency R\n", @@ -163,52 +182,38 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.49 sec\n", + "Timer: Objective build = 1.53 sec\n", "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", - "Rank 2 waiting for message\n", - "Rank 3 waiting for message\n", - "Timer: LinearConstraintProjection build = 4.38 sec\n", + "Timer: LinearConstraintProjection build = 4.31 sec\n", "Number of parameters: 609\n", "Number of objectives: 15000\n", - "Timer: Initializing the optimization = 5.93 sec\n", + "Timer: Initializing the optimization = 5.90 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", - "Rank 1 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", - "Rank 3 computing compute_scaled_error\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 2 waiting for message\n", - "Rank 1 waiting for message\n", - "Rank 3 waiting for message\n", - "Rank 1 computing jvp_scaled_error\n", - "Rank 2 computing jvp_scaled_error\n", - "Rank 3 computing jvp_scaled_error\n", + "Rank 1 : jvp_scaled_error\n", + "Rank 2 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 1 waiting for message\n", - "Rank 2 waiting for message\n", - "Rank 3 waiting for message\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 5.928e+00 2.480e+00 \n", - "Rank 1 computing compute_scaled_error\n", - "Rank 3 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 1 waiting for message\n", - "Rank 2 waiting for message\n", - "Rank 3 waiting for message\n", - "Rank 1 computing jvp_scaled_error\n", - "Rank 2 computing jvp_scaled_error\n", - "Rank 3 computing jvp_scaled_error\n", + "Rank 1 : jvp_scaled_error\n", + "Rank 2 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 1 waiting for message\n", - "Rank 2 waiting for message\n", - "Rank 3 waiting for message\n", " 1 2 9.876e-01 4.941e+00 3.322e-01 6.448e-01 \n", "Warning: Maximum number of iterations has been exceeded.\n", " Current function value: 9.876e-01\n", @@ -216,24 +221,18 @@ " Iterations: 1\n", " Function evaluations: 2\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 20.3 sec\n", - "Timer: Avg time per step = 10.1 sec\n", + "Timer: Solution time = 19.2 sec\n", + "Timer: Avg time per step = 9.60 sec\n", "==============================================================================================================\n", " Start --> End\n", - "Rank 1 computing compute_scaled_error\n", - "Rank 3 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 1 waiting for message\n", - "Rank 2 waiting for message\n", - "Rank 3 waiting for message\n", - "Rank 1 computing compute_scaled_error\n", - "Rank 3 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", + "Rank 1 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 1 waiting for message\n", - "Rank 2 waiting for message\n", - "Rank 3 waiting for message\n", "Total (sum of squares): 7.355e+00 --> 9.876e-01, \n", "Maximum absolute Force error: 1.077e+05 --> 7.858e+04 (N)\n", "Minimum absolute Force error: 1.201e-10 --> 1.307e-10 (N)\n", @@ -289,35 +288,38 @@ "metadata": {}, "source": [ "```python\n", - "import sys\n", "import os\n", + "import sys\n", "\n", + "# Add the path to the parent directory to augment search for module\n", "sys.path.insert(0, os.path.abspath(\".\"))\n", "sys.path.append(os.path.abspath(\"../../../\"))\n", "\n", - "from desc import set_device, _set_cpu_count\n", + "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# such that JAX and XLA thinks there are multiple devices\n", + "from desc import _set_cpu_count, set_device\n", + "\n", "num_device = 3\n", "_set_cpu_count(num_device)\n", "set_device(\"cpu\", num_device=num_device)\n", "\n", - "from mpi4py import MPI\n", "import numpy as np\n", + "from mpi4py import MPI\n", "\n", + "from desc.backend import jax, jnp\n", + "from desc.examples import get\n", + "from desc.grid import LinearGrid\n", "from desc.objectives import (\n", - " ForceBalance, \n", - " ObjectiveFunction, \n", - " QuasisymmetryTwoTerm, \n", - " AspectRatio, \n", - " FixBoundaryR, \n", + " AspectRatio,\n", + " FixBoundaryR,\n", " FixBoundaryZ,\n", + " FixCurrent,\n", " FixPressure,\n", " FixPsi,\n", - " FixCurrent,\n", + " ForceBalance,\n", + " ObjectiveFunction,\n", + " QuasisymmetryTwoTerm,\n", ")\n", - "from desc.grid import LinearGrid\n", - "from desc.examples import get\n", - "\n", - "from desc.backend import jax, jnp\n", "from desc.optimize import Optimizer\n", "\n", "if __name__ == \"__main__\":\n", @@ -326,6 +328,8 @@ " eq = get(\"precise_QA\")\n", " eq.change_resolution(3, 3, 3, 6, 6, 6)\n", "\n", + " # create two grids with different rho values, this will effectively separate\n", + " # the quasisymmetry objective into two parts\n", " grid1 = LinearGrid(\n", " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.2, 0.5, 4), sym=True\n", " )\n", @@ -333,27 +337,35 @@ " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.6, 1.0, 6), sym=True\n", " )\n", "\n", + " # when using parallel objectives, the user needs to supply the device_id\n", " obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", " obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", " obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2)\n", "\n", " objs = [obj1, obj2, obj3]\n", + " # this part will probably be automatized in the future\n", " for obji in objs:\n", " obji.build(verbose=3)\n", " obji = jax.device_put(obji, obji._device)\n", " obji.things[0] = eq\n", "\n", + " # Parallel objective function needs the MPI communicator\n", " objective = ObjectiveFunction(objs, mpi=MPI)\n", " objective.build(verbose=3)\n", "\n", + " # we will fix some modes as usual\n", " k = 1\n", " R_modes = np.vstack(\n", " (\n", " [0, 0, 0],\n", - " eq.surface.R_basis.modes[np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :],\n", + " eq.surface.R_basis.modes[\n", + " np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :\n", + " ],\n", " )\n", " )\n", - " Z_modes = eq.surface.Z_basis.modes[np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :]\n", + " Z_modes = eq.surface.Z_basis.modes[\n", + " np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :\n", + " ]\n", " constraints = (\n", " ForceBalance(eq=eq),\n", " FixBoundaryR(eq=eq, modes=R_modes),\n", @@ -364,7 +376,15 @@ " )\n", " optimizer = Optimizer(\"proximal-lsq-exact\")\n", "\n", + " # Until this line, the code is performed on all ranks, so it might print some\n", + " # information multiple times. The following part will only be performed on the\n", + " # master rank\n", + "\n", + " # this context manager will put the workers in a loop to listen to the master\n", + " # to compute the objective function and its derivatives\n", " with objective as objective:\n", + " # apart from cost evaluation and derivatives, everything else will be only\n", + " # performed on the master rank\n", " if rank == 0:\n", " eq.optimize(\n", " objective=objective,\n", @@ -376,12 +396,15 @@ " \"initial_trust_ratio\": 1.0,\n", " },\n", " )\n", + "\n", + " # if you put a code here, it will be performed on all ranks\n", + "\n", "```" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -394,74 +417,64 @@ " warnings.warn(colored(msg, \"yellow\"), err)\n", "Precomputing transforms\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 799 ms\n", - "Timer: Precomputing transforms = 801 ms\n", - "Precomputing transforms\n", - "Precomputing transforms\n", - "Timer: Precomputing transforms = 736 ms\n", - "Timer: Precomputing transforms = 741 ms\n", + "Timer: Precomputing transforms = 1.37 sec\n", + "Timer: Precomputing transforms = 1.40 sec\n", "Precomputing transforms\n", "Precomputing transforms\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", - "Timer: Precomputing transforms = 767 ms\n", - "Timer: Precomputing transforms = 767 ms\n", - "Timer: Objective build = 12.6 ms\n", - "Rank 1 waiting for message\n", - "Timer: Objective build = 13.3 ms\n", - "Building objective: force\n", + "Timer: Precomputing transforms = 1.25 sec\n", "Precomputing transforms\n", + "Timer: Precomputing transforms = 1.27 sec\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 947 ms\n", - "Timer: Objective build = 1.49 sec\n", - "Timer: Precomputing transforms = 1.60 sec\n", - "Timer: Objective build = 15.1 ms\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.47 sec\n", + "Timer: Precomputing transforms = 1.24 sec\n", + "Timer: Objective build = 21.9 ms\n", + "Timer: Precomputing transforms = 1.26 sec\n", + "Timer: Objective build = 22.0 ms\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 2.27 sec\n", + "Timer: Precomputing transforms = 1.57 sec\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.49 sec\n", + "Timer: Objective build = 2.37 sec\n", "Timer: Objective build = 25.1 ms\n", - "Rank 2 waiting for message\n", - "Timer: Eq Update LinearConstraintProjection build = 3.55 sec\n", - "Timer: Proximal projection build = 6.11 sec\n", + "Timer: Precomputing transforms = 2.02 sec\n", + "Precomputing transforms\n", + "Timer: Precomputing transforms = 2.01 sec\n", + "Timer: Objective build = 36.1 ms\n", + "Timer: Eq Update LinearConstraintProjection build = 5.33 sec\n", + "Timer: Proximal projection build = 9.29 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 510 ms\n", - "Timer: LinearConstraintProjection build = 1.30 sec\n", + "Timer: Objective build = 727 ms\n", + "Timer: LinearConstraintProjection build = 1.94 sec\n", "Number of parameters: 8\n", "Number of objectives: 911\n", - "Timer: Initializing the optimization = 7.96 sec\n", + "Timer: Initializing the optimization = 12.0 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", - "Rank 1 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", - "Rank 2 waiting for message\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 1 waiting for message\n", "This should run on GPU id:0\n", "This should run on GPU id:1\n", "This should run on GPU id:2\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 2.011e+04 1.952e+02 \n", - "Rank 1 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", - "Rank 2 waiting for message\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 1 waiting for message\n", - "Rank 1 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 2 waiting for message\n", - "Rank 1 waiting for message\n", - "Rank 1 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 2 waiting for message\n", - "Rank 1 waiting for message\n", "This should run on GPU id:0\n", "This should run on GPU id:1\n", "This should run on GPU id:2\n", @@ -472,20 +485,16 @@ " Iterations: 1\n", " Function evaluations: 4\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 36.1 sec\n", - "Timer: Avg time per step = 18.0 sec\n", + "Timer: Solution time = 52.5 sec\n", + "Timer: Avg time per step = 26.2 sec\n", "==============================================================================================================\n", " Start --> End\n", - "Rank 1 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 2 waiting for message\n", - "Rank 1 waiting for message\n", - "Rank 1 computing compute_scaled_error\n", - "Rank 2 computing compute_scaled_error\n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Rank 2 waiting for message\n", - "Rank 1 waiting for message\n", "Total (sum of squares): 2.011e+04 --> 8.735e+03, \n", "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.813e-01 --> 6.254e-01 (T^3)\n", "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.150e-04 --> 4.713e-03 (T^3)\n", From a47667e2efa728552a9658aa22baf254ea8f0c5b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 25 Feb 2025 18:16:46 -0500 Subject: [PATCH 078/199] remove redundant lines, fix changelog, update minor stuff --- CHANGELOG.md | 2 - desc/backend.py | 9 -- desc/objectives/utils.py | 2 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 2 +- docs/notebooks/tutorials/multi_device.ipynb | 84 +++++++++---------- 5 files changed, 44 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88abaea2a6..f251288620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,6 @@ New Features - Adds a new function ``desc.coils.initialize_helical_coils`` for creating an initial guess for stage 2 helical coil optimization. - Adds ``desc.vmec_utils.make_boozmn_output `` for writing boozmn.nc style output files for compatibility with other codes which expect such files from the Booz_Xform code. -- Renames compute quantity ``sqrt(g)_B`` to ``sqrt(g)_Boozer_DESC`` to more accurately reflect what the quantity is (the jacobian from (rho,theta_B,zeta_B) to (rho,theta,zeta)), and adds a new function to compute ``sqrt(g)_Boozer`` which is the jacobian from (rho,theta_B,zeta_B) to (R,phi,Z). -- Allows specification of Nyquist spectrum maximum mode-numbers when using ``VMECIO.save`` to save a DESC .h5 file as a VMEC-format wout file - Adds initial support for multiple GPU optimization. This allows to compute derivatives on multiple GPU, and allows more memory intense objectives. Note that: at this phase, the multi-device support is for memory, not speed. - Renames compute quantity ``sqrt(g)_B`` to ``sqrt(g)_Boozer_DESC`` to more accurately reflect what the quantiy is (the jacobian from (rho,theta_B,zeta_B) to (rho,theta,zeta)), and adds a new function to compute ``sqrt(g)_Boozer`` which is the jacobian from (rho,theta_B,zeta_B) to (R,phi,Z). - Allows specification of Nyquist spectrum maximum modenumbers when using ``VMECIO.save`` to save a DESC .h5 file as a VMEC-format wout file diff --git a/desc/backend.py b/desc/backend.py index dcdc37e71d..fea39d35c0 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -28,15 +28,6 @@ from jax import config as jax_config jax_config.update("jax_enable_x64", True) - if desc_config["num_device"] != 1: - # for now, these are not used. Delete them if they are not needed. - mesh = jax.make_mesh((desc_config["num_device"],), ("grid")) - desc_config["sharding"] = jax.sharding.NamedSharding( - mesh, jax.sharding.PartitionSpec("grid") - ) - desc_config["sharding_replicated"] = jax.sharding.NamedSharding( - mesh, jax.sharding.PartitionSpec() - ) if desc_config.get("kind") == "gpu" and len(jax.devices("gpu")) == 0: warnings.warn( "JAX failed to detect GPU, are you sure you " diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 6eba412320..b0f906818d 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -139,7 +139,7 @@ def factorize_linear_constraints(objective, constraint, x_scale="auto"): # noqa xp = put(xp, unfixed_idx, A_inv @ b) xp = put(xp, fixed_idx, ((1 / D) * xp)[fixed_idx]) # cast to jnp arrays - # TODO: might consider sharding these too + # TODO: might consider sharding these xp = jnp.asarray(xp) A = jnp.asarray(A) b = jnp.asarray(b) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index ebc8ae726d..e5c8e65da4 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -60,7 +60,7 @@ obji.things[0] = eq # Parallel objective function needs the MPI communicator - objective = ObjectiveFunction(objs, mpi=MPI) + objective = ObjectiveFunction(objs, deriv_mode="blocked", mpi=MPI) objective.build(verbose=3) # we will fix some modes as usual diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 385a1bec9a..0d02ceeb20 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -49,20 +49,20 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.13.0+1675.g3b4f847fd.dirty.\n", + "DESC version=0.13.0+1676.g5444157e3.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.15 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.15 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.15 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 7.15 GB available memory\n" + "\t CPU 0: TFRT_CPU_0 with 6.49 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 6.49 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 6.49 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 6.49 GB available memory\n" ] } ], @@ -172,47 +172,47 @@ " warnings.warn(colored(msg, \"yellow\"), err)\n", "Building objective: lcfs Z\n", "Building objective: fixed Psi\n", - "Precomputing transforms\n", "Building objective: fixed pressure\n", - "Precomputing transforms\n", "Building objective: fixed iota\n", "Building objective: fixed sheet current\n", "Building objective: self_consistency R\n", + "Precomputing transforms\n", + "Precomputing transforms\n", "Building objective: self_consistency Z\n", "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.53 sec\n", + "Timer: Objective build = 1.44 sec\n", "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", "Precomputing transforms\n", - "Timer: LinearConstraintProjection build = 4.31 sec\n", + "Timer: LinearConstraintProjection build = 4.21 sec\n", "Number of parameters: 609\n", "Number of objectives: 15000\n", - "Timer: Initializing the optimization = 5.90 sec\n", + "Timer: Initializing the optimization = 5.71 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", "Rank 3 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 2 : jvp_scaled_error\n", "Rank 3 : jvp_scaled_error\n", + "Rank 2 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 5.928e+00 2.480e+00 \n", "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", "Rank 3 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 2 : jvp_scaled_error\n", "Rank 3 : jvp_scaled_error\n", + "Rank 2 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " 1 2 9.876e-01 4.941e+00 3.322e-01 6.448e-01 \n", "Warning: Maximum number of iterations has been exceeded.\n", @@ -221,8 +221,8 @@ " Iterations: 1\n", " Function evaluations: 2\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 19.2 sec\n", - "Timer: Avg time per step = 9.60 sec\n", + "Timer: Solution time = 18.0 sec\n", + "Timer: Avg time per step = 9.03 sec\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", @@ -230,8 +230,8 @@ "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Total (sum of squares): 7.355e+00 --> 9.876e-01, \n", "Maximum absolute Force error: 1.077e+05 --> 7.858e+04 (N)\n", @@ -350,7 +350,7 @@ " obji.things[0] = eq\n", "\n", " # Parallel objective function needs the MPI communicator\n", - " objective = ObjectiveFunction(objs, mpi=MPI)\n", + " objective = ObjectiveFunction(objs, deriv_mode=\"blocked\", mpi=MPI)\n", " objective.build(verbose=3)\n", "\n", " # we will fix some modes as usual\n", @@ -417,44 +417,44 @@ " warnings.warn(colored(msg, \"yellow\"), err)\n", "Precomputing transforms\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.37 sec\n", - "Timer: Precomputing transforms = 1.40 sec\n", + "Timer: Precomputing transforms = 988 ms\n", + "Timer: Precomputing transforms = 995 ms\n", "Precomputing transforms\n", "Precomputing transforms\n", + "Timer: Precomputing transforms = 921 ms\n", + "Timer: Precomputing transforms = 927 ms\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", - "Timer: Precomputing transforms = 1.25 sec\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.27 sec\n", "Precomputing transforms\n", + "Timer: Precomputing transforms = 954 ms\n", + "Timer: Precomputing transforms = 954 ms\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.24 sec\n", - "Timer: Objective build = 21.9 ms\n", - "Timer: Precomputing transforms = 1.26 sec\n", - "Timer: Objective build = 22.0 ms\n", + "Timer: Objective build = 17.9 ms\n", + "Timer: Objective build = 17.4 ms\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 2.27 sec\n", - "Timer: Precomputing transforms = 1.57 sec\n", + "Timer: Precomputing transforms = 1.28 sec\n", + "Timer: Precomputing transforms = 1.93 sec\n", + "Timer: Objective build = 1.90 sec\n", "Precomputing transforms\n", - "Timer: Objective build = 2.37 sec\n", - "Timer: Objective build = 25.1 ms\n", - "Timer: Precomputing transforms = 2.02 sec\n", + "Timer: Objective build = 24.1 ms\n", + "Timer: Precomputing transforms = 1.77 sec\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 2.01 sec\n", - "Timer: Objective build = 36.1 ms\n", - "Timer: Eq Update LinearConstraintProjection build = 5.33 sec\n", - "Timer: Proximal projection build = 9.29 sec\n", + "Timer: Precomputing transforms = 1.52 sec\n", + "Timer: Objective build = 25.2 ms\n", + "Timer: Eq Update LinearConstraintProjection build = 4.09 sec\n", + "Timer: Proximal projection build = 7.35 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 727 ms\n", - "Timer: LinearConstraintProjection build = 1.94 sec\n", + "Timer: Objective build = 598 ms\n", + "Timer: LinearConstraintProjection build = 1.56 sec\n", "Number of parameters: 8\n", "Number of objectives: 911\n", - "Timer: Initializing the optimization = 12.0 sec\n", + "Timer: Initializing the optimization = 9.57 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", @@ -485,8 +485,8 @@ " Iterations: 1\n", " Function evaluations: 4\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 52.5 sec\n", - "Timer: Avg time per step = 26.2 sec\n", + "Timer: Solution time = 37.8 sec\n", + "Timer: Avg time per step = 18.9 sec\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", From 482e8d996d6b03d0b4c760f6fd2cc7b204af911d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 25 Feb 2025 18:50:51 -0500 Subject: [PATCH 079/199] automize obj device placement --- desc/objectives/getters.py | 8 +- desc/objectives/objective_funs.py | 8 ++ .../tutorials/mpi-tutorials/mpi-proximal.py | 6 - docs/notebooks/tutorials/multi_device.ipynb | 122 ++++++++++++------ 4 files changed, 89 insertions(+), 55 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index bd5e0e3ebd..258a697af1 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -362,7 +362,7 @@ def get_parallel_forcebalance(eq, num_device, mpi, grid=None, use_jit=True, verb A built objective function with force balance objectives. Each objective is computed on a separate device. """ - from desc.backend import desc_config, jax, jnp + from desc.backend import desc_config, jnp from desc.grid import LinearGrid if desc_config["num_device"] < num_device: @@ -400,12 +400,6 @@ def get_parallel_forcebalance(eq, num_device, mpi, grid=None, use_jit=True, verb else: gridi = grid[i] obj = ForceBalance(eq, grid=gridi, device_id=i) - obj.build(use_jit=use_jit, verbose=verbose) - obj = jax.device_put(obj, obj._device) - # if the eq is also distrubuted across GPUs, then some internal logic - # that checks if the things are different will fail, so we need to - # set the eq to be the same manually - obj._things[0] = eq objs += (obj,) objective = ObjectiveFunction(objs, mpi=mpi, deriv_mode="blocked") objective.build(use_jit=use_jit, verbose=verbose) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 9f1a756f6d..9010511766 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -428,11 +428,19 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 # build objectives self._dim_f = 0 for objective in self.objectives: + obj_things = objective._things if not objective.built: if verbose > 0: print("Building objective: " + objective.name) objective.build(use_jit=self.use_jit, verbose=verbose) self._dim_f += objective.dim_f + if objective._device_id != 0: + print( + f"Putting objective {objective.name} on device " + f"{objective._device_id}" + ) + objective = jax.device_put(objective, objective._device) + objective._things = obj_things if self._dim_f == 1: self._scalar = True else: diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index e5c8e65da4..a9827f3812 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -51,13 +51,7 @@ obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2) - objs = [obj1, obj2, obj3] - # this part will probably be automatized in the future - for obji in objs: - obji.build(verbose=3) - obji = jax.device_put(obji, obji._device) - obji.things[0] = eq # Parallel objective function needs the MPI communicator objective = ObjectiveFunction(objs, deriv_mode="blocked", mpi=MPI) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 0d02ceeb20..b20b299e75 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -56,13 +56,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.13.0+1676.g5444157e3.dirty.\n", + "DESC version=0.13.0+1677.ga47667e2e.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 6.49 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 6.49 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 6.49 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 6.49 GB available memory\n" + "\t CPU 0: TFRT_CPU_0 with 6.53 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 6.53 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 6.53 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 6.53 GB available memory\n" ] } ], @@ -77,6 +77,7 @@ "metadata": {}, "source": [ "```python\n", + "\n", "import os\n", "import sys\n", "\n", @@ -157,14 +158,28 @@ " warnings.warn(colored(msg, \"yellow\"), err)\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", + "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 1\n", + "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 1\n", + "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 2\n", + "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 2\n", + "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 3\n", + "Putting objective force on device 3\n", "Building objective: lcfs R\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", @@ -176,23 +191,37 @@ "Building objective: fixed iota\n", "Building objective: fixed sheet current\n", "Building objective: self_consistency R\n", - "Precomputing transforms\n", - "Precomputing transforms\n", "Building objective: self_consistency Z\n", "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.44 sec\n", + "Timer: Objective build = 950 ms\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 1\n", + "Putting objective force on device 1\n", + "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 2\n", + "Putting objective force on device 2\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Building objective: force\n", "Precomputing transforms\n", - "Timer: LinearConstraintProjection build = 4.21 sec\n", + "Putting objective force on device 3\n", + "Putting objective force on device 3\n", + "Timer: LinearConstraintProjection build = 4.22 sec\n", "Number of parameters: 609\n", "Number of objectives: 15000\n", - "Timer: Initializing the optimization = 5.71 sec\n", + "Timer: Initializing the optimization = 5.22 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", @@ -201,14 +230,14 @@ "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", "Rank 2 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 5.928e+00 2.480e+00 \n", "Rank 1 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", "Rank 3 : jvp_scaled_error\n", @@ -221,8 +250,8 @@ " Iterations: 1\n", " Function evaluations: 2\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 18.0 sec\n", - "Timer: Avg time per step = 9.03 sec\n", + "Timer: Solution time = 18.2 sec\n", + "Timer: Avg time per step = 9.14 sec\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", @@ -341,13 +370,7 @@ " obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", " obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", " obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2)\n", - "\n", " objs = [obj1, obj2, obj3]\n", - " # this part will probably be automatized in the future\n", - " for obji in objs:\n", - " obji.build(verbose=3)\n", - " obji = jax.device_put(obji, obji._device)\n", - " obji.things[0] = eq\n", "\n", " # Parallel objective function needs the MPI communicator\n", " objective = ObjectiveFunction(objs, deriv_mode=\"blocked\", mpi=MPI)\n", @@ -415,46 +438,61 @@ " warnings.warn(colored(msg, \"yellow\"), err)\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", + "Building objective: QS two-term\n", "Precomputing transforms\n", + "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 988 ms\n", - "Timer: Precomputing transforms = 995 ms\n", + "Timer: Precomputing transforms = 1.12 sec\n", + "Timer: Precomputing transforms = 1.12 sec\n", + "Building objective: QS two-term\n", "Precomputing transforms\n", + "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 921 ms\n", - "Timer: Precomputing transforms = 927 ms\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", + "Timer: Precomputing transforms = 949 ms\n", + "Timer: Precomputing transforms = 949 ms\n", + "Putting objective QS two-term on device 1\n", + "Putting objective QS two-term on device 1\n", + "Building objective: aspect ratio\n", "Precomputing transforms\n", + "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 954 ms\n", - "Timer: Precomputing transforms = 954 ms\n", + "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Objective build = 17.9 ms\n", - "Timer: Objective build = 17.4 ms\n", + "Timer: Precomputing transforms = 971 ms\n", + "Timer: Precomputing transforms = 970 ms\n", + "Putting objective aspect ratio on device 2\n", + "Putting objective aspect ratio on device 2\n", + "Timer: Objective build = 3.78 sec\n", + "Timer: Objective build = 3.78 sec\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.28 sec\n", - "Timer: Precomputing transforms = 1.93 sec\n", - "Timer: Objective build = 1.90 sec\n", + "Timer: Precomputing transforms = 1.19 sec\n", + "Timer: Objective build = 1.25 sec\n", + "Timer: Precomputing transforms = 1.85 sec\n", + "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Objective build = 24.1 ms\n", - "Timer: Precomputing transforms = 1.77 sec\n", + "Timer: Objective build = 23.2 ms\n", + "Timer: Precomputing transforms = 1.88 sec\n", + "Putting objective QS two-term on device 1\n", + "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.52 sec\n", - "Timer: Objective build = 25.2 ms\n", - "Timer: Eq Update LinearConstraintProjection build = 4.09 sec\n", - "Timer: Proximal projection build = 7.35 sec\n", + "Timer: Precomputing transforms = 1.77 sec\n", + "Putting objective aspect ratio on device 2\n", + "Timer: Objective build = 6.66 sec\n", + "Timer: Eq Update LinearConstraintProjection build = 4.53 sec\n", + "Timer: Proximal projection build = 7.78 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 598 ms\n", - "Timer: LinearConstraintProjection build = 1.56 sec\n", + "Timer: Objective build = 602 ms\n", + "Timer: LinearConstraintProjection build = 1.60 sec\n", "Number of parameters: 8\n", "Number of objectives: 911\n", - "Timer: Initializing the optimization = 9.57 sec\n", + "Timer: Initializing the optimization = 10.0 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", @@ -485,8 +523,8 @@ " Iterations: 1\n", " Function evaluations: 4\n", " Jacobian evaluations: 2\n", - "Timer: Solution time = 37.8 sec\n", - "Timer: Avg time per step = 18.9 sec\n", + "Timer: Solution time = 38.2 sec\n", + "Timer: Avg time per step = 19.1 sec\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", From f75ae6abf0956a248d5e7873c7d7b446a9c349df Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 27 Feb 2025 00:51:50 -0500 Subject: [PATCH 080/199] add errors for context manager, it must be used by parallel objective and it must be built before entering --- desc/objectives/objective_funs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 13506cd9b5..f6e62e92c6 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -297,6 +297,17 @@ def __init__( ) def __enter__(self): + errorif( + not self._built, + RuntimeError, + "In parallel mode, ObjectiveFunction must be built before entering " + "context manager.", + ) + errorif( + not self._is_multi_device, + RuntimeError, + "ObjectiveFunction must be parallel to be used as a context manager.", + ) # when entering the context manager, we start the worker loop # this will allow the root rank to send messages to the workers # to compute and to stop From 17c14ba68356d411cbe1417df04c95cc8482e86c Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 27 Feb 2025 02:05:59 -0500 Subject: [PATCH 081/199] update proximal jvp methods, divide constraint and objective part, add batched and blocked options, also implement mpi for proximal --- desc/objectives/objective_funs.py | 28 +- desc/optimize/_constraint_wrappers.py | 126 ++++++--- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 2 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 2 +- docs/notebooks/tutorials/multi_device.ipynb | 248 ++++++++++-------- 5 files changed, 251 insertions(+), 155 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index f6e62e92c6..b37119d3a4 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -358,7 +358,7 @@ def _worker_loop(self): if message[0] == "STOP": print(f"Rank {self.rank} STOPPING") break - elif "jvp" in message[0]: + elif "jvp" in message[0] and "proximal" not in message[0]: print(f"Rank {self.rank} : {message[0]}") # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective @@ -381,6 +381,32 @@ def _worker_loop(self): f_rank = getattr(obj, message[0])(*par, constants=const) f_rank = np.asarray(f_rank) self.comm.gather(f_rank, root=0) + elif "proximal_jvp" in message[0]: + print(f"Rank {self.rank} : {message[0]}") + obj = self.objectives[self.rank] + const = self.constants[self.rank] + op = message[0].replace("proximal_jvp_", "") + + thing_idx = self._things_per_objective_idx[self.rank] + xi = [message[1][i] for i in thing_idx] + vi = [message[2][i] for i in thing_idx] + assert len(xi) > 0 + assert len(vi) > 0 + assert len(xi) == len(vi) + if obj._deriv_mode == "rev": + # obj might not allow fwd mode, so compute full rev mode jacobian + # and do matmul manually. This is slightly inefficient, but usually + # when rev mode is used, dim_f <<< dim_x, so its not too bad. + Ji = getattr(obj, "jac_" + op)(*xi, constants=const) + J_rank = jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum( + axis=0 + ) + else: + J_rank = getattr(obj, "jvp_" + op)( + [_vi for _vi in vi], xi, constants=const + ).T + J_rank = np.asarray(J_rank) + self.comm.gather(J_rank, root=0) def _unjit(self): """Remove jit compiled methods.""" diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 68903472d6..7f3fbdcf9c 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import jax, jit, jnp, pconcat, put +from desc.backend import jit, jnp, pconcat, put from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -1063,13 +1063,8 @@ def jvp_scaled(self, v, x, constants=None): Constant parameters passed to sub-objectives. """ - v = v[0] if isinstance(v, (tuple, list)) else v - constants = setdefault(constants, self.constants) - xg, xf = self._update_equilibrium(x, store=True) - jvpfun = lambda u: self._jvp(u, xf, xg, constants, op="scaled") - return batched_vectorize( - jvpfun, signature="(n)->(k)", chunk_size=self._objective._jac_chunk_size - )(v) + op = "scaled" + return self._jvp(v, x, constants, op) def jvp_scaled_error(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_scaled_error. @@ -1085,13 +1080,8 @@ def jvp_scaled_error(self, v, x, constants=None): Constant parameters passed to sub-objectives. """ - v = v[0] if isinstance(v, (tuple, list)) else v - constants = setdefault(constants, self.constants) - xg, xf = self._update_equilibrium(x, store=True) - jvpfun = lambda u: self._jvp(u, xf, xg, constants, op="scaled_error") - return batched_vectorize( - jvpfun, signature="(n)->(k)", chunk_size=self._objective._jac_chunk_size - )(v) + op = "scaled_error" + return self._jvp(v, x, constants, op) def jvp_unscaled(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_unscaled. @@ -1107,15 +1097,55 @@ def jvp_unscaled(self, v, x, constants=None): Constant parameters passed to sub-objectives. """ + op = "unscaled" + return self._jvp(v, x, constants, op) + + def _jvp(self, v, x, constants=None, op="jvp_scaled_error"): v = v[0] if isinstance(v, (tuple, list)) else v constants = setdefault(constants, self.constants) xg, xf = self._update_equilibrium(x, store=True) - jvpfun = lambda u: self._jvp(u, xf, xg, constants, op="unscaled") - return batched_vectorize( - jvpfun, signature="(n)->(k)", chunk_size=self._objective._jac_chunk_size - )(v) + if not self._constraint._is_multi_device: + jvpfun = lambda u: self._get_tangent(u, xf, xg, constants, op=op) + tangents = batched_vectorize( + jvpfun, + signature="(n)->(k)", + chunk_size=self._constraint._jac_chunk_size, + )(v) + else: + # TODO: implement parallel constraint for ProximalProjection + raise NotImplementedError( + "Parallel constraint for ProximalProjection not implemented yet. Note: " + "This would require putting workers into a second infinite loop which " + "break things. One way to do this could be to give pre-build objective " + "and constraint to the optimizer and use 2 context managers. Also, " + "divide workers for force balance constraint loop and objective loop. " + "This is probably a rare use case, so not a priority for now." + ) - def _jvp(self, v, xf, xg, constants, op): + if self._objective._deriv_mode in ["batched"]: + # objective's method already know about its jac_chunk_size + return -getattr(self._objective, "jvp_" + op)(tangents, xg, constants[0]) + elif not self._objective._is_multi_device: + xgs = jnp.split(xg, np.cumsum(self._dimx_per_thing)) + jvpfun = lambda u: _proximal_jvp_blocked_pure( + self._objective, jnp.split(u, np.cumsum(self._dimx_per_thing)), xgs, op + ) + return batched_vectorize( + jvpfun, + signature="(n)->(k)", + chunk_size=self._objective._jac_chunk_size, + )(tangents) + else: + xgs = jnp.split(xg, np.cumsum(self._dimx_per_thing)) + vgs = jnp.split(tangents, np.cumsum(self._dimx_per_thing), axis=-1) + return _proximal_jvp_blocked_parallel( + self._objective, + vgs, + xgs, + op, + ) + + def _get_tangent(self, v, xf, xg, constants, op): # we're replacing stuff like this with jvps # Fx_reduced = Fx[:, unfixed_idx] @ Z # noqa: E800 # Gx_reduced = Gx[:, unfixed_idx] @ Z # noqa: E800 @@ -1154,13 +1184,7 @@ def _jvp(self, v, xf, xg, constants, op): ] ) tangent = self._unfixed_idx_mat @ dfdc - dxdcv - if self._objective._deriv_mode in ["batched"]: - out = getattr(self._objective, "jvp_" + op)(tangent, xg, constants[0]) - else: # deriv_mode == "blocked" - vgs = jnp.split(tangent, np.cumsum(self._dimx_per_thing)) - xgs = jnp.split(xg, np.cumsum(self._dimx_per_thing)) - out = _proximal_jvp_blocked_pure(self._objective, vgs, xgs, op) - return -out + return tangent @property def constants(self): @@ -1198,8 +1222,10 @@ def wrapper(*args, **kwargs): @jit_if_not_parallel def _proximal_jvp_f_pure(constraint, xf, constants, dc, unfixed_idx, Z, D, dxdc, op): + # if the constraint has multiple devices, we can't jit this part Fx = getattr(constraint, "jac_" + op)(xf, constants) + # this part is still jittable even with MPI @jit def fun(Fx, dxdc, dc, unfixed_idx, Z, D): # F_reduced @@ -1215,21 +1241,13 @@ def fun(Fx, dxdc, dc, unfixed_idx, Z, D): return fun(Fx, dxdc, dc, unfixed_idx, Z, D) -@jit_if_not_parallel +@functools.partial(jit, static_argnames=["op"]) def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): out = [] for k, (obj, const) in enumerate(zip(objective.objectives, objective.constants)): - # TODO: this is for debugging purposes, must be deleted before merging! - if objective._is_multi_device: - print(f"This should run on GPU id:{obj._device_id}") thing_idx = objective._things_per_objective_idx[k] xi = [xgs[i] for i in thing_idx] vi = [vgs[i] for i in thing_idx] - if objective._is_multi_device: # pragma: no cover - # inputs to jitted functions must live on the same device. Need to - # put xi and vi on the same device as the objective - xi = jax.device_put(xi, obj._device) - vi = jax.device_put(vi, obj._device) assert len(xi) > 0 assert len(vi) > 0 assert len(xi) == len(vi) @@ -1243,8 +1261,34 @@ def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): else: outi = getattr(obj, "jvp_" + op)([_vi for _vi in vi], xi, constants=const).T out.append(outi) - if objective._is_multi_device: # pragma: no cover - out = pconcat(out) - else: - out = jnp.concatenate(out) - return out + return -jnp.concatenate(out) + + +@jit_if_not_parallel +def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): + if objective.rank == 0: + message = ("proximal_jvp_" + op, xgs, vgs) + objective.comm.bcast(message, root=0) + + obj = objective.objectives[0] + const = objective.constants[0] + + thing_idx = objective._things_per_objective_idx[0] + xi = [xgs[i] for i in thing_idx] + vi = [vgs[i] for i in thing_idx] + assert len(xi) > 0 + assert len(vi) > 0 + assert len(xi) == len(vi) + if obj._deriv_mode == "rev": + # obj might not allow fwd mode, so compute full rev mode jacobian + # and do matmul manually. This is slightly inefficient, but usually + # when rev mode is used, dim_f <<< dim_x, so its not too bad. + Ji = getattr(obj, "jac_" + op)(*xi, constants=const) + J_rank = jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum(axis=0) + else: + J_rank = getattr(obj, "jvp_" + op)( + [_vi for _vi in vi], xi, constants=const + ).T + print(f"Rank {objective.rank} waiting to gather") + J = objective.comm.gather(J_rank, root=0) + return -pconcat(J).T diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 52ae2231ef..fc0bab0430 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -45,7 +45,7 @@ eq.solve( objective=obj, constraints=cons, - maxiter=1, + maxiter=3, ftol=0, gtol=0, xtol=0, diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index a9827f3812..d94bc4d0b7 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -94,7 +94,7 @@ objective=objective, constraints=constraints, optimizer=optimizer, - maxiter=1, + maxiter=3, verbose=3, options={ "initial_trust_ratio": 1.0, diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index b20b299e75..a7e06fc76d 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -56,13 +56,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.13.0+1677.ga47667e2e.dirty.\n", + "DESC version=0.13.0+1687.gf75ae6abf.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 6.53 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 6.53 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 6.53 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 6.53 GB available memory\n" + "\t CPU 0: TFRT_CPU_0 with 6.76 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 6.76 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 6.76 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 6.76 GB available memory\n" ] } ], @@ -139,17 +139,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/yigit/miniconda3/envs/mpi/lib/python3.12/pty.py:95: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n", - " pid, fd = os.forkpty()\n" - ] - }, { "name": "stdout", "output_type": "stream", @@ -192,14 +184,14 @@ "Building objective: fixed sheet current\n", "Building objective: self_consistency R\n", "Building objective: self_consistency Z\n", - "Building objective: lambda gauge\n", - "Building objective: axis R self consistency\n", - "Building objective: axis Z self consistency\n", - "Timer: Objective build = 950 ms\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", + "Building objective: lambda gauge\n", + "Building objective: axis R self consistency\n", + "Building objective: axis Z self consistency\n", + "\u001b[32mTimer: Objective build = 1.30 sec\u001b[0m\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", @@ -218,16 +210,16 @@ "Precomputing transforms\n", "Putting objective force on device 3\n", "Putting objective force on device 3\n", - "Timer: LinearConstraintProjection build = 4.22 sec\n", + "\u001b[32mTimer: LinearConstraintProjection build = 8.91 sec\u001b[0m\n", "Number of parameters: 609\n", "Number of objectives: 15000\n", - "Timer: Initializing the optimization = 5.22 sec\n", + "\u001b[32mTimer: Initializing the optimization = 10.3 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", "Rank 1 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", "Rank 2 : jvp_scaled_error\n", @@ -244,14 +236,32 @@ "Rank 2 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " 1 2 9.876e-01 4.941e+00 3.322e-01 6.448e-01 \n", + "Rank 1 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", + "Rank 2 : jvp_scaled_error\n", + "Rank 0 waiting to gather\n", + " 2 3 4.419e-02 9.434e-01 2.363e-01 8.993e-02 \n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 : jvp_scaled_error\n", + "Rank 2 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", + "Rank 0 waiting to gather\n", + " 3 4 1.048e-02 3.370e-02 1.497e-01 3.808e-02 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 9.876e-01\n", - " Total delta_x: 3.322e-01\n", - " Iterations: 1\n", - " Function evaluations: 2\n", - " Jacobian evaluations: 2\n", - "Timer: Solution time = 18.2 sec\n", - "Timer: Avg time per step = 9.14 sec\n", + " Current function value: 1.048e-02\n", + " Total delta_x: 3.604e-01\n", + " Iterations: 3\n", + " Function evaluations: 4\n", + " Jacobian evaluations: 4\n", + "\u001b[32mTimer: Solution time = 41.4 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 10.3 sec\u001b[0m\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", @@ -259,34 +269,34 @@ "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", "Rank 3 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Total (sum of squares): 7.355e+00 --> 9.876e-01, \n", - "Maximum absolute Force error: 1.077e+05 --> 7.858e+04 (N)\n", - "Minimum absolute Force error: 1.201e-10 --> 1.307e-10 (N)\n", - "Average absolute Force error: 3.587e+04 --> 1.527e+04 (N)\n", - "Maximum absolute Force error: 8.664e-03 --> 6.319e-03 (normalized)\n", - "Minimum absolute Force error: 9.659e-18 --> 1.052e-17 (normalized)\n", - "Average absolute Force error: 2.885e-03 --> 1.228e-03 (normalized)\n", - "Maximum absolute Force error: 4.334e+05 --> 2.316e+05 (N)\n", - "Minimum absolute Force error: 1.482e-10 --> 1.411e-10 (N)\n", - "Average absolute Force error: 7.335e+04 --> 3.237e+04 (N)\n", - "Maximum absolute Force error: 3.485e-02 --> 1.863e-02 (normalized)\n", - "Minimum absolute Force error: 1.192e-17 --> 1.135e-17 (normalized)\n", - "Average absolute Force error: 5.899e-03 --> 2.604e-03 (normalized)\n", - "Maximum absolute Force error: 1.057e+06 --> 9.148e+05 (N)\n", - "Minimum absolute Force error: 1.000e-10 --> 7.018e-11 (N)\n", - "Average absolute Force error: 1.205e+05 --> 5.606e+04 (N)\n", - "Maximum absolute Force error: 8.500e-02 --> 7.357e-02 (normalized)\n", - "Minimum absolute Force error: 8.043e-18 --> 5.644e-18 (normalized)\n", - "Average absolute Force error: 9.693e-03 --> 4.509e-03 (normalized)\n", - "Maximum absolute Force error: 5.498e+07 --> 5.636e+06 (N)\n", - "Minimum absolute Force error: 5.034e-13 --> 1.381e-11 (N)\n", - "Average absolute Force error: 3.746e+05 --> 1.295e+05 (N)\n", - "Maximum absolute Force error: 4.422e+00 --> 4.533e-01 (normalized)\n", - "Minimum absolute Force error: 4.048e-20 --> 1.110e-18 (normalized)\n", - "Average absolute Force error: 3.013e-02 --> 1.042e-02 (normalized)\n", + "Total (sum of squares): 7.355e+00 --> 1.048e-02, \n", + "Maximum absolute Force error: 1.077e+05 --> 1.607e+04 (N)\n", + "Minimum absolute Force error: 1.201e-10 --> 1.374e-10 (N)\n", + "Average absolute Force error: 3.587e+04 --> 2.943e+03 (N)\n", + "Maximum absolute Force error: 8.664e-03 --> 1.293e-03 (normalized)\n", + "Minimum absolute Force error: 9.659e-18 --> 1.105e-17 (normalized)\n", + "Average absolute Force error: 2.885e-03 --> 2.367e-04 (normalized)\n", + "Maximum absolute Force error: 4.334e+05 --> 4.199e+04 (N)\n", + "Minimum absolute Force error: 1.482e-10 --> 1.459e-10 (N)\n", + "Average absolute Force error: 7.335e+04 --> 7.372e+03 (N)\n", + "Maximum absolute Force error: 3.485e-02 --> 3.377e-03 (normalized)\n", + "Minimum absolute Force error: 1.192e-17 --> 1.174e-17 (normalized)\n", + "Average absolute Force error: 5.899e-03 --> 5.929e-04 (normalized)\n", + "Maximum absolute Force error: 1.057e+06 --> 6.054e+04 (N)\n", + "Minimum absolute Force error: 1.000e-10 --> 6.288e-11 (N)\n", + "Average absolute Force error: 1.205e+05 --> 1.017e+04 (N)\n", + "Maximum absolute Force error: 8.500e-02 --> 4.869e-03 (normalized)\n", + "Minimum absolute Force error: 8.043e-18 --> 5.057e-18 (normalized)\n", + "Average absolute Force error: 9.693e-03 --> 8.179e-04 (normalized)\n", + "Maximum absolute Force error: 5.498e+07 --> 4.127e+05 (N)\n", + "Minimum absolute Force error: 5.034e-13 --> 6.121e-12 (N)\n", + "Average absolute Force error: 3.746e+05 --> 1.181e+04 (N)\n", + "Maximum absolute Force error: 4.422e+00 --> 3.319e-02 (normalized)\n", + "Minimum absolute Force error: 4.048e-20 --> 4.923e-19 (normalized)\n", + "Average absolute Force error: 3.013e-02 --> 9.495e-04 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", @@ -296,7 +306,8 @@ "==============================================================================================================\n", "Rank 1 STOPPING\n", "Rank 2 STOPPING\n", - "Rank 3 STOPPING\n" + "Rank 3 STOPPING\n", + "\u001b[0m\u001b[0m\u001b[0m\u001b[0m" ] } ], @@ -427,7 +438,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -442,66 +453,66 @@ "Precomputing transforms\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.12 sec\n", - "Timer: Precomputing transforms = 1.12 sec\n", + "\u001b[32mTimer: Precomputing transforms = 1.66 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 1.69 sec\u001b[0m\n", "Building objective: QS two-term\n", "Precomputing transforms\n", "Building objective: QS two-term\n", "Precomputing transforms\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", - "Timer: Precomputing transforms = 949 ms\n", - "Timer: Precomputing transforms = 949 ms\n", - "Putting objective QS two-term on device 1\n", + "\u001b[32mTimer: Precomputing transforms = 1.50 sec\u001b[0m\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", + "\u001b[32mTimer: Precomputing transforms = 1.51 sec\u001b[0m\n", + "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 971 ms\n", - "Timer: Precomputing transforms = 970 ms\n", + "\u001b[32mTimer: Precomputing transforms = 1.39 sec\u001b[0m\n", "Putting objective aspect ratio on device 2\n", + "\u001b[32mTimer: Objective build = 5.64 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 1.39 sec\u001b[0m\n", "Putting objective aspect ratio on device 2\n", - "Timer: Objective build = 3.78 sec\n", - "Timer: Objective build = 3.78 sec\n", + "\u001b[32mTimer: Objective build = 5.68 sec\u001b[0m\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.19 sec\n", - "Timer: Objective build = 1.25 sec\n", - "Timer: Precomputing transforms = 1.85 sec\n", + "\u001b[32mTimer: Precomputing transforms = 2.47 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 1.76 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 1.85 sec\u001b[0m\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Objective build = 23.2 ms\n", - "Timer: Precomputing transforms = 1.88 sec\n", + "\u001b[32mTimer: Objective build = 35.8 ms\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 2.57 sec\u001b[0m\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.77 sec\n", + "\u001b[32mTimer: Precomputing transforms = 2.57 sec\u001b[0m\n", "Putting objective aspect ratio on device 2\n", - "Timer: Objective build = 6.66 sec\n", - "Timer: Eq Update LinearConstraintProjection build = 4.53 sec\n", - "Timer: Proximal projection build = 7.78 sec\n", + "\u001b[32mTimer: Objective build = 9.15 sec\u001b[0m\n", + "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 7.13 sec\u001b[0m\n", + "\u001b[32mTimer: Proximal projection build = 11.7 sec\u001b[0m\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 602 ms\n", - "Timer: LinearConstraintProjection build = 1.60 sec\n", + "\u001b[32mTimer: Objective build = 757 ms\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 2.43 sec\u001b[0m\n", "Number of parameters: 8\n", "Number of objectives: 911\n", - "Timer: Initializing the optimization = 10.0 sec\n", + "\u001b[32mTimer: Initializing the optimization = 15.0 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", "Rank 1 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "This should run on GPU id:0\n", - "This should run on GPU id:1\n", - "This should run on GPU id:2\n", + "Rank 1 : proximal_jvp_scaled_error\n", + "Rank 2 : proximal_jvp_scaled_error\n", + "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 2.011e+04 1.952e+02 \n", "Rank 1 : compute_scaled_error\n", @@ -513,18 +524,32 @@ "Rank 1 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "This should run on GPU id:0\n", - "This should run on GPU id:1\n", - "This should run on GPU id:2\n", + "Rank 1 : proximal_jvp_scaled_error\n", + "Rank 2 : proximal_jvp_scaled_error\n", + "Rank 0 waiting to gather\n", " 1 4 8.735e+03 1.138e+04 4.838e-02 1.104e+02 \n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 : proximal_jvp_scaled_error\n", + "Rank 2 : proximal_jvp_scaled_error\n", + "Rank 0 waiting to gather\n", + " 2 5 1.528e+03 7.207e+03 5.365e-02 3.401e+01 \n", + "Rank 1 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", + "Rank 0 waiting to gather\n", + "Rank 1 : proximal_jvp_scaled_error\n", + "Rank 2 : proximal_jvp_scaled_error\n", + "Rank 0 waiting to gather\n", + " 3 6 5.325e+02 9.957e+02 8.313e-02 1.565e+01 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 8.735e+03\n", - " Total delta_x: 4.838e-02\n", - " Iterations: 1\n", - " Function evaluations: 4\n", - " Jacobian evaluations: 2\n", - "Timer: Solution time = 38.2 sec\n", - "Timer: Avg time per step = 19.1 sec\n", + " Current function value: 5.325e+02\n", + " Total delta_x: 8.950e-02\n", + " Iterations: 3\n", + " Function evaluations: 6\n", + " Jacobian evaluations: 4\n", + "\u001b[32mTimer: Solution time = 52.8 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 13.2 sec\u001b[0m\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", @@ -533,26 +558,26 @@ "Rank 1 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", - "Total (sum of squares): 2.011e+04 --> 8.735e+03, \n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.813e-01 --> 6.254e-01 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.150e-04 --> 4.713e-03 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 5.169e-02 --> 2.630e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.978e-01 --> 6.824e-01 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.346e-04 --> 5.143e-03 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 5.640e-02 --> 2.869e-01 (normalized)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.161e+00 --> 9.141e-01 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.945e-03 --> 1.241e-03 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.051e-01 --> 2.811e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.267e+00 --> 9.974e-01 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.122e-03 --> 1.354e-03 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.147e-01 --> 3.067e-01 (normalized)\n", - "Aspect ratio: 5.996e+00 --> 6.709e+00 (dimensionless)\n", - "Maximum absolute Force error: 1.345e+05 --> 1.302e+04 (N)\n", - "Minimum absolute Force error: 8.350e+00 --> 2.077e+00 (N)\n", - "Average absolute Force error: 5.462e+03 --> 1.001e+03 (N)\n", - "Maximum absolute Force error: 9.614e-02 --> 9.309e-03 (normalized)\n", - "Minimum absolute Force error: 5.969e-06 --> 1.485e-06 (normalized)\n", - "Average absolute Force error: 3.904e-03 --> 7.158e-04 (normalized)\n", + "Total (sum of squares): 2.011e+04 --> 5.325e+02, \n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.813e-01 --> 7.713e-01 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.150e-04 --> 1.620e-03 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 5.169e-02 --> 2.275e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.978e-01 --> 8.416e-01 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.346e-04 --> 1.768e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 5.640e-02 --> 2.483e-01 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.161e+00 --> 1.203e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.945e-03 --> 7.572e-04 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.051e-01 --> 3.026e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.267e+00 --> 1.313e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.122e-03 --> 8.262e-04 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.147e-01 --> 3.302e-01 (normalized)\n", + "Aspect ratio: 5.996e+00 --> 7.859e+00 (dimensionless)\n", + "Maximum absolute Force error: 1.345e+05 --> 4.971e+04 (N)\n", + "Minimum absolute Force error: 8.350e+00 --> 6.376e-01 (N)\n", + "Average absolute Force error: 5.462e+03 --> 2.762e+03 (N)\n", + "Maximum absolute Force error: 9.614e-02 --> 3.554e-02 (normalized)\n", + "Minimum absolute Force error: 5.969e-06 --> 4.558e-07 (normalized)\n", + "Average absolute Force error: 3.904e-03 --> 1.974e-03 (normalized)\n", "R boundary error: 0.000e+00 --> 4.734e-19 (m)\n", "Z boundary error: 0.000e+00 --> 3.478e-18 (m)\n", "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", @@ -560,7 +585,8 @@ "Fixed current profile error: 0.000e+00 --> 0.000e+00 (A)\n", "==============================================================================================================\n", "Rank 1 STOPPING\n", - "Rank 2 STOPPING\n" + "Rank 2 STOPPING\n", + "\u001b[0m\u001b[0m\u001b[0m" ] } ], From a9366bf9f3ace3f665c6c0ff673b99c4a49fdeca Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 27 Feb 2025 03:06:46 -0500 Subject: [PATCH 082/199] fix for multiple node cases, each process will see only one CPU device, we should still be able to parallelize --- desc/objectives/objective_funs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index b37119d3a4..4a5a778084 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -279,6 +279,11 @@ def __init__( device_ids = [obj._device_id for obj in objectives] self._is_multi_device = len(set(device_ids)) > 1 if mpi is not None: + # for multiple node cases, each process sees 1 CPU, for those cases, + # we cannot put objectives to different devices. Instead, we will + # run each objective on a different rank. That is also why we will + # run 1 objective per process. + self._is_multi_device = True self.mpi = mpi self.comm = self.mpi.COMM_WORLD self.rank = self.comm.Get_rank() From 7be080fd0847f5a5ea8a5f19a44dcaa47c730bb8 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 27 Feb 2025 17:02:57 -0500 Subject: [PATCH 083/199] document more --- desc/objectives/objective_funs.py | 15 +- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 38 ++- .../tutorials/mpi-tutorials/mpi-proximal.py | 46 ++- docs/notebooks/tutorials/multi_device.ipynb | 291 ++++++++++++++---- 4 files changed, 312 insertions(+), 78 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 4a5a778084..7e3be878d2 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -283,17 +283,22 @@ def __init__( # we cannot put objectives to different devices. Instead, we will # run each objective on a different rank. That is also why we will # run 1 objective per process. + # TODO: add an argument for node of the objective. FOr example, let's say + # we have 3 nodes and 4 GPUs per node. First of all there should be 12 + # objectives in total. We should create 4 processes per node and each + # process should run 1 objective. This way we can utilize all the GPUs. + # Alternatively, we can specify the rank for the objective. This way, we + # can have multiple objectives on the same rank. self._is_multi_device = True self.mpi = mpi self.comm = self.mpi.COMM_WORLD self.rank = self.comm.Get_rank() self.size = self.comm.Get_size() self.running = True - if not all(device_ids == np.arange(self.size)): - raise ValueError( - "When using multiple devices, device_id of the objectives " - "must be consecutive and must be the same as ranks. Got " - f"device_ids: {device_ids}, ranks: {np.arange(self.size)}" + if self.size != len(self._objectives): + raise NotImplementedError( + "Number of objectives should be equal to the number of ranks. " + "Running multiple objectives per rank is not supported yet." ) if self._is_multi_device and mpi is None: diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index fc0bab0430..3df6662228 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -5,16 +5,31 @@ sys.path.insert(0, os.path.abspath(".")) sys.path.append(os.path.abspath("../../../")) -# These will be used for diving the single CPU into multiple virtual CPUs -# such that JAX and XLA thinks there are multiple devices from desc import _set_cpu_count, set_device +# ====== Using CPUs ====== num_device = 4 +# These will be used for diving the single CPU into multiple virtual CPUs +# such that JAX and XLA thinks there are multiple devices +# If you have multiple CPUs, you don't need to call `_set_cpu_count` _set_cpu_count(num_device) set_device("cpu", num_device=num_device) +# ====== Using GPUs ====== +# When we have multiple processing using the same devices (for example, 3 processes +# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will +# cause the memory allocation to fail. To avoid this, we can set the memory fraction +# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of +# the GPU memory. This is a bit conservative, but if a process needs more memory, it can +# allocate more memory on the fly. +# +# os.environ["XLA_PYTHON_CLIENT_MEM_FRACTION"] = str(1 / (num_device + 2)) +# set_device("gpu", num_device=num_device) + from mpi4py import MPI +from desc import config as desc_config +from desc.backend import jax, print_backend_info from desc.examples import get from desc.objectives.getters import ( get_fixed_boundary_constraints, @@ -23,6 +38,25 @@ if __name__ == "__main__": rank = MPI.COMM_WORLD.Get_rank() + size = MPI.COMM_WORLD.Get_size() + if rank == 0: + print(f"====== TOTAL OF {size} RANKS ======") + + # see which rank is running on which device + # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()` + # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()` + # will return only the devices that are available to the current process. This is + # useful when you have multiple processes running on multiple nodes and you want + # to see which devices are available to each process. + if desc_config["kind"] == "gpu": + print( + f"Rank {rank} is running on {jax.local_devices(backend="gpu")} " + f"and {jax.local_devices(backend="cpu")}" + ) + else: + print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}") + print_backend_info() + eq = get("HELIOTRON") eq.change_resolution(6, 6, 6, 12, 12, 12) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index d94bc4d0b7..20f99a8d7b 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -5,18 +5,33 @@ sys.path.insert(0, os.path.abspath(".")) sys.path.append(os.path.abspath("../../../")) -# These will be used for diving the single CPU into multiple virtual CPUs -# such that JAX and XLA thinks there are multiple devices from desc import _set_cpu_count, set_device +# ====== Using CPUs ====== num_device = 3 +# These will be used for diving the single CPU into multiple virtual CPUs +# such that JAX and XLA thinks there are multiple devices +# If you have multiple CPUs, you don't need to call `_set_cpu_count` _set_cpu_count(num_device) set_device("cpu", num_device=num_device) +# ====== Using GPUs ====== +# When we have multiple processing using the same devices (for example, 3 processes +# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will +# cause the memory allocation to fail. To avoid this, we can set the memory fraction +# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of +# the GPU memory. This is a bit conservative, but if a process needs more memory, it can +# allocate more memory on the fly. +# +# os.environ["XLA_PYTHON_CLIENT_MEM_FRACTION"] = str(1 / (num_device + 2)) +# set_device("gpu", num_device=num_device) + + import numpy as np from mpi4py import MPI -from desc.backend import jax, jnp +from desc import config as desc_config +from desc.backend import jax, jnp, print_backend_info from desc.examples import get from desc.grid import LinearGrid from desc.objectives import ( @@ -34,6 +49,24 @@ if __name__ == "__main__": rank = MPI.COMM_WORLD.Get_rank() + size = MPI.COMM_WORLD.Get_size() + if rank == 0: + print(f"====== TOTAL OF {size} RANKS ======") + + # see which rank is running on which device + # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()` + # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()` + # will return only the devices that are available to the current process. This is + # useful when you have multiple processes running on multiple nodes and you want + # to see which devices are available to each process. + if desc_config["kind"] == "gpu": + print( + f"Rank {rank} is running on {jax.local_devices(backend="gpu")} " + f"and {jax.local_devices(backend="cpu")}" + ) + else: + print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}") + print_backend_info() eq = get("precise_QA") eq.change_resolution(3, 3, 3, 6, 6, 6) @@ -54,8 +87,13 @@ objs = [obj1, obj2, obj3] # Parallel objective function needs the MPI communicator + # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will + # automatically switch to `blocked`. objective = ObjectiveFunction(objs, deriv_mode="blocked", mpi=MPI) - objective.build(verbose=3) + if rank == 0: + objective.build(verbose=3) + else: + objective.build(verbose=0) # we will fix some modes as usual k = 1 diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index a7e06fc76d..3cd2e78cb5 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -56,13 +56,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.13.0+1687.gf75ae6abf.dirty.\n", + "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 6.76 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 6.76 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 6.76 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 6.76 GB available memory\n" + "\t CPU 0: TFRT_CPU_0 with 8.41 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.41 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.41 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 8.41 GB available memory\n" ] } ], @@ -85,16 +85,31 @@ "sys.path.insert(0, os.path.abspath(\".\"))\n", "sys.path.append(os.path.abspath(\"../../../\"))\n", "\n", - "# These will be used for diving the single CPU into multiple virtual CPUs\n", - "# such that JAX and XLA thinks there are multiple devices\n", "from desc import _set_cpu_count, set_device\n", "\n", + "# ====== Using CPUs ======\n", "num_device = 4\n", + "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# such that JAX and XLA thinks there are multiple devices\n", + "# If you have multiple CPUs, you don't need to call `_set_cpu_count`\n", "_set_cpu_count(num_device)\n", "set_device(\"cpu\", num_device=num_device)\n", "\n", + "# ====== Using GPUs ======\n", + "# When we have multiple processing using the same devices (for example, 3 processes\n", + "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", + "# cause the memory allocation to fail. To avoid this, we can set the memory fraction\n", + "# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of\n", + "# the GPU memory. This is a bit conservative, but if a process needs more memory, it can\n", + "# allocate more memory on the fly.\n", + "#\n", + "# os.environ[\"XLA_PYTHON_CLIENT_MEM_FRACTION\"] = str(1 / (num_device + 2))\n", + "# set_device(\"gpu\", num_device=num_device)\n", + "\n", "from mpi4py import MPI\n", "\n", + "from desc import config as desc_config\n", + "from desc.backend import print_backend_info, jax\n", "from desc.examples import get\n", "from desc.objectives.getters import (\n", " get_fixed_boundary_constraints,\n", @@ -103,6 +118,25 @@ "\n", "if __name__ == \"__main__\":\n", " rank = MPI.COMM_WORLD.Get_rank()\n", + " size = MPI.COMM_WORLD.Get_size()\n", + " if rank == 0:\n", + " print(f\"====== TOTAL OF {size} RANKS ======\")\n", + "\n", + " # see which rank is running on which device\n", + " # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()`\n", + " # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()`\n", + " # will return only the devices that are available to the current process. This is\n", + " # useful when you have multiple processes running on multiple nodes and you want\n", + " # to see which devices are available to each process.\n", + " if desc_config[\"kind\"] == \"gpu\":\n", + " print(\n", + " f\"Rank {rank} is running on {jax.local_devices(backend=\"gpu\")} \"\n", + " f\"and {jax.local_devices(backend=\"cpu\")}\"\n", + " )\n", + " else:\n", + " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\")\n", + " print_backend_info()\n", + "\n", " eq = get(\"HELIOTRON\")\n", " eq.change_resolution(6, 6, 6, 12, 12, 12)\n", "\n", @@ -125,7 +159,7 @@ " eq.solve(\n", " objective=obj,\n", " constraints=cons,\n", - " maxiter=1,\n", + " maxiter=3,\n", " ftol=0,\n", " gtol=0,\n", " xtol=0,\n", @@ -134,18 +168,44 @@ "\n", " # if you put a code here, it will be performed on all ranks\n", "\n", + "\n", "```" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 4, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/yigit/miniconda3/envs/mpi/lib/python3.12/pty.py:95: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n", + " pid, fd = os.forkpty()\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ + "====== TOTAL OF 4 RANKS ======\n", + "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", + "Using 4 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 8.33 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.33 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.33 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 8.33 GB available memory\n", + "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", + "Using 4 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 8.33 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.33 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.33 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 8.33 GB available memory\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", @@ -154,14 +214,30 @@ "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", + "Rank 2 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", + "Using 4 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 8.31 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.31 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.31 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 8.31 GB available memory\n", + "Rank 3 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", + "Using 4 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 8.31 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.31 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.31 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 8.31 GB available memory\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", "Putting objective force on device 1\n", + "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", "Putting objective force on device 2\n", @@ -172,11 +248,11 @@ "Precomputing transforms\n", "Putting objective force on device 3\n", "Putting objective force on device 3\n", - "Building objective: lcfs R\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", + "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed Psi\n", "Building objective: fixed pressure\n", @@ -191,7 +267,7 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "\u001b[32mTimer: Objective build = 1.30 sec\u001b[0m\n", + "Timer: Objective build = 991 ms\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", @@ -210,10 +286,10 @@ "Precomputing transforms\n", "Putting objective force on device 3\n", "Putting objective force on device 3\n", - "\u001b[32mTimer: LinearConstraintProjection build = 8.91 sec\u001b[0m\n", + "Timer: LinearConstraintProjection build = 7.36 sec\n", "Number of parameters: 609\n", "Number of objectives: 15000\n", - "\u001b[32mTimer: Initializing the optimization = 10.3 sec\u001b[0m\n", + "Timer: Initializing the optimization = 8.43 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", @@ -222,14 +298,14 @@ "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 2 : jvp_scaled_error\n", "Rank 3 : jvp_scaled_error\n", + "Rank 2 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 5.928e+00 2.480e+00 \n", "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", "Rank 3 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", "Rank 3 : jvp_scaled_error\n", @@ -246,12 +322,12 @@ "Rank 0 waiting to gather\n", " 2 3 4.419e-02 9.434e-01 2.363e-01 8.993e-02 \n", "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", "Rank 3 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 2 : jvp_scaled_error\n", "Rank 3 : jvp_scaled_error\n", + "Rank 2 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " 3 4 1.048e-02 3.370e-02 1.497e-01 3.808e-02 \n", "Warning: Maximum number of iterations has been exceeded.\n", @@ -260,13 +336,13 @@ " Iterations: 3\n", " Function evaluations: 4\n", " Jacobian evaluations: 4\n", - "\u001b[32mTimer: Solution time = 41.4 sec\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 10.3 sec\u001b[0m\n", + "Timer: Solution time = 36.4 sec\n", + "Timer: Avg time per step = 9.10 sec\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", "Rank 3 : compute_scaled_error\n", + "Rank 2 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error\n", "Rank 3 : compute_scaled_error\n", @@ -306,8 +382,7 @@ "==============================================================================================================\n", "Rank 1 STOPPING\n", "Rank 2 STOPPING\n", - "Rank 3 STOPPING\n", - "\u001b[0m\u001b[0m\u001b[0m\u001b[0m" + "Rank 3 STOPPING\n" ] } ], @@ -320,7 +395,7 @@ "metadata": {}, "source": [ "## Using other Objectives\n", - "Above we used the convenience function for force balance objective, but we can also other objectives with this approach. There are some extra steps you need to apply though." + "Above we used the convenience function for force balance objective, but we can also other objectives with this approach." ] }, { @@ -335,18 +410,35 @@ "sys.path.insert(0, os.path.abspath(\".\"))\n", "sys.path.append(os.path.abspath(\"../../../\"))\n", "\n", - "# These will be used for diving the single CPU into multiple virtual CPUs\n", - "# such that JAX and XLA thinks there are multiple devices\n", "from desc import _set_cpu_count, set_device\n", "\n", + "# ====== Using CPUs ======\n", "num_device = 3\n", + "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# such that JAX and XLA thinks there are multiple devices\n", + "# If you have multiple CPUs, you don't need to call `_set_cpu_count`\n", "_set_cpu_count(num_device)\n", "set_device(\"cpu\", num_device=num_device)\n", "\n", - "import numpy as np\n", + "# ====== Using GPUs ======\n", + "# When we have multiple processing using the same devices (for example, 3 processes\n", + "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", + "# cause the memory allocation to fail. To avoid this, we can set the memory fraction\n", + "# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of\n", + "# the GPU memory. This is a bit conservative, but if a process needs more memory, it can\n", + "# allocate more memory on the fly.\n", + "#\n", + "# os.environ[\"XLA_PYTHON_CLIENT_MEM_FRACTION\"] = str(1 / (num_device + 2))\n", + "# set_device(\"gpu\", num_device=num_device)\n", + "\n", + "\n", "from mpi4py import MPI\n", "\n", - "from desc.backend import jax, jnp\n", + "from desc import config as desc_config\n", + "from desc.backend import jax, jnp, print_backend_info\n", + "\n", + "\n", + "import numpy as np\n", "from desc.examples import get\n", "from desc.grid import LinearGrid\n", "from desc.objectives import (\n", @@ -360,10 +452,29 @@ " ObjectiveFunction,\n", " QuasisymmetryTwoTerm,\n", ")\n", + "\n", "from desc.optimize import Optimizer\n", "\n", "if __name__ == \"__main__\":\n", " rank = MPI.COMM_WORLD.Get_rank()\n", + " size = MPI.COMM_WORLD.Get_size()\n", + " if rank == 0:\n", + " print(f\"====== TOTAL OF {size} RANKS ======\")\n", + "\n", + " # see which rank is running on which device\n", + " # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()`\n", + " # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()`\n", + " # will return only the devices that are available to the current process. This is\n", + " # useful when you have multiple processes running on multiple nodes and you want\n", + " # to see which devices are available to each process.\n", + " if desc_config[\"kind\"] == \"gpu\":\n", + " print(\n", + " f\"Rank {rank} is running on {jax.local_devices(backend=\"gpu\")} \"\n", + " f\"and {jax.local_devices(backend=\"cpu\")}\"\n", + " )\n", + " else:\n", + " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\")\n", + " print_backend_info()\n", "\n", " eq = get(\"precise_QA\")\n", " eq.change_resolution(3, 3, 3, 6, 6, 6)\n", @@ -384,8 +495,13 @@ " objs = [obj1, obj2, obj3]\n", "\n", " # Parallel objective function needs the MPI communicator\n", + " # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will\n", + " # automatically switch to `blocked`.\n", " objective = ObjectiveFunction(objs, deriv_mode=\"blocked\", mpi=MPI)\n", - " objective.build(verbose=3)\n", + " if rank == 0:\n", + " objective.build(verbose=3)\n", + " else:\n", + " objective.build(verbose=0)\n", "\n", " # we will fix some modes as usual\n", " k = 1\n", @@ -424,7 +540,7 @@ " objective=objective,\n", " constraints=constraints,\n", " optimizer=optimizer,\n", - " maxiter=1,\n", + " maxiter=3,\n", " verbose=3,\n", " options={\n", " \"initial_trust_ratio\": 1.0,\n", @@ -433,77 +549,80 @@ "\n", " # if you put a code here, it will be performed on all ranks\n", "\n", + "\n", "```" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", + "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", + "Using 3 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 8.25 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.25 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.25 GB available memory\n", + "====== TOTAL OF 3 RANKS ======\n", + "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", + "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", + "Using 3 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 8.25 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.25 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.25 GB available memory\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Building objective: QS two-term\n", - "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.66 sec\u001b[0m\n", - "\u001b[32mTimer: Precomputing transforms = 1.69 sec\u001b[0m\n", - "Building objective: QS two-term\n", - "Precomputing transforms\n", + "Rank 2 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", + "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", + "Using 3 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 8.25 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.25 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.25 GB available memory\n", + "Timer: Precomputing transforms = 1.33 sec\n", "Building objective: QS two-term\n", "Precomputing transforms\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", - "\u001b[32mTimer: Precomputing transforms = 1.50 sec\u001b[0m\n", + "Timer: Precomputing transforms = 1.16 sec\n", "Putting objective QS two-term on device 1\n", - "Building objective: aspect ratio\n", - "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.51 sec\u001b[0m\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Building objective: QS two-term\n", - "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.39 sec\u001b[0m\n", + "Timer: Precomputing transforms = 1.16 sec\n", "Putting objective aspect ratio on device 2\n", - "\u001b[32mTimer: Objective build = 5.64 sec\u001b[0m\n", - "\u001b[32mTimer: Precomputing transforms = 1.39 sec\u001b[0m\n", "Putting objective aspect ratio on device 2\n", - "\u001b[32mTimer: Objective build = 5.68 sec\u001b[0m\n", + "Timer: Objective build = 4.59 sec\n", "Building objective: force\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 2.47 sec\u001b[0m\n", - "\u001b[32mTimer: Precomputing transforms = 1.76 sec\u001b[0m\n", - "\u001b[32mTimer: Objective build = 1.85 sec\u001b[0m\n", - "Building objective: QS two-term\n", - "Precomputing transforms\n", - "\u001b[32mTimer: Objective build = 35.8 ms\u001b[0m\n", - "\u001b[32mTimer: Precomputing transforms = 2.57 sec\u001b[0m\n", + "Timer: Precomputing transforms = 1.46 sec\n", + "Timer: Objective build = 1.52 sec\n", + "Timer: Objective build = 23.6 ms\n", "Putting objective QS two-term on device 1\n", - "Building objective: aspect ratio\n", - "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 2.57 sec\u001b[0m\n", "Putting objective aspect ratio on device 2\n", - "\u001b[32mTimer: Objective build = 9.15 sec\u001b[0m\n", - "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 7.13 sec\u001b[0m\n", - "\u001b[32mTimer: Proximal projection build = 11.7 sec\u001b[0m\n", + "Timer: Eq Update LinearConstraintProjection build = 5.07 sec\n", + "Timer: Proximal projection build = 8.80 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "\u001b[32mTimer: Objective build = 757 ms\u001b[0m\n", - "\u001b[32mTimer: LinearConstraintProjection build = 2.43 sec\u001b[0m\n", + "Timer: Objective build = 700 ms\n", + "Timer: LinearConstraintProjection build = 1.85 sec\n", "Number of parameters: 8\n", "Number of objectives: 911\n", - "\u001b[32mTimer: Initializing the optimization = 15.0 sec\u001b[0m\n", + "Timer: Initializing the optimization = 11.4 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", @@ -548,8 +667,8 @@ " Iterations: 3\n", " Function evaluations: 6\n", " Jacobian evaluations: 4\n", - "\u001b[32mTimer: Solution time = 52.8 sec\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 13.2 sec\u001b[0m\n", + "Timer: Solution time = 47.0 sec\n", + "Timer: Avg time per step = 11.7 sec\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", @@ -585,14 +704,52 @@ "Fixed current profile error: 0.000e+00 --> 0.000e+00 (A)\n", "==============================================================================================================\n", "Rank 1 STOPPING\n", - "Rank 2 STOPPING\n", - "\u001b[0m\u001b[0m\u001b[0m" + "Rank 2 STOPPING\n" ] } ], "source": [ "!mpirun -n 3 python mpi-tutorials/mpi-proximal.py" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Slurm for Multi-Node and Multi-Process Scripts\n", + "\n", + "**Note :** These instructions may differ for the cluster you are trying to use. The reason we give this example is to set some terminology for users that are not familiar with multi-node and multi-processing.\n", + "\n", + "**Note :** For more details, one can check Princeton University Research Computing page [here](https://researchcomputing.princeton.edu/support/knowledge-base/slurm#Multinode--Multithreaded-Jobs).\n", + "\n", + "One needs to use proper slurm script to run parallel code on a cluster. Here, we will give an example in which we use 2 nodes, 8 processes per node and 4 CPU cores per process. *Node* means the actual CPU chip, so we will have 2 CPUs or you can think of it as, we will have 2 computers that are connected to each other. We will have 16 processes and 64 CPU cores in total. Additionally, you can specify number of GPUs per node." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```bash\n", + "\n", + "#!/bin/bash\n", + "#SBATCH --job-name=mpi-example # create a short name for your job\n", + "#SBATCH --nodes=2 # node count\n", + "#SBATCH --ntasks-per-node=8 # total number of tasks per node\n", + "#SBATCH --cpus-per-task=4 # cpu-cores per task (>1 if multi-threaded tasks)\n", + "#SBATCH --mem-per-cpu=4G # memory per cpu-core (4G is default)\n", + "#SBATCH --time=00:10:00 # total run time limit (HH:MM:SS)\n", + "#SBATCH --gres=gpu:4 # number of GPUs per node (in this case 8 GPUs in total)\n", + "\n", + "export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK\n", + "export SRUN_CPUS_PER_TASK=$SLURM_CPUS_PER_TASK\n", + "module purge\n", + "module load intel/2022.2.0\n", + "module load intel-mpi/intel/2021.7.0\n", + "\n", + "srun python your-script.py\n", + "\n", + "```" + ] } ], "metadata": { From b37073f98f85e5bc9ebf2f0608239c75f374f04b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 27 Feb 2025 18:31:55 -0500 Subject: [PATCH 084/199] document more, fix backend print --- desc/backend.py | 4 + desc/objectives/objective_funs.py | 9 +- docs/notebooks/tutorials/multi_device.ipynb | 196 +++++++++++--------- 3 files changed, 119 insertions(+), 90 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index fea39d35c0..1819193b03 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -71,6 +71,10 @@ def print_backend_info(): ) if desc_config["kind"] == "gpu": + print( + f"CPU Info: {desc_config['cpu_info']} with {desc_config['cpu_mem']:.2f} " + "GB available memory" + ) print(f"Using {desc_config['num_device']} device:") for i, dev in enumerate(desc_config["devices"]): print( diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 7e3be878d2..41aaf40437 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -482,10 +482,11 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 objective.build(use_jit=self.use_jit, verbose=verbose) self._dim_f += objective.dim_f if objective._device_id != 0: - print( - f"Putting objective {objective.name} on device " - f"{objective._device_id}" - ) + if verbose > 0 and self.rank == 0: + print( + f"Putting objective {objective.name} on device " + f"{objective._device_id}" + ) objective = jax.device_put(objective, objective._device) objective._things = obj_things if self._dim_f == 1: diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 3cd2e78cb5..aa3f704060 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -56,13 +56,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.41 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.41 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.41 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 8.41 GB available memory\n" + "\t CPU 0: TFRT_CPU_0 with 7.93 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.93 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.93 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 7.93 GB available memory\n" ] } ], @@ -189,23 +189,23 @@ "name": "stdout", "output_type": "stream", "text": [ - "====== TOTAL OF 4 RANKS ======\n", - "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.33 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.33 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.33 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 8.33 GB available memory\n", - "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "\t CPU 0: TFRT_CPU_0 with 7.86 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.86 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.86 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 7.86 GB available memory\n", + "====== TOTAL OF 4 RANKS ======\n", + "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.33 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.33 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.33 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 8.33 GB available memory\n", + "\t CPU 0: TFRT_CPU_0 with 7.86 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.86 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.86 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 7.86 GB available memory\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", @@ -215,44 +215,41 @@ "Building objective: force\n", "Precomputing transforms\n", "Rank 2 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.31 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.31 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.31 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 8.31 GB available memory\n", + "\t CPU 0: TFRT_CPU_0 with 7.84 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.84 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.84 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 7.84 GB available memory\n", "Rank 3 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.31 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.31 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.31 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 8.31 GB available memory\n", + "\t CPU 0: TFRT_CPU_0 with 7.84 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.84 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.84 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 7.84 GB available memory\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 1\n", - "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 2\n", "Building objective: force\n", "Precomputing transforms\n", "Putting objective force on device 2\n", "Building objective: force\n", "Precomputing transforms\n", "Putting objective force on device 3\n", - "Putting objective force on device 3\n", + "Building objective: lcfs R\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", - "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed Psi\n", "Building objective: fixed pressure\n", @@ -260,36 +257,30 @@ "Building objective: fixed sheet current\n", "Building objective: self_consistency R\n", "Building objective: self_consistency Z\n", + "Building objective: lambda gauge\n", + "Building objective: axis R self consistency\n", + "Building objective: axis Z self consistency\n", "Building objective: force\n", "Precomputing transforms\n", + "Timer: Objective build = 987 ms\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: lambda gauge\n", - "Building objective: axis R self consistency\n", - "Building objective: axis Z self consistency\n", - "Timer: Objective build = 991 ms\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 1\n", - "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 2\n", - "Putting objective force on device 2\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 3\n", - "Putting objective force on device 3\n", - "Timer: LinearConstraintProjection build = 7.36 sec\n", + "Timer: LinearConstraintProjection build = 4.35 sec\n", "Number of parameters: 609\n", "Number of objectives: 15000\n", - "Timer: Initializing the optimization = 8.43 sec\n", + "Timer: Initializing the optimization = 5.39 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", @@ -298,36 +289,36 @@ "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", "Rank 2 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 5.928e+00 2.480e+00 \n", "Rank 1 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", "Rank 2 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " 1 2 9.876e-01 4.941e+00 3.322e-01 6.448e-01 \n", "Rank 1 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", "Rank 2 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " 2 3 4.419e-02 9.434e-01 2.363e-01 8.993e-02 \n", "Rank 1 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", "Rank 2 : jvp_scaled_error\n", + "Rank 3 : jvp_scaled_error\n", "Rank 0 waiting to gather\n", " 3 4 1.048e-02 3.370e-02 1.497e-01 3.808e-02 \n", "Warning: Maximum number of iterations has been exceeded.\n", @@ -336,17 +327,17 @@ " Iterations: 3\n", " Function evaluations: 4\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 36.4 sec\n", - "Timer: Avg time per step = 9.10 sec\n", + "Timer: Solution time = 26.4 sec\n", + "Timer: Avg time per step = 6.62 sec\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", "Rank 2 : compute_scaled_error\n", + "Rank 3 : compute_scaled_error\n", "Rank 0 waiting to gather\n", "Total (sum of squares): 7.355e+00 --> 1.048e-02, \n", "Maximum absolute Force error: 1.077e+05 --> 1.607e+04 (N)\n", @@ -563,20 +554,20 @@ "output_type": "stream", "text": [ "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", - "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 3 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.25 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.25 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.25 GB available memory\n", + "\t CPU 0: TFRT_CPU_0 with 7.86 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.86 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.86 GB available memory\n", "====== TOTAL OF 3 RANKS ======\n", "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", - "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 3 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.25 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.25 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.25 GB available memory\n", + "\t CPU 0: TFRT_CPU_0 with 7.86 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.86 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.86 GB available memory\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", @@ -584,45 +575,41 @@ "Building objective: QS two-term\n", "Precomputing transforms\n", "Rank 2 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", - "DESC version=0.13.0+1689.ga9366bf9f.dirty.\n", + "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 3 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.25 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.25 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.25 GB available memory\n", - "Timer: Precomputing transforms = 1.33 sec\n", + "\t CPU 0: TFRT_CPU_0 with 7.85 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 7.85 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 7.85 GB available memory\n", + "Timer: Precomputing transforms = 1.41 sec\n", "Building objective: QS two-term\n", "Precomputing transforms\n", "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", " warnings.warn(colored(msg, \"yellow\"), err)\n", - "Timer: Precomputing transforms = 1.16 sec\n", - "Putting objective QS two-term on device 1\n", + "Timer: Precomputing transforms = 1.11 sec\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.16 sec\n", + "Timer: Precomputing transforms = 1.12 sec\n", "Putting objective aspect ratio on device 2\n", - "Putting objective aspect ratio on device 2\n", - "Timer: Objective build = 4.59 sec\n", + "Timer: Objective build = 4.53 sec\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.46 sec\n", - "Timer: Objective build = 1.52 sec\n", - "Timer: Objective build = 23.6 ms\n", - "Putting objective QS two-term on device 1\n", - "Putting objective aspect ratio on device 2\n", - "Timer: Eq Update LinearConstraintProjection build = 5.07 sec\n", - "Timer: Proximal projection build = 8.80 sec\n", + "Timer: Precomputing transforms = 1.39 sec\n", + "Timer: Objective build = 1.46 sec\n", + "Timer: Objective build = 24.4 ms\n", + "Timer: Eq Update LinearConstraintProjection build = 5.13 sec\n", + "Timer: Proximal projection build = 8.77 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 700 ms\n", - "Timer: LinearConstraintProjection build = 1.85 sec\n", + "Timer: Objective build = 874 ms\n", + "Timer: LinearConstraintProjection build = 2.11 sec\n", "Number of parameters: 8\n", "Number of objectives: 911\n", - "Timer: Initializing the optimization = 11.4 sec\n", + "Timer: Initializing the optimization = 11.8 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", @@ -667,8 +654,8 @@ " Iterations: 3\n", " Function evaluations: 6\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 47.0 sec\n", - "Timer: Avg time per step = 11.7 sec\n", + "Timer: Solution time = 48.9 sec\n", + "Timer: Avg time per step = 12.2 sec\n", "==============================================================================================================\n", " Start --> End\n", "Rank 1 : compute_scaled_error\n", @@ -750,6 +737,43 @@ "\n", "```" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When using MPI with multiple nodes, each process will see 1 CPU, and if you requested GPUs, only the GPUs connected to that CPU will be visible to your program. With this in mind, for example, if you want to use 2 nodes, and 3 GPUs per nodes with 3 processes per node, you can use 6 objectives in this way.\n", + "\n", + "```python\n", + "\n", + "# each node will see 3 GPUs\n", + "num_device = 3\n", + "os.environ[\"XLA_PYTHON_CLIENT_MEM_FRACTION\"] = str(1 / (num_device + 2))\n", + "set_device(\"gpu\", num_device=num_device)\n", + "\n", + "\n", + "...\n", + "\n", + "\n", + "# this will run on node 1, GPU 0 (rank=0)\n", + "obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", + "# this will run on node 1, GPU 1 (rank=1)\n", + "obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", + "# this will run on node 1, GPU 2 (rank=2)\n", + "obj3 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid3, device_id=2)\n", + "# this will run on node 2, GPU 0 (rank=3)\n", + "obj4 = AspectRatio(eq=eq, target=8, weight=100, device_id=0)\n", + "# this will run on node 2, GPU 2 (rank=4)\n", + "obj5 = Objective(..., device_id=1)\n", + "# this will run on node 2, GPU 2 (rank=5)\n", + "obj6 = Objective(..., device_id=2)\n", + "objs = [obj1, obj2, obj3, obj4, obj5, obj6]\n", + "\n", + "# Parallel objective function needs the MPI communicator\n", + "objective = ObjectiveFunction(objs, deriv_mode=\"blocked\", mpi=MPI)\n", + "\n", + "```" + ] } ], "metadata": { From b4362b45d5ab92290dec3f2a38029c938ab0783c Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 15 May 2025 16:24:27 -0400 Subject: [PATCH 085/199] fixes after merge --- desc/optimize/_constraint_wrappers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 24379dfc8f..5b8717d954 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1158,7 +1158,6 @@ def jvp_unscaled(self, v, x, constants=None): op = "unscaled" return self._jvp(v, x, constants, op) - def _jvp(self, v, x, constants=None, op="scaled_error"): # The goal is to compute the Jacobian of the objective function with respect to # the optimization variables (c). Before taking the Jacobian, we update the @@ -1297,7 +1296,7 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper - + @functools.partial(jit, static_argnames=["op"]) def _proximal_jvp_f_pure(constraint, xf, constants, dc, eq_feasible_tangents, dxdc, op): @@ -1353,8 +1352,8 @@ def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): else: outi = getattr(obj, "jvp_" + op)([_vi for _vi in vi], xi, constants=const).T out.append(outi) - return jnp.concatenate(out).T - + return jnp.concatenate(out).T + def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): if objective.rank == 0: From f76e986b2e09b4c493c2e5e7c406cac736a4aa80 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Fri, 16 May 2025 16:53:52 -0400 Subject: [PATCH 086/199] first commit for multiple objective per rank change, need testing --- desc/objectives/objective_funs.py | 194 +++++++++++++++----------- desc/optimize/_constraint_wrappers.py | 48 ++++--- tests/test_multidevice.py | 4 +- 3 files changed, 145 insertions(+), 101 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 0b9eaa7f9d..3a1e2601da 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -253,6 +253,7 @@ def __init__( name="ObjectiveFunction", jac_chunk_size="auto", mpi=None, + rank_per_objective=None, ): if not isinstance(objectives, (tuple, list)): objectives = (objectives,) @@ -281,7 +282,7 @@ def __init__( self._compiled = False self._name = name device_ids = [obj._device_id for obj in objectives] - self._is_multi_device = len(set(device_ids)) > 1 + self._is_mpi = len(set(device_ids)) > 1 if mpi is not None: # for multiple node cases, each process sees 1 CPU, for those cases, # we cannot put objectives to different devices. Instead, we will @@ -293,7 +294,17 @@ def __init__( # process should run 1 objective. This way we can utilize all the GPUs. # Alternatively, we can specify the rank for the objective. This way, we # can have multiple objectives on the same rank. - self._is_multi_device = True + self._is_mpi = True + self._rank_per_objective = rank_per_objective + errorif( + ( + np.mod(self._rank_per_objective, max(device_ids) + 1) != device_ids + ).any(), + ValueError, + "Same rank objectives should also have the same device id. Supplied " + f"ranks {self._rank_per_objective} and device ids {device_ids} are " + "not compatible.", + ) self.mpi = mpi self.comm = self.mpi.COMM_WORLD self.rank = self.comm.Get_rank() @@ -305,7 +316,7 @@ def __init__( "Running multiple objectives per rank is not supported yet." ) - if self._is_multi_device and mpi is None: + if self._is_mpi and mpi is None: raise ValueError( "When using multiple devices, MPI communicator must be passed." ) @@ -318,7 +329,7 @@ def __enter__(self): "context manager.", ) errorif( - not self._is_multi_device, + not self._is_mpi, RuntimeError, "ObjectiveFunction must be parallel to be used as a context manager.", ) @@ -369,57 +380,65 @@ def _worker_loop(self): # message[2] is the output (for only jvp's) message = (None, None, None) message = self.comm.bcast(message, root=0) + obj_idx_rank = jnp.where(self._rank_per_objective == self.rank)[0] if message[0] == "STOP": print(f"Rank {self.rank} STOPPING") break elif "jvp" in message[0] and "proximal" not in message[0]: - print(f"Rank {self.rank} : {message[0]}") + print(f"Rank {self.rank} : {message[0]} for objectives {obj_idx_rank}") # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective - obj = self.objectives[self.rank] - const = self.constants[self.rank] - thing_idx = self._things_per_objective_idx[self.rank] - xi = [message[1][i] for i in thing_idx] - vi = [message[2][i] for i in thing_idx] - xi = jax.device_put(xi, obj._device) - vi = jax.device_put(vi, obj._device) - J_rank = getattr(obj, message[0])(vi, xi, constants=const) - J_rank = np.asarray(J_rank) + J_rank = [] + for idx in obj_idx_rank: + obj = self.objectives[idx] + const = self.constants[idx] + thing_idx = self._things_per_objective_idx[idx] + xi = [message[1][i] for i in thing_idx] + vi = [message[2][i] for i in thing_idx] + xi = jax.device_put(xi, obj._device) + vi = jax.device_put(vi, obj._device) + J_rank += getattr(obj, message[0])(vi, xi, constants=const) + J_rank = np.hstack(J_rank) self.comm.gather(J_rank, root=0) elif "compute" in message[0]: - print(f"Rank {self.rank} : {message[0]}") - obj = self.objectives[self.rank] - const = self.constants[self.rank] - par = message[1][self.rank] - par = jax.device_put(par, obj._device) - f_rank = getattr(obj, message[0])(*par, constants=const) - f_rank = np.asarray(f_rank) + print(f"Rank {self.rank} : {message[0]} for objectives {obj_idx_rank}") + f_rank = [] + for idx in obj_idx_rank: + obj = self.objectives[idx] + const = self.constants[idx] + par = message[1][idx] + par = jax.device_put(par, obj._device) + f_rank += getattr(obj, message[0])(*par, constants=const) + f_rank = np.concatenate(f_rank) self.comm.gather(f_rank, root=0) elif "proximal_jvp" in message[0]: - print(f"Rank {self.rank} : {message[0]}") - obj = self.objectives[self.rank] - const = self.constants[self.rank] - op = message[0].replace("proximal_jvp_", "") - - thing_idx = self._things_per_objective_idx[self.rank] - xi = [message[1][i] for i in thing_idx] - vi = [message[2][i] for i in thing_idx] - assert len(xi) > 0 - assert len(vi) > 0 - assert len(xi) == len(vi) - if obj._deriv_mode == "rev": - # obj might not allow fwd mode, so compute full rev mode jacobian - # and do matmul manually. This is slightly inefficient, but usually - # when rev mode is used, dim_f <<< dim_x, so its not too bad. - Ji = getattr(obj, "jac_" + op)(*xi, constants=const) - J_rank = jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum( - axis=0 - ) - else: - J_rank = getattr(obj, "jvp_" + op)( - [_vi for _vi in vi], xi, constants=const - ).T - J_rank = np.asarray(J_rank) + print(f"Rank {self.rank} : {message[0]} for objectives {obj_idx_rank}") + J_rank = [] + for idx in obj_idx_rank: + obj = self.objectives[idx] + const = self.constants[idx] + op = message[0].replace("proximal_jvp_", "") + + thing_idx = self._things_per_objective_idx[idx] + xi = [message[1][i] for i in thing_idx] + vi = [message[2][i] for i in thing_idx] + assert len(xi) > 0 + assert len(vi) > 0 + assert len(xi) == len(vi) + if obj._deriv_mode == "rev": + # obj might not allow fwd mode, so compute full rev mode + # jacobian and do matmul manually. This is slightly + # inefficient, but usuallywhen rev mode is used, + # dim_f <<< dim_x, so its not too bad. + Ji = getattr(obj, "jac_" + op)(*xi, constants=const) + J_rank += jnp.array( + [Jii @ vii.T for Jii, vii in zip(Ji, vi)] + ).sum(axis=0) + else: + J_rank += getattr(obj, "jvp_" + op)( + [_vi for _vi in vi], xi, constants=const + ).T + J_rank = np.hstack(J_rank) self.comm.gather(J_rank, root=0) def _unjit(self): @@ -470,7 +489,10 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 # methods. If we are using multiple GPUs, we don't want to jit them. use_jit_wrapper = use_jit - if self._is_multi_device: + device_ids = [obj._device_id for obj in self._objectives] + is_multi_device = len(set(device_ids)) > 1 + # we must be able to jit, if the device ids are the same? + if self._is_mpi and is_multi_device: use_jit_wrapper = False timer = Timer() @@ -533,14 +555,14 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 self._deriv_mode = "blocked" warnif( - self._is_multi_device and self._deriv_mode != "blocked", + self._is_mpi and self._deriv_mode != "blocked", UserWarning, "\nWhen using multiple devices, the ObjectiveFunction will run each \n" "sub-objective on the device specified in the sub-objective. \n" "Setting the deriv_mode to 'blocked' to ensure that each sub-objective\n" "runs on the correct device.", ) - if self._is_multi_device: + if self._is_mpi: self._deriv_mode = "blocked" errorif( isposint(self._jac_chunk_size) and self._deriv_mode in ["blocked"], @@ -562,7 +584,11 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 if self._deriv_mode == "blocked" and len(self.objectives) > 1: # blocked mode should never use this chunk size if there # are multiple sub-objectives - self._jac_chunk_size = None + self._jac_chunk_size = ( + "ObjectiveFunction is using `blocked` mode, you shouldn't " + "try to use this jac_chunk_size. Instead use the sub-objective's " + "`jac_chunk_size`." + ) elif self._deriv_mode == "blocked" and len(self.objectives) == 1: # if there is only one objective i.e. wrapped ForceBalance in # ProximalProjection, we can use the chunk size of @@ -640,7 +666,7 @@ def compute_unscaled(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - if not self._is_multi_device: + if not self._is_mpi: f = jnp.concatenate( [ obj.compute_unscaled(*par, constants=const) @@ -652,11 +678,14 @@ def compute_unscaled(self, x, constants=None): message = ("compute_unscaled", params, None) self.comm.bcast(message, root=0) - par = params[0] - obj = self.objectives[0] - const = self.constants[0] - f_rank = obj.compute_unscaled(*par, constants=const) - f_rank = np.asarray(f_rank) + obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + f_rank = [] + for idx in obj_idx_rank: + par = params[idx] + obj = self.objectives[idx] + const = self.constants[idx] + f_rank += obj.compute_unscaled(*par, constants=const) + f_rank = jnp.concatenate(f_rank) print(f"Rank {self.rank} waiting to gather") fs = self.comm.gather(f_rank, root=0) f = pconcat(fs) @@ -683,7 +712,7 @@ def compute_scaled(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - if not self._is_multi_device: + if not self._is_mpi: f = jnp.concatenate( [ obj.compute_scaled(*par, constants=const) @@ -695,11 +724,14 @@ def compute_scaled(self, x, constants=None): message = ("compute_scaled", params, None) self.comm.bcast(message, root=0) - par = params[0] - obj = self.objectives[0] - const = self.constants[0] - f_rank = obj.compute_scaled(*par, constants=const) - f_rank = np.asarray(f_rank) + obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + f_rank = [] + for idx in obj_idx_rank: + par = params[idx] + obj = self.objectives[idx] + const = self.constants[idx] + f_rank += obj.compute_scaled(*par, constants=const) + f_rank = jnp.concatenate(f_rank) print(f"Rank {self.rank} waiting to gather") fs = self.comm.gather(f_rank, root=0) f = pconcat(fs) @@ -726,7 +758,7 @@ def compute_scaled_error(self, x, constants=None): if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) - if not self._is_multi_device: + if not self._is_mpi: f = jnp.concatenate( [ obj.compute_scaled_error(*par, constants=const) @@ -738,11 +770,14 @@ def compute_scaled_error(self, x, constants=None): message = ("compute_scaled_error", params, None) self.comm.bcast(message, root=0) - par = params[0] - obj = self.objectives[0] - const = self.constants[0] - f_rank = obj.compute_scaled_error(*par, constants=const) - f_rank = np.asarray(f_rank) + obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + f_rank = [] + for idx in obj_idx_rank: + par = params[idx] + obj = self.objectives[idx] + const = self.constants[idx] + f_rank += obj.compute_scaled_error(*par, constants=const) + f_rank = jnp.concatenate(f_rank) print(f"Rank {self.rank} waiting to gather") fs = self.comm.gather(f_rank, root=0) f = pconcat(fs) @@ -818,7 +853,7 @@ def print_value(self, x, x0=None, constants=None): for par, par0, obj, const in zip( params, params0, self.objectives, constants ): - if self._is_multi_device: # pragma: no cover + if self._is_mpi: # pragma: no cover par = jax.device_put(par, obj._device) par0 = jax.device_put(par0, obj._device) outi = obj.print_value(par, par0, constants=const) @@ -828,7 +863,7 @@ def print_value(self, x, x0=None, constants=None): out[obj._print_value_fmt] = [outi] else: # pragma: no cover for par, obj, const in zip(params, self.objectives, constants): - if self._is_multi_device: + if self._is_mpi: par = jax.device_put(par, obj._device) outi = obj.print_value(par, constants=const) if obj._print_value_fmt in out: @@ -939,7 +974,7 @@ def jac_unscaled(self, x, constants=None): return self.jvp_unscaled(v, x, constants).T def _jvp_blocked(self, v, x, constants=None, op="scaled"): - if not self._is_multi_device: + if not self._is_mpi: v = ensure_tuple(v) if len(v) > 1: # using blocked for higher order derivatives is a pain, and only really @@ -980,19 +1015,22 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): message = ("jvp_" + op, xs, vs) self.comm.bcast(message, root=0) - obj = self.objectives[0] - const = self.constants[0] - thing_idx = self._things_per_objective_idx[0] - xi = [xs[i] for i in thing_idx] - vi = [vs[i] for i in thing_idx] - J_rank = getattr(obj, "jvp_" + op)(vi, xi, constants=const) - J_rank = np.asarray(J_rank) + obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + J_rank = [] + for idx in obj_idx_rank: + obj = self.objectives[idx] + const = self.constants[idx] + thing_idx = self._things_per_objective_idx[idx] + xi = [xs[i] for i in thing_idx] + vi = [vs[i] for i in thing_idx] + J_rank += getattr(obj, "jvp_" + op)(vi, xi, constants=const) + J_rank = jnp.hstack(J_rank) print(f"Rank {self.rank} waiting to gather") J = self.comm.gather(J_rank, root=0) # this is the transpose of the jvp when v is a matrix, for consistency with # jvp_batched - if not self._is_multi_device: + if not self._is_mpi: J = jnp.hstack(J) else: # this will handle the device placement of the J matrix diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 5b8717d954..5d4cca8221 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1180,7 +1180,7 @@ def _jvp(self, v, x, constants=None, op="scaled_error"): # we don't need to divide this part into blocked and batched because # self._constraint._deriv_mode will handle it - if not self._constraint._is_multi_device: + if not self._constraint._is_mpi: jvpfun = lambda u: self._get_tangent(u, xf, constants, op=op) tangents = batched_vectorize( jvpfun, @@ -1201,7 +1201,7 @@ def _jvp(self, v, x, constants=None, op="scaled_error"): if self._objective._deriv_mode == "batched": # objective's method already know about its jac_chunk_size return getattr(self._objective, "jvp_" + op)(tangents, xg, constants[0]) - elif not self._objective._is_multi_device: + elif not self._objective._is_mpi: return _proximal_jvp_blocked_pure( self._objective, jnp.split(tangents, np.cumsum(self._dimx_per_thing), axis=-1), @@ -1360,25 +1360,31 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): message = ("proximal_jvp_" + op, xgs, vgs) objective.comm.bcast(message, root=0) - obj = objective.objectives[0] - const = objective.constants[0] - - thing_idx = objective._things_per_objective_idx[0] - xi = [xgs[i] for i in thing_idx] - vi = [vgs[i] for i in thing_idx] - assert len(xi) > 0 - assert len(vi) > 0 - assert len(xi) == len(vi) - if obj._deriv_mode == "rev": - # obj might not allow fwd mode, so compute full rev mode jacobian - # and do matmul manually. This is slightly inefficient, but usually - # when rev mode is used, dim_f <<< dim_x, so its not too bad. - Ji = getattr(obj, "jac_" + op)(*xi, constants=const) - J_rank = jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum(axis=0) - else: - J_rank = getattr(obj, "jvp_" + op)( - [_vi for _vi in vi], xi, constants=const - ).T + obj_idx_rank = jnp.where(objective._rank_per_objective == 0)[0] + J_rank = [] + for idx in obj_idx_rank: + obj = objective.objectives[idx] + const = objective.constants[idx] + + thing_idx = objective._things_per_objective_idx[idx] + xi = [xgs[i] for i in thing_idx] + vi = [vgs[i] for i in thing_idx] + assert len(xi) > 0 + assert len(vi) > 0 + assert len(xi) == len(vi) + if obj._deriv_mode == "rev": + # obj might not allow fwd mode, so compute full rev mode jacobian + # and do matmul manually. This is slightly inefficient, but usually + # when rev mode is used, dim_f <<< dim_x, so its not too bad. + Ji = getattr(obj, "jac_" + op)(*xi, constants=const) + J_rank += jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum( + axis=0 + ) + else: + J_rank += getattr(obj, "jvp_" + op)( + [_vi for _vi in vi], xi, constants=const + ).T + J_rank = jnp.concatenate(J_rank).T print(f"Rank {objective.rank} waiting to gather") J = objective.comm.gather(J_rank, root=0) return pconcat(J).T diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index dbfc57a4a9..603f56a5ea 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -58,8 +58,8 @@ def test_multidevice_jac(): obj1.build() obj2.build() - assert obj1._is_multi_device - assert not obj2._is_multi_device + assert obj1._is_mpi + assert not obj2._is_mpi np.testing.assert_allclose(obj1.x(eq1), obj2.x(eq2)) From 199b09f737c0ae6dc6959c2aa1aa80b481520c28 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Sun, 18 May 2025 14:51:55 -0400 Subject: [PATCH 087/199] fix blocked mode jac_chunk_size error, now if there is a different sub-objective chunk size will error. Add try/except to get_processor name --- desc/__init__.py | 4 ++-- desc/objectives/objective_funs.py | 28 +++++++++++++++------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 37428255e3..0dd16834b7 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -77,8 +77,8 @@ def _get_processor_name(): all_info = subprocess.check_output(command, shell=True).decode().strip() for line in all_info.split("\n"): if "model name" in line: - return re.sub(".*model name.*:", "", line, 1) - return "" + return re.sub(pattern=".*model name.*:", repl="", string=line, count=1) + return "CPU" def _set_cpu_count(n): diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 3a1e2601da..7db8ed899a 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -581,19 +581,21 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 ) self._jac_chunk_size = max([1, max_chunk_size]) - if self._deriv_mode == "blocked" and len(self.objectives) > 1: - # blocked mode should never use this chunk size if there - # are multiple sub-objectives - self._jac_chunk_size = ( - "ObjectiveFunction is using `blocked` mode, you shouldn't " - "try to use this jac_chunk_size. Instead use the sub-objective's " - "`jac_chunk_size`." - ) - elif self._deriv_mode == "blocked" and len(self.objectives) == 1: - # if there is only one objective i.e. wrapped ForceBalance in - # ProximalProjection, we can use the chunk size of - # that objective as if this is batched mode - self._jac_chunk_size = self.objectives[0]._jac_chunk_size + if self._deriv_mode == "blocked": + chunk_sizes = [obj._jac_chunk_size for obj in self.objectives] + if len(set(chunk_sizes)) > 1: + # blocked mode should never use this chunk size if there + # are multiple sub-objectives with different chunk sizes + self._jac_chunk_size = ( + "ObjectiveFunction is using `blocked` mode, you shouldn't " + "try to use this jac_chunk_size. Instead use the sub-objective's " + "`jac_chunk_size`." + ) + else: + # if there is only one objective i.e. wrapped ForceBalance in + # ProximalProjection or only one value of jac_chunk_size, we can + # use the chunk size of the first objective + self._jac_chunk_size = self.objectives[0]._jac_chunk_size if not use_jit_wrapper: self._unjit() From 1cf917d2ed489633dfc49b0b62b5bfe9d32385e8 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Sun, 18 May 2025 17:35:27 -0400 Subject: [PATCH 088/199] fixes after debugging multiple bjective per rank, update tutorials --- desc/objectives/objective_funs.py | 119 ++++- desc/optimize/_constraint_wrappers.py | 17 +- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 13 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 22 +- docs/notebooks/tutorials/multi_device.ipynb | 477 +++++++++--------- 5 files changed, 366 insertions(+), 282 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 7db8ed899a..034889ee85 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -240,6 +240,10 @@ class ObjectiveFunction(IOAble): to manually choose a chunk_size if an OOM error is experienced in this case. mpi : MPI object, optional MPI communicator. Required when using multiple devices. + rank_per_objective : ndarray, optional + Specifies which rank each objective should run on. This will allow for multiple + objectives to run on the same rank. By default, each objective will be assigned + to different ranks. """ @@ -288,14 +292,18 @@ def __init__( # we cannot put objectives to different devices. Instead, we will # run each objective on a different rank. That is also why we will # run 1 objective per process. - # TODO: add an argument for node of the objective. FOr example, let's say + # TODO: add an argument for node of the objective. For example, let's say # we have 3 nodes and 4 GPUs per node. First of all there should be 12 # objectives in total. We should create 4 processes per node and each # process should run 1 objective. This way we can utilize all the GPUs. # Alternatively, we can specify the rank for the objective. This way, we # can have multiple objectives on the same rank. self._is_mpi = True - self._rank_per_objective = rank_per_objective + self._rank_per_objective = ( + rank_per_objective + if rank_per_objective is not None + else np.arange(len(objectives)) + ) errorif( ( np.mod(self._rank_per_objective, max(device_ids) + 1) != device_ids @@ -305,16 +313,37 @@ def __init__( f"ranks {self._rank_per_objective} and device ids {device_ids} are " "not compatible.", ) + warnif( + max(device_ids) != desc_config["num_device"] - 1, + UserWarning, + "You are not using all the devices available. You asked for " + f"{desc_config['num_device']} devices, but the maximum device id is " + f"{max(device_ids)}. This means that some devices are not being used.", + ) self.mpi = mpi self.comm = self.mpi.COMM_WORLD self.rank = self.comm.Get_rank() self.size = self.comm.Get_size() self.running = True - if self.size != len(self._objectives): - raise NotImplementedError( - "Number of objectives should be equal to the number of ranks. " - "Running multiple objectives per rank is not supported yet." - ) + warnif( + ( + np.unique(device_ids, return_counts=True)[1] + < self.size // desc_config["num_device"] + ).any(), + UserWarning, + "You are not using all the devices available. You asked for " + f"{self.size // desc_config["num_device"]} nodes, but there are " + f"{np.unique(device_ids, return_counts=True)[1]} objectives per " + "same device_id. Note that for multiple nodes, each node has same " + "number of devices and their indices start from 0. So, device_id=0 " + "on node 1 is not same as device_id=0 on node 2. ", + ) + errorif( + max(self._rank_per_objective) > self.size, + ValueError, + "The maximum value of rank_per_objective is greater than the number " + "of ranks. There are not enough ranks to run the objectives.", + ) if self._is_mpi and mpi is None: raise ValueError( @@ -385,7 +414,10 @@ def _worker_loop(self): print(f"Rank {self.rank} STOPPING") break elif "jvp" in message[0] and "proximal" not in message[0]: - print(f"Rank {self.rank} : {message[0]} for objectives {obj_idx_rank}") + print( + f"Rank {self.rank} : {message[0]} for objectives ids: " + + f"{obj_idx_rank}" + ) # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective J_rank = [] @@ -397,22 +429,28 @@ def _worker_loop(self): vi = [message[2][i] for i in thing_idx] xi = jax.device_put(xi, obj._device) vi = jax.device_put(vi, obj._device) - J_rank += getattr(obj, message[0])(vi, xi, constants=const) + J_rank.append(getattr(obj, message[0])(vi, xi, constants=const)) J_rank = np.hstack(J_rank) self.comm.gather(J_rank, root=0) elif "compute" in message[0]: - print(f"Rank {self.rank} : {message[0]} for objectives {obj_idx_rank}") + print( + f"Rank {self.rank} : {message[0]} for objectives ids: " + + f"{obj_idx_rank}" + ) f_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] const = self.constants[idx] par = message[1][idx] par = jax.device_put(par, obj._device) - f_rank += getattr(obj, message[0])(*par, constants=const) + f_rank.append(getattr(obj, message[0])(*par, constants=const)) f_rank = np.concatenate(f_rank) self.comm.gather(f_rank, root=0) elif "proximal_jvp" in message[0]: - print(f"Rank {self.rank} : {message[0]} for objectives {obj_idx_rank}") + print( + f"Rank {self.rank} : {message[0]} for objectives ids: " + + f"{obj_idx_rank}" + ) J_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] @@ -431,13 +469,17 @@ def _worker_loop(self): # inefficient, but usuallywhen rev mode is used, # dim_f <<< dim_x, so its not too bad. Ji = getattr(obj, "jac_" + op)(*xi, constants=const) - J_rank += jnp.array( - [Jii @ vii.T for Jii, vii in zip(Ji, vi)] - ).sum(axis=0) + J_rank.append( + jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum( + axis=0 + ) + ) else: - J_rank += getattr(obj, "jvp_" + op)( - [_vi for _vi in vi], xi, constants=const - ).T + J_rank.append( + getattr(obj, "jvp_" + op)( + [_vi for _vi in vi], xi, constants=const + ).T + ) J_rank = np.hstack(J_rank) self.comm.gather(J_rank, root=0) @@ -597,6 +639,23 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 # use the chunk size of the first objective self._jac_chunk_size = self.objectives[0]._jac_chunk_size + if self._is_mpi and verbose > 0: + if self.rank == 0: + objective_ids_per_rank = [ + np.where(self._rank_per_objective == i)[0] for i in range(self.size) + ] + objective_names_per_rank = [ + [self._objectives[i].__class__.__name__ for i in objective_ids] + for objective_ids in objective_ids_per_rank + ] + print("-" * 60) + for rank in range(self.size): + print( + f"Rank {rank} will run objective(s): " + f"{objective_names_per_rank[rank]}" + ) + print("-" * 60) + if not use_jit_wrapper: self._unjit() @@ -681,12 +740,16 @@ def compute_unscaled(self, x, constants=None): self.comm.bcast(message, root=0) obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + print( + f"Rank {self.rank} : {message[0]} for objectives ids: " + + f"{obj_idx_rank}" + ) f_rank = [] for idx in obj_idx_rank: par = params[idx] obj = self.objectives[idx] const = self.constants[idx] - f_rank += obj.compute_unscaled(*par, constants=const) + f_rank.append(obj.compute_unscaled(*par, constants=const)) f_rank = jnp.concatenate(f_rank) print(f"Rank {self.rank} waiting to gather") fs = self.comm.gather(f_rank, root=0) @@ -727,12 +790,16 @@ def compute_scaled(self, x, constants=None): self.comm.bcast(message, root=0) obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + print( + f"Rank {self.rank} : {message[0]} for objectives ids: " + + f"{obj_idx_rank}" + ) f_rank = [] for idx in obj_idx_rank: par = params[idx] obj = self.objectives[idx] const = self.constants[idx] - f_rank += obj.compute_scaled(*par, constants=const) + f_rank.append(obj.compute_scaled(*par, constants=const)) f_rank = jnp.concatenate(f_rank) print(f"Rank {self.rank} waiting to gather") fs = self.comm.gather(f_rank, root=0) @@ -773,12 +840,16 @@ def compute_scaled_error(self, x, constants=None): self.comm.bcast(message, root=0) obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + print( + f"Rank {self.rank} : {message[0]} for objectives ids: " + + f"{obj_idx_rank}" + ) f_rank = [] for idx in obj_idx_rank: par = params[idx] obj = self.objectives[idx] const = self.constants[idx] - f_rank += obj.compute_scaled_error(*par, constants=const) + f_rank.append(obj.compute_scaled_error(*par, constants=const)) f_rank = jnp.concatenate(f_rank) print(f"Rank {self.rank} waiting to gather") fs = self.comm.gather(f_rank, root=0) @@ -1018,6 +1089,10 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): self.comm.bcast(message, root=0) obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + print( + f"Rank {self.rank} : {message[0]} for objectives ids: " + + f"{obj_idx_rank}" + ) J_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] @@ -1025,7 +1100,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): thing_idx = self._things_per_objective_idx[idx] xi = [xs[i] for i in thing_idx] vi = [vs[i] for i in thing_idx] - J_rank += getattr(obj, "jvp_" + op)(vi, xi, constants=const) + J_rank.append(getattr(obj, "jvp_" + op)(vi, xi, constants=const)) J_rank = jnp.hstack(J_rank) print(f"Rank {self.rank} waiting to gather") J = self.comm.gather(J_rank, root=0) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 5d4cca8221..e9ba9a3675 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1361,6 +1361,9 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): objective.comm.bcast(message, root=0) obj_idx_rank = jnp.where(objective._rank_per_objective == 0)[0] + print( + f"Rank {objective.rank} : {message[0]} for objectives ids: {obj_idx_rank}" + ) J_rank = [] for idx in obj_idx_rank: obj = objective.objectives[idx] @@ -1377,14 +1380,16 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): # and do matmul manually. This is slightly inefficient, but usually # when rev mode is used, dim_f <<< dim_x, so its not too bad. Ji = getattr(obj, "jac_" + op)(*xi, constants=const) - J_rank += jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum( - axis=0 + J_rank.append( + jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum(axis=0) ) else: - J_rank += getattr(obj, "jvp_" + op)( - [_vi for _vi in vi], xi, constants=const - ).T - J_rank = jnp.concatenate(J_rank).T + J_rank.append( + getattr(obj, "jvp_" + op)( + [_vi for _vi in vi], xi, constants=const + ).T + ) + J_rank = jnp.concatenate(J_rank) print(f"Rank {objective.rank} waiting to gather") J = objective.comm.gather(J_rank, root=0) return pconcat(J).T diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 3df6662228..a4e998ea84 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -16,7 +16,7 @@ set_device("cpu", num_device=num_device) # ====== Using GPUs ====== -# When we have multiple processing using the same devices (for example, 3 processes +# When we have multiple processes using the same devices (for example, 3 processes # using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will # cause the memory allocation to fail. To avoid this, we can set the memory fraction # to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of @@ -51,14 +51,17 @@ if desc_config["kind"] == "gpu": print( f"Rank {rank} is running on {jax.local_devices(backend="gpu")} " - f"and {jax.local_devices(backend="cpu")}" + f"and {jax.local_devices(backend="cpu")}\n" ) else: - print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}") - print_backend_info() + print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") + if rank == 0: + print(f"====== BACKEND INFO ======") + print_backend_info() + print("\n") eq = get("HELIOTRON") - eq.change_resolution(6, 6, 6, 12, 12, 12) + eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) # this will create a parallel objective function # user can create their own parallel objective function as well which will be diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 20f99a8d7b..93162f48c4 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -8,7 +8,7 @@ from desc import _set_cpu_count, set_device # ====== Using CPUs ====== -num_device = 3 +num_device = 2 # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices # If you have multiple CPUs, you don't need to call `_set_cpu_count` @@ -16,7 +16,7 @@ set_device("cpu", num_device=num_device) # ====== Using GPUs ====== -# When we have multiple processing using the same devices (for example, 3 processes +# When we have multiple processes using the same devices (for example, 3 processes # using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will # cause the memory allocation to fail. To avoid this, we can set the memory fraction # to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of @@ -62,14 +62,18 @@ if desc_config["kind"] == "gpu": print( f"Rank {rank} is running on {jax.local_devices(backend="gpu")} " - f"and {jax.local_devices(backend="cpu")}" + f"and {jax.local_devices(backend="cpu")}\n" ) else: - print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}") - print_backend_info() + print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") + + if rank == 0: + print(f"====== BACKEND INFO ======") + print_backend_info() + print("\n") eq = get("precise_QA") - eq.change_resolution(3, 3, 3, 6, 6, 6) + eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) # create two grids with different rho values, this will effectively separate # the quasisymmetry objective into two parts @@ -83,13 +87,15 @@ # when using parallel objectives, the user needs to supply the device_id obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) - obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2) + obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) objs = [obj1, obj2, obj3] # Parallel objective function needs the MPI communicator # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will # automatically switch to `blocked`. - objective = ObjectiveFunction(objs, deriv_mode="blocked", mpi=MPI) + objective = ObjectiveFunction( + objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 0]) + ) if rank == 0: objective.build(verbose=3) else: diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index aa3f704060..255c2c9837 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -49,20 +49,20 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", + "DESC version=0.14.2+102.g199b09f73.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.93 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.93 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.93 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 7.93 GB available memory\n" + "\t CPU 0: TFRT_CPU_0 with 8.26 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.26 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.26 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 8.26 GB available memory\n" ] } ], @@ -96,7 +96,7 @@ "set_device(\"cpu\", num_device=num_device)\n", "\n", "# ====== Using GPUs ======\n", - "# When we have multiple processing using the same devices (for example, 3 processes\n", + "# When we have multiple processes using the same devices (for example, 3 processes\n", "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", "# cause the memory allocation to fail. To avoid this, we can set the memory fraction\n", "# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of\n", @@ -109,7 +109,7 @@ "from mpi4py import MPI\n", "\n", "from desc import config as desc_config\n", - "from desc.backend import print_backend_info, jax\n", + "from desc.backend import jax, print_backend_info\n", "from desc.examples import get\n", "from desc.objectives.getters import (\n", " get_fixed_boundary_constraints,\n", @@ -131,14 +131,17 @@ " if desc_config[\"kind\"] == \"gpu\":\n", " print(\n", " f\"Rank {rank} is running on {jax.local_devices(backend=\"gpu\")} \"\n", - " f\"and {jax.local_devices(backend=\"cpu\")}\"\n", + " f\"and {jax.local_devices(backend=\"cpu\")}\\n\"\n", " )\n", " else:\n", - " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\")\n", - " print_backend_info()\n", + " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\\n\")\n", + " if rank == 0:\n", + " print(f\"====== BACKEND INFO ======\")\n", + " print_backend_info()\n", + " print(\"\\n\")\n", "\n", " eq = get(\"HELIOTRON\")\n", - " eq.change_resolution(6, 6, 6, 12, 12, 12)\n", + " eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4)\n", "\n", " # this will create a parallel objective function\n", " # user can create their own parallel objective function as well which will be\n", @@ -169,6 +172,7 @@ " # if you put a code here, it will be performed on all ranks\n", "\n", "\n", + "\n", "```" ] }, @@ -189,70 +193,58 @@ "name": "stdout", "output_type": "stream", "text": [ - "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", - "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.86 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.86 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.86 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 7.86 GB available memory\n", "====== TOTAL OF 4 RANKS ======\n", "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", + "\n", + "====== BACKEND INFO ======\n", + "DESC version=0.14.2+102.g199b09f73.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.86 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.86 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.86 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 7.86 GB available memory\n", - "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n", - "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n", + "\t CPU 0: TFRT_CPU_0 with 8.15 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.15 GB available memory\n", + "\t CPU 2: TFRT_CPU_2 with 8.15 GB available memory\n", + "\t CPU 3: TFRT_CPU_3 with 8.15 GB available memory\n", + "\n", + "\n", + "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", "Rank 2 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", - "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.84 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.84 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.84 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 7.84 GB available memory\n", + "\n", "Rank 3 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", - "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.84 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.84 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.84 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 7.84 GB available memory\n", + "\n", "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 1\n", + "Putting objective force on device 2\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 2\n", + "Putting objective force on device 3\n", + "------------------------------------------------------------\n", + "Rank 0 will run objective(s): ['ForceBalance']\n", + "Rank 1 will run objective(s): ['ForceBalance']\n", + "Rank 2 will run objective(s): ['ForceBalance']\n", + "Rank 3 will run objective(s): ['ForceBalance']\n", + "------------------------------------------------------------\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 3\n", "Building objective: lcfs R\n", - "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n", - "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n", "Building objective: lcfs Z\n", "Building objective: fixed Psi\n", "Building objective: fixed pressure\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", "Building objective: fixed iota\n", "Building objective: fixed sheet current\n", "Building objective: self_consistency R\n", @@ -260,9 +252,7 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Timer: Objective build = 987 ms\n", + "Timer: Objective build = 1.74 sec\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", @@ -275,95 +265,108 @@ "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Timer: LinearConstraintProjection build = 4.35 sec\n", - "Number of parameters: 609\n", - "Number of objectives: 15000\n", - "Timer: Initializing the optimization = 5.39 sec\n", + "Timer: LinearConstraintProjection build = 6.62 sec\n", + "Number of parameters: 551\n", + "Number of objectives: 8424\n", + "Timer: Initializing the optimization = 8.43 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error\n", - "Rank 2 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 5.928e+00 2.480e+00 \n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", + " 0 1 2.500e+00 1.228e+00 \n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error\n", - "Rank 2 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - " 1 2 9.876e-01 4.941e+00 3.322e-01 6.448e-01 \n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", + " 1 2 8.226e-01 1.678e+00 2.256e-01 5.198e-01 \n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error\n", - "Rank 2 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - " 2 3 4.419e-02 9.434e-01 2.363e-01 8.993e-02 \n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", + " 2 3 2.473e-02 7.978e-01 1.993e-01 7.096e-02 \n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error\n", - "Rank 2 : jvp_scaled_error\n", - "Rank 3 : jvp_scaled_error\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - " 3 4 1.048e-02 3.370e-02 1.497e-01 3.808e-02 \n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 0 waiting to gather\n", + " 3 5 3.451e-03 2.128e-02 8.908e-02 3.526e-02 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 1.048e-02\n", - " Total delta_x: 3.604e-01\n", + " Current function value: 3.451e-03\n", + " Total delta_x: 2.505e-01\n", " Iterations: 3\n", - " Function evaluations: 4\n", + " Function evaluations: 5\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 26.4 sec\n", - "Timer: Avg time per step = 6.62 sec\n", + "Timer: Solution time = 36.6 sec\n", + "Timer: Avg time per step = 9.16 sec\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "==============================================================================================================\n", " Start --> End\n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", - "Rank 3 : compute_scaled_error\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", - "Total (sum of squares): 7.355e+00 --> 1.048e-02, \n", - "Maximum absolute Force error: 1.077e+05 --> 1.607e+04 (N)\n", - "Minimum absolute Force error: 1.201e-10 --> 1.374e-10 (N)\n", - "Average absolute Force error: 3.587e+04 --> 2.943e+03 (N)\n", - "Maximum absolute Force error: 8.664e-03 --> 1.293e-03 (normalized)\n", - "Minimum absolute Force error: 9.659e-18 --> 1.105e-17 (normalized)\n", - "Average absolute Force error: 2.885e-03 --> 2.367e-04 (normalized)\n", - "Maximum absolute Force error: 4.334e+05 --> 4.199e+04 (N)\n", - "Minimum absolute Force error: 1.482e-10 --> 1.459e-10 (N)\n", - "Average absolute Force error: 7.335e+04 --> 7.372e+03 (N)\n", - "Maximum absolute Force error: 3.485e-02 --> 3.377e-03 (normalized)\n", - "Minimum absolute Force error: 1.192e-17 --> 1.174e-17 (normalized)\n", - "Average absolute Force error: 5.899e-03 --> 5.929e-04 (normalized)\n", - "Maximum absolute Force error: 1.057e+06 --> 6.054e+04 (N)\n", - "Minimum absolute Force error: 1.000e-10 --> 6.288e-11 (N)\n", - "Average absolute Force error: 1.205e+05 --> 1.017e+04 (N)\n", - "Maximum absolute Force error: 8.500e-02 --> 4.869e-03 (normalized)\n", - "Minimum absolute Force error: 8.043e-18 --> 5.057e-18 (normalized)\n", - "Average absolute Force error: 9.693e-03 --> 8.179e-04 (normalized)\n", - "Maximum absolute Force error: 5.498e+07 --> 4.127e+05 (N)\n", - "Minimum absolute Force error: 5.034e-13 --> 6.121e-12 (N)\n", - "Average absolute Force error: 3.746e+05 --> 1.181e+04 (N)\n", - "Maximum absolute Force error: 4.422e+00 --> 3.319e-02 (normalized)\n", - "Minimum absolute Force error: 4.048e-20 --> 4.923e-19 (normalized)\n", - "Average absolute Force error: 3.013e-02 --> 9.495e-04 (normalized)\n", + "Total (sum of squares): 2.500e+00 --> 3.451e-03, \n", + "Maximum absolute Force error: 6.794e+04 --> 6.655e+03 (N)\n", + "Minimum absolute Force error: 1.059e-10 --> 1.232e-10 (N)\n", + "Average absolute Force error: 1.304e+04 --> 1.696e+03 (N)\n", + "Maximum absolute Force error: 5.464e-03 --> 5.352e-04 (normalized)\n", + "Minimum absolute Force error: 8.514e-18 --> 9.912e-18 (normalized)\n", + "Average absolute Force error: 1.049e-03 --> 1.364e-04 (normalized)\n", + "Maximum absolute Force error: 2.032e+05 --> 1.704e+04 (N)\n", + "Minimum absolute Force error: 1.502e-10 --> 1.535e-10 (N)\n", + "Average absolute Force error: 4.494e+04 --> 2.436e+03 (N)\n", + "Maximum absolute Force error: 1.634e-02 --> 1.370e-03 (normalized)\n", + "Minimum absolute Force error: 1.208e-17 --> 1.235e-17 (normalized)\n", + "Average absolute Force error: 3.614e-03 --> 1.959e-04 (normalized)\n", + "Maximum absolute Force error: 6.807e+05 --> 2.238e+04 (N)\n", + "Minimum absolute Force error: 8.896e-11 --> 6.677e-11 (N)\n", + "Average absolute Force error: 7.775e+04 --> 2.548e+03 (N)\n", + "Maximum absolute Force error: 5.474e-02 --> 1.800e-03 (normalized)\n", + "Minimum absolute Force error: 7.155e-18 --> 5.370e-18 (normalized)\n", + "Average absolute Force error: 6.253e-03 --> 2.049e-04 (normalized)\n", + "Maximum absolute Force error: 1.149e+07 --> 1.001e+06 (N)\n", + "Minimum absolute Force error: 3.304e-12 --> 6.674e-13 (N)\n", + "Average absolute Force error: 1.493e+05 --> 6.575e+03 (N)\n", + "Maximum absolute Force error: 9.238e-01 --> 8.054e-02 (normalized)\n", + "Minimum absolute Force error: 2.657e-19 --> 5.368e-20 (normalized)\n", + "Average absolute Force error: 1.200e-02 --> 5.288e-04 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", @@ -371,9 +374,10 @@ "Fixed iota profile error: 0.000e+00 --> 0.000e+00 (dimensionless)\n", "Fixed sheet current error: 0.000e+00 --> 0.000e+00 (~)\n", "==============================================================================================================\n", + "\n", "Rank 1 STOPPING\n", - "Rank 2 STOPPING\n", - "Rank 3 STOPPING\n" + "Rank 3 STOPPING\n", + "Rank 2 STOPPING\n" ] } ], @@ -386,7 +390,7 @@ "metadata": {}, "source": [ "## Using other Objectives\n", - "Above we used the convenience function for force balance objective, but we can also other objectives with this approach." + "Above we used the convenience function for force balance objective, but we can also use other objectives with this approach." ] }, { @@ -404,7 +408,7 @@ "from desc import _set_cpu_count, set_device\n", "\n", "# ====== Using CPUs ======\n", - "num_device = 3\n", + "num_device = 2\n", "# These will be used for diving the single CPU into multiple virtual CPUs\n", "# such that JAX and XLA thinks there are multiple devices\n", "# If you have multiple CPUs, you don't need to call `_set_cpu_count`\n", @@ -412,7 +416,7 @@ "set_device(\"cpu\", num_device=num_device)\n", "\n", "# ====== Using GPUs ======\n", - "# When we have multiple processing using the same devices (for example, 3 processes\n", + "# When we have multiple processes using the same devices (for example, 3 processes\n", "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", "# cause the memory allocation to fail. To avoid this, we can set the memory fraction\n", "# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of\n", @@ -423,13 +427,11 @@ "# set_device(\"gpu\", num_device=num_device)\n", "\n", "\n", + "import numpy as np\n", "from mpi4py import MPI\n", "\n", "from desc import config as desc_config\n", "from desc.backend import jax, jnp, print_backend_info\n", - "\n", - "\n", - "import numpy as np\n", "from desc.examples import get\n", "from desc.grid import LinearGrid\n", "from desc.objectives import (\n", @@ -443,7 +445,6 @@ " ObjectiveFunction,\n", " QuasisymmetryTwoTerm,\n", ")\n", - "\n", "from desc.optimize import Optimizer\n", "\n", "if __name__ == \"__main__\":\n", @@ -461,14 +462,18 @@ " if desc_config[\"kind\"] == \"gpu\":\n", " print(\n", " f\"Rank {rank} is running on {jax.local_devices(backend=\"gpu\")} \"\n", - " f\"and {jax.local_devices(backend=\"cpu\")}\"\n", + " f\"and {jax.local_devices(backend=\"cpu\")}\\n\"\n", " )\n", " else:\n", - " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\")\n", - " print_backend_info()\n", + " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\\n\")\n", + "\n", + " if rank == 0:\n", + " print(f\"====== BACKEND INFO ======\")\n", + " print_backend_info()\n", + " print(\"\\n\")\n", "\n", " eq = get(\"precise_QA\")\n", - " eq.change_resolution(3, 3, 3, 6, 6, 6)\n", + " eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4)\n", "\n", " # create two grids with different rho values, this will effectively separate\n", " # the quasisymmetry objective into two parts\n", @@ -482,13 +487,15 @@ " # when using parallel objectives, the user needs to supply the device_id\n", " obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", " obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", - " obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=2)\n", + " obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0)\n", " objs = [obj1, obj2, obj3]\n", "\n", " # Parallel objective function needs the MPI communicator\n", " # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will\n", " # automatically switch to `blocked`.\n", - " objective = ObjectiveFunction(objs, deriv_mode=\"blocked\", mpi=MPI)\n", + " objective = ObjectiveFunction(\n", + " objs, deriv_mode=\"blocked\", mpi=MPI, rank_per_objective=np.array([0, 1, 0])\n", + " )\n", " if rank == 0:\n", " objective.build(verbose=3)\n", " else:\n", @@ -538,10 +545,7 @@ " },\n", " )\n", "\n", - " # if you put a code here, it will be performed on all ranks\n", - "\n", - "\n", - "```" + " # if you put a code here, it will be performed on all ranks\n" ] }, { @@ -553,150 +557,141 @@ "name": "stdout", "output_type": "stream", "text": [ - "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", - "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", - "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 3 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.86 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.86 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.86 GB available memory\n", - "====== TOTAL OF 3 RANKS ======\n", - "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", - "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", + "====== TOTAL OF 2 RANKS ======\n", + "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", + "\n", + "====== BACKEND INFO ======\n", + "DESC version=0.14.2+102.g199b09f73.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 3 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.86 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.86 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.86 GB available memory\n", - "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n", - "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n", + "Using 2 CPUs:\n", + "\t CPU 0: TFRT_CPU_0 with 8.13 GB available memory\n", + "\t CPU 1: TFRT_CPU_1 with 8.13 GB available memory\n", + "\n", + "\n", + "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", + "\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Rank 2 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2)]\n", - "DESC version=0.13.0+1690.g7be080fd0.dirty.\n", - "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 3 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 7.85 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 7.85 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 7.85 GB available memory\n", - "Timer: Precomputing transforms = 1.41 sec\n", + "Timer: Precomputing transforms = 1.54 sec\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "/home/yigit/Codes/DESC/desc/utils.py:562: UserWarning: Reducing radial (L) resolution can make plasma boundary inconsistent. Recommend calling `eq.surface = eq.get_surface_at(rho=1.0)`\n", - " warnings.warn(colored(msg, \"yellow\"), err)\n", - "Timer: Precomputing transforms = 1.11 sec\n", + "Timer: Precomputing transforms = 1.37 sec\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.12 sec\n", - "Putting objective aspect ratio on device 2\n", - "Timer: Objective build = 4.53 sec\n", + "Timer: Precomputing transforms = 1.37 sec\n", + "------------------------------------------------------------\n", + "Rank 0 will run objective(s): ['QuasisymmetryTwoTerm', 'AspectRatio']\n", + "Rank 1 will run objective(s): ['QuasisymmetryTwoTerm']\n", + "------------------------------------------------------------\n", + "Timer: Objective build = 5.35 sec\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.39 sec\n", - "Timer: Objective build = 1.46 sec\n", - "Timer: Objective build = 24.4 ms\n", - "Timer: Eq Update LinearConstraintProjection build = 5.13 sec\n", - "Timer: Proximal projection build = 8.77 sec\n", + "Timer: Precomputing transforms = 2.00 sec\n", + "Timer: Objective build = 2.10 sec\n", + "Timer: Objective build = 2.37 ms\n", + "Timer: Eq Update LinearConstraintProjection build = 5.87 sec\n", + "Timer: Proximal projection build = 10.9 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 874 ms\n", - "Timer: LinearConstraintProjection build = 2.11 sec\n", + "Timer: Objective build = 910 ms\n", + "Timer: LinearConstraintProjection build = 2.09 sec\n", "Number of parameters: 8\n", - "Number of objectives: 911\n", - "Timer: Initializing the optimization = 11.8 sec\n", + "Number of objectives: 631\n", + "Timer: Initializing the optimization = 14.0 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error\n", - "Rank 2 : proximal_jvp_scaled_error\n", + "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 2.011e+04 1.952e+02 \n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", + " 0 1 2.005e+04 1.926e+02 \n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 waiting to gather\n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", + "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error\n", - "Rank 2 : proximal_jvp_scaled_error\n", + " 1 4 8.123e+03 1.193e+04 4.964e-02 9.847e+01 \n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - " 1 4 8.735e+03 1.138e+04 4.838e-02 1.104e+02 \n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", + "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error\n", - "Rank 2 : proximal_jvp_scaled_error\n", + " 2 5 2.617e+03 5.507e+03 5.877e-02 6.065e+01 \n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - " 2 5 1.528e+03 7.207e+03 5.365e-02 3.401e+01 \n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error\n", - "Rank 2 : proximal_jvp_scaled_error\n", + "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", - " 3 6 5.325e+02 9.957e+02 8.313e-02 1.565e+01 \n", + " 3 7 7.564e+02 1.860e+03 7.212e-02 3.935e+00 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 5.325e+02\n", - " Total delta_x: 8.950e-02\n", + " Current function value: 7.564e+02\n", + " Total delta_x: 7.271e-02\n", " Iterations: 3\n", - " Function evaluations: 6\n", + " Function evaluations: 7\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 48.9 sec\n", - "Timer: Avg time per step = 12.2 sec\n", + "Timer: Solution time = 1.28 min\n", + "Timer: Avg time per step = 19.3 sec\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "==============================================================================================================\n", " Start --> End\n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error\n", - "Rank 2 : compute_scaled_error\n", + "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", - "Total (sum of squares): 2.011e+04 --> 5.325e+02, \n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.813e-01 --> 7.713e-01 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.150e-04 --> 1.620e-03 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 5.169e-02 --> 2.275e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.978e-01 --> 8.416e-01 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.346e-04 --> 1.768e-03 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 5.640e-02 --> 2.483e-01 (normalized)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.161e+00 --> 1.203e+00 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.945e-03 --> 7.572e-04 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.051e-01 --> 3.026e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.267e+00 --> 1.313e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.122e-03 --> 8.262e-04 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.147e-01 --> 3.302e-01 (normalized)\n", - "Aspect ratio: 5.996e+00 --> 7.859e+00 (dimensionless)\n", - "Maximum absolute Force error: 1.345e+05 --> 4.971e+04 (N)\n", - "Minimum absolute Force error: 8.350e+00 --> 6.376e-01 (N)\n", - "Average absolute Force error: 5.462e+03 --> 2.762e+03 (N)\n", - "Maximum absolute Force error: 9.614e-02 --> 3.554e-02 (normalized)\n", - "Minimum absolute Force error: 5.969e-06 --> 4.558e-07 (normalized)\n", - "Average absolute Force error: 3.904e-03 --> 1.974e-03 (normalized)\n", - "R boundary error: 0.000e+00 --> 4.734e-19 (m)\n", - "Z boundary error: 0.000e+00 --> 3.478e-18 (m)\n", + "Total (sum of squares): 2.005e+04 --> 7.564e+02, \n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.038e-01 --> 1.333e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.569e-04 --> 2.875e-04 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.039e-01 --> 2.474e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.406e-01 --> 1.455e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.803e-04 --> 3.137e-04 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.134e-01 --> 2.699e-01 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 9.615e-01 --> 2.043e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 3.670e-04 --> 1.044e-02 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.474e-01 --> 3.819e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.049e+00 --> 2.229e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 4.004e-04 --> 1.139e-02 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.609e-01 --> 4.167e-01 (normalized)\n", + "Aspect ratio: 6.002e+00 --> 7.856e+00 (dimensionless)\n", + "Maximum absolute Force error: 1.435e+05 --> 2.352e+04 (N)\n", + "Minimum absolute Force error: 1.480e+00 --> 6.889e+00 (N)\n", + "Average absolute Force error: 7.215e+03 --> 2.171e+03 (N)\n", + "Maximum absolute Force error: 1.026e-01 --> 1.681e-02 (normalized)\n", + "Minimum absolute Force error: 1.058e-06 --> 4.925e-06 (normalized)\n", + "Average absolute Force error: 5.157e-03 --> 1.552e-03 (normalized)\n", + "R boundary error: 0.000e+00 --> 4.600e-19 (m)\n", + "Z boundary error: 0.000e+00 --> 3.469e-18 (m)\n", "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", "Fixed current profile error: 0.000e+00 --> 0.000e+00 (A)\n", "==============================================================================================================\n", - "Rank 1 STOPPING\n", - "Rank 2 STOPPING\n" + "\n", + "Rank 1 STOPPING\n" ] } ], "source": [ - "!mpirun -n 3 python mpi-tutorials/mpi-proximal.py" + "!mpirun -n 2 python mpi-tutorials/mpi-proximal.py" ] }, { From 4446c7f0542203a5174312715be1bd090c110726 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Sun, 18 May 2025 17:40:46 -0400 Subject: [PATCH 089/199] fix syntax error --- desc/objectives/objective_funs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 034889ee85..f2c4fffe1f 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -332,7 +332,7 @@ def __init__( ).any(), UserWarning, "You are not using all the devices available. You asked for " - f"{self.size // desc_config["num_device"]} nodes, but there are " + f"{self.size // desc_config['num_device']} nodes, but there are " f"{np.unique(device_ids, return_counts=True)[1]} objectives per " "same device_id. Note that for multiple nodes, each node has same " "number of devices and their indices start from 0. So, device_id=0 " From 844ccea3acb83101e843b356c6eadd38c46411d2 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 19 May 2025 15:09:38 -0400 Subject: [PATCH 090/199] fix the formatting of the notebook --- docs/notebooks/tutorials/multi_device.ipynb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 255c2c9837..28b97ce0e9 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -170,9 +170,6 @@ " )\n", "\n", " # if you put a code here, it will be performed on all ranks\n", - "\n", - "\n", - "\n", "```" ] }, @@ -545,7 +542,8 @@ " },\n", " )\n", "\n", - " # if you put a code here, it will be performed on all ranks\n" + " # if you put a code here, it will be performed on all ranks\n", + "```" ] }, { @@ -698,7 +696,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Using Slurm for Multi-Node and Multi-Process Scripts\n", + "## Using Slurm for Multi-Node and Multi-Process Scripts\n", "\n", "**Note :** These instructions may differ for the cluster you are trying to use. The reason we give this example is to set some terminology for users that are not familiar with multi-node and multi-processing.\n", "\n", From 81e2377f4e52f32d3e048489e840713caf587700 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 19 May 2025 15:37:39 -0400 Subject: [PATCH 091/199] update the memory allocator for GPU --- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 9 ++++----- .../tutorials/mpi-tutorials/mpi-proximal.py | 9 ++++----- docs/notebooks/tutorials/multi_device.ipynb | 19 +++++++++---------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index a4e998ea84..e2ba4c4f85 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -18,12 +18,11 @@ # ====== Using GPUs ====== # When we have multiple processes using the same devices (for example, 3 processes # using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will -# cause the memory allocation to fail. To avoid this, we can set the memory fraction -# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of -# the GPU memory. This is a bit conservative, but if a process needs more memory, it can -# allocate more memory on the fly. +# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` +# such that there is no pre-allocation. This is a bit conservative (and probably there is room +# for improvement), but if a process needs more memory, it can use more memory on the fly. # -# os.environ["XLA_PYTHON_CLIENT_MEM_FRACTION"] = str(1 / (num_device + 2)) +# os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" # set_device("gpu", num_device=num_device) from mpi4py import MPI diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 93162f48c4..8ddfa8f6e1 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -18,12 +18,11 @@ # ====== Using GPUs ====== # When we have multiple processes using the same devices (for example, 3 processes # using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will -# cause the memory allocation to fail. To avoid this, we can set the memory fraction -# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of -# the GPU memory. This is a bit conservative, but if a process needs more memory, it can -# allocate more memory on the fly. +# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` +# such that there is no pre-allocation. This is a bit conservative (and probably there is room +# for improvement), but if a process needs more memory, it can use more memory on the fly. # -# os.environ["XLA_PYTHON_CLIENT_MEM_FRACTION"] = str(1 / (num_device + 2)) +# os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" # set_device("gpu", num_device=num_device) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 28b97ce0e9..aa7307ab49 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -98,12 +98,11 @@ "# ====== Using GPUs ======\n", "# When we have multiple processes using the same devices (for example, 3 processes\n", "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", - "# cause the memory allocation to fail. To avoid this, we can set the memory fraction\n", - "# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of\n", - "# the GPU memory. This is a bit conservative, but if a process needs more memory, it can\n", - "# allocate more memory on the fly.\n", + "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` \n", + "# such that there is no pre-allocation. This is a bit conservative (and probably there is room \n", + "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", "#\n", - "# os.environ[\"XLA_PYTHON_CLIENT_MEM_FRACTION\"] = str(1 / (num_device + 2))\n", + "# os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", "# set_device(\"gpu\", num_device=num_device)\n", "\n", "from mpi4py import MPI\n", @@ -395,6 +394,7 @@ "metadata": {}, "source": [ "```python\n", + "\n", "import os\n", "import sys\n", "\n", @@ -415,12 +415,11 @@ "# ====== Using GPUs ======\n", "# When we have multiple processes using the same devices (for example, 3 processes\n", "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", - "# cause the memory allocation to fail. To avoid this, we can set the memory fraction\n", - "# to 1/(num_device + 2) which will allow each process to allocate 1/(num_device + 2) of\n", - "# the GPU memory. This is a bit conservative, but if a process needs more memory, it can\n", - "# allocate more memory on the fly.\n", + "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` \n", + "# such that there is no pre-allocation. This is a bit conservative (and probably there is room \n", + "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", "#\n", - "# os.environ[\"XLA_PYTHON_CLIENT_MEM_FRACTION\"] = str(1 / (num_device + 2))\n", + "# os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", "# set_device(\"gpu\", num_device=num_device)\n", "\n", "\n", From c67035d1f3c9c9a0e7ecf1a60f809208ad37f1dc Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 19 May 2025 16:36:23 -0400 Subject: [PATCH 092/199] fix the formatting for good --- docs/notebooks/tutorials/multi_device.ipynb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index aa7307ab49..cdc7abc0b6 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -169,6 +169,7 @@ " )\n", "\n", " # if you put a code here, it will be performed on all ranks\n", + " \n", "```" ] }, @@ -542,6 +543,7 @@ " )\n", "\n", " # if you put a code here, it will be performed on all ranks\n", + " \n", "```" ] }, From e107635abb348d51dc9d6d4b9e3162659ed27fc9 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 20 May 2025 00:42:48 -0400 Subject: [PATCH 093/199] remove repeated jnp.where instead add self._obj_per_rank --- desc/objectives/objective_funs.py | 18 +++++++++--------- desc/optimize/_constraint_wrappers.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index f2c4fffe1f..4023cb637b 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -344,6 +344,9 @@ def __init__( "The maximum value of rank_per_objective is greater than the number " "of ranks. There are not enough ranks to run the objectives.", ) + self._obj_per_rank = [ + np.where(self._rank_per_objective == i)[0] for i in range(self.size) + ] if self._is_mpi and mpi is None: raise ValueError( @@ -409,7 +412,7 @@ def _worker_loop(self): # message[2] is the output (for only jvp's) message = (None, None, None) message = self.comm.bcast(message, root=0) - obj_idx_rank = jnp.where(self._rank_per_objective == self.rank)[0] + obj_idx_rank = self._obj_per_rank[self.rank] if message[0] == "STOP": print(f"Rank {self.rank} STOPPING") break @@ -641,12 +644,9 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 if self._is_mpi and verbose > 0: if self.rank == 0: - objective_ids_per_rank = [ - np.where(self._rank_per_objective == i)[0] for i in range(self.size) - ] objective_names_per_rank = [ [self._objectives[i].__class__.__name__ for i in objective_ids] - for objective_ids in objective_ids_per_rank + for objective_ids in self._obj_per_rank ] print("-" * 60) for rank in range(self.size): @@ -739,7 +739,7 @@ def compute_unscaled(self, x, constants=None): message = ("compute_unscaled", params, None) self.comm.bcast(message, root=0) - obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + obj_idx_rank = self._obj_per_rank[self.rank] print( f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" @@ -789,7 +789,7 @@ def compute_scaled(self, x, constants=None): message = ("compute_scaled", params, None) self.comm.bcast(message, root=0) - obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + obj_idx_rank = self._obj_per_rank[self.rank] print( f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" @@ -839,7 +839,7 @@ def compute_scaled_error(self, x, constants=None): message = ("compute_scaled_error", params, None) self.comm.bcast(message, root=0) - obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + obj_idx_rank = self._obj_per_rank[self.rank] print( f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" @@ -1088,7 +1088,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): message = ("jvp_" + op, xs, vs) self.comm.bcast(message, root=0) - obj_idx_rank = jnp.where(self._rank_per_objective == 0)[0] + obj_idx_rank = self._obj_per_rank[self.rank] print( f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index e9ba9a3675..3f59873426 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1360,7 +1360,7 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): message = ("proximal_jvp_" + op, xgs, vgs) objective.comm.bcast(message, root=0) - obj_idx_rank = jnp.where(objective._rank_per_objective == 0)[0] + obj_idx_rank = objective._obj_per_rank[objective.rank] print( f"Rank {objective.rank} : {message[0]} for objectives ids: {obj_idx_rank}" ) From 1ea7605c27dcddaf9226c4fdacbf95cf3684001d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 20 May 2025 17:54:13 -0400 Subject: [PATCH 094/199] add CPU model name, they have to be the same anyways --- desc/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desc/__init__.py b/desc/__init__.py index 0dd16834b7..7c5dee72ed 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -157,7 +157,10 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 jax_cpu = jax.devices("cpu") assert len(jax_cpu) == num_device - config["devices"] = [f"{dev}" for dev in jax_cpu] + # These CPUs doesn't have to be the same model, but I think slurm will + # always give same model + config["devices"] = [f"{cpu_info + " " + dev}" for dev in jax_cpu] + # This memory is not individual but the total memory config["avail_mems"] = [cpu_mem for _ in range(num_device)] except ModuleNotFoundError: raise ValueError( From 50522ba252d459a6211d69696a2b6887489f6921 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 20 May 2025 17:55:38 -0400 Subject: [PATCH 095/199] has to be string --- desc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/__init__.py b/desc/__init__.py index 7c5dee72ed..b46de001f2 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -159,7 +159,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 assert len(jax_cpu) == num_device # These CPUs doesn't have to be the same model, but I think slurm will # always give same model - config["devices"] = [f"{cpu_info + " " + dev}" for dev in jax_cpu] + config["devices"] = [f"{cpu_info + " " + str(dev)}" for dev in jax_cpu] # This memory is not individual but the total memory config["avail_mems"] = [cpu_mem for _ in range(num_device)] except ModuleNotFoundError: From ebd1af7d31892ee0fc5a37ec75e9c3484dbe3ba2 Mon Sep 17 00:00:00 2001 From: Yigit Gunsur Elmacioglu <102380275+YigitElma@users.noreply.github.com> Date: Tue, 20 May 2025 18:13:59 -0400 Subject: [PATCH 096/199] Update desc/__init__.py --- desc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/__init__.py b/desc/__init__.py index b46de001f2..d281a0e0e0 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -159,7 +159,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 assert len(jax_cpu) == num_device # These CPUs doesn't have to be the same model, but I think slurm will # always give same model - config["devices"] = [f"{cpu_info + " " + str(dev)}" for dev in jax_cpu] + config["devices"] = [f"{cpu_info + ' ' + str(dev)}" for dev in jax_cpu] # This memory is not individual but the total memory config["avail_mems"] = [cpu_mem for _ in range(num_device)] except ModuleNotFoundError: From 50e5239ed41ba173a4814226329eb5d8dee2b50b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 01:45:28 -0400 Subject: [PATCH 097/199] make the CPU initialization automatic --- desc/__init__.py | 27 +++- desc/backend.py | 12 +- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 3 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 3 +- docs/notebooks/tutorials/multi_device.ipynb | 118 +++++++++--------- 5 files changed, 94 insertions(+), 69 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index d281a0e0e0..1d6e10d602 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -82,7 +82,7 @@ def _get_processor_name(): def _set_cpu_count(n): - """Set the number of CPUs visible to JAX. + """Divide 1 physical CPU into multiple virtual CPUs. By default, JAX sees the whole CPU as a single device, regardless of the number of cores or threads. It then uses multiple cores and threads for lower level @@ -93,6 +93,8 @@ def _set_cpu_count(n): multiple objective functions.) This function is mainly for testing on CI purposes of the parallelism in DESC. + It won't use multiple CPUs even if there are multiple CPUs available on the + machine. It will just divide the first CPU into multiple virtual CPUs. Parameters ---------- @@ -155,10 +157,29 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 try: import jax + # by default, Jax only sees 1 CPU (host), to make it see other CPUs that + # are being used by the same MPI process, we need to initialize the + # distributed environment. This is not needed if we are in GPU mode + # (we have another syntax for device_id etc). By seeing other CPUs, our + # logic for moving objectives to different devices will work correctly. + # Note that this is different from _set_cpu_count which is for testing + # purposes only by emulating multiple CPUs. + if ( + os.environ.get("XLA_FLAGS", "").find( + "--xla_force_host_platform_device_count=" + ) + == -1 + ): + # this condition basically detects if there are actual CPUs + # or fake ones created by _set_cpu_count + # Fake CPUs are already visible to JAX + jax.distributed.initialize() + jax_cpu = jax.devices("cpu") assert len(jax_cpu) == num_device - # These CPUs doesn't have to be the same model, but I think slurm will - # always give same model + # These CPUs might not be the same model, but I think slurm will + # always give same model (and getting model of each CPU is not + # straightforward) config["devices"] = [f"{cpu_info + ' ' + str(dev)}" for dev in jax_cpu] # This memory is not individual but the total memory config["avail_mems"] = [cpu_mem for _ in range(num_device)] diff --git a/desc/backend.py b/desc/backend.py index 935b068dc7..e813f1b64b 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -52,7 +52,7 @@ def print_backend_info(): if use_jax: print( f"Using JAX backend: jax version={jax.__version__}, " - + f"jaxlib version={jaxlib.__version__}, dtype={y.dtype}." + f"jaxlib version={jaxlib.__version__}, dtype={y.dtype}." ) else: print(f"Using NumPy backend: version={np.__version__}, dtype={y.dtype}.") @@ -63,12 +63,12 @@ def print_backend_info(): "GB available memory" ) elif desc_config["kind"] == "cpu": - print(f"Using {desc_config['num_device']} CPUs:") + print( + f"Using {desc_config['num_device']} CPUs with " + + f"{desc_config['avail_mems'][0]:.2f} GB total available memory:" + ) for i, dev in enumerate(desc_config["devices"]): - print( - f"\t CPU {i}: {dev} with {desc_config['avail_mems'][i]:.2f} " - "GB available memory" - ) + print(f"\t CPU {i}: {dev}") if desc_config["kind"] == "gpu": print( diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index e2ba4c4f85..907a2a2a44 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -11,7 +11,8 @@ num_device = 4 # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices -# If you have multiple CPUs, you don't need to call `_set_cpu_count` + +# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! _set_cpu_count(num_device) set_device("cpu", num_device=num_device) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 8ddfa8f6e1..3fc34fa644 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -11,7 +11,8 @@ num_device = 2 # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices -# If you have multiple CPUs, you don't need to call `_set_cpu_count` + +# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! _set_cpu_count(num_device) set_device("cpu", num_device=num_device) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index cdc7abc0b6..8e9180571c 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -49,20 +49,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.14.2+102.g199b09f73.dirty.\n", + "DESC version=0.14.2+132.g76de8db8c.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.26 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.26 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.26 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 8.26 GB available memory\n" + "Using 4 CPUs with 9.83 GB total available memory:\n", + "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", + "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", + "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_2\n", + "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_3\n" ] } ], @@ -91,7 +91,8 @@ "num_device = 4\n", "# These will be used for diving the single CPU into multiple virtual CPUs\n", "# such that JAX and XLA thinks there are multiple devices\n", - "# If you have multiple CPUs, you don't need to call `_set_cpu_count`\n", + "\n", + "# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", "_set_cpu_count(num_device)\n", "set_device(\"cpu\", num_device=num_device)\n", "\n", @@ -190,21 +191,21 @@ "name": "stdout", "output_type": "stream", "text": [ + "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "\n", "====== TOTAL OF 4 RANKS ======\n", "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+102.g199b09f73.dirty.\n", + "DESC version=0.14.2+132.g76de8db8c.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.15 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.15 GB available memory\n", - "\t CPU 2: TFRT_CPU_2 with 8.15 GB available memory\n", - "\t CPU 3: TFRT_CPU_3 with 8.15 GB available memory\n", + "Using 4 CPUs with 9.70 GB total available memory:\n", + "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", + "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", + "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_2\n", + "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_3\n", "\n", "\n", - "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", @@ -215,14 +216,16 @@ "\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Putting objective force on device 2\n", + "Putting objective force on device 1\n", + "Building objective: force\n", + "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", + "Putting objective force on device 2\n", "Building objective: force\n", "Precomputing transforms\n", "Putting objective force on device 3\n", @@ -232,8 +235,6 @@ "Rank 2 will run objective(s): ['ForceBalance']\n", "Rank 3 will run objective(s): ['ForceBalance']\n", "------------------------------------------------------------\n", - "Building objective: force\n", - "Precomputing transforms\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed Psi\n", @@ -249,7 +250,7 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.74 sec\n", + "Timer: Objective build = 1.73 sec\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", @@ -262,17 +263,17 @@ "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: LinearConstraintProjection build = 6.62 sec\n", + "Timer: LinearConstraintProjection build = 6.43 sec\n", "Number of parameters: 551\n", "Number of objectives: 8424\n", - "Timer: Initializing the optimization = 8.43 sec\n", + "Timer: Initializing the optimization = 8.23 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", @@ -283,40 +284,40 @@ " 0 1 2.500e+00 1.228e+00 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 1 2 8.226e-01 1.678e+00 2.256e-01 5.198e-01 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 2 3 2.473e-02 7.978e-01 1.993e-01 7.096e-02 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 3 5 3.451e-03 2.128e-02 8.908e-02 3.526e-02 \n", "Warning: Maximum number of iterations has been exceeded.\n", @@ -325,8 +326,8 @@ " Iterations: 3\n", " Function evaluations: 5\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 36.6 sec\n", - "Timer: Avg time per step = 9.16 sec\n", + "Timer: Solution time = 33.6 sec\n", + "Timer: Avg time per step = 8.42 sec\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", @@ -373,8 +374,8 @@ "==============================================================================================================\n", "\n", "Rank 1 STOPPING\n", - "Rank 3 STOPPING\n", - "Rank 2 STOPPING\n" + "Rank 2 STOPPING\n", + "Rank 3 STOPPING\n" ] } ], @@ -409,7 +410,8 @@ "num_device = 2\n", "# These will be used for diving the single CPU into multiple virtual CPUs\n", "# such that JAX and XLA thinks there are multiple devices\n", - "# If you have multiple CPUs, you don't need to call `_set_cpu_count`\n", + "\n", + "# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", "_set_cpu_count(num_device)\n", "set_device(\"cpu\", num_device=num_device)\n", "\n", @@ -560,55 +562,55 @@ "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+102.g199b09f73.dirty.\n", + "DESC version=0.14.2+132.g76de8db8c.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 2 CPUs:\n", - "\t CPU 0: TFRT_CPU_0 with 8.13 GB available memory\n", - "\t CPU 1: TFRT_CPU_1 with 8.13 GB available memory\n", + "Using 2 CPUs with 10.29 GB total available memory:\n", + "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", + "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", "\n", "\n", "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.54 sec\n", + "Timer: Precomputing transforms = 1.45 sec\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.37 sec\n", + "Timer: Precomputing transforms = 1.36 sec\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.37 sec\n", + "Timer: Precomputing transforms = 1.27 sec\n", "------------------------------------------------------------\n", "Rank 0 will run objective(s): ['QuasisymmetryTwoTerm', 'AspectRatio']\n", "Rank 1 will run objective(s): ['QuasisymmetryTwoTerm']\n", "------------------------------------------------------------\n", - "Timer: Objective build = 5.35 sec\n", + "Timer: Objective build = 5.11 sec\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 2.00 sec\n", - "Timer: Objective build = 2.10 sec\n", - "Timer: Objective build = 2.37 ms\n", - "Timer: Eq Update LinearConstraintProjection build = 5.87 sec\n", - "Timer: Proximal projection build = 10.9 sec\n", + "Timer: Precomputing transforms = 1.91 sec\n", + "Timer: Objective build = 2.00 sec\n", + "Timer: Objective build = 2.36 ms\n", + "Timer: Eq Update LinearConstraintProjection build = 5.63 sec\n", + "Timer: Proximal projection build = 10.4 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 910 ms\n", - "Timer: LinearConstraintProjection build = 2.09 sec\n", + "Timer: Objective build = 849 ms\n", + "Timer: LinearConstraintProjection build = 1.99 sec\n", "Number of parameters: 8\n", "Number of objectives: 631\n", - "Timer: Initializing the optimization = 14.0 sec\n", + "Timer: Initializing the optimization = 13.3 sec\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 2.005e+04 1.926e+02 \n", @@ -628,8 +630,8 @@ "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", " 2 5 2.617e+03 5.507e+03 5.877e-02 6.065e+01 \n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", @@ -648,8 +650,8 @@ " Iterations: 3\n", " Function evaluations: 7\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 1.28 min\n", - "Timer: Avg time per step = 19.3 sec\n", + "Timer: Solution time = 1.23 min\n", + "Timer: Avg time per step = 18.5 sec\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "==============================================================================================================\n", @@ -742,7 +744,7 @@ "\n", "# each node will see 3 GPUs\n", "num_device = 3\n", - "os.environ[\"XLA_PYTHON_CLIENT_MEM_FRACTION\"] = str(1 / (num_device + 2))\n", + "os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", "set_device(\"gpu\", num_device=num_device)\n", "\n", "\n", From 464a11572e86c7b709e43d5924d24a0dc5b36f40 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 12:40:31 -0400 Subject: [PATCH 098/199] add error for 1 device case with MPI, fix syntax --- desc/objectives/objective_funs.py | 6 ++++++ docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py | 4 ++-- docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py | 4 ++-- docs/notebooks/tutorials/multi_device.ipynb | 8 ++++---- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 4023cb637b..4ca6eb9886 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -347,6 +347,12 @@ def __init__( self._obj_per_rank = [ np.where(self._rank_per_objective == i)[0] for i in range(self.size) ] + errorif( + np.unique(self._rank_per_objective).size == 1, + ValueError, + "There is only one rank. You cannot use MPI for this case. Call " + "ObjectiveFunction with `mpi=None`.", + ) if self._is_mpi and mpi is None: raise ValueError( diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 907a2a2a44..3e55410f42 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -50,8 +50,8 @@ # to see which devices are available to each process. if desc_config["kind"] == "gpu": print( - f"Rank {rank} is running on {jax.local_devices(backend="gpu")} " - f"and {jax.local_devices(backend="cpu")}\n" + f"Rank {rank} is running on {jax.local_devices(backend='gpu')} " + f"and {jax.local_devices(backend='cpu')}\n" ) else: print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 3fc34fa644..51b7a2639c 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -61,8 +61,8 @@ # to see which devices are available to each process. if desc_config["kind"] == "gpu": print( - f"Rank {rank} is running on {jax.local_devices(backend="gpu")} " - f"and {jax.local_devices(backend="cpu")}\n" + f"Rank {rank} is running on {jax.local_devices(backend='gpu')} " + f"and {jax.local_devices(backend='cpu')}\n" ) else: print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 8e9180571c..41915e0110 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -130,8 +130,8 @@ " # to see which devices are available to each process.\n", " if desc_config[\"kind\"] == \"gpu\":\n", " print(\n", - " f\"Rank {rank} is running on {jax.local_devices(backend=\"gpu\")} \"\n", - " f\"and {jax.local_devices(backend=\"cpu\")}\\n\"\n", + " f\"Rank {rank} is running on {jax.local_devices(backend='gpu')} \"\n", + " f\"and {jax.local_devices(backend='cpu')}\\n\"\n", " )\n", " else:\n", " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\\n\")\n", @@ -460,8 +460,8 @@ " # to see which devices are available to each process.\n", " if desc_config[\"kind\"] == \"gpu\":\n", " print(\n", - " f\"Rank {rank} is running on {jax.local_devices(backend=\"gpu\")} \"\n", - " f\"and {jax.local_devices(backend=\"cpu\")}\\n\"\n", + " f\"Rank {rank} is running on {jax.local_devices(backend='gpu')} \"\n", + " f\"and {jax.local_devices(backend='cpu')}\\n\"\n", " )\n", " else:\n", " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\\n\")\n", From 834e9cf0dca42b6cdb2e181db5f280b316cb0f6b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 17:57:17 -0400 Subject: [PATCH 099/199] fixes for multiple CPU data transfer --- desc/objectives/objective_funs.py | 8 ++++++-- docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 4ca6eb9886..fe1e8cde7f 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1488,9 +1488,13 @@ def __init__( # _device to a jaxlib.xla_extension.Device type, jit will throw error expecting # it to be static. So we set _device to None in that case which is simpler then # making it static. - if device_id != 0: - self._device = jax.devices(desc_config["kind"])[device_id] + if device_id != 0 and desc_config["kind"] == "gpu": + # jax.device_put cannot put data to other CPUs + # it can only transfer data to local devices + self._device = jax.local_devices("gpu")[device_id] else: + # we won't transfer data for multiple CPUs because their rank should + # already have that data. This is annoying but during hackathon can be fixed self._device = None self._target = target diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 3e55410f42..219e0110e3 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -8,7 +8,7 @@ from desc import _set_cpu_count, set_device # ====== Using CPUs ====== -num_device = 4 +num_device = 2 # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices From 65ac31fae6b31a4cd619d1daa3b5b388c07c4247 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 18:21:17 -0400 Subject: [PATCH 100/199] don't try to initialize twice --- desc/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desc/__init__.py b/desc/__init__.py index 1d6e10d602..529b69772c 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -156,6 +156,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 else: try: import jax + from jax._src import xla_bridge # by default, Jax only sees 1 CPU (host), to make it see other CPUs that # are being used by the same MPI process, we need to initialize the @@ -169,6 +170,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 "--xla_force_host_platform_device_count=" ) == -1 + and not xla_bridge.backends_are_initialized() ): # this condition basically detects if there are actual CPUs # or fake ones created by _set_cpu_count From d62a832a723cda2d0f448ae320822ee301604edd Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 18:53:55 -0400 Subject: [PATCH 101/199] test --- desc/__init__.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 529b69772c..84fbae48aa 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -156,7 +156,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 else: try: import jax - from jax._src import xla_bridge + # from jax._src import xla_bridge # by default, Jax only sees 1 CPU (host), to make it see other CPUs that # are being used by the same MPI process, we need to initialize the @@ -165,20 +165,22 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 # logic for moving objectives to different devices will work correctly. # Note that this is different from _set_cpu_count which is for testing # purposes only by emulating multiple CPUs. - if ( - os.environ.get("XLA_FLAGS", "").find( - "--xla_force_host_platform_device_count=" - ) - == -1 - and not xla_bridge.backends_are_initialized() - ): - # this condition basically detects if there are actual CPUs - # or fake ones created by _set_cpu_count - # Fake CPUs are already visible to JAX - jax.distributed.initialize() - - jax_cpu = jax.devices("cpu") - assert len(jax_cpu) == num_device + # if ( + # os.environ.get("XLA_FLAGS", "").find( + # "--xla_force_host_platform_device_count=" + # ) + # == -1 + # and not xla_bridge.backends_are_initialized() + # ): + # # this condition basically detects if there are actual CPUs + # # or fake ones created by _set_cpu_count + # # Fake CPUs are already visible to JAX + # jax.distributed.initialize() + + jax_cpu = [ + str(jax.local_devices("cpu")[0]) + i for i in range(num_device) + ] + # assert len(jax_cpu) == num_device # These CPUs might not be the same model, but I think slurm will # always give same model (and getting model of each CPU is not # straightforward) From 6b5e1f28bfdad6f35ddb8b45050ebe4898608395 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 18:55:56 -0400 Subject: [PATCH 102/199] test --- desc/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 84fbae48aa..e1b3ad719d 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -177,9 +177,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 # # Fake CPUs are already visible to JAX # jax.distributed.initialize() - jax_cpu = [ - str(jax.local_devices("cpu")[0]) + i for i in range(num_device) - ] + jax_cpu = [str(jax.devices("cpu")[0]) + i for i in range(num_device)] # assert len(jax_cpu) == num_device # These CPUs might not be the same model, but I think slurm will # always give same model (and getting model of each CPU is not From 8f30c380b9c197475bbb3f0fe3226dbe3c146b68 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 18:57:03 -0400 Subject: [PATCH 103/199] test --- desc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/__init__.py b/desc/__init__.py index e1b3ad719d..d86354a7ac 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -177,7 +177,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 # # Fake CPUs are already visible to JAX # jax.distributed.initialize() - jax_cpu = [str(jax.devices("cpu")[0]) + i for i in range(num_device)] + jax_cpu = [jax.devices("cpu") for _ in range(num_device)] # assert len(jax_cpu) == num_device # These CPUs might not be the same model, but I think slurm will # always give same model (and getting model of each CPU is not From 023df017d3bb83864c21e5b964fe442163e92e19 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 19:27:03 -0400 Subject: [PATCH 104/199] add distributed initilize back, update error for empty ranks --- desc/__init__.py | 31 +++--- desc/objectives/objective_funs.py | 8 +- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 6 +- docs/notebooks/tutorials/multi_device.ipynb | 94 +++++++++---------- 4 files changed, 67 insertions(+), 72 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index d86354a7ac..e8554672c5 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -156,7 +156,7 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 else: try: import jax - # from jax._src import xla_bridge + from jax._src import xla_bridge # by default, Jax only sees 1 CPU (host), to make it see other CPUs that # are being used by the same MPI process, we need to initialize the @@ -165,20 +165,21 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 # logic for moving objectives to different devices will work correctly. # Note that this is different from _set_cpu_count which is for testing # purposes only by emulating multiple CPUs. - # if ( - # os.environ.get("XLA_FLAGS", "").find( - # "--xla_force_host_platform_device_count=" - # ) - # == -1 - # and not xla_bridge.backends_are_initialized() - # ): - # # this condition basically detects if there are actual CPUs - # # or fake ones created by _set_cpu_count - # # Fake CPUs are already visible to JAX - # jax.distributed.initialize() - - jax_cpu = [jax.devices("cpu") for _ in range(num_device)] - # assert len(jax_cpu) == num_device + if ( + os.environ.get("XLA_FLAGS", "").find( + "--xla_force_host_platform_device_count=" + ) + == -1 + and not xla_bridge.backends_are_initialized() + ): + print("Initializing JAX distributed environment ...") + # this condition basically detects if there are actual CPUs + # or fake ones created by _set_cpu_count + # Fake CPUs are already visible to JAX + jax.distributed.initialize() + + jax_cpu = jax.devices("cpu") + assert len(jax_cpu) == num_device # These CPUs might not be the same model, but I think slurm will # always give same model (and getting model of each CPU is not # straightforward) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index fe1e8cde7f..f2e15741d8 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -339,10 +339,12 @@ def __init__( "on node 1 is not same as device_id=0 on node 2. ", ) errorif( - max(self._rank_per_objective) > self.size, + max(self._rank_per_objective) != self.size - 1, ValueError, - "The maximum value of rank_per_objective is greater than the number " - "of ranks. There are not enough ranks to run the objectives.", + "The maximum value of rank_per_objective " + f"({max(self._rank_per_objective)+1}) " + f"is not equal to the number of ranks ({self.size}). There " + "should be at least 1 objective per rank.", ) self._obj_per_rank = [ np.where(self._rank_per_objective == i)[0] for i in range(self.size) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 219e0110e3..e8a58821f6 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -8,7 +8,7 @@ from desc import _set_cpu_count, set_device # ====== Using CPUs ====== -num_device = 2 +num_device = 4 # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices @@ -50,11 +50,11 @@ # to see which devices are available to each process. if desc_config["kind"] == "gpu": print( - f"Rank {rank} is running on {jax.local_devices(backend='gpu')} " + f"Rank {rank} can see {jax.local_devices(backend='gpu')} " f"and {jax.local_devices(backend='cpu')}\n" ) else: - print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") + print(f"Rank {rank} can see {jax.local_devices(backend='cpu')}\n") if rank == 0: print(f"====== BACKEND INFO ======") print_backend_info() diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 41915e0110..2dd737829c 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -56,9 +56,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.14.2+132.g76de8db8c.dirty.\n", + "DESC version=0.14.2+139.g8f30c380b.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs with 9.83 GB total available memory:\n", + "Using 4 CPUs with 9.06 GB total available memory:\n", "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_2\n", @@ -176,43 +176,35 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/yigit/miniconda3/envs/mpi/lib/python3.12/pty.py:95: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n", - " pid, fd = os.forkpty()\n" - ] - }, { "name": "stdout", "output_type": "stream", "text": [ - "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "\n", "====== TOTAL OF 4 RANKS ======\n", - "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "Rank 0 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+132.g76de8db8c.dirty.\n", + "DESC version=0.14.2+139.g8f30c380b.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs with 9.70 GB total available memory:\n", + "Using 4 CPUs with 8.97 GB total available memory:\n", "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_2\n", "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_3\n", "\n", "\n", + "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Rank 2 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "Rank 3 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", - "Rank 3 is running on [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "Rank 2 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", "Building objective: force\n", "Precomputing transforms\n", @@ -239,18 +231,18 @@ "Building objective: lcfs Z\n", "Building objective: fixed Psi\n", "Building objective: fixed pressure\n", + "Building objective: fixed iota\n", + "Building objective: fixed sheet current\n", + "Building objective: self_consistency R\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: fixed iota\n", - "Building objective: fixed sheet current\n", - "Building objective: self_consistency R\n", "Building objective: self_consistency Z\n", "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.73 sec\n", + "Timer: Objective build = 1.74 sec\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", @@ -259,26 +251,26 @@ "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", + "Timer: LinearConstraintProjection build = 6.60 sec\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Timer: LinearConstraintProjection build = 6.43 sec\n", "Number of parameters: 551\n", "Number of objectives: 8424\n", - "Timer: Initializing the optimization = 8.23 sec\n", + "Timer: Initializing the optimization = 8.41 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 2.500e+00 1.228e+00 \n", @@ -288,36 +280,36 @@ "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 1 2 8.226e-01 1.678e+00 2.256e-01 5.198e-01 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", " 2 3 2.473e-02 7.978e-01 1.993e-01 7.096e-02 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", " 3 5 3.451e-03 2.128e-02 8.908e-02 3.526e-02 \n", "Warning: Maximum number of iterations has been exceeded.\n", @@ -326,14 +318,14 @@ " Iterations: 3\n", " Function evaluations: 5\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 33.6 sec\n", - "Timer: Avg time per step = 8.42 sec\n", + "Timer: Solution time = 34.5 sec\n", + "Timer: Avg time per step = 8.63 sec\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "==============================================================================================================\n", " Start --> End\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", @@ -374,8 +366,8 @@ "==============================================================================================================\n", "\n", "Rank 1 STOPPING\n", - "Rank 2 STOPPING\n", - "Rank 3 STOPPING\n" + "Rank 3 STOPPING\n", + "Rank 2 STOPPING\n" ] } ], @@ -562,9 +554,9 @@ "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+132.g76de8db8c.dirty.\n", + "DESC version=0.14.2+139.g8f30c380b.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 2 CPUs with 10.29 GB total available memory:\n", + "Using 2 CPUs with 9.41 GB total available memory:\n", "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", "\n", @@ -573,33 +565,33 @@ "\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.45 sec\n", + "Timer: Precomputing transforms = 1.44 sec\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.36 sec\n", + "Timer: Precomputing transforms = 1.34 sec\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.27 sec\n", + "Timer: Precomputing transforms = 1.29 sec\n", "------------------------------------------------------------\n", "Rank 0 will run objective(s): ['QuasisymmetryTwoTerm', 'AspectRatio']\n", "Rank 1 will run objective(s): ['QuasisymmetryTwoTerm']\n", "------------------------------------------------------------\n", - "Timer: Objective build = 5.11 sec\n", + "Timer: Objective build = 5.22 sec\n", "Building objective: force\n", "Precomputing transforms\n", "Timer: Precomputing transforms = 1.91 sec\n", "Timer: Objective build = 2.00 sec\n", - "Timer: Objective build = 2.36 ms\n", - "Timer: Eq Update LinearConstraintProjection build = 5.63 sec\n", + "Timer: Objective build = 2.35 ms\n", + "Timer: Eq Update LinearConstraintProjection build = 5.62 sec\n", "Timer: Proximal projection build = 10.4 sec\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 849 ms\n", - "Timer: LinearConstraintProjection build = 1.99 sec\n", + "Timer: Objective build = 857 ms\n", + "Timer: LinearConstraintProjection build = 1.97 sec\n", "Number of parameters: 8\n", "Number of objectives: 631\n", "Timer: Initializing the optimization = 13.3 sec\n", From a8027a35e4dd5964adf2c6d93d052308a502100b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 20:13:57 -0400 Subject: [PATCH 105/199] distributed initialize will require bunch of additional code since it tries to put each array to a single CPU that is remote to some other processes and hence error out. This removes distributed initialize. Updates some error cases --- desc/__init__.py | 29 +-------- desc/objectives/objective_funs.py | 36 ++++------- docs/notebooks/tutorials/multi_device.ipynb | 68 ++++++++++----------- 3 files changed, 48 insertions(+), 85 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index e8554672c5..bc223cc7f6 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -155,35 +155,12 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 config["avail_mems"] = [cpu_mem] else: try: - import jax - from jax._src import xla_bridge - - # by default, Jax only sees 1 CPU (host), to make it see other CPUs that - # are being used by the same MPI process, we need to initialize the - # distributed environment. This is not needed if we are in GPU mode - # (we have another syntax for device_id etc). By seeing other CPUs, our - # logic for moving objectives to different devices will work correctly. - # Note that this is different from _set_cpu_count which is for testing - # purposes only by emulating multiple CPUs. - if ( - os.environ.get("XLA_FLAGS", "").find( - "--xla_force_host_platform_device_count=" - ) - == -1 - and not xla_bridge.backends_are_initialized() - ): - print("Initializing JAX distributed environment ...") - # this condition basically detects if there are actual CPUs - # or fake ones created by _set_cpu_count - # Fake CPUs are already visible to JAX - jax.distributed.initialize() - - jax_cpu = jax.devices("cpu") - assert len(jax_cpu) == num_device # These CPUs might not be the same model, but I think slurm will # always give same model (and getting model of each CPU is not # straightforward) - config["devices"] = [f"{cpu_info + ' ' + str(dev)}" for dev in jax_cpu] + config["devices"] = [ + f"{cpu_info + ' ' + str(i)}" for i in range(num_device) + ] # This memory is not individual but the total memory config["avail_mems"] = [cpu_mem for _ in range(num_device)] except ModuleNotFoundError: diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index f2e15741d8..0fd92b190b 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -290,20 +290,19 @@ def __init__( if mpi is not None: # for multiple node cases, each process sees 1 CPU, for those cases, # we cannot put objectives to different devices. Instead, we will - # run each objective on a different rank. That is also why we will - # run 1 objective per process. - # TODO: add an argument for node of the objective. For example, let's say - # we have 3 nodes and 4 GPUs per node. First of all there should be 12 - # objectives in total. We should create 4 processes per node and each - # process should run 1 objective. This way we can utilize all the GPUs. - # Alternatively, we can specify the rank for the objective. This way, we - # can have multiple objectives on the same rank. + # run each objective on the given rank. self._is_mpi = True self._rank_per_objective = ( rank_per_objective if rank_per_objective is not None else np.arange(len(objectives)) ) + errorif( + np.unique(self._rank_per_objective).size == 1, + ValueError, + "There is only one rank. You cannot use MPI for this case. Call " + "ObjectiveFunction with `mpi=None`.", + ) errorif( ( np.mod(self._rank_per_objective, max(device_ids) + 1) != device_ids @@ -325,19 +324,6 @@ def __init__( self.rank = self.comm.Get_rank() self.size = self.comm.Get_size() self.running = True - warnif( - ( - np.unique(device_ids, return_counts=True)[1] - < self.size // desc_config["num_device"] - ).any(), - UserWarning, - "You are not using all the devices available. You asked for " - f"{self.size // desc_config['num_device']} nodes, but there are " - f"{np.unique(device_ids, return_counts=True)[1]} objectives per " - "same device_id. Note that for multiple nodes, each node has same " - "number of devices and their indices start from 0. So, device_id=0 " - "on node 1 is not same as device_id=0 on node 2. ", - ) errorif( max(self._rank_per_objective) != self.size - 1, ValueError, @@ -350,10 +336,10 @@ def __init__( np.where(self._rank_per_objective == i)[0] for i in range(self.size) ] errorif( - np.unique(self._rank_per_objective).size == 1, + np.array([foo == [] for foo in self._obj_per_rank]).any(), ValueError, - "There is only one rank. You cannot use MPI for this case. Call " - "ObjectiveFunction with `mpi=None`.", + "There is at least one rank that does not have any objective assigned. " + f"Objectives per rank are {self._obj_per_rank}.", ) if self._is_mpi and mpi is None: @@ -1496,7 +1482,7 @@ def __init__( self._device = jax.local_devices("gpu")[device_id] else: # we won't transfer data for multiple CPUs because their rank should - # already have that data. This is annoying but during hackathon can be fixed + # already have that data. self._device = None self._target = target diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 2dd737829c..51dd63b7bc 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -56,13 +56,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "DESC version=0.14.2+139.g8f30c380b.dirty.\n", + "DESC version=0.14.2+140.g023df017d.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs with 9.06 GB total available memory:\n", - "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", - "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", - "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_2\n", - "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_3\n" + "Using 4 CPUs with 8.66 GB total available memory:\n", + "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U 0\n", + "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U 1\n", + "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U 2\n", + "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U 3\n" ] } ], @@ -176,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -187,13 +187,13 @@ "Rank 0 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+139.g8f30c380b.dirty.\n", + "DESC version=0.14.2+140.g023df017d.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs with 8.97 GB total available memory:\n", - "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", - "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", - "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_2\n", - "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_3\n", + "Using 4 CPUs with 8.73 GB total available memory:\n", + "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U 0\n", + "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U 1\n", + "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U 2\n", + "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U 3\n", "\n", "\n", "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", @@ -208,15 +208,9 @@ "\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", "Putting objective force on device 2\n", "Building objective: force\n", "Precomputing transforms\n", @@ -228,6 +222,12 @@ "Rank 3 will run objective(s): ['ForceBalance']\n", "------------------------------------------------------------\n", "Building objective: lcfs R\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", "Building objective: lcfs Z\n", "Building objective: fixed Psi\n", "Building objective: fixed pressure\n", @@ -236,34 +236,34 @@ "Building objective: self_consistency R\n", "Building objective: force\n", "Precomputing transforms\n", + "Building objective: self_consistency Z\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: self_consistency Z\n", "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.74 sec\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", + "Timer: Objective build = 1.78 sec\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: LinearConstraintProjection build = 6.60 sec\n", "Building objective: force\n", "Precomputing transforms\n", + "Timer: LinearConstraintProjection build = 6.77 sec\n", "Number of parameters: 551\n", "Number of objectives: 8424\n", - "Timer: Initializing the optimization = 8.41 sec\n", + "Timer: Initializing the optimization = 8.62 sec\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", @@ -280,9 +280,9 @@ "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 1 2 8.226e-01 1.678e+00 2.256e-01 5.198e-01 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", @@ -291,8 +291,8 @@ "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", " 2 3 2.473e-02 7.978e-01 1.993e-01 7.096e-02 \n", @@ -301,8 +301,8 @@ "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", @@ -318,8 +318,8 @@ " Iterations: 3\n", " Function evaluations: 5\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 34.5 sec\n", - "Timer: Avg time per step = 8.63 sec\n", + "Timer: Solution time = 34.4 sec\n", + "Timer: Avg time per step = 8.60 sec\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", From 646e037f2ae654752429acc45000b3fa1a1e29a7 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 21:36:34 -0400 Subject: [PATCH 106/199] fix error check --- desc/objectives/objective_funs.py | 2 +- docs/notebooks/tutorials/multi_device.ipynb | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 0fd92b190b..edfaa6b6a2 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -336,7 +336,7 @@ def __init__( np.where(self._rank_per_objective == i)[0] for i in range(self.size) ] errorif( - np.array([foo == [] for foo in self._obj_per_rank]).any(), + np.array([foo.size == 0 for foo in self._obj_per_rank]).any(), ValueError, "There is at least one rank that does not have any objective assigned. " f"Objectives per rank are {self._obj_per_rank}.", diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 51dd63b7bc..a85253543d 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -718,8 +718,14 @@ "export OMP_NUM_THREADS=$SLURM_CPUS_PER_TASK\n", "export SRUN_CPUS_PER_TASK=$SLURM_CPUS_PER_TASK\n", "module purge\n", - "module load intel/2022.2.0\n", - "module load intel-mpi/intel/2021.7.0\n", + "\n", + "# module names and version might be different for clusters\n", + "module load anaconda3/2024.6\n", + "module load openmpi/gcc/4.1.6\n", + "\n", + "# activate the environment that has DESC requirements\n", + "# as well as proper mpi4py installation\n", + "conda activate mpi-env\n", "\n", "srun python your-script.py\n", "\n", From b14571a2008debb572c5738aec64db9cae9359d4 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 22:34:17 -0400 Subject: [PATCH 107/199] gather cpu infor ranks by MPI --- desc/__init__.py | 19 ++- desc/backend.py | 4 +- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 9 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 7 +- docs/notebooks/tutorials/multi_device.ipynb | 133 +++++++----------- 5 files changed, 73 insertions(+), 99 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index bc223cc7f6..d8f6275588 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -115,7 +115,7 @@ def _set_cpu_count(n): ) -def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 +def set_device(kind="cpu", gpuid=None, num_device=1, mpi=None): # noqa: C901 """Sets the device to use for computation. If kind==``'gpu'`` and a gpuid is specified, uses the specified GPU. If @@ -155,12 +155,23 @@ def set_device(kind="cpu", gpuid=None, num_device=1): # noqa: C901 config["avail_mems"] = [cpu_mem] else: try: + if mpi is None: + warnings.warn( + "To get the fu list of CPUs, provide the MPI communicator.", + UserWarning, + ) + cpu_names = [ + f"{str(i) + ' ' + cpu_info}" for i in range(num_device) + ] + else: + comm = mpi.COMM_WORLD + rank = comm.Get_rank() + cpu_name = f"{str(rank) + ' ' + cpu_info}" + cpu_names = comm.allgather(cpu_name) # These CPUs might not be the same model, but I think slurm will # always give same model (and getting model of each CPU is not # straightforward) - config["devices"] = [ - f"{cpu_info + ' ' + str(i)}" for i in range(num_device) - ] + config["devices"] = [name for name in cpu_names] # This memory is not individual but the total memory config["avail_mems"] = [cpu_mem for _ in range(num_device)] except ModuleNotFoundError: diff --git a/desc/backend.py b/desc/backend.py index e813f1b64b..40cf13fc28 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -68,7 +68,7 @@ def print_backend_info(): + f"{desc_config['avail_mems'][0]:.2f} GB total available memory:" ) for i, dev in enumerate(desc_config["devices"]): - print(f"\t CPU {i}: {dev}") + print(f"\t CPU : {dev}") if desc_config["kind"] == "gpu": print( @@ -78,7 +78,7 @@ def print_backend_info(): print(f"Using {desc_config['num_device']} device:") for i, dev in enumerate(desc_config["devices"]): print( - f"\t Device {i}: {dev} with {desc_config['avail_mems'][i]:.2f} " + f"\t Device : {dev} with {desc_config['avail_mems'][i]:.2f} " "GB available memory" ) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index e8a58821f6..c3d8e41bde 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -5,6 +5,8 @@ sys.path.insert(0, os.path.abspath(".")) sys.path.append(os.path.abspath("../../../")) +from mpi4py import MPI + from desc import _set_cpu_count, set_device # ====== Using CPUs ====== @@ -14,7 +16,7 @@ # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! _set_cpu_count(num_device) -set_device("cpu", num_device=num_device) +set_device("cpu", num_device=num_device, mpi=MPI) # ====== Using GPUs ====== # When we have multiple processes using the same devices (for example, 3 processes @@ -26,8 +28,6 @@ # os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" # set_device("gpu", num_device=num_device) -from mpi4py import MPI - from desc import config as desc_config from desc.backend import jax, print_backend_info from desc.examples import get @@ -55,8 +55,9 @@ ) else: print(f"Rank {rank} can see {jax.local_devices(backend='cpu')}\n") + if rank == 0: - print(f"====== BACKEND INFO ======") + print("====== BACKEND INFO ======") print_backend_info() print("\n") diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 51b7a2639c..8a39ecc6f4 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -5,6 +5,8 @@ sys.path.insert(0, os.path.abspath(".")) sys.path.append(os.path.abspath("../../../")) +from mpi4py import MPI + from desc import _set_cpu_count, set_device # ====== Using CPUs ====== @@ -14,7 +16,7 @@ # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! _set_cpu_count(num_device) -set_device("cpu", num_device=num_device) +set_device("cpu", num_device=num_device, mpi=MPI) # ====== Using GPUs ====== # When we have multiple processes using the same devices (for example, 3 processes @@ -28,7 +30,6 @@ import numpy as np -from mpi4py import MPI from desc import config as desc_config from desc.backend import jax, jnp, print_backend_info @@ -68,7 +69,7 @@ print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") if rank == 0: - print(f"====== BACKEND INFO ======") + print("====== BACKEND INFO ======") print_backend_info() print("\n") diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index a85253543d..cbd0a651b1 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -30,48 +30,6 @@ "sys.path.append(os.path.abspath(\"../../../\"))" ] }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "num_device = 4\n", - "from desc import set_device, _set_cpu_count\n", - "\n", - "# These will be used for diving the single CPU into multiple virtual CPUs\n", - "# such that JAX and XLA thinks there are multiple devices\n", - "# Note that this is just to trick JAX. Since JAX can already use multiple core and threads\n", - "# for single CPU, this will not give a speedup. This is just to test the code\n", - "_set_cpu_count(num_device)\n", - "set_device(\"cpu\", num_device=num_device)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DESC version=0.14.2+140.g023df017d.dirty.\n", - "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs with 8.66 GB total available memory:\n", - "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U 0\n", - "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U 1\n", - "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U 2\n", - "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U 3\n" - ] - } - ], - "source": [ - "from desc.backend import print_backend_info\n", - "\n", - "print_backend_info()" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -85,6 +43,7 @@ "sys.path.insert(0, os.path.abspath(\".\"))\n", "sys.path.append(os.path.abspath(\"../../../\"))\n", "\n", + "from mpi4py import MPI\n", "from desc import _set_cpu_count, set_device\n", "\n", "# ====== Using CPUs ======\n", @@ -94,20 +53,18 @@ "\n", "# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", "_set_cpu_count(num_device)\n", - "set_device(\"cpu\", num_device=num_device)\n", + "set_device(\"cpu\", num_device=num_device, mpi=MPI)\n", "\n", "# ====== Using GPUs ======\n", "# When we have multiple processes using the same devices (for example, 3 processes\n", "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", - "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` \n", - "# such that there is no pre-allocation. This is a bit conservative (and probably there is room \n", + "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform`\n", + "# such that there is no pre-allocation. This is a bit conservative (and probably there is room\n", "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", "#\n", "# os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", "# set_device(\"gpu\", num_device=num_device)\n", "\n", - "from mpi4py import MPI\n", - "\n", "from desc import config as desc_config\n", "from desc.backend import jax, print_backend_info\n", "from desc.examples import get\n", @@ -130,13 +87,14 @@ " # to see which devices are available to each process.\n", " if desc_config[\"kind\"] == \"gpu\":\n", " print(\n", - " f\"Rank {rank} is running on {jax.local_devices(backend='gpu')} \"\n", + " f\"Rank {rank} can see {jax.local_devices(backend='gpu')} \"\n", " f\"and {jax.local_devices(backend='cpu')}\\n\"\n", " )\n", " else:\n", - " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\\n\")\n", + " print(f\"Rank {rank} can see {jax.local_devices(backend='cpu')}\\n\")\n", + "\n", " if rank == 0:\n", - " print(f\"====== BACKEND INFO ======\")\n", + " print(\"====== BACKEND INFO ======\")\n", " print_backend_info()\n", " print(\"\\n\")\n", "\n", @@ -170,33 +128,34 @@ " )\n", "\n", " # if you put a code here, it will be performed on all ranks\n", + "\n", " \n", - "```" + "```\n" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "\n", "====== TOTAL OF 4 RANKS ======\n", "Rank 0 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+140.g023df017d.dirty.\n", + "DESC version=0.14.2+142.g646e037f2.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs with 8.73 GB total available memory:\n", - "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U 0\n", - "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U 1\n", - "\t CPU 2: 13th Gen Intel(R) Core(TM) i5-1335U 2\n", - "\t CPU 3: 13th Gen Intel(R) Core(TM) i5-1335U 3\n", - "\n", + "Using 4 CPUs with 8.68 GB total available memory:\n", + "\t CPU : 0 13th Gen Intel(R) Core(TM) i5-1335U\n", + "\t CPU : 1 13th Gen Intel(R) Core(TM) i5-1335U\n", + "\t CPU : 2 13th Gen Intel(R) Core(TM) i5-1335U\n", + "\t CPU : 3 13th Gen Intel(R) Core(TM) i5-1335U\n", "\n", - "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", "Building objective: force\n", "Precomputing transforms\n", @@ -208,9 +167,15 @@ "\n", "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", "Putting objective force on device 2\n", "Building objective: force\n", "Precomputing transforms\n", @@ -222,12 +187,6 @@ "Rank 3 will run objective(s): ['ForceBalance']\n", "------------------------------------------------------------\n", "Building objective: lcfs R\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", "Building objective: lcfs Z\n", "Building objective: fixed Psi\n", "Building objective: fixed pressure\n", @@ -236,23 +195,25 @@ "Building objective: self_consistency R\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: self_consistency Z\n", "Building objective: force\n", "Precomputing transforms\n", + "Building objective: self_consistency Z\n", "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "Timer: Objective build = 1.78 sec\n", + "\u001b[32mTimer: Objective build = 1.72 sec\u001b[0m\n", + "Building objective: force\n", + "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: LinearConstraintProjection build = 6.77 sec\n", + "\u001b[32mTimer: LinearConstraintProjection build = 6.47 sec\u001b[0m\n", "Number of parameters: 551\n", "Number of objectives: 8424\n", - "Timer: Initializing the optimization = 8.62 sec\n", + "\u001b[32mTimer: Initializing the optimization = 8.26 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", @@ -260,8 +221,6 @@ "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", @@ -276,35 +235,35 @@ " 0 1 2.500e+00 1.228e+00 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 1 2 8.226e-01 1.678e+00 2.256e-01 5.198e-01 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 2 3 2.473e-02 7.978e-01 1.993e-01 7.096e-02 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", @@ -318,8 +277,8 @@ " Iterations: 3\n", " Function evaluations: 5\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 34.4 sec\n", - "Timer: Avg time per step = 8.60 sec\n", + "\u001b[32mTimer: Solution time = 34.3 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 8.58 sec\u001b[0m\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", @@ -366,8 +325,9 @@ "==============================================================================================================\n", "\n", "Rank 1 STOPPING\n", + "Rank 2 STOPPING\n", "Rank 3 STOPPING\n", - "Rank 2 STOPPING\n" + "\u001b[0m\u001b[0m\u001b[0m\u001b[0m" ] } ], @@ -396,6 +356,7 @@ "sys.path.insert(0, os.path.abspath(\".\"))\n", "sys.path.append(os.path.abspath(\"../../../\"))\n", "\n", + "from mpi4py import MPI\n", "from desc import _set_cpu_count, set_device\n", "\n", "# ====== Using CPUs ======\n", @@ -405,13 +366,13 @@ "\n", "# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", "_set_cpu_count(num_device)\n", - "set_device(\"cpu\", num_device=num_device)\n", + "set_device(\"cpu\", num_device=num_device, mpi=MPI)\n", "\n", "# ====== Using GPUs ======\n", "# When we have multiple processes using the same devices (for example, 3 processes\n", "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", - "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` \n", - "# such that there is no pre-allocation. This is a bit conservative (and probably there is room \n", + "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform`\n", + "# such that there is no pre-allocation. This is a bit conservative (and probably there is room\n", "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", "#\n", "# os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", @@ -419,7 +380,6 @@ "\n", "\n", "import numpy as np\n", - "from mpi4py import MPI\n", "\n", "from desc import config as desc_config\n", "from desc.backend import jax, jnp, print_backend_info\n", @@ -459,7 +419,7 @@ " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\\n\")\n", "\n", " if rank == 0:\n", - " print(f\"====== BACKEND INFO ======\")\n", + " print(\"====== BACKEND INFO ======\")\n", " print_backend_info()\n", " print(\"\\n\")\n", "\n", @@ -537,6 +497,7 @@ " )\n", "\n", " # if you put a code here, it will be performed on all ranks\n", + "\n", " \n", "```" ] From e93d0c9379ae1dd139e932f376e0a71c55a1807c Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 21 May 2025 22:55:33 -0400 Subject: [PATCH 108/199] add notes to the backend print --- desc/backend.py | 13 +++ docs/notebooks/tutorials/multi_device.ipynb | 101 ++++++++++---------- 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 40cf13fc28..36aa898d51 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -70,6 +70,12 @@ def print_backend_info(): for i, dev in enumerate(desc_config["devices"]): print(f"\t CPU : {dev}") + print( + "\nNote: The backend information assumes that the user has 1 " + "process per CPU (node). Using multiple processes per CPU (node) is " + "not the most efficient way to use MPI with purely CPUs." + ) + if desc_config["kind"] == "gpu": print( f"CPU Info: {desc_config['cpu_info']} with {desc_config['cpu_mem']:.2f} " @@ -82,6 +88,13 @@ def print_backend_info(): "GB available memory" ) + if desc_config["num_device"] != 1: + print( + "\nNote: The backend information only reflects the devices for " + "the current process. The full set of devices used by other processes " + "may be different." + ) + if use_jax: # noqa: C901 from jax import custom_jvp, jit, vmap diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index cbd0a651b1..56a6d1feeb 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -135,27 +135,29 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "\n", "====== TOTAL OF 4 RANKS ======\n", "Rank 0 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+142.g646e037f2.dirty.\n", + "DESC version=0.14.2+143.gb14571a20.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs with 8.68 GB total available memory:\n", + "Using 4 CPUs with 7.90 GB total available memory:\n", "\t CPU : 0 13th Gen Intel(R) Core(TM) i5-1335U\n", "\t CPU : 1 13th Gen Intel(R) Core(TM) i5-1335U\n", "\t CPU : 2 13th Gen Intel(R) Core(TM) i5-1335U\n", "\t CPU : 3 13th Gen Intel(R) Core(TM) i5-1335U\n", "\n", + "Note: The backend information assumes that the user has 1 process per CPU (node). Using multiple processes per CPU (node) is not the most efficient way to use MPI with purely CPUs.\n", + "\n", + "\n", + "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", "\n", "Building objective: force\n", "Precomputing transforms\n", @@ -169,8 +171,6 @@ "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", "Putting objective force on device 1\n", "Building objective: force\n", "Precomputing transforms\n", @@ -179,6 +179,8 @@ "Putting objective force on device 2\n", "Building objective: force\n", "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", "Putting objective force on device 3\n", "------------------------------------------------------------\n", "Rank 0 will run objective(s): ['ForceBalance']\n", @@ -193,15 +195,11 @@ "Building objective: fixed iota\n", "Building objective: fixed sheet current\n", "Building objective: self_consistency R\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", "Building objective: self_consistency Z\n", "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "\u001b[32mTimer: Objective build = 1.72 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 949 ms\u001b[0m\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", @@ -210,10 +208,10 @@ "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "\u001b[32mTimer: LinearConstraintProjection build = 6.47 sec\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 5.00 sec\u001b[0m\n", "Number of parameters: 551\n", "Number of objectives: 8424\n", - "\u001b[32mTimer: Initializing the optimization = 8.26 sec\u001b[0m\n", + "\u001b[32mTimer: Initializing the optimization = 6.01 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", @@ -221,26 +219,30 @@ "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Building objective: force\n", + "Precomputing transforms\n", + "Building objective: force\n", + "Precomputing transforms\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 2.500e+00 1.228e+00 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 1 2 8.226e-01 1.678e+00 2.256e-01 5.198e-01 \n", @@ -251,8 +253,8 @@ "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", " 2 3 2.473e-02 7.978e-01 1.993e-01 7.096e-02 \n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", @@ -262,13 +264,13 @@ "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " 3 5 3.451e-03 2.128e-02 8.908e-02 3.526e-02 \n", "Warning: Maximum number of iterations has been exceeded.\n", @@ -277,14 +279,14 @@ " Iterations: 3\n", " Function evaluations: 5\n", " Jacobian evaluations: 4\n", - "\u001b[32mTimer: Solution time = 34.3 sec\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 8.58 sec\u001b[0m\n", + "\u001b[32mTimer: Solution time = 28.9 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 7.24 sec\u001b[0m\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "==============================================================================================================\n", " Start --> End\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", @@ -504,7 +506,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -515,55 +517,57 @@ "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+139.g8f30c380b.dirty.\n", + "DESC version=0.14.2+143.gb14571a20.dirty.\n", "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 2 CPUs with 9.41 GB total available memory:\n", - "\t CPU 0: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_0\n", - "\t CPU 1: 13th Gen Intel(R) Core(TM) i5-1335U TFRT_CPU_1\n", + "Using 2 CPUs with 8.90 GB total available memory:\n", + "\t CPU : 0 13th Gen Intel(R) Core(TM) i5-1335U\n", + "\t CPU : 1 13th Gen Intel(R) Core(TM) i5-1335U\n", + "\n", + "Note: The backend information assumes that the user has 1 process per CPU (node). Using multiple processes per CPU (node) is not the most efficient way to use MPI with purely CPUs.\n", "\n", "\n", "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.44 sec\n", + "\u001b[32mTimer: Precomputing transforms = 1.06 sec\u001b[0m\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.34 sec\n", + "\u001b[32mTimer: Precomputing transforms = 1.02 sec\u001b[0m\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.29 sec\n", + "\u001b[32mTimer: Precomputing transforms = 1.00 sec\u001b[0m\n", "------------------------------------------------------------\n", "Rank 0 will run objective(s): ['QuasisymmetryTwoTerm', 'AspectRatio']\n", "Rank 1 will run objective(s): ['QuasisymmetryTwoTerm']\n", "------------------------------------------------------------\n", - "Timer: Objective build = 5.22 sec\n", + "\u001b[32mTimer: Objective build = 3.80 sec\u001b[0m\n", "Building objective: force\n", "Precomputing transforms\n", - "Timer: Precomputing transforms = 1.91 sec\n", - "Timer: Objective build = 2.00 sec\n", - "Timer: Objective build = 2.35 ms\n", - "Timer: Eq Update LinearConstraintProjection build = 5.62 sec\n", - "Timer: Proximal projection build = 10.4 sec\n", + "\u001b[32mTimer: Precomputing transforms = 1.34 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 1.40 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 1.70 ms\u001b[0m\n", + "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 3.74 sec\u001b[0m\n", + "\u001b[32mTimer: Proximal projection build = 7.02 sec\u001b[0m\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "Timer: Objective build = 857 ms\n", - "Timer: LinearConstraintProjection build = 1.97 sec\n", + "\u001b[32mTimer: Objective build = 555 ms\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 1.34 sec\u001b[0m\n", "Number of parameters: 8\n", "Number of objectives: 631\n", - "Timer: Initializing the optimization = 13.3 sec\n", + "\u001b[32mTimer: Initializing the optimization = 8.97 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", " 0 1 2.005e+04 1.926e+02 \n", @@ -603,8 +607,8 @@ " Iterations: 3\n", " Function evaluations: 7\n", " Jacobian evaluations: 4\n", - "Timer: Solution time = 1.23 min\n", - "Timer: Avg time per step = 18.5 sec\n", + "\u001b[32mTimer: Solution time = 52.3 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 13.0 sec\u001b[0m\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "==============================================================================================================\n", @@ -640,7 +644,8 @@ "Fixed current profile error: 0.000e+00 --> 0.000e+00 (A)\n", "==============================================================================================================\n", "\n", - "Rank 1 STOPPING\n" + "Rank 1 STOPPING\n", + "\u001b[0m\u001b[0m" ] } ], From 8c951a7aaf48345941911e551247cb0c0b64c630 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 3 Jun 2025 17:21:20 -0400 Subject: [PATCH 109/199] fix the jax.local_devices error for gpu backend --- desc/objectives/objective_funs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index edfaa6b6a2..770d09880a 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1477,9 +1477,7 @@ def __init__( # it to be static. So we set _device to None in that case which is simpler then # making it static. if device_id != 0 and desc_config["kind"] == "gpu": - # jax.device_put cannot put data to other CPUs - # it can only transfer data to local devices - self._device = jax.local_devices("gpu")[device_id] + self._device = jax.devices("gpu")[device_id] else: # we won't transfer data for multiple CPUs because their rank should # already have that data. From 4b018f719e7b4f8d1dd755f903c8586a1b328870 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 3 Jun 2025 17:48:23 -0400 Subject: [PATCH 110/199] fix the scripts os path --- desc/objectives/objective_funs.py | 2 +- docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py | 2 +- docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 770d09880a..9dab84235e 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -403,7 +403,7 @@ def _worker_loop(self): # The message contains 3 parts, # message[0] is the operation to be performed # message[1] is the state vector (for compute and jvp's) - # message[2] is the output (for only jvp's) + # message[2] is the tangents (for only jvp's) message = (None, None, None) message = self.comm.bcast(message, root=0) obj_idx_rank = self._obj_per_rank[self.rank] diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index c3d8e41bde..7cfec555d3 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -3,7 +3,7 @@ # Add the path to the parent directory to augment search for module sys.path.insert(0, os.path.abspath(".")) -sys.path.append(os.path.abspath("../../../")) +sys.path.append(os.path.abspath("../../../../")) from mpi4py import MPI diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 8a39ecc6f4..638cb8f3a7 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -3,7 +3,7 @@ # Add the path to the parent directory to augment search for module sys.path.insert(0, os.path.abspath(".")) -sys.path.append(os.path.abspath("../../../")) +sys.path.append(os.path.abspath("../../../../")) from mpi4py import MPI From 857c43ccc362f23dbad7c4ea93f1ce5964d7e883 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 3 Jun 2025 18:10:43 -0400 Subject: [PATCH 111/199] add the script with nvtx --- .../mpi-tutorials/mpi-proximal-nvtx.py | 165 ++++++++++++++++++ .../tutorials/mpi-tutorials/mpi-proximal.py | 8 +- 2 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py new file mode 100644 index 0000000000..a9c171fac6 --- /dev/null +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py @@ -0,0 +1,165 @@ +import os +import sys + +# Add the path to the parent directory to augment search for module +sys.path.insert(0, os.path.abspath(".")) +sys.path.append(os.path.abspath("../../../../")) + +from mpi4py import MPI + +from desc import _set_cpu_count, set_device + +# ====== Using CPUs ====== +num_device = 2 +# These will be used for diving the single CPU into multiple virtual CPUs +# such that JAX and XLA thinks there are multiple devices + +# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! +# _set_cpu_count(num_device) +# set_device("cpu", num_device=num_device, mpi=MPI) + +# ====== Using GPUs ====== +# When we have multiple processes using the same devices (for example, 3 processes +# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will +# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` +# such that there is no pre-allocation. This is a bit conservative (and probably there is room +# for improvement), but if a process needs more memory, it can use more memory on the fly. +# +os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" +set_device("gpu", num_device=num_device) + + +import numpy as np +import nvtx + +from desc import config as desc_config +from desc.backend import jax, jnp, print_backend_info +from desc.examples import get +from desc.grid import LinearGrid +from desc.objectives import ( + AspectRatio, + FixBoundaryR, + FixBoundaryZ, + FixCurrent, + FixPressure, + FixPsi, + ForceBalance, + ObjectiveFunction, + QuasisymmetryTwoTerm, +) +from desc.optimize import Optimizer + +if __name__ == "__main__": + rank = MPI.COMM_WORLD.Get_rank() + size = MPI.COMM_WORLD.Get_size() + if rank == 0: + print(f"====== TOTAL OF {size} RANKS ======") + + # see which rank is running on which device + # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()` + # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()` + # will return only the devices that are available to the current process. This is + # useful when you have multiple processes running on multiple nodes and you want + # to see which devices are available to each process. + if desc_config["kind"] == "gpu": + print( + f"Rank {rank} is running on {jax.local_devices(backend='gpu')} " + f"and {jax.local_devices(backend='cpu')}\n" + ) + else: + print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") + + if rank == 0: + print("====== BACKEND INFO ======") + print_backend_info() + print("\n") + + with nvtx.annotate("setup"): + eq = get("precise_QA") + eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) + + # create two grids with different rho values, this will effectively separate + # the quasisymmetry objective into two parts + grid1 = LinearGrid( + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + rho=jnp.linspace(0.2, 0.5, 4), + sym=True, + ) + grid2 = LinearGrid( + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + rho=jnp.linspace(0.6, 1.0, 6), + sym=True, + ) + + # when using parallel objectives, the user needs to supply the device_id + obj1 = QuasisymmetryTwoTerm( + eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0 + ) + obj2 = QuasisymmetryTwoTerm( + eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1 + ) + obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) + objs = [obj1, obj2, obj3] + + with nvtx.annotate("Build Objectives"): + # Parallel objective function needs the MPI communicator + # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will + # automatically switch to `blocked`. + objective = ObjectiveFunction( + objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 0]) + ) + if rank == 0: + objective.build(verbose=3) + else: + objective.build(verbose=0) + + # we will fix some modes as usual + k = 1 + R_modes = np.vstack( + ( + [0, 0, 0], + eq.surface.R_basis.modes[ + np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : + ], + ) + ) + Z_modes = eq.surface.Z_basis.modes[ + np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : + ] + constraints = ( + ForceBalance(eq=eq), + FixBoundaryR(eq=eq, modes=R_modes), + FixBoundaryZ(eq=eq, modes=Z_modes), + FixPressure(eq=eq), + FixPsi(eq=eq), + FixCurrent(eq=eq), + ) + optimizer = Optimizer("proximal-lsq-exact") + + # Until this line, the code is performed on all ranks, so it might print some + # information multiple times. The following part will only be performed on the + # master rank + + # this context manager will put the workers in a loop to listen to the master + # to compute the objective function and its derivatives + with nvtx.annotate("Optimization"): + with objective as objective: + # apart from cost evaluation and derivatives, everything else will be only + # performed on the master rank + if rank == 0: + eq.optimize( + objective=objective, + constraints=constraints, + optimizer=optimizer, + maxiter=3, + verbose=3, + options={ + "initial_trust_ratio": 1.0, + }, + ) + + # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 638cb8f3a7..d35583162e 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -15,8 +15,8 @@ # such that JAX and XLA thinks there are multiple devices # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! -_set_cpu_count(num_device) -set_device("cpu", num_device=num_device, mpi=MPI) +# _set_cpu_count(num_device) +# set_device("cpu", num_device=num_device, mpi=MPI) # ====== Using GPUs ====== # When we have multiple processes using the same devices (for example, 3 processes @@ -25,8 +25,8 @@ # such that there is no pre-allocation. This is a bit conservative (and probably there is room # for improvement), but if a process needs more memory, it can use more memory on the fly. # -# os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" -# set_device("gpu", num_device=num_device) +os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" +set_device("gpu", num_device=num_device) import numpy as np From 28c05b0b3cc1090223076c22c3495b05bb4e86fe Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 3 Jun 2025 19:09:55 -0400 Subject: [PATCH 112/199] add force balance nvtx script, make the nvtx scripts more demanding --- .../mpi-tutorials/mpi-eq-solve-nvtx.py | 95 +++++++++++++++++++ .../mpi-tutorials/mpi-proximal-nvtx.py | 3 +- 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py new file mode 100644 index 0000000000..36b0aee026 --- /dev/null +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py @@ -0,0 +1,95 @@ +import os +import sys + +# Add the path to the parent directory to augment search for module +sys.path.insert(0, os.path.abspath(".")) +sys.path.append(os.path.abspath("../../../../")) + +import nvtx +from mpi4py import MPI + +from desc import set_device + +# ====== Using CPUs ====== +num_device = 4 +# These will be used for diving the single CPU into multiple virtual CPUs +# such that JAX and XLA thinks there are multiple devices + +# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! +# _set_cpu_count(num_device) +# set_device("cpu", num_device=num_device, mpi=MPI) + +# ====== Using GPUs ====== +# When we have multiple processes using the same devices (for example, 3 processes +# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will +# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` +# such that there is no pre-allocation. This is a bit conservative (and probably there is room +# for improvement), but if a process needs more memory, it can use more memory on the fly. +# +os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" +set_device("gpu", num_device=num_device) + +from desc import config as desc_config +from desc.backend import jax, print_backend_info +from desc.examples import get +from desc.objectives.getters import ( + get_fixed_boundary_constraints, + get_parallel_forcebalance, +) + +if __name__ == "__main__": + rank = MPI.COMM_WORLD.Get_rank() + size = MPI.COMM_WORLD.Get_size() + if rank == 0: + print(f"====== TOTAL OF {size} RANKS ======") + + # see which rank is running on which device + # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()` + # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()` + # will return only the devices that are available to the current process. This is + # useful when you have multiple processes running on multiple nodes and you want + # to see which devices are available to each process. + if desc_config["kind"] == "gpu": + print( + f"Rank {rank} can see {jax.local_devices(backend='gpu')} " + f"and {jax.local_devices(backend='cpu')}\n" + ) + else: + print(f"Rank {rank} can see {jax.local_devices(backend='cpu')}\n") + + if rank == 0: + print("====== BACKEND INFO ======") + print_backend_info() + print("\n") + + with nvtx.annotate("Setup"): + eq = get("HELIOTRON") + + # this will create a parallel objective function + # user can create their own parallel objective function as well which will be + # shown in the next example + obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1) + cons = get_fixed_boundary_constraints(eq) + + # Until this line, the code is performed on all ranks, so it might print some + # information multiple times. The following part will only be performed on the + # master rank + + # this context manager will put the workers in a loop to listen to the master + # to compute the objective function and its derivatives + with nvtx.annotate("Solve"): + with obj as obj: + # apart from cost evaluation and derivatives, everything else will be only + # performed on the master rank + if rank == 0: + eq.solve( + objective=obj, + constraints=cons, + maxiter=3, + ftol=0, + gtol=0, + xtol=0, + verbose=3, + ) + + # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py index a9c171fac6..0767d99c59 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py @@ -7,7 +7,7 @@ from mpi4py import MPI -from desc import _set_cpu_count, set_device +from desc import set_device # ====== Using CPUs ====== num_device = 2 @@ -76,7 +76,6 @@ with nvtx.annotate("setup"): eq = get("precise_QA") - eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) # create two grids with different rho values, this will effectively separate # the quasisymmetry objective into two parts From d5f9efa9503e007cd05610ac1ae171f5ceccdb5d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 4 Jun 2025 18:36:40 -0400 Subject: [PATCH 113/199] for hackathon purposes add nvtx flags, add 2 node script --- desc/objectives/objective_funs.py | 13 +- desc/optimize/least_squares.py | 20 ++ desc/optimize/optimizer.py | 5 + .../mpi-tutorials/mpi-eq-solve-nvtx.py | 2 +- .../mpi-tutorials/mpi-proximal-nvtx-2node.py | 171 ++++++++++++++++++ requirements.txt | 1 + 6 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx-2node.py diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 9dab84235e..bf3002c09f 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod import numpy as np +import nvtx from desc.backend import ( desc_config, @@ -328,7 +329,8 @@ def __init__( max(self._rank_per_objective) != self.size - 1, ValueError, "The maximum value of rank_per_objective " - f"({max(self._rank_per_objective)+1}) " + f"({max(self._rank_per_objective)+1}, supplied as " + f"({max(self._rank_per_objective)} in the array) " f"is not equal to the number of ranks ({self.size}). There " "should be at least 1 objective per rank.", ) @@ -407,6 +409,7 @@ def _worker_loop(self): message = (None, None, None) message = self.comm.bcast(message, root=0) obj_idx_rank = self._obj_per_rank[self.rank] + if message[0] == "STOP": print(f"Rank {self.rank} STOPPING") break @@ -418,6 +421,7 @@ def _worker_loop(self): # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective J_rank = [] + rng_rank = nvtx.start_range(message="Worker Job JVP", color="red") for idx in obj_idx_rank: obj = self.objectives[idx] const = self.constants[idx] @@ -428,6 +432,7 @@ def _worker_loop(self): vi = jax.device_put(vi, obj._device) J_rank.append(getattr(obj, message[0])(vi, xi, constants=const)) J_rank = np.hstack(J_rank) + nvtx.end_range(rng_rank) self.comm.gather(J_rank, root=0) elif "compute" in message[0]: print( @@ -435,6 +440,7 @@ def _worker_loop(self): + f"{obj_idx_rank}" ) f_rank = [] + rng_rank = nvtx.start_range(message="Worker Job Compute", color="red") for idx in obj_idx_rank: obj = self.objectives[idx] const = self.constants[idx] @@ -442,6 +448,7 @@ def _worker_loop(self): par = jax.device_put(par, obj._device) f_rank.append(getattr(obj, message[0])(*par, constants=const)) f_rank = np.concatenate(f_rank) + nvtx.end_range(rng_rank) self.comm.gather(f_rank, root=0) elif "proximal_jvp" in message[0]: print( @@ -449,6 +456,9 @@ def _worker_loop(self): + f"{obj_idx_rank}" ) J_rank = [] + rng_rank = nvtx.start_range( + message="Worker Job JVP Proximal", color="red" + ) for idx in obj_idx_rank: obj = self.objectives[idx] const = self.constants[idx] @@ -478,6 +488,7 @@ def _worker_loop(self): ).T ) J_rank = np.hstack(J_rank) + nvtx.end_range(rng_rank) self.comm.gather(J_rank, root=0) def _unjit(self): diff --git a/desc/optimize/least_squares.py b/desc/optimize/least_squares.py index c969435f88..5dd96e1037 100644 --- a/desc/optimize/least_squares.py +++ b/desc/optimize/least_squares.py @@ -1,5 +1,6 @@ """Function for solving nonlinear least squares problems.""" +import nvtx from scipy.optimize import OptimizeResult from desc.backend import jnp, qr @@ -175,6 +176,7 @@ def lsqtr( # noqa: C901 assert in_bounds(x, lb, ub), "x0 is infeasible" x = make_strictly_feasible(x, lb, ub) + rng_comp = nvtx.start_range(message="First Compute/Jac", color="red") f = fun(x, *args) nfev += 1 cost = 0.5 * jnp.dot(f, f) @@ -183,6 +185,7 @@ def lsqtr( # noqa: C901 J = jac(x, *args).block_until_ready() njev += 1 g = jnp.dot(J.T, f) + nvtx.end_range(rng_comp) maxiter = setdefault(maxiter, n * 100) max_nfev = options.pop("max_nfev", 5 * maxiter + 1) @@ -272,11 +275,13 @@ def lsqtr( # noqa: C901 alpha = None # "Levenberg-Marquardt" parameter while iteration < maxiter and success is None: + rng = nvtx.start_range(message="ITERATION", color="blue") # we don't want to factorize the extra stuff if we don't need to J_a = jnp.vstack([J_h, jnp.diag(diag_h**0.5)]) if bounded else J_h f_a = jnp.concatenate([f, jnp.zeros(diag_h.size)]) if bounded else f + rng_qr0 = nvtx.start_range(message="QR Newton", color="green") if tr_method == "svd": U, s, Vt = jnp.linalg.svd(J_a, full_matrices=False) elif tr_method == "cho": @@ -294,6 +299,7 @@ def lsqtr( # noqa: C901 # Trust region solver will solve the augmented system # with a new Q and R del Q, R + nvtx.end_range(rng_qr0) actual_reduction = -1 @@ -305,6 +311,7 @@ def lsqtr( # noqa: C901 # This gives us the proposed step relative to the current position # and it tells us whether the proposed step # has reached the trust region boundary or not. + rng_qr = nvtx.start_range(message="QR subproblem", color="green") if tr_method == "svd": step_h, hits_boundary, alpha = trust_region_step_exact_svd( f_a, U, s, Vt.T, trust_radius, alpha @@ -317,8 +324,10 @@ def lsqtr( # noqa: C901 step_h, hits_boundary, alpha = trust_region_step_exact_qr( p_newton, f_a, J_a, trust_radius, alpha ) + nvtx.end_range(rng_qr) step = d * step_h # Trust-region solution in the original space. + rng_ss = nvtx.start_range(message="Select Step", color="red") step, step_h, predicted_reduction = select_step( x, J_h, @@ -333,13 +342,16 @@ def lsqtr( # noqa: C901 theta, mode="jac", ) + nvtx.end_range(rng_ss) step_h_norm = jnp.linalg.norm(step_h, ord=2) step_norm = jnp.linalg.norm(step, ord=2) + rng_fn = nvtx.start_range(message="F new and Make feasible", color="red") x_new = make_strictly_feasible(x + step, lb, ub, rstep=0) f_new = fun(x_new, *args) nfev += 1 + nvtx.end_range(rng_fn) cost_new = 0.5 * jnp.dot(f_new, f_new) actual_reduction = cost - cost_new @@ -388,9 +400,11 @@ def lsqtr( # noqa: C901 allx.append(x) f = f_new cost = cost_new + rng_jac = nvtx.start_range(message="Jac per iter", color="green") J = jac(x, *args) njev += 1 g = jnp.dot(J.T, f) + nvtx.end_range(rng_jac) if jac_scale: scale, scale_inv = compute_jac_scale(J, scale_inv) @@ -425,9 +439,13 @@ def lsqtr( # noqa: C901 iteration += 1 if verbose > 1: + rng_print = nvtx.start_range(message="Print Iter", color="red") print_iteration_nonlinear( iteration, nfev, cost, actual_reduction, step_norm, g_norm ) + nvtx.end_range(rng_print) + + nvtx.end_range(rng) if g_norm < gtol: success, message = True, STATUS_MESSAGES["gtol"] + f" ({gtol=:.2e})" @@ -452,6 +470,7 @@ def lsqtr( # noqa: C901 alltr=alltr, ) if verbose > 0: + rng_print = nvtx.start_range(message="Print Last", color="red") if result["success"]: print(result["message"]) else: @@ -463,5 +482,6 @@ def lsqtr( # noqa: C901 print(" Iterations: {:d}".format(result["nit"])) print(" Function evaluations: {:d}".format(result["nfev"])) print(" Jacobian evaluations: {:d}".format(result["njev"])) + nvtx.end_range(rng_print) return result diff --git a/desc/optimize/optimizer.py b/desc/optimize/optimizer.py index 49019bf457..4d76de32cc 100644 --- a/desc/optimize/optimizer.py +++ b/desc/optimize/optimizer.py @@ -5,6 +5,7 @@ import warnings import numpy as np +import nvtx from termcolor import colored from desc.io import IOAble @@ -275,6 +276,7 @@ def optimize( # noqa: C901 timer.start("Solution time") + rng_opt = nvtx.start_range(message="Actual Optimization", color="red") result = optimizers[method]["fun"]( objective, nonlinear_constraint, @@ -285,7 +287,9 @@ def optimize( # noqa: C901 stoptol, options, ) + nvtx.end_range(rng_opt) + rng_po = nvtx.start_range(message="Post Optimization", color="red") if isinstance(objective, LinearConstraintProjection): # remove wrapper to get at underlying objective result["allx"] = [objective.recover(x) for x in result["allx"]] @@ -338,6 +342,7 @@ def optimize( # noqa: C901 t0.params_dict = final_params return things0, result + nvtx.end_range(rng_po) return things, result diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py index 36b0aee026..7b0103742c 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py @@ -11,7 +11,7 @@ from desc import set_device # ====== Using CPUs ====== -num_device = 4 +num_device = 2 # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx-2node.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx-2node.py new file mode 100644 index 0000000000..dec915ee83 --- /dev/null +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx-2node.py @@ -0,0 +1,171 @@ +import os +import sys + +# Add the path to the parent directory to augment search for module +sys.path.insert(0, os.path.abspath(".")) +sys.path.append(os.path.abspath("../../../../")) + +from mpi4py import MPI + +from desc import set_device + +# ====== Using CPUs ====== +num_device = 2 +# These will be used for diving the single CPU into multiple virtual CPUs +# such that JAX and XLA thinks there are multiple devices + +# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! +# _set_cpu_count(num_device) +# set_device("cpu", num_device=num_device, mpi=MPI) + +# ====== Using GPUs ====== +# When we have multiple processes using the same devices (for example, 3 processes +# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will +# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` +# such that there is no pre-allocation. This is a bit conservative (and probably there is room +# for improvement), but if a process needs more memory, it can use more memory on the fly. +# +os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" +set_device("gpu", num_device=num_device) + + +import numpy as np +import nvtx + +from desc import config as desc_config +from desc.backend import jax, jnp, print_backend_info +from desc.examples import get +from desc.grid import LinearGrid +from desc.objectives import ( + AspectRatio, + FixBoundaryR, + FixBoundaryZ, + FixCurrent, + FixPressure, + FixPsi, + ForceBalance, + ObjectiveFunction, + QuasisymmetryTwoTerm, +) +from desc.optimize import Optimizer + +if __name__ == "__main__": + rank = MPI.COMM_WORLD.Get_rank() + size = MPI.COMM_WORLD.Get_size() + if rank == 0: + print(f"====== TOTAL OF {size} RANKS ======") + + # see which rank is running on which device + # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()` + # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()` + # will return only the devices that are available to the current process. This is + # useful when you have multiple processes running on multiple nodes and you want + # to see which devices are available to each process. + if desc_config["kind"] == "gpu": + print( + f"Rank {rank} is running on {jax.local_devices(backend='gpu')} " + f"and {jax.local_devices(backend='cpu')}\n" + ) + else: + print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") + + if rank == 0: + print("====== BACKEND INFO ======") + print_backend_info() + print("\n") + + with nvtx.annotate("setup"): + eq = get("precise_QA") + + # create two grids with different rho values, this will effectively separate + # the quasisymmetry objective into two parts + grid1 = LinearGrid( + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + rho=jnp.linspace(0.2, 0.5, 4), + sym=True, + ) + grid2 = LinearGrid( + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + rho=jnp.linspace(0.6, 1.0, 6), + sym=True, + ) + + # when using parallel objectives, the user needs to supply the device_id + obj1 = QuasisymmetryTwoTerm( + eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0 + ) + obj2 = QuasisymmetryTwoTerm( + eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1 + ) + obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) + obj4 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) + obj5 = QuasisymmetryTwoTerm( + eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1 + ) + objs = [obj1, obj2, obj3, obj4, obj5] + + with nvtx.annotate("Build Objectives"): + # Parallel objective function needs the MPI communicator + # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will + # automatically switch to `blocked`. + objective = ObjectiveFunction( + objs, + deriv_mode="blocked", + mpi=MPI, + rank_per_objective=np.array([0, 1, 0, 2, 3]), + ) + if rank == 0: + objective.build(verbose=3) + else: + objective.build(verbose=0) + + # we will fix some modes as usual + k = 1 + R_modes = np.vstack( + ( + [0, 0, 0], + eq.surface.R_basis.modes[ + np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : + ], + ) + ) + Z_modes = eq.surface.Z_basis.modes[ + np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : + ] + constraints = ( + ForceBalance(eq=eq), + FixBoundaryR(eq=eq, modes=R_modes), + FixBoundaryZ(eq=eq, modes=Z_modes), + FixPressure(eq=eq), + FixPsi(eq=eq), + FixCurrent(eq=eq), + ) + optimizer = Optimizer("proximal-lsq-exact") + + # Until this line, the code is performed on all ranks, so it might print some + # information multiple times. The following part will only be performed on the + # master rank + + # this context manager will put the workers in a loop to listen to the master + # to compute the objective function and its derivatives + with nvtx.annotate("Optimization"): + with objective as objective: + # apart from cost evaluation and derivatives, everything else will be only + # performed on the master rank + if rank == 0: + eq.optimize( + objective=objective, + constraints=constraints, + optimizer=optimizer, + maxiter=3, + verbose=3, + options={ + "initial_trust_ratio": 1.0, + }, + ) + + # if you put a code here, it will be performed on all ranks diff --git a/requirements.txt b/requirements.txt index 5890d4d7fd..d750f21d46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ mpmath >= 1.0.0, <= 1.3.0 netcdf4 >= 1.5.4, <= 1.7.2 numpy >= 1.20.0, <= 2.2.6 nvgpu <= 0.10.0 +nvtx # remove once the debugging is done orthax <= 0.2.4 plotly >= 5.16, <= 6.1.2 psutil <= 7.0.0 From c68c1c71a01cb54a84b59f641e095d88f7667dc9 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 4 Jun 2025 18:44:35 -0400 Subject: [PATCH 114/199] solve concatenation bug for scalar and non-sclaar objective grad/jacobian --- desc/backend.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/desc/backend.py b/desc/backend.py index 36aa898d51..526e39d34d 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -5,6 +5,7 @@ import warnings import numpy as np +import nvtx from packaging.version import Version from termcolor import colored @@ -503,6 +504,7 @@ def pconcat(arrays, mode="concat"): # pragma: no cover """ # we will use either CPU or GPU[0] for the matrix decompositions, so the # array of float64 should fit into single device + rng_pconcat = nvtx.start_range(message="Print Last", color="red") size = jnp.array([x.size for x in arrays]) size = jnp.sum(size) if ( @@ -527,9 +529,13 @@ def pconcat(arrays, mode="concat"): # pragma: no cover if mode == "concat": out = jnp.concatenate([jax.device_put(x, device=device) for x in arrays]) elif mode == "hstack": - out = jnp.hstack([jax.device_put(x, device=device) for x in arrays]) + out = jnp.hstack( + [jnp.atleast_2d(jax.device_put(x, device=device)) for x in arrays] + ) elif mode == "vstack": out = jnp.vstack([jax.device_put(x, device=device) for x in arrays]) + + nvtx.end_range(rng_pconcat) return out def jit_with_device(method): From cf6074bbe6bf0150cba20da59256f769ed90aafe Mon Sep 17 00:00:00 2001 From: "Dario G. Panici" Date: Tue, 10 Jun 2025 14:09:36 -0400 Subject: [PATCH 115/199] change hstack to vstack to fix concat error --- desc/objectives/objective_funs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index bf3002c09f..acbc54623d 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -487,7 +487,7 @@ def _worker_loop(self): [_vi for _vi in vi], xi, constants=const ).T ) - J_rank = np.hstack(J_rank) + J_rank = np.vstack(J_rank) nvtx.end_range(rng_rank) self.comm.gather(J_rank, root=0) From dc50352884ffa8964df77f26b01e16d303059a32 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 00:49:37 -0400 Subject: [PATCH 116/199] add more and more nvtx flags for stages --- desc/objectives/objective_funs.py | 56 +++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index acbc54623d..d2b6726f00 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -407,7 +407,9 @@ def _worker_loop(self): # message[1] is the state vector (for compute and jvp's) # message[2] is the tangents (for only jvp's) message = (None, None, None) + rng_wait = nvtx.start_range(message="Wait for message", color="red") message = self.comm.bcast(message, root=0) + nvtx.end_range(rng_wait) obj_idx_rank = self._obj_per_rank[self.rank] if message[0] == "STOP": @@ -420,8 +422,8 @@ def _worker_loop(self): ) # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective - J_rank = [] rng_rank = nvtx.start_range(message="Worker Job JVP", color="red") + J_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] const = self.constants[idx] @@ -431,34 +433,42 @@ def _worker_loop(self): xi = jax.device_put(xi, obj._device) vi = jax.device_put(vi, obj._device) J_rank.append(getattr(obj, message[0])(vi, xi, constants=const)) + rng_con = nvtx.start_range(message="concat/numpy", color="red") J_rank = np.hstack(J_rank) + nvtx.end_range(rng_con) nvtx.end_range(rng_rank) + rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(J_rank, root=0) + nvtx.end_range(rng) elif "compute" in message[0]: print( f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) - f_rank = [] rng_rank = nvtx.start_range(message="Worker Job Compute", color="red") + f_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] const = self.constants[idx] par = message[1][idx] par = jax.device_put(par, obj._device) f_rank.append(getattr(obj, message[0])(*par, constants=const)) + rng_con = nvtx.start_range(message="concat/numpy", color="red") f_rank = np.concatenate(f_rank) + nvtx.end_range(rng_con) nvtx.end_range(rng_rank) + rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(f_rank, root=0) + nvtx.end_range(rng) elif "proximal_jvp" in message[0]: print( f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) - J_rank = [] rng_rank = nvtx.start_range( message="Worker Job JVP Proximal", color="red" ) + J_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] const = self.constants[idx] @@ -487,9 +497,13 @@ def _worker_loop(self): [_vi for _vi in vi], xi, constants=const ).T ) + rng_con = nvtx.start_range(message="concat/numpy", color="red") J_rank = np.vstack(J_rank) + nvtx.end_range(rng_con) nvtx.end_range(rng_rank) + rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(J_rank, root=0) + nvtx.end_range(rng) def _unjit(self): """Remove jit compiled methods.""" @@ -749,16 +763,26 @@ def compute_unscaled(self, x, constants=None): f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) + rng = nvtx.start_range( + message="compute_unscaled on master", color="blue" + ) f_rank = [] for idx in obj_idx_rank: par = params[idx] obj = self.objectives[idx] const = self.constants[idx] f_rank.append(obj.compute_unscaled(*par, constants=const)) + rng_con = nvtx.start_range(message="concat/jax", color="red") f_rank = jnp.concatenate(f_rank) + nvtx.end_range(rng_con) + nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") + rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) + nvtx.end_range(rng_gather) + rng_concat = nvtx.start_range(message="Pconcat", color="blue") f = pconcat(fs) + nvtx.end_range(rng_concat) return f @jit @@ -799,16 +823,24 @@ def compute_scaled(self, x, constants=None): f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) + rng = nvtx.start_range(message="compute_scaled on master", color="blue") f_rank = [] for idx in obj_idx_rank: par = params[idx] obj = self.objectives[idx] const = self.constants[idx] f_rank.append(obj.compute_scaled(*par, constants=const)) + rng_con = nvtx.start_range(message="concat/jax", color="red") f_rank = jnp.concatenate(f_rank) + nvtx.end_range(rng_con) + nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") + rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) + nvtx.end_range(rng_gather) + rng_concat = nvtx.start_range(message="Pconcat", color="blue") f = pconcat(fs) + nvtx.end_range(rng_concat) return f @jit @@ -849,16 +881,26 @@ def compute_scaled_error(self, x, constants=None): f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) + rng = nvtx.start_range( + message="compute_scaled_error on master", color="blue" + ) f_rank = [] for idx in obj_idx_rank: par = params[idx] obj = self.objectives[idx] const = self.constants[idx] f_rank.append(obj.compute_scaled_error(*par, constants=const)) + rng_con = nvtx.start_range(message="concat/jax", color="red") f_rank = jnp.concatenate(f_rank) + nvtx.end_range(rng_con) + nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") + rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) + nvtx.end_range(rng_gather) + rng_concat = nvtx.start_range(message="Pconcat", color="blue") f = pconcat(fs) + nvtx.end_range(rng_concat) return f @jit @@ -1098,6 +1140,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) + rng = nvtx.start_range(message="JVP on master", color="blue") J_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] @@ -1106,9 +1149,14 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): xi = [xs[i] for i in thing_idx] vi = [vs[i] for i in thing_idx] J_rank.append(getattr(obj, "jvp_" + op)(vi, xi, constants=const)) + rng_con = nvtx.start_range(message="concat/jax", color="red") J_rank = jnp.hstack(J_rank) + nvtx.end_range(rng_con) + nvtx.end_range(rng) + rng_gather = nvtx.start_range(message="Gather to master", color="red") print(f"Rank {self.rank} waiting to gather") J = self.comm.gather(J_rank, root=0) + nvtx.end_range(rng_gather) # this is the transpose of the jvp when v is a matrix, for consistency with # jvp_batched @@ -1116,7 +1164,9 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): J = jnp.hstack(J) else: # this will handle the device placement of the J matrix + rng = nvtx.start_range(message="Pconcat", color="blue") J = pconcat(J, mode="hstack") + nvtx.end_range(rng) return J From bd5aa6a0495164216910a537cbffd7901b34feea Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 01:16:09 -0400 Subject: [PATCH 117/199] fix some of the annotations --- desc/backend.py | 2 +- desc/objectives/objective_funs.py | 14 +++----------- desc/optimize/least_squares.py | 6 ++++-- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 526e39d34d..e9f964d870 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -504,7 +504,7 @@ def pconcat(arrays, mode="concat"): # pragma: no cover """ # we will use either CPU or GPU[0] for the matrix decompositions, so the # array of float64 should fit into single device - rng_pconcat = nvtx.start_range(message="Print Last", color="red") + rng_pconcat = nvtx.start_range(message="Pconcat", color="blue") size = jnp.array([x.size for x in arrays]) size = jnp.sum(size) if ( diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index d2b6726f00..405cdf9687 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -422,7 +422,7 @@ def _worker_loop(self): ) # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective - rng_rank = nvtx.start_range(message="Worker Job JVP", color="red") + rng_rank = nvtx.start_range(message="Worker Job JVP", color="green") J_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] @@ -445,7 +445,7 @@ def _worker_loop(self): f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) - rng_rank = nvtx.start_range(message="Worker Job Compute", color="red") + rng_rank = nvtx.start_range(message="Worker Job Compute", color="green") f_rank = [] for idx in obj_idx_rank: obj = self.objectives[idx] @@ -466,7 +466,7 @@ def _worker_loop(self): + f"{obj_idx_rank}" ) rng_rank = nvtx.start_range( - message="Worker Job JVP Proximal", color="red" + message="Worker Job JVP Proximal", color="green" ) J_rank = [] for idx in obj_idx_rank: @@ -780,9 +780,7 @@ def compute_unscaled(self, x, constants=None): rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) nvtx.end_range(rng_gather) - rng_concat = nvtx.start_range(message="Pconcat", color="blue") f = pconcat(fs) - nvtx.end_range(rng_concat) return f @jit @@ -838,9 +836,7 @@ def compute_scaled(self, x, constants=None): rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) nvtx.end_range(rng_gather) - rng_concat = nvtx.start_range(message="Pconcat", color="blue") f = pconcat(fs) - nvtx.end_range(rng_concat) return f @jit @@ -898,9 +894,7 @@ def compute_scaled_error(self, x, constants=None): rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) nvtx.end_range(rng_gather) - rng_concat = nvtx.start_range(message="Pconcat", color="blue") f = pconcat(fs) - nvtx.end_range(rng_concat) return f @jit @@ -1164,9 +1158,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): J = jnp.hstack(J) else: # this will handle the device placement of the J matrix - rng = nvtx.start_range(message="Pconcat", color="blue") J = pconcat(J, mode="hstack") - nvtx.end_range(rng) return J diff --git a/desc/optimize/least_squares.py b/desc/optimize/least_squares.py index 5dd96e1037..17e0566b56 100644 --- a/desc/optimize/least_squares.py +++ b/desc/optimize/least_squares.py @@ -347,11 +347,13 @@ def lsqtr( # noqa: C901 step_h_norm = jnp.linalg.norm(step_h, ord=2) step_norm = jnp.linalg.norm(step, ord=2) - rng_fn = nvtx.start_range(message="F new and Make feasible", color="red") + rng_fea = nvtx.start_range(message="Make feasible", color="red") x_new = make_strictly_feasible(x + step, lb, ub, rstep=0) + nvtx.end_range(rng_fea) + rng_fn = nvtx.start_range(message="Fnew", color="red") f_new = fun(x_new, *args) - nfev += 1 nvtx.end_range(rng_fn) + nfev += 1 cost_new = 0.5 * jnp.dot(f_new, f_new) actual_reduction = cost - cost_new From b86d98a1f0917f3224be67b96d104a40781a0ddb Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 01:43:12 -0400 Subject: [PATCH 118/199] add annotate to unpacking --- desc/objectives/objective_funs.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 405cdf9687..6a7edfb7b7 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -742,7 +742,9 @@ def compute_unscaled(self, x, constants=None): Objective function value(s). """ + rng_unpack = nvtx.start_range(message="unpack state", color="red") params = self.unpack_state(x) + nvtx.end_range(rng_unpack) if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) @@ -800,7 +802,9 @@ def compute_scaled(self, x, constants=None): Objective function value(s). """ + rng_unpack = nvtx.start_range(message="unpack state", color="red") params = self.unpack_state(x) + nvtx.end_range(rng_unpack) if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) @@ -856,7 +860,9 @@ def compute_scaled_error(self, x, constants=None): Objective function value(s). """ + rng_unpack = nvtx.start_range(message="unpack state", color="red") params = self.unpack_state(x) + nvtx.end_range(rng_unpack) if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) @@ -1114,6 +1120,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): J += [Ji_] else: if self.rank == 0: + rng_unpack = nvtx.start_range(message="precheck", color="red") v = ensure_tuple(v) if len(v) > 1: # using blocked for higher order derivatives is a pain, and only @@ -1123,9 +1130,11 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): if constants is None: constants = self.constants + xs_splits = np.cumsum([t.dim_x for t in self.things]) xs = jnp.split(x, xs_splits) vs = jnp.split(v[0], xs_splits, axis=-1) + nvtx.end_range(rng_unpack) message = ("jvp_" + op, xs, vs) self.comm.bcast(message, root=0) From 85298cf46a799e901b8b5be67c21ef63c14983b4 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 02:56:57 -0400 Subject: [PATCH 119/199] jit whatever you can, works on cpu, hopefull will on GPUs. unpack_state and per process computations (for loops) are jitted, should be faster? --- desc/objectives/objective_funs.py | 198 +++++++++++------- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 2 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 8 +- 3 files changed, 126 insertions(+), 82 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 6a7edfb7b7..f3abadf6b6 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -249,6 +249,7 @@ class ObjectiveFunction(IOAble): """ _io_attrs_ = ["_objectives"] + _static_attrs = [] def __init__( self, @@ -343,6 +344,7 @@ def __init__( "There is at least one rank that does not have any objective assigned. " f"Objectives per rank are {self._obj_per_rank}.", ) + self._static_attrs += ["mpi", "comm", "rank", "size"] if self._is_mpi and mpi is None: raise ValueError( @@ -420,21 +422,30 @@ def _worker_loop(self): f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) + x = jax.device_put(message[1], self.objectives[obj_idx_rank[0]]._device) + v = jax.device_put(message[2], self.objectives[obj_idx_rank[0]]._device) + # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective rng_rank = nvtx.start_range(message="Worker Job JVP", color="green") - J_rank = [] - for idx in obj_idx_rank: - obj = self.objectives[idx] - const = self.constants[idx] - thing_idx = self._things_per_objective_idx[idx] - xi = [message[1][i] for i in thing_idx] - vi = [message[2][i] for i in thing_idx] - xi = jax.device_put(xi, obj._device) - vi = jax.device_put(vi, obj._device) - J_rank.append(getattr(obj, message[0])(vi, xi, constants=const)) - rng_con = nvtx.start_range(message="concat/numpy", color="red") - J_rank = np.hstack(J_rank) + + @jit + def body(x, v): + J_rank = jnp.hstack( + [ + getattr(self.objectives[idx], message[0])( + [v[i] for i in self._things_per_objective_idx[idx]], + [x[i] for i in self._things_per_objective_idx[idx]], + constants=self.constants[idx], + ) + for idx in obj_idx_rank + ] + ) + return J_rank + + J_rank = body(x, v) + rng_con = nvtx.start_range(message="numpy", color="red") + J_rank = np.array(J_rank) nvtx.end_range(rng_con) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") @@ -446,15 +457,26 @@ def _worker_loop(self): + f"{obj_idx_rank}" ) rng_rank = nvtx.start_range(message="Worker Job Compute", color="green") - f_rank = [] - for idx in obj_idx_rank: - obj = self.objectives[idx] - const = self.constants[idx] - par = message[1][idx] - par = jax.device_put(par, obj._device) - f_rank.append(getattr(obj, message[0])(*par, constants=const)) - rng_con = nvtx.start_range(message="concat/numpy", color="red") - f_rank = np.concatenate(f_rank) + params = jax.device_put( + message[1], self.objectives[obj_idx_rank[0]]._device + ) + + @jit + def body(params): + f_rank = jnp.concatenate( + [ + getattr(self.objectives[idx], message[0])( + *params[idx], + constants=self.constants[idx], + ) + for idx in obj_idx_rank + ] + ) + return f_rank + + f_rank = body(params) + rng_con = nvtx.start_range(message="numpy", color="red") + f_rank = np.array(f_rank) nvtx.end_range(rng_con) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") @@ -465,40 +487,46 @@ def _worker_loop(self): f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) + op = message[0].replace("proximal_jvp_", "") + x = jax.device_put(message[1], self.objectives[obj_idx_rank[0]]._device) + v = jax.device_put(message[2], self.objectives[obj_idx_rank[0]]._device) + rng_rank = nvtx.start_range( message="Worker Job JVP Proximal", color="green" ) - J_rank = [] - for idx in obj_idx_rank: - obj = self.objectives[idx] - const = self.constants[idx] - op = message[0].replace("proximal_jvp_", "") - thing_idx = self._things_per_objective_idx[idx] - xi = [message[1][i] for i in thing_idx] - vi = [message[2][i] for i in thing_idx] - assert len(xi) > 0 - assert len(vi) > 0 - assert len(xi) == len(vi) - if obj._deriv_mode == "rev": - # obj might not allow fwd mode, so compute full rev mode - # jacobian and do matmul manually. This is slightly - # inefficient, but usuallywhen rev mode is used, - # dim_f <<< dim_x, so its not too bad. - Ji = getattr(obj, "jac_" + op)(*xi, constants=const) - J_rank.append( - jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum( - axis=0 + @jit + def body(x, v): + J_rank = [] + for idx in obj_idx_rank: + obj = self.objectives[idx] + const = self.constants[idx] + + thing_idx = self._things_per_objective_idx[idx] + xi = [x[i] for i in thing_idx] + vi = [v[i] for i in thing_idx] + if obj._deriv_mode == "rev": + # obj might not allow fwd mode, so compute full rev mode + # jacobian and do matmul manually. This is slightly + # inefficient, but usuallywhen rev mode is used, + # dim_f <<< dim_x, so its not too bad. + Ji = getattr(obj, "jac_" + op)(*xi, constants=const) + J_rank.append( + jnp.array( + [Jii @ vii.T for Jii, vii in zip(Ji, vi)] + ).sum(axis=0) + ) + else: + J_rank.append( + getattr(obj, "jvp_" + op)( + [_vi for _vi in vi], xi, constants=const + ).T ) - ) - else: - J_rank.append( - getattr(obj, "jvp_" + op)( - [_vi for _vi in vi], xi, constants=const - ).T - ) - rng_con = nvtx.start_range(message="concat/numpy", color="red") - J_rank = np.vstack(J_rank) + return jnp.vstack(J_rank) + + J_rank = body(x, v) + rng_con = nvtx.start_range(message="numpy", color="red") + J_rank = np.array(J_rank) nvtx.end_range(rng_con) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") @@ -768,15 +796,20 @@ def compute_unscaled(self, x, constants=None): rng = nvtx.start_range( message="compute_unscaled on master", color="blue" ) - f_rank = [] - for idx in obj_idx_rank: - par = params[idx] - obj = self.objectives[idx] - const = self.constants[idx] - f_rank.append(obj.compute_unscaled(*par, constants=const)) - rng_con = nvtx.start_range(message="concat/jax", color="red") - f_rank = jnp.concatenate(f_rank) - nvtx.end_range(rng_con) + + @jit + def body(params): + f_rank = jnp.concatenate( + [ + self.objectives[idx].compute_unscaled( + *params[idx], constants=self.constants[idx] + ) + for idx in obj_idx_rank + ] + ) + return f_rank + + f_rank = body(params) nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -826,15 +859,20 @@ def compute_scaled(self, x, constants=None): + f"{obj_idx_rank}" ) rng = nvtx.start_range(message="compute_scaled on master", color="blue") - f_rank = [] - for idx in obj_idx_rank: - par = params[idx] - obj = self.objectives[idx] - const = self.constants[idx] - f_rank.append(obj.compute_scaled(*par, constants=const)) - rng_con = nvtx.start_range(message="concat/jax", color="red") - f_rank = jnp.concatenate(f_rank) - nvtx.end_range(rng_con) + + @jit + def body(params): + f_rank = jnp.concatenate( + [ + self.objectives[idx].compute_scaled( + *params[idx], constants=self.constants[idx] + ) + for idx in obj_idx_rank + ] + ) + return f_rank + + f_rank = body(params) nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -886,15 +924,20 @@ def compute_scaled_error(self, x, constants=None): rng = nvtx.start_range( message="compute_scaled_error on master", color="blue" ) - f_rank = [] - for idx in obj_idx_rank: - par = params[idx] - obj = self.objectives[idx] - const = self.constants[idx] - f_rank.append(obj.compute_scaled_error(*par, constants=const)) - rng_con = nvtx.start_range(message="concat/jax", color="red") - f_rank = jnp.concatenate(f_rank) - nvtx.end_range(rng_con) + + @jit + def body(params): + f_rank = jnp.concatenate( + [ + self.objectives[idx].compute_scaled_error( + *params[idx], constants=self.constants[idx] + ) + for idx in obj_idx_rank + ] + ) + return f_rank + + f_rank = body(params) nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -992,6 +1035,7 @@ def print_value(self, x, x0=None, constants=None): out[obj._print_value_fmt] = [outi] return out + @functools.partial(jit, static_argnames="per_objective") def unpack_state(self, x, per_objective=True): """Unpack the state vector into its components. diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 7cfec555d3..e401a0d319 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -10,7 +10,7 @@ from desc import _set_cpu_count, set_device # ====== Using CPUs ====== -num_device = 4 +num_device = 2 # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index d35583162e..638cb8f3a7 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -15,8 +15,8 @@ # such that JAX and XLA thinks there are multiple devices # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! -# _set_cpu_count(num_device) -# set_device("cpu", num_device=num_device, mpi=MPI) +_set_cpu_count(num_device) +set_device("cpu", num_device=num_device, mpi=MPI) # ====== Using GPUs ====== # When we have multiple processes using the same devices (for example, 3 processes @@ -25,8 +25,8 @@ # such that there is no pre-allocation. This is a bit conservative (and probably there is room # for improvement), but if a process needs more memory, it can use more memory on the fly. # -os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" -set_device("gpu", num_device=num_device) +# os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" +# set_device("gpu", num_device=num_device) import numpy as np From eb4e844656297daef2f8c449207e43253b3b53f5 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 03:12:51 -0400 Subject: [PATCH 120/199] jit the functions on the proper device --- desc/objectives/objective_funs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index f3abadf6b6..33a4e14cfe 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -429,7 +429,7 @@ def _worker_loop(self): # put xi and vi on the same device as the objective rng_rank = nvtx.start_range(message="Worker Job JVP", color="green") - @jit + @functools.partial(jit, device=self.objectives[obj_idx_rank[0]]._device) def body(x, v): J_rank = jnp.hstack( [ @@ -461,7 +461,7 @@ def body(x, v): message[1], self.objectives[obj_idx_rank[0]]._device ) - @jit + @functools.partial(jit, device=self.objectives[obj_idx_rank[0]]._device) def body(params): f_rank = jnp.concatenate( [ @@ -495,7 +495,7 @@ def body(params): message="Worker Job JVP Proximal", color="green" ) - @jit + @functools.partial(jit, device=self.objectives[obj_idx_rank[0]]._device) def body(x, v): J_rank = [] for idx in obj_idx_rank: From 5fbef6e252bc01a6b8b5aa392b3f77aa6f9ef101 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 11:15:22 -0400 Subject: [PATCH 121/199] move the jitted function out of self to prevent recompilation for compute methods --- desc/objectives/objective_funs.py | 109 +++++++++++++++--------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 33a4e14cfe..972f321034 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -461,20 +461,16 @@ def body(x, v): message[1], self.objectives[obj_idx_rank[0]]._device ) - @functools.partial(jit, device=self.objectives[obj_idx_rank[0]]._device) - def body(params): - f_rank = jnp.concatenate( - [ - getattr(self.objectives[idx], message[0])( - *params[idx], - constants=self.constants[idx], - ) - for idx in obj_idx_rank - ] - ) - return f_rank - - f_rank = body(params) + f_rank = jit( + compute_per_process, + device=self.objectives[obj_idx_rank[0]]._device, + static_argnames="op", + )( + [params[i] for i in obj_idx_rank], + [self.objectives[i] for i in obj_idx_rank], + [self.constants[i] for i in obj_idx_rank], + op=message[0], + ) rng_con = nvtx.start_range(message="numpy", color="red") f_rank = np.array(f_rank) nvtx.end_range(rng_con) @@ -797,19 +793,16 @@ def compute_unscaled(self, x, constants=None): message="compute_unscaled on master", color="blue" ) - @jit - def body(params): - f_rank = jnp.concatenate( - [ - self.objectives[idx].compute_unscaled( - *params[idx], constants=self.constants[idx] - ) - for idx in obj_idx_rank - ] - ) - return f_rank - - f_rank = body(params) + f_rank = jit( + compute_per_process, + device=self.objectives[obj_idx_rank[0]]._device, + static_argnames="op", + )( + [params[i] for i in obj_idx_rank], + [self.objectives[i] for i in obj_idx_rank], + [self.constants[i] for i in obj_idx_rank], + op=message[0], + ) nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -860,19 +853,16 @@ def compute_scaled(self, x, constants=None): ) rng = nvtx.start_range(message="compute_scaled on master", color="blue") - @jit - def body(params): - f_rank = jnp.concatenate( - [ - self.objectives[idx].compute_scaled( - *params[idx], constants=self.constants[idx] - ) - for idx in obj_idx_rank - ] - ) - return f_rank - - f_rank = body(params) + f_rank = jit( + compute_per_process, + device=self.objectives[obj_idx_rank[0]]._device, + static_argnames="op", + )( + [params[i] for i in obj_idx_rank], + [self.objectives[i] for i in obj_idx_rank], + [self.constants[i] for i in obj_idx_rank], + op=message[0], + ) nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -925,19 +915,16 @@ def compute_scaled_error(self, x, constants=None): message="compute_scaled_error on master", color="blue" ) - @jit - def body(params): - f_rank = jnp.concatenate( - [ - self.objectives[idx].compute_scaled_error( - *params[idx], constants=self.constants[idx] - ) - for idx in obj_idx_rank - ] - ) - return f_rank - - f_rank = body(params) + f_rank = jit( + compute_per_process, + device=self.objectives[obj_idx_rank[0]]._device, + static_argnames="op", + )( + [params[i] for i in obj_idx_rank], + [self.objectives[i] for i in obj_idx_rank], + [self.constants[i] for i in obj_idx_rank], + op=message[0], + ) nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -2197,3 +2184,19 @@ def __call__(self, things): assert len(flat) == self.length unique, _ = unique_list(flat) return unique + + +# These will run on workers, and we wan to safely jit them +@functools.partial(jit, static_argnames="op") +def compute_per_process(params, objectives, constants, op): + """Compute the objective function on each process.""" + f_rank = jnp.concatenate( + [ + getattr(obj, op)( + *param, + constants=constant, + ) + for (obj, param, constant) in zip(objectives, params, constants) + ] + ) + return f_rank From 161cf2dd7c145b7370feda1bf5b83cfa70d5c1f5 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 11:49:38 -0400 Subject: [PATCH 122/199] add one more annotate --- desc/objectives/objective_funs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 972f321034..cb3cd98d57 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -781,8 +781,10 @@ def compute_unscaled(self, x, constants=None): ) else: # pragma: no cover if self.rank == 0: + rng_bcast = nvtx.start_range(message="bcast to workers", color="red") message = ("compute_unscaled", params, None) self.comm.bcast(message, root=0) + nvtx.end_range(rng_bcast) obj_idx_rank = self._obj_per_rank[self.rank] print( @@ -843,8 +845,10 @@ def compute_scaled(self, x, constants=None): ) else: # pragma: no cover if self.rank == 0: + rng_bcast = nvtx.start_range(message="bcast to workers", color="red") message = ("compute_scaled", params, None) self.comm.bcast(message, root=0) + nvtx.end_range(rng_bcast) obj_idx_rank = self._obj_per_rank[self.rank] print( @@ -903,8 +907,10 @@ def compute_scaled_error(self, x, constants=None): ) else: # pragma: no cover if self.rank == 0: + rng_bcast = nvtx.start_range(message="bcast to workers", color="red") message = ("compute_scaled_error", params, None) self.comm.bcast(message, root=0) + nvtx.end_range(rng_bcast) obj_idx_rank = self._obj_per_rank[self.rank] print( @@ -1166,8 +1172,10 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): xs = jnp.split(x, xs_splits) vs = jnp.split(v[0], xs_splits, axis=-1) nvtx.end_range(rng_unpack) + rng_bcast = nvtx.start_range(message="bcast to workers", color="red") message = ("jvp_" + op, xs, vs) self.comm.bcast(message, root=0) + nvtx.end_range(rng_bcast) obj_idx_rank = self._obj_per_rank[self.rank] print( From 8172f8c238cefeff3168cce4f3f55d865bd1ca5c Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 12:50:02 -0400 Subject: [PATCH 123/199] fix the jvp jit --- desc/objectives/objective_funs.py | 81 ++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index cb3cd98d57..b6c80b6ad1 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -422,28 +422,35 @@ def _worker_loop(self): f"Rank {self.rank} : {message[0]} for objectives ids: " + f"{obj_idx_rank}" ) - x = jax.device_put(message[1], self.objectives[obj_idx_rank[0]]._device) - v = jax.device_put(message[2], self.objectives[obj_idx_rank[0]]._device) + xs = jax.device_put( + message[1], self.objectives[obj_idx_rank[0]]._device + ) + vs = jax.device_put( + message[2], self.objectives[obj_idx_rank[0]]._device + ) # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective rng_rank = nvtx.start_range(message="Worker Job JVP", color="green") - @functools.partial(jit, device=self.objectives[obj_idx_rank[0]]._device) - def body(x, v): - J_rank = jnp.hstack( - [ - getattr(self.objectives[idx], message[0])( - [v[i] for i in self._things_per_objective_idx[idx]], - [x[i] for i in self._things_per_objective_idx[idx]], - constants=self.constants[idx], - ) - for idx in obj_idx_rank - ] - ) - return J_rank + J_rank = jit( + jvp_per_process, + device=self.objectives[obj_idx_rank[0]]._device, + static_argnames="op", + )( + [ + [xs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ], + [ + [vs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ], + [self.objectives[i] for i in obj_idx_rank], + [self.constants[i] for i in obj_idx_rank], + op=message[0], + ) - J_rank = body(x, v) rng_con = nvtx.start_range(message="numpy", color="red") J_rank = np.array(J_rank) nvtx.end_range(rng_con) @@ -1183,17 +1190,23 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): + f"{obj_idx_rank}" ) rng = nvtx.start_range(message="JVP on master", color="blue") - J_rank = [] - for idx in obj_idx_rank: - obj = self.objectives[idx] - const = self.constants[idx] - thing_idx = self._things_per_objective_idx[idx] - xi = [xs[i] for i in thing_idx] - vi = [vs[i] for i in thing_idx] - J_rank.append(getattr(obj, "jvp_" + op)(vi, xi, constants=const)) - rng_con = nvtx.start_range(message="concat/jax", color="red") - J_rank = jnp.hstack(J_rank) - nvtx.end_range(rng_con) + J_rank = jit( + jvp_per_process, + device=self.objectives[obj_idx_rank[0]]._device, + static_argnames="op", + )( + [ + [xs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ], + [ + [vs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ], + [self.objectives[i] for i in obj_idx_rank], + [self.constants[i] for i in obj_idx_rank], + op=message[0], + ) nvtx.end_range(rng) rng_gather = nvtx.start_range(message="Gather to master", color="red") print(f"Rank {self.rank} waiting to gather") @@ -2196,7 +2209,7 @@ def __call__(self, things): # These will run on workers, and we wan to safely jit them @functools.partial(jit, static_argnames="op") -def compute_per_process(params, objectives, constants, op): +def compute_per_process(params, objectives, constants, op="compute_scaled_error"): """Compute the objective function on each process.""" f_rank = jnp.concatenate( [ @@ -2208,3 +2221,15 @@ def compute_per_process(params, objectives, constants, op): ] ) return f_rank + + +@functools.partial(jit, static_argnames="op") +def jvp_per_process(x, v, objectives, constants, op="jvp_scaled_error"): + """Compute the Jacobian-vector product on each process.""" + J_rank = jnp.hstack( + [ + getattr(obj, op)(v[idx], x[idx], constants=constant) + for idx, (obj, constant) in enumerate(zip(objectives, constants)) + ] + ) + return J_rank From 0322f0954f3c95c27448ea5e47f070bd387f770b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 13:52:45 -0400 Subject: [PATCH 124/199] remove numpy calls to use cuda aware mpi --- desc/objectives/objective_funs.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index b6c80b6ad1..dc89cdd538 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -450,10 +450,6 @@ def _worker_loop(self): [self.constants[i] for i in obj_idx_rank], op=message[0], ) - - rng_con = nvtx.start_range(message="numpy", color="red") - J_rank = np.array(J_rank) - nvtx.end_range(rng_con) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(J_rank, root=0) @@ -478,9 +474,6 @@ def _worker_loop(self): [self.constants[i] for i in obj_idx_rank], op=message[0], ) - rng_con = nvtx.start_range(message="numpy", color="red") - f_rank = np.array(f_rank) - nvtx.end_range(rng_con) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(f_rank, root=0) @@ -498,6 +491,7 @@ def _worker_loop(self): message="Worker Job JVP Proximal", color="green" ) + # TODO: jit this one too as above functions!!!! @functools.partial(jit, device=self.objectives[obj_idx_rank[0]]._device) def body(x, v): J_rank = [] @@ -528,9 +522,6 @@ def body(x, v): return jnp.vstack(J_rank) J_rank = body(x, v) - rng_con = nvtx.start_range(message="numpy", color="red") - J_rank = np.array(J_rank) - nvtx.end_range(rng_con) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(J_rank, root=0) From 57f13d9b15e5baa9ced40a24a4c5a62df8763114 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 14:33:46 -0400 Subject: [PATCH 125/199] jit the proximal too, re add the numpy calls until we figure out how to install cuda aware mpi --- desc/objectives/objective_funs.py | 89 +++++++++++++++++++------------ 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index dc89cdd538..98e3bf1ac9 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -450,6 +450,9 @@ def _worker_loop(self): [self.constants[i] for i in obj_idx_rank], op=message[0], ) + rng_np = nvtx.start_range(message="numpy", color="red") + J_rank = np.asarray(J_rank) + nvtx.end_range(rng_np) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(J_rank, root=0) @@ -474,6 +477,9 @@ def _worker_loop(self): [self.constants[i] for i in obj_idx_rank], op=message[0], ) + rng_np = nvtx.start_range(message="numpy", color="red") + f_rank = np.asarray(f_rank) + nvtx.end_range(rng_np) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(f_rank, root=0) @@ -484,44 +490,36 @@ def _worker_loop(self): + f"{obj_idx_rank}" ) op = message[0].replace("proximal_jvp_", "") - x = jax.device_put(message[1], self.objectives[obj_idx_rank[0]]._device) - v = jax.device_put(message[2], self.objectives[obj_idx_rank[0]]._device) + xs = jax.device_put( + message[1], self.objectives[obj_idx_rank[0]]._device + ) + vs = jax.device_put( + message[2], self.objectives[obj_idx_rank[0]]._device + ) rng_rank = nvtx.start_range( message="Worker Job JVP Proximal", color="green" ) - - # TODO: jit this one too as above functions!!!! - @functools.partial(jit, device=self.objectives[obj_idx_rank[0]]._device) - def body(x, v): - J_rank = [] - for idx in obj_idx_rank: - obj = self.objectives[idx] - const = self.constants[idx] - - thing_idx = self._things_per_objective_idx[idx] - xi = [x[i] for i in thing_idx] - vi = [v[i] for i in thing_idx] - if obj._deriv_mode == "rev": - # obj might not allow fwd mode, so compute full rev mode - # jacobian and do matmul manually. This is slightly - # inefficient, but usuallywhen rev mode is used, - # dim_f <<< dim_x, so its not too bad. - Ji = getattr(obj, "jac_" + op)(*xi, constants=const) - J_rank.append( - jnp.array( - [Jii @ vii.T for Jii, vii in zip(Ji, vi)] - ).sum(axis=0) - ) - else: - J_rank.append( - getattr(obj, "jvp_" + op)( - [_vi for _vi in vi], xi, constants=const - ).T - ) - return jnp.vstack(J_rank) - - J_rank = body(x, v) + J_rank = jit( + jvp_proximal_per_process, + device=self.objectives[obj_idx_rank[0]]._device, + static_argnames="op", + )( + [ + [xs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ], + [ + [vs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ], + [self.objectives[i] for i in obj_idx_rank], + [self.constants[i] for i in obj_idx_rank], + op=op, + ) + rng_np = nvtx.start_range(message="numpy", color="red") + J_rank = np.asarray(J_rank) + nvtx.end_range(rng_np) nvtx.end_range(rng_rank) rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(J_rank, root=0) @@ -2224,3 +2222,26 @@ def jvp_per_process(x, v, objectives, constants, op="jvp_scaled_error"): ] ) return J_rank + + +@functools.partial(jit, static_argnames="op") +def jvp_proximal_per_process(x, v, objectives, constants, op="scaled_error"): + """Compute the Jacobian-vector product on each process, for proximal.""" + J_rank = [] + for idx, (obj, constant) in enumerate(zip(objectives, constants)): + if obj._deriv_mode == "rev": + # obj might not allow fwd mode, so compute full rev mode + # jacobian and do matmul manually. This is slightly + # inefficient, but usuallywhen rev mode is used, + # dim_f <<< dim_x, so its not too bad. + Ji = getattr(obj, "jac_" + op)(*x[idx], constants=constant) + J_rank.append( + jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, v[idx])]).sum(axis=0) + ) + else: + J_rank.append( + getattr(obj, "jvp_" + op)( + [_vi for _vi in v[idx]], x[idx], constants=constant + ).T + ) + return jnp.vstack(J_rank) From 5b15199992508d4fd4d9b06efd504358c1cf04f2 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 15:02:07 -0400 Subject: [PATCH 126/199] add annotations and use jit for proximal everywhere --- desc/optimize/_constraint_wrappers.py | 54 +++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 3f59873426..59e93fba3c 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -3,6 +3,7 @@ import functools import numpy as np +import nvtx from desc.backend import jit, jnp, pconcat, put from desc.batching import batched_vectorize @@ -13,6 +14,7 @@ get_fixed_boundary_constraints, maybe_add_self_consistency, ) +from desc.objectives.objective_funs import jvp_proximal_per_process from desc.objectives.utils import ( _Project, _Recover, @@ -1364,32 +1366,30 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): print( f"Rank {objective.rank} : {message[0]} for objectives ids: {obj_idx_rank}" ) - J_rank = [] - for idx in obj_idx_rank: - obj = objective.objectives[idx] - const = objective.constants[idx] - - thing_idx = objective._things_per_objective_idx[idx] - xi = [xgs[i] for i in thing_idx] - vi = [vgs[i] for i in thing_idx] - assert len(xi) > 0 - assert len(vi) > 0 - assert len(xi) == len(vi) - if obj._deriv_mode == "rev": - # obj might not allow fwd mode, so compute full rev mode jacobian - # and do matmul manually. This is slightly inefficient, but usually - # when rev mode is used, dim_f <<< dim_x, so its not too bad. - Ji = getattr(obj, "jac_" + op)(*xi, constants=const) - J_rank.append( - jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, vi)]).sum(axis=0) - ) - else: - J_rank.append( - getattr(obj, "jvp_" + op)( - [_vi for _vi in vi], xi, constants=const - ).T - ) - J_rank = jnp.concatenate(J_rank) + rng_rank = nvtx.start_range(message="JVP Proximal on master", color="green") + J_rank = jit( + jvp_proximal_per_process, + device=objective.objectives[obj_idx_rank[0]]._device, + static_argnames="op", + )( + [ + [xgs[i] for i in objective._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ], + [ + [vgs[i] for i in objective._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ], + [objective.objectives[i] for i in obj_idx_rank], + [objective.constants[i] for i in obj_idx_rank], + op=op, + ) + nvtx.end_range(rng_rank) print(f"Rank {objective.rank} waiting to gather") + rng_gat = nvtx.start_range(message="Gather to master", color="green") J = objective.comm.gather(J_rank, root=0) - return pconcat(J).T + nvtx.end_range(rng_gat) + rng_pcat = nvtx.start_range(message="Pconcat", color="blue") + J = pconcat(J).T + nvtx.end_range(rng_pcat) + return J From 57cb64af942c62d1ecc468d5ff388275a879d87b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 15:32:55 -0400 Subject: [PATCH 127/199] add more flags, form lists outside --- desc/objectives/objective_funs.py | 66 +++++++++++++++++---------- desc/optimize/_constraint_wrappers.py | 28 ++++++++---- 2 files changed, 61 insertions(+), 33 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 98e3bf1ac9..512ebbfd69 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -432,22 +432,32 @@ def _worker_loop(self): # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective rng_rank = nvtx.start_range(message="Worker Job JVP", color="green") + rng_xv = nvtx.start_range(message="form x and v", color="red") + xs = [ + [xs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ] + vs = [ + [vs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ] + nvtx.end_range(rng_xv) + rng_obj = nvtx.start_range( + message="form objs and constants", color="red" + ) + objs = [self.objectives[i] for i in obj_idx_rank] + constants = [self.constants[i] for i in obj_idx_rank] + nvtx.end_range(rng_obj) J_rank = jit( jvp_per_process, device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( - [ - [xs[i] for i in self._things_per_objective_idx[idx]] - for idx in obj_idx_rank - ], - [ - [vs[i] for i in self._things_per_objective_idx[idx]] - for idx in obj_idx_rank - ], - [self.objectives[i] for i in obj_idx_rank], - [self.constants[i] for i in obj_idx_rank], + xs, + vs, + objs, + constants, op=message[0], ) rng_np = nvtx.start_range(message="numpy", color="red") @@ -500,21 +510,31 @@ def _worker_loop(self): rng_rank = nvtx.start_range( message="Worker Job JVP Proximal", color="green" ) + rng_xv = nvtx.start_range(message="form x and v", color="red") + xs = [ + [xs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ] + vs = [ + [vs[i] for i in self._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ] + nvtx.end_range(rng_xv) + rng_obj = nvtx.start_range( + message="form objs and constants", color="red" + ) + objs = [self.objectives[i] for i in obj_idx_rank] + constants = [self.constants[i] for i in obj_idx_rank] + nvtx.end_range(rng_obj) J_rank = jit( jvp_proximal_per_process, device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( - [ - [xs[i] for i in self._things_per_objective_idx[idx]] - for idx in obj_idx_rank - ], - [ - [vs[i] for i in self._things_per_objective_idx[idx]] - for idx in obj_idx_rank - ], - [self.objectives[i] for i in obj_idx_rank], - [self.constants[i] for i in obj_idx_rank], + xs, + vs, + objs, + constants, op=op, ) rng_np = nvtx.start_range(message="numpy", color="red") @@ -2209,7 +2229,7 @@ def compute_per_process(params, objectives, constants, op="compute_scaled_error" for (obj, param, constant) in zip(objectives, params, constants) ] ) - return f_rank + return f_rank.block_until_ready() @functools.partial(jit, static_argnames="op") @@ -2221,7 +2241,7 @@ def jvp_per_process(x, v, objectives, constants, op="jvp_scaled_error"): for idx, (obj, constant) in enumerate(zip(objectives, constants)) ] ) - return J_rank + return J_rank.block_until_ready() @functools.partial(jit, static_argnames="op") @@ -2244,4 +2264,4 @@ def jvp_proximal_per_process(x, v, objectives, constants, op="scaled_error"): [_vi for _vi in v[idx]], x[idx], constants=constant ).T ) - return jnp.vstack(J_rank) + return jnp.vstack(J_rank).block_until_ready() diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 59e93fba3c..a776590e6b 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1367,21 +1367,29 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): f"Rank {objective.rank} : {message[0]} for objectives ids: {obj_idx_rank}" ) rng_rank = nvtx.start_range(message="JVP Proximal on master", color="green") + rng_xv = nvtx.start_range(message="form x and v", color="red") + xs = [ + [xgs[i] for i in objective._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ] + vs = [ + [vgs[i] for i in objective._things_per_objective_idx[idx]] + for idx in obj_idx_rank + ] + nvtx.end_range(rng_xv) + rng_obj = nvtx.start_range(message="form objs and constants", color="red") + objs = [objective.objectives[i] for i in obj_idx_rank] + constants = [objective.constants[i] for i in obj_idx_rank] + nvtx.end_range(rng_obj) J_rank = jit( jvp_proximal_per_process, device=objective.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( - [ - [xgs[i] for i in objective._things_per_objective_idx[idx]] - for idx in obj_idx_rank - ], - [ - [vgs[i] for i in objective._things_per_objective_idx[idx]] - for idx in obj_idx_rank - ], - [objective.objectives[i] for i in obj_idx_rank], - [objective.constants[i] for i in obj_idx_rank], + xs, + vs, + objs, + constants, op=op, ) nvtx.end_range(rng_rank) From 85acda15a35847210ae8c95fff79fd60c031ee44 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 15:47:59 -0400 Subject: [PATCH 128/199] fix block_until_ready --- desc/objectives/objective_funs.py | 20 ++++++++++---------- desc/optimize/_constraint_wrappers.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 512ebbfd69..eb3cc7dd09 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -459,7 +459,7 @@ def _worker_loop(self): objs, constants, op=message[0], - ) + ).block_until_ready() rng_np = nvtx.start_range(message="numpy", color="red") J_rank = np.asarray(J_rank) nvtx.end_range(rng_np) @@ -486,7 +486,7 @@ def _worker_loop(self): [self.objectives[i] for i in obj_idx_rank], [self.constants[i] for i in obj_idx_rank], op=message[0], - ) + ).block_until_ready() rng_np = nvtx.start_range(message="numpy", color="red") f_rank = np.asarray(f_rank) nvtx.end_range(rng_np) @@ -536,7 +536,7 @@ def _worker_loop(self): objs, constants, op=op, - ) + ).block_until_ready() rng_np = nvtx.start_range(message="numpy", color="red") J_rank = np.asarray(J_rank) nvtx.end_range(rng_np) @@ -820,7 +820,7 @@ def compute_unscaled(self, x, constants=None): [self.objectives[i] for i in obj_idx_rank], [self.constants[i] for i in obj_idx_rank], op=message[0], - ) + ).block_until_ready() nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -882,7 +882,7 @@ def compute_scaled(self, x, constants=None): [self.objectives[i] for i in obj_idx_rank], [self.constants[i] for i in obj_idx_rank], op=message[0], - ) + ).block_until_ready() nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -946,7 +946,7 @@ def compute_scaled_error(self, x, constants=None): [self.objectives[i] for i in obj_idx_rank], [self.constants[i] for i in obj_idx_rank], op=message[0], - ) + ).block_until_ready() nvtx.end_range(rng) print(f"Rank {self.rank} waiting to gather") rng_gather = nvtx.start_range(message="Gather to master", color="red") @@ -1215,7 +1215,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): [self.objectives[i] for i in obj_idx_rank], [self.constants[i] for i in obj_idx_rank], op=message[0], - ) + ).block_until_ready() nvtx.end_range(rng) rng_gather = nvtx.start_range(message="Gather to master", color="red") print(f"Rank {self.rank} waiting to gather") @@ -2229,7 +2229,7 @@ def compute_per_process(params, objectives, constants, op="compute_scaled_error" for (obj, param, constant) in zip(objectives, params, constants) ] ) - return f_rank.block_until_ready() + return f_rank @functools.partial(jit, static_argnames="op") @@ -2241,7 +2241,7 @@ def jvp_per_process(x, v, objectives, constants, op="jvp_scaled_error"): for idx, (obj, constant) in enumerate(zip(objectives, constants)) ] ) - return J_rank.block_until_ready() + return J_rank @functools.partial(jit, static_argnames="op") @@ -2264,4 +2264,4 @@ def jvp_proximal_per_process(x, v, objectives, constants, op="scaled_error"): [_vi for _vi in v[idx]], x[idx], constants=constant ).T ) - return jnp.vstack(J_rank).block_until_ready() + return jnp.vstack(J_rank) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index a776590e6b..ca4f1bf116 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1400,4 +1400,4 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): rng_pcat = nvtx.start_range(message="Pconcat", color="blue") J = pconcat(J).T nvtx.end_range(rng_pcat) - return J + return J.block_until_ready() From 9951c57e77cc60d2de79db0fd98c4c5db5748407 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 12 Jun 2025 17:29:14 -0400 Subject: [PATCH 129/199] no mpi benchmark case --- .../mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py new file mode 100644 index 0000000000..3d027e952c --- /dev/null +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py @@ -0,0 +1,70 @@ +import os +import sys + +# Add the path to the parent directory to augment search for module +sys.path.insert(0, os.path.abspath(".")) +sys.path.append(os.path.abspath("../../../../")) + +import nvtx + +from desc import set_device + +os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" +set_device("gpu") +import desc +from desc import config as desc_config +from desc.backend import jax, jnp, print_backend_info +from desc.examples import get +from desc.objectives import ForceBalance, ObjectiveFunction +from desc.objectives.getters import get_fixed_boundary_constraints + +if __name__ == "__main__": + print("====== BACKEND INFO ======") + print_backend_info() + print("\n") + + with nvtx.annotate("Setup"): + eq = get("HELIOTRON") + rhos = jnp.linspace(0.01, 1.0, eq.L_grid) + grid1 = desc.grid.LinearGrid( + rho=rhos[0 : eq.L_grid // 2], + # kind of experimental way of set giving + # less grid points to inner part, but seems + # to make transforms way slower + # M=int(eq.M_grid * i / num_device), # noqa: E800 + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + ) + grid2 = desc.grid.LinearGrid( + rho=rhos[eq.L_grid // 2 :], + # kind of experimental way of set giving + # less grid points to inner part, but seems + # to make transforms way slower + # M=int(eq.M_grid * i / num_device), # noqa: E800 + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + ) + obj = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1), + ForceBalance(eq, grid=grid2), + ], + deriv_mode="blocked", + ) + obj.build() + cons = get_fixed_boundary_constraints(eq) + + with nvtx.annotate("Solve"): + eq.solve( + objective=obj, + constraints=cons, + maxiter=3, + ftol=0, + gtol=0, + xtol=0, + verbose=3, + ) + + # if you put a code here, it will be performed on all ranks From fe96f2ec99f999db961aff8eb3577423cceda5df Mon Sep 17 00:00:00 2001 From: YigitElma Date: Fri, 13 Jun 2025 11:47:19 -0400 Subject: [PATCH 130/199] increase maxiter --- .../tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py | 2 +- docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py index 3d027e952c..ab43ed6de1 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py @@ -60,7 +60,7 @@ eq.solve( objective=obj, constraints=cons, - maxiter=3, + maxiter=10, ftol=0, gtol=0, xtol=0, diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py index 7b0103742c..56350cc53a 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py @@ -25,7 +25,7 @@ # cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` # such that there is no pre-allocation. This is a bit conservative (and probably there is room # for improvement), but if a process needs more memory, it can use more memory on the fly. -# + os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" set_device("gpu", num_device=num_device) @@ -85,7 +85,7 @@ eq.solve( objective=obj, constraints=cons, - maxiter=3, + maxiter=10, ftol=0, gtol=0, xtol=0, From 8fc2769a970a4484201e8c365806641e3bd5a379 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Fri, 13 Jun 2025 13:25:02 -0400 Subject: [PATCH 131/199] remove constants from args --- desc/objectives/objective_funs.py | 53 +++++---------------------- desc/optimize/_constraint_wrappers.py | 3 -- 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index eb3cc7dd09..8ea50a1892 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -413,6 +413,7 @@ def _worker_loop(self): message = self.comm.bcast(message, root=0) nvtx.end_range(rng_wait) obj_idx_rank = self._obj_per_rank[self.rank] + objs = [self.objectives[i] for i in obj_idx_rank] if message[0] == "STOP": print(f"Rank {self.rank} STOPPING") @@ -442,22 +443,14 @@ def _worker_loop(self): for idx in obj_idx_rank ] nvtx.end_range(rng_xv) - rng_obj = nvtx.start_range( - message="form objs and constants", color="red" - ) - objs = [self.objectives[i] for i in obj_idx_rank] - constants = [self.constants[i] for i in obj_idx_rank] - nvtx.end_range(rng_obj) J_rank = jit( jvp_per_process, - device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( xs, vs, objs, - constants, op=message[0], ).block_until_ready() rng_np = nvtx.start_range(message="numpy", color="red") @@ -479,12 +472,10 @@ def _worker_loop(self): f_rank = jit( compute_per_process, - device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( [params[i] for i in obj_idx_rank], - [self.objectives[i] for i in obj_idx_rank], - [self.constants[i] for i in obj_idx_rank], + objs, op=message[0], ).block_until_ready() rng_np = nvtx.start_range(message="numpy", color="red") @@ -520,21 +511,13 @@ def _worker_loop(self): for idx in obj_idx_rank ] nvtx.end_range(rng_xv) - rng_obj = nvtx.start_range( - message="form objs and constants", color="red" - ) - objs = [self.objectives[i] for i in obj_idx_rank] - constants = [self.constants[i] for i in obj_idx_rank] - nvtx.end_range(rng_obj) J_rank = jit( jvp_proximal_per_process, - device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( xs, vs, objs, - constants, op=op, ).block_until_ready() rng_np = nvtx.start_range(message="numpy", color="red") @@ -813,12 +796,10 @@ def compute_unscaled(self, x, constants=None): f_rank = jit( compute_per_process, - device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( [params[i] for i in obj_idx_rank], [self.objectives[i] for i in obj_idx_rank], - [self.constants[i] for i in obj_idx_rank], op=message[0], ).block_until_ready() nvtx.end_range(rng) @@ -875,12 +856,10 @@ def compute_scaled(self, x, constants=None): f_rank = jit( compute_per_process, - device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( [params[i] for i in obj_idx_rank], [self.objectives[i] for i in obj_idx_rank], - [self.constants[i] for i in obj_idx_rank], op=message[0], ).block_until_ready() nvtx.end_range(rng) @@ -939,12 +918,10 @@ def compute_scaled_error(self, x, constants=None): f_rank = jit( compute_per_process, - device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( [params[i] for i in obj_idx_rank], [self.objectives[i] for i in obj_idx_rank], - [self.constants[i] for i in obj_idx_rank], op=message[0], ).block_until_ready() nvtx.end_range(rng) @@ -1201,7 +1178,6 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): rng = nvtx.start_range(message="JVP on master", color="blue") J_rank = jit( jvp_per_process, - device=self.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( [ @@ -1213,7 +1189,6 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): for idx in obj_idx_rank ], [self.objectives[i] for i in obj_idx_rank], - [self.constants[i] for i in obj_idx_rank], op=message[0], ).block_until_ready() nvtx.end_range(rng) @@ -2218,50 +2193,42 @@ def __call__(self, things): # These will run on workers, and we wan to safely jit them @functools.partial(jit, static_argnames="op") -def compute_per_process(params, objectives, constants, op="compute_scaled_error"): +def compute_per_process(params, objectives, op="compute_scaled_error"): """Compute the objective function on each process.""" f_rank = jnp.concatenate( [ getattr(obj, op)( *param, - constants=constant, ) - for (obj, param, constant) in zip(objectives, params, constants) + for (obj, param) in zip(objectives, params) ] ) return f_rank @functools.partial(jit, static_argnames="op") -def jvp_per_process(x, v, objectives, constants, op="jvp_scaled_error"): +def jvp_per_process(x, v, objectives, op="jvp_scaled_error"): """Compute the Jacobian-vector product on each process.""" J_rank = jnp.hstack( - [ - getattr(obj, op)(v[idx], x[idx], constants=constant) - for idx, (obj, constant) in enumerate(zip(objectives, constants)) - ] + [getattr(obj, op)(v[idx], x[idx]) for idx, obj in enumerate(objectives)] ) return J_rank @functools.partial(jit, static_argnames="op") -def jvp_proximal_per_process(x, v, objectives, constants, op="scaled_error"): +def jvp_proximal_per_process(x, v, objectives, op="scaled_error"): """Compute the Jacobian-vector product on each process, for proximal.""" J_rank = [] - for idx, (obj, constant) in enumerate(zip(objectives, constants)): + for idx, obj in enumerate(objectives): if obj._deriv_mode == "rev": # obj might not allow fwd mode, so compute full rev mode # jacobian and do matmul manually. This is slightly # inefficient, but usuallywhen rev mode is used, # dim_f <<< dim_x, so its not too bad. - Ji = getattr(obj, "jac_" + op)(*x[idx], constants=constant) + Ji = getattr(obj, "jac_" + op)(*x[idx]) J_rank.append( jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, v[idx])]).sum(axis=0) ) else: - J_rank.append( - getattr(obj, "jvp_" + op)( - [_vi for _vi in v[idx]], x[idx], constants=constant - ).T - ) + J_rank.append(getattr(obj, "jvp_" + op)([_vi for _vi in v[idx]], x[idx]).T) return jnp.vstack(J_rank) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index ca4f1bf116..6892f1716a 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1379,17 +1379,14 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): nvtx.end_range(rng_xv) rng_obj = nvtx.start_range(message="form objs and constants", color="red") objs = [objective.objectives[i] for i in obj_idx_rank] - constants = [objective.constants[i] for i in obj_idx_rank] nvtx.end_range(rng_obj) J_rank = jit( jvp_proximal_per_process, - device=objective.objectives[obj_idx_rank[0]]._device, static_argnames="op", )( xs, vs, objs, - constants, op=op, ) nvtx.end_range(rng_rank) From 63f7b5de73a152b7904fdda061241833d99ee147 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 30 Jun 2025 13:34:49 -0400 Subject: [PATCH 132/199] remove jit_with_device, remove some debugging --- desc/backend.py | 24 --- desc/objectives/objective_funs.py | 42 +++-- .../mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py | 70 ------- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 93 ---------- .../mpi-tutorials/mpi-proximal-nvtx-2node.py | 171 ------------------ .../tutorials/mpi-tutorials/mpi-proximal.py | 149 --------------- 6 files changed, 23 insertions(+), 526 deletions(-) delete mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py delete mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py delete mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx-2node.py delete mode 100644 docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py diff --git a/desc/backend.py b/desc/backend.py index e9f964d870..e20b021d55 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -538,35 +538,11 @@ def pconcat(arrays, mode="concat"): # pragma: no cover nvtx.end_range(rng_pconcat) return out - def jit_with_device(method): - """Decorator to Just-in-time compile a class method with a dynamic device. - - Decorates a method of a class with a dynamic device, allowing the method to be - compiled with jax.jit for the specific device. This is needed since - @functools.partial(jax.jit, device=obj._device) is not - allowed in a class definition. - - Parameters - ---------- - method : callable - Class method to decorate. If DESC is running on GPU, the class should have - a device_id attribute. - """ - - @functools.wraps(method) - def wrapper(self, *args, **kwargs): - # Compile the method with jax.jit for the specific device - wrapped = jax.jit(method, device=self._device) - return wrapped(self, *args, **kwargs) - - return wrapper - # we can't really test the numpy backend stuff in automated testing, so we ignore it # for coverage purposes else: # pragma: no cover jit = lambda func, *args, **kwargs: func - jit_with_device = jit execute_on_cpu = lambda func: func import scipy.optimize from numpy.fft import ifft, irfft, irfft2, rfft, rfft2 # noqa: F401 diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 8ea50a1892..d41bcf8798 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -11,7 +11,6 @@ execute_on_cpu, jax, jit, - jit_with_device, jnp, pconcat, tree_flatten, @@ -1569,12 +1568,14 @@ def __init__( self._jac_chunk_size = jac_chunk_size self._device_id = device_id - # if device_id is not 0, this typically means we are using multiple devices and - # we won't jit the ObjectiveFunction methods. For single device, if we set - # _device to a jaxlib.xla_extension.Device type, jit will throw error expecting - # it to be static. So we set _device to None in that case which is simpler then - # making it static. - if device_id != 0 and desc_config["kind"] == "gpu": + # if we have multiple GPU devices, this will help the data placement + # TODO: figure out why we cannot use it with constraints? Computation + # gets stuck! + if ( + desc_config["num_device"] != 1 + and desc_config["kind"] == "gpu" + and not self._linear + ): self._device = jax.devices("gpu")[device_id] else: # we won't transfer data for multiple CPUs because their rank should @@ -1693,6 +1694,9 @@ def build(self, use_jit=True, verbose=1): self._unjit() self._built = True + # put the constants to device as jax arrays + if desc_config["kind"] == "gpu": + self._constants = jax.device_put(self.constants, self._device) @abstractmethod def compute(self, *args, **kwargs): @@ -1708,7 +1712,7 @@ def _maybe_array_to_params(self, *args): argsout += (arg,) return argsout - @jit_with_device + @jit def compute_unscaled(self, *args, **kwargs): """Compute the raw value of the objective.""" args = self._maybe_array_to_params(*args) @@ -1717,7 +1721,7 @@ def compute_unscaled(self, *args, **kwargs): f = self._loss_function(f) return jnp.atleast_1d(f) - @jit_with_device + @jit def compute_scaled(self, *args, **kwargs): """Compute and apply weighting and normalization.""" args = self._maybe_array_to_params(*args) @@ -1726,7 +1730,7 @@ def compute_scaled(self, *args, **kwargs): f = self._loss_function(f) return jnp.atleast_1d(self._scale(f, **kwargs)) - @jit_with_device + @jit def compute_scaled_error(self, *args, **kwargs): """Compute and apply the target/bounds, weighting, and normalization.""" args = self._maybe_array_to_params(*args) @@ -1769,7 +1773,7 @@ def _scale(self, f, *args, **kwargs): f_norm = jnp.atleast_1d(f) / self.normalization # normalization return f_norm * w * self.weight - @jit_with_device + @jit def compute_scalar(self, *args, **kwargs): """Compute the scalar form of the objective.""" if self.scalar: @@ -1778,19 +1782,19 @@ def compute_scalar(self, *args, **kwargs): f = jnp.sum(self.compute_scaled_error(*args, **kwargs) ** 2) / 2 return f.squeeze() - @jit_with_device + @jit def grad(self, *args, **kwargs): """Compute gradient vector of self.compute_scalar wrt x.""" argnums = tuple(range(len(self.things))) return Derivative(self.compute_scalar, argnums, mode="grad")(*args, **kwargs) - @jit_with_device + @jit def hess(self, *args, **kwargs): """Compute Hessian matrix of self.compute_scalar wrt x.""" argnums = tuple(range(len(self.things))) return Derivative(self.compute_scalar, argnums, mode="hess")(*args, **kwargs) - @jit_with_device + @jit def jac_scaled(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_scaled wrt x.""" argnums = tuple(range(len(self.things))) @@ -1801,7 +1805,7 @@ def jac_scaled(self, *args, **kwargs): chunk_size=self._jac_chunk_size, )(*args, **kwargs) - @jit_with_device + @jit def jac_scaled_error(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_scaled_error wrt x.""" argnums = tuple(range(len(self.things))) @@ -1812,7 +1816,7 @@ def jac_scaled_error(self, *args, **kwargs): chunk_size=self._jac_chunk_size, )(*args, **kwargs) - @jit_with_device + @jit def jac_unscaled(self, *args, **kwargs): """Compute Jacobian matrix of self.compute_unscaled wrt x.""" argnums = tuple(range(len(self.things))) @@ -1846,7 +1850,7 @@ def _jvp(self, v, x, constants=None, op="scaled"): # sum over different things. return jnp.sum(jnp.asarray(Jv), axis=0).T - @jit_with_device + @jit def jvp_scaled(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_scaled. @@ -1862,7 +1866,7 @@ def jvp_scaled(self, v, x, constants=None): """ return self._jvp(v, x, constants, "scaled") - @jit_with_device + @jit def jvp_scaled_error(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_scaled_error. @@ -1878,7 +1882,7 @@ def jvp_scaled_error(self, v, x, constants=None): """ return self._jvp(v, x, constants, "scaled_error") - @jit_with_device + @jit def jvp_unscaled(self, v, x, constants=None): """Compute Jacobian-vector product of self.compute_unscaled. diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py deleted file mode 100644 index ab43ed6de1..0000000000 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx-no-mpi.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import sys - -# Add the path to the parent directory to augment search for module -sys.path.insert(0, os.path.abspath(".")) -sys.path.append(os.path.abspath("../../../../")) - -import nvtx - -from desc import set_device - -os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" -set_device("gpu") -import desc -from desc import config as desc_config -from desc.backend import jax, jnp, print_backend_info -from desc.examples import get -from desc.objectives import ForceBalance, ObjectiveFunction -from desc.objectives.getters import get_fixed_boundary_constraints - -if __name__ == "__main__": - print("====== BACKEND INFO ======") - print_backend_info() - print("\n") - - with nvtx.annotate("Setup"): - eq = get("HELIOTRON") - rhos = jnp.linspace(0.01, 1.0, eq.L_grid) - grid1 = desc.grid.LinearGrid( - rho=rhos[0 : eq.L_grid // 2], - # kind of experimental way of set giving - # less grid points to inner part, but seems - # to make transforms way slower - # M=int(eq.M_grid * i / num_device), # noqa: E800 - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - ) - grid2 = desc.grid.LinearGrid( - rho=rhos[eq.L_grid // 2 :], - # kind of experimental way of set giving - # less grid points to inner part, but seems - # to make transforms way slower - # M=int(eq.M_grid * i / num_device), # noqa: E800 - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - ) - obj = ObjectiveFunction( - [ - ForceBalance(eq, grid=grid1), - ForceBalance(eq, grid=grid2), - ], - deriv_mode="blocked", - ) - obj.build() - cons = get_fixed_boundary_constraints(eq) - - with nvtx.annotate("Solve"): - eq.solve( - objective=obj, - constraints=cons, - maxiter=10, - ftol=0, - gtol=0, - xtol=0, - verbose=3, - ) - - # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py deleted file mode 100644 index e401a0d319..0000000000 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -import sys - -# Add the path to the parent directory to augment search for module -sys.path.insert(0, os.path.abspath(".")) -sys.path.append(os.path.abspath("../../../../")) - -from mpi4py import MPI - -from desc import _set_cpu_count, set_device - -# ====== Using CPUs ====== -num_device = 2 -# These will be used for diving the single CPU into multiple virtual CPUs -# such that JAX and XLA thinks there are multiple devices - -# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! -_set_cpu_count(num_device) -set_device("cpu", num_device=num_device, mpi=MPI) - -# ====== Using GPUs ====== -# When we have multiple processes using the same devices (for example, 3 processes -# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will -# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` -# such that there is no pre-allocation. This is a bit conservative (and probably there is room -# for improvement), but if a process needs more memory, it can use more memory on the fly. -# -# os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" -# set_device("gpu", num_device=num_device) - -from desc import config as desc_config -from desc.backend import jax, print_backend_info -from desc.examples import get -from desc.objectives.getters import ( - get_fixed_boundary_constraints, - get_parallel_forcebalance, -) - -if __name__ == "__main__": - rank = MPI.COMM_WORLD.Get_rank() - size = MPI.COMM_WORLD.Get_size() - if rank == 0: - print(f"====== TOTAL OF {size} RANKS ======") - - # see which rank is running on which device - # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()` - # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()` - # will return only the devices that are available to the current process. This is - # useful when you have multiple processes running on multiple nodes and you want - # to see which devices are available to each process. - if desc_config["kind"] == "gpu": - print( - f"Rank {rank} can see {jax.local_devices(backend='gpu')} " - f"and {jax.local_devices(backend='cpu')}\n" - ) - else: - print(f"Rank {rank} can see {jax.local_devices(backend='cpu')}\n") - - if rank == 0: - print("====== BACKEND INFO ======") - print_backend_info() - print("\n") - - eq = get("HELIOTRON") - eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) - - # this will create a parallel objective function - # user can create their own parallel objective function as well which will be - # shown in the next example - obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1) - cons = get_fixed_boundary_constraints(eq) - - # Until this line, the code is performed on all ranks, so it might print some - # information multiple times. The following part will only be performed on the - # master rank - - # this context manager will put the workers in a loop to listen to the master - # to compute the objective function and its derivatives - with obj as obj: - # apart from cost evaluation and derivatives, everything else will be only - # performed on the master rank - if rank == 0: - eq.solve( - objective=obj, - constraints=cons, - maxiter=3, - ftol=0, - gtol=0, - xtol=0, - verbose=3, - ) - - # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx-2node.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx-2node.py deleted file mode 100644 index dec915ee83..0000000000 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx-2node.py +++ /dev/null @@ -1,171 +0,0 @@ -import os -import sys - -# Add the path to the parent directory to augment search for module -sys.path.insert(0, os.path.abspath(".")) -sys.path.append(os.path.abspath("../../../../")) - -from mpi4py import MPI - -from desc import set_device - -# ====== Using CPUs ====== -num_device = 2 -# These will be used for diving the single CPU into multiple virtual CPUs -# such that JAX and XLA thinks there are multiple devices - -# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! -# _set_cpu_count(num_device) -# set_device("cpu", num_device=num_device, mpi=MPI) - -# ====== Using GPUs ====== -# When we have multiple processes using the same devices (for example, 3 processes -# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will -# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` -# such that there is no pre-allocation. This is a bit conservative (and probably there is room -# for improvement), but if a process needs more memory, it can use more memory on the fly. -# -os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" -set_device("gpu", num_device=num_device) - - -import numpy as np -import nvtx - -from desc import config as desc_config -from desc.backend import jax, jnp, print_backend_info -from desc.examples import get -from desc.grid import LinearGrid -from desc.objectives import ( - AspectRatio, - FixBoundaryR, - FixBoundaryZ, - FixCurrent, - FixPressure, - FixPsi, - ForceBalance, - ObjectiveFunction, - QuasisymmetryTwoTerm, -) -from desc.optimize import Optimizer - -if __name__ == "__main__": - rank = MPI.COMM_WORLD.Get_rank() - size = MPI.COMM_WORLD.Get_size() - if rank == 0: - print(f"====== TOTAL OF {size} RANKS ======") - - # see which rank is running on which device - # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()` - # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()` - # will return only the devices that are available to the current process. This is - # useful when you have multiple processes running on multiple nodes and you want - # to see which devices are available to each process. - if desc_config["kind"] == "gpu": - print( - f"Rank {rank} is running on {jax.local_devices(backend='gpu')} " - f"and {jax.local_devices(backend='cpu')}\n" - ) - else: - print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") - - if rank == 0: - print("====== BACKEND INFO ======") - print_backend_info() - print("\n") - - with nvtx.annotate("setup"): - eq = get("precise_QA") - - # create two grids with different rho values, this will effectively separate - # the quasisymmetry objective into two parts - grid1 = LinearGrid( - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - rho=jnp.linspace(0.2, 0.5, 4), - sym=True, - ) - grid2 = LinearGrid( - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - rho=jnp.linspace(0.6, 1.0, 6), - sym=True, - ) - - # when using parallel objectives, the user needs to supply the device_id - obj1 = QuasisymmetryTwoTerm( - eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0 - ) - obj2 = QuasisymmetryTwoTerm( - eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1 - ) - obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) - obj4 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) - obj5 = QuasisymmetryTwoTerm( - eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1 - ) - objs = [obj1, obj2, obj3, obj4, obj5] - - with nvtx.annotate("Build Objectives"): - # Parallel objective function needs the MPI communicator - # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will - # automatically switch to `blocked`. - objective = ObjectiveFunction( - objs, - deriv_mode="blocked", - mpi=MPI, - rank_per_objective=np.array([0, 1, 0, 2, 3]), - ) - if rank == 0: - objective.build(verbose=3) - else: - objective.build(verbose=0) - - # we will fix some modes as usual - k = 1 - R_modes = np.vstack( - ( - [0, 0, 0], - eq.surface.R_basis.modes[ - np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : - ], - ) - ) - Z_modes = eq.surface.Z_basis.modes[ - np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : - ] - constraints = ( - ForceBalance(eq=eq), - FixBoundaryR(eq=eq, modes=R_modes), - FixBoundaryZ(eq=eq, modes=Z_modes), - FixPressure(eq=eq), - FixPsi(eq=eq), - FixCurrent(eq=eq), - ) - optimizer = Optimizer("proximal-lsq-exact") - - # Until this line, the code is performed on all ranks, so it might print some - # information multiple times. The following part will only be performed on the - # master rank - - # this context manager will put the workers in a loop to listen to the master - # to compute the objective function and its derivatives - with nvtx.annotate("Optimization"): - with objective as objective: - # apart from cost evaluation and derivatives, everything else will be only - # performed on the master rank - if rank == 0: - eq.optimize( - objective=objective, - constraints=constraints, - optimizer=optimizer, - maxiter=3, - verbose=3, - options={ - "initial_trust_ratio": 1.0, - }, - ) - - # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py deleted file mode 100644 index 638cb8f3a7..0000000000 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ /dev/null @@ -1,149 +0,0 @@ -import os -import sys - -# Add the path to the parent directory to augment search for module -sys.path.insert(0, os.path.abspath(".")) -sys.path.append(os.path.abspath("../../../../")) - -from mpi4py import MPI - -from desc import _set_cpu_count, set_device - -# ====== Using CPUs ====== -num_device = 2 -# These will be used for diving the single CPU into multiple virtual CPUs -# such that JAX and XLA thinks there are multiple devices - -# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! -_set_cpu_count(num_device) -set_device("cpu", num_device=num_device, mpi=MPI) - -# ====== Using GPUs ====== -# When we have multiple processes using the same devices (for example, 3 processes -# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will -# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` -# such that there is no pre-allocation. This is a bit conservative (and probably there is room -# for improvement), but if a process needs more memory, it can use more memory on the fly. -# -# os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" -# set_device("gpu", num_device=num_device) - - -import numpy as np - -from desc import config as desc_config -from desc.backend import jax, jnp, print_backend_info -from desc.examples import get -from desc.grid import LinearGrid -from desc.objectives import ( - AspectRatio, - FixBoundaryR, - FixBoundaryZ, - FixCurrent, - FixPressure, - FixPsi, - ForceBalance, - ObjectiveFunction, - QuasisymmetryTwoTerm, -) -from desc.optimize import Optimizer - -if __name__ == "__main__": - rank = MPI.COMM_WORLD.Get_rank() - size = MPI.COMM_WORLD.Get_size() - if rank == 0: - print(f"====== TOTAL OF {size} RANKS ======") - - # see which rank is running on which device - # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()` - # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()` - # will return only the devices that are available to the current process. This is - # useful when you have multiple processes running on multiple nodes and you want - # to see which devices are available to each process. - if desc_config["kind"] == "gpu": - print( - f"Rank {rank} is running on {jax.local_devices(backend='gpu')} " - f"and {jax.local_devices(backend='cpu')}\n" - ) - else: - print(f"Rank {rank} is running on {jax.local_devices(backend='cpu')}\n") - - if rank == 0: - print("====== BACKEND INFO ======") - print_backend_info() - print("\n") - - eq = get("precise_QA") - eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) - - # create two grids with different rho values, this will effectively separate - # the quasisymmetry objective into two parts - grid1 = LinearGrid( - M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.2, 0.5, 4), sym=True - ) - grid2 = LinearGrid( - M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.6, 1.0, 6), sym=True - ) - - # when using parallel objectives, the user needs to supply the device_id - obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) - obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) - obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) - objs = [obj1, obj2, obj3] - - # Parallel objective function needs the MPI communicator - # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will - # automatically switch to `blocked`. - objective = ObjectiveFunction( - objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 0]) - ) - if rank == 0: - objective.build(verbose=3) - else: - objective.build(verbose=0) - - # we will fix some modes as usual - k = 1 - R_modes = np.vstack( - ( - [0, 0, 0], - eq.surface.R_basis.modes[ - np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : - ], - ) - ) - Z_modes = eq.surface.Z_basis.modes[ - np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : - ] - constraints = ( - ForceBalance(eq=eq), - FixBoundaryR(eq=eq, modes=R_modes), - FixBoundaryZ(eq=eq, modes=Z_modes), - FixPressure(eq=eq), - FixPsi(eq=eq), - FixCurrent(eq=eq), - ) - optimizer = Optimizer("proximal-lsq-exact") - - # Until this line, the code is performed on all ranks, so it might print some - # information multiple times. The following part will only be performed on the - # master rank - - # this context manager will put the workers in a loop to listen to the master - # to compute the objective function and its derivatives - with objective as objective: - # apart from cost evaluation and derivatives, everything else will be only - # performed on the master rank - if rank == 0: - eq.optimize( - objective=objective, - constraints=constraints, - optimizer=optimizer, - maxiter=3, - verbose=3, - options={ - "initial_trust_ratio": 1.0, - }, - ) - - # if you put a code here, it will be performed on all ranks From 8fa17718f973ebe520d9ff694c5421d8fad937a4 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 30 Jun 2025 15:43:33 -0400 Subject: [PATCH 133/199] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index befcedc3d2..51f7ee27b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ New Features - Adds ``grid.meshgrid_flatten`` for flattening 3d data to a 1d array in the correct order. - Ability to obtain the top eigenvalues and the corresponding eigenfunctions from the ``ideal ballooning lambda`` compute function by specifying the variable ``Neigvals``. - Adds ``FourierXYCoil`` to compatible coils for ``CoilSetArclengthVariance`` objective. +- Adds initial support for multiple GPU optimization. This allows to compute derivatives and costs on multiple GPU, and allows more memory intense objectives. Note that, at this phase, the multi-device support is for memory, not speed. Bug Fixes @@ -86,7 +87,6 @@ New Features - Adds a new function ``desc.coils.initialize_helical_coils`` for creating an initial guess for stage 2 helical coil optimization. - Adds ``desc.vmec_utils.make_boozmn_output `` for writing boozmn.nc style output files for compatibility with other codes which expect such files from the Booz_Xform code. -- Adds initial support for multiple GPU optimization. This allows to compute derivatives on multiple GPU, and allows more memory intense objectives. Note that: at this phase, the multi-device support is for memory, not speed. - Renames compute quantity ``sqrt(g)_B`` to ``sqrt(g)_Boozer_DESC`` to more accurately reflect what the quantiy is (the jacobian from (rho,theta_B,zeta_B) to (rho,theta,zeta)), and adds a new function to compute ``sqrt(g)_Boozer`` which is the jacobian from (rho,theta_B,zeta_B) to (R,phi,Z). - Allows specification of Nyquist spectrum maximum modenumbers when using ``VMECIO.save`` to save a DESC .h5 file as a VMEC-format wout file - Adds a new objective ``desc.objectives.ExternalObjective`` for wrapping external codes with finite differences. From 16adfc982d69479eb4c6cd547e4c5de897e90061 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 30 Jun 2025 16:24:27 -0400 Subject: [PATCH 134/199] update tutorials --- .../{mpi-eq-solve-nvtx.py => mpi-eq-solve.py} | 64 ++-- .../{mpi-proximal-nvtx.py => mpi-proximal.py} | 126 ++++--- docs/notebooks/tutorials/multi_device.ipynb | 343 +++++++++--------- 3 files changed, 270 insertions(+), 263 deletions(-) rename docs/notebooks/tutorials/mpi-tutorials/{mpi-eq-solve-nvtx.py => mpi-eq-solve.py} (66%) rename docs/notebooks/tutorials/mpi-tutorials/{mpi-proximal-nvtx.py => mpi-proximal.py} (59%) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py similarity index 66% rename from docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py rename to docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 56350cc53a..19fb518f72 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve-nvtx.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -3,21 +3,22 @@ # Add the path to the parent directory to augment search for module sys.path.insert(0, os.path.abspath(".")) +sys.path.append(os.path.abspath("../../../")) sys.path.append(os.path.abspath("../../../../")) -import nvtx from mpi4py import MPI -from desc import set_device +from desc import _set_cpu_count, set_device -# ====== Using CPUs ====== +kind = "cpu" # or "gpu" num_device = 2 +# ====== Using CPUs ====== # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices - -# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! -# _set_cpu_count(num_device) -# set_device("cpu", num_device=num_device, mpi=MPI) +if kind == "cpu": + # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! + _set_cpu_count(num_device) + set_device("cpu", num_device=num_device, mpi=MPI) # ====== Using GPUs ====== # When we have multiple processes using the same devices (for example, 3 processes @@ -25,9 +26,9 @@ # cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` # such that there is no pre-allocation. This is a bit conservative (and probably there is room # for improvement), but if a process needs more memory, it can use more memory on the fly. - -os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" -set_device("gpu", num_device=num_device) +elif kind == "gpu": + os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" + set_device("gpu", num_device=num_device) from desc import config as desc_config from desc.backend import jax, print_backend_info @@ -62,14 +63,16 @@ print_backend_info() print("\n") - with nvtx.annotate("Setup"): - eq = get("HELIOTRON") + eq = get("HELIOTRON") + if desc_config["kind"] == "cpu": + # for local testing use lower resolution + eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) - # this will create a parallel objective function - # user can create their own parallel objective function as well which will be - # shown in the next example - obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1) - cons = get_fixed_boundary_constraints(eq) + # this will create a parallel objective function + # user can create their own parallel objective function as well which will be + # shown in the next example + obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1) + cons = get_fixed_boundary_constraints(eq) # Until this line, the code is performed on all ranks, so it might print some # information multiple times. The following part will only be performed on the @@ -77,19 +80,18 @@ # this context manager will put the workers in a loop to listen to the master # to compute the objective function and its derivatives - with nvtx.annotate("Solve"): - with obj as obj: - # apart from cost evaluation and derivatives, everything else will be only - # performed on the master rank - if rank == 0: - eq.solve( - objective=obj, - constraints=cons, - maxiter=10, - ftol=0, - gtol=0, - xtol=0, - verbose=3, - ) + with obj as obj: + # apart from cost evaluation and derivatives, everything else will be only + # performed on the master rank + if rank == 0: + eq.solve( + objective=obj, + constraints=cons, + maxiter=10, + ftol=0, + gtol=0, + xtol=0, + verbose=3, + ) # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py similarity index 59% rename from docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py rename to docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 0767d99c59..4f2dfa6bde 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal-nvtx.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -3,20 +3,22 @@ # Add the path to the parent directory to augment search for module sys.path.insert(0, os.path.abspath(".")) +sys.path.append(os.path.abspath("../../../")) sys.path.append(os.path.abspath("../../../../")) from mpi4py import MPI -from desc import set_device +from desc import _set_cpu_count, set_device -# ====== Using CPUs ====== +kind = "cpu" # or "gpu" num_device = 2 +# ====== Using CPUs ====== # These will be used for diving the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices - -# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! -# _set_cpu_count(num_device) -# set_device("cpu", num_device=num_device, mpi=MPI) +if kind == "cpu": + # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! + _set_cpu_count(num_device) + set_device("cpu", num_device=num_device, mpi=MPI) # ====== Using GPUs ====== # When we have multiple processes using the same devices (for example, 3 processes @@ -24,13 +26,12 @@ # cause the memory allocation to fail. To avoid this, we can set the allocator to `platform` # such that there is no pre-allocation. This is a bit conservative (and probably there is room # for improvement), but if a process needs more memory, it can use more memory on the fly. -# -os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" -set_device("gpu", num_device=num_device) +elif kind == "gpu": + os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform" + set_device("gpu", num_device=num_device) import numpy as np -import nvtx from desc import config as desc_config from desc.backend import jax, jnp, print_backend_info @@ -74,47 +75,43 @@ print_backend_info() print("\n") - with nvtx.annotate("setup"): - eq = get("precise_QA") - - # create two grids with different rho values, this will effectively separate - # the quasisymmetry objective into two parts - grid1 = LinearGrid( - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - rho=jnp.linspace(0.2, 0.5, 4), - sym=True, - ) - grid2 = LinearGrid( - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - rho=jnp.linspace(0.6, 1.0, 6), - sym=True, - ) + eq = get("precise_QA") + if desc_config["kind"] == "cpu": + eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) + + # create two grids with different rho values, this will effectively separate + # the quasisymmetry objective into two parts + grid1 = LinearGrid( + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + rho=jnp.linspace(0.2, 0.5, 4), + sym=True, + ) + grid2 = LinearGrid( + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + rho=jnp.linspace(0.6, 1.0, 6), + sym=True, + ) - # when using parallel objectives, the user needs to supply the device_id - obj1 = QuasisymmetryTwoTerm( - eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0 - ) - obj2 = QuasisymmetryTwoTerm( - eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1 - ) - obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) - objs = [obj1, obj2, obj3] - - with nvtx.annotate("Build Objectives"): - # Parallel objective function needs the MPI communicator - # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will - # automatically switch to `blocked`. - objective = ObjectiveFunction( - objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 0]) - ) - if rank == 0: - objective.build(verbose=3) - else: - objective.build(verbose=0) + # when using parallel objectives, the user needs to supply the device_id + obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) + obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) + obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) + objs = [obj1, obj2, obj3] + + # Parallel objective function needs the MPI communicator + # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will + # automatically switch to `blocked`. + objective = ObjectiveFunction( + objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 0]) + ) + if rank == 0: + objective.build(verbose=3) + else: + objective.build(verbose=0) # we will fix some modes as usual k = 1 @@ -145,20 +142,19 @@ # this context manager will put the workers in a loop to listen to the master # to compute the objective function and its derivatives - with nvtx.annotate("Optimization"): - with objective as objective: - # apart from cost evaluation and derivatives, everything else will be only - # performed on the master rank - if rank == 0: - eq.optimize( - objective=objective, - constraints=constraints, - optimizer=optimizer, - maxiter=3, - verbose=3, - options={ - "initial_trust_ratio": 1.0, - }, - ) + with objective as objective: + # apart from cost evaluation and derivatives, everything else will be only + # performed on the master rank + if rank == 0: + eq.optimize( + objective=objective, + constraints=constraints, + optimizer=optimizer, + maxiter=3, + verbose=3, + options={ + "initial_trust_ratio": 1.0, + }, + ) # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 56a6d1feeb..137a64d78b 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -41,13 +41,15 @@ "\n", "# Add the path to the parent directory to augment search for module\n", "sys.path.insert(0, os.path.abspath(\".\"))\n", - "sys.path.append(os.path.abspath(\"../../../\"))\n", + "sys.path.append(os.path.abspath(\"../../../../\"))\n", "\n", + "import nvtx\n", "from mpi4py import MPI\n", - "from desc import _set_cpu_count, set_device\n", + "\n", + "from desc import set_device, _set_cpu_count\n", "\n", "# ====== Using CPUs ======\n", - "num_device = 4\n", + "num_device = 2\n", "# These will be used for diving the single CPU into multiple virtual CPUs\n", "# such that JAX and XLA thinks there are multiple devices\n", "\n", @@ -61,7 +63,7 @@ "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform`\n", "# such that there is no pre-allocation. This is a bit conservative (and probably there is room\n", "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", - "#\n", + "\n", "# os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", "# set_device(\"gpu\", num_device=num_device)\n", "\n", @@ -99,7 +101,6 @@ " print(\"\\n\")\n", "\n", " eq = get(\"HELIOTRON\")\n", - " eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4)\n", "\n", " # this will create a parallel objective function\n", " # user can create their own parallel objective function as well which will be\n", @@ -120,7 +121,7 @@ " eq.solve(\n", " objective=obj,\n", " constraints=cons,\n", - " maxiter=3,\n", + " maxiter=10,\n", " ftol=0,\n", " gtol=0,\n", " xtol=0,\n", @@ -129,8 +130,7 @@ "\n", " # if you put a code here, it will be performed on all ranks\n", "\n", - " \n", - "```\n" + "```" ] }, { @@ -142,51 +142,33 @@ "name": "stdout", "output_type": "stream", "text": [ - "====== TOTAL OF 4 RANKS ======\n", - "Rank 0 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", + "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1)]\n", + "\n", + "====== TOTAL OF 2 RANKS ======\n", + "Rank 0 can see [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+143.gb14571a20.dirty.\n", - "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 4 CPUs with 7.90 GB total available memory:\n", + "DESC version=0.14.2+322.g8fa17718f.dirty.\n", + "Using JAX backend: jax version=0.6.2, jaxlib version=0.6.2, dtype=float64.\n", + "Using 2 CPUs with 9.89 GB total available memory:\n", "\t CPU : 0 13th Gen Intel(R) Core(TM) i5-1335U\n", "\t CPU : 1 13th Gen Intel(R) Core(TM) i5-1335U\n", - "\t CPU : 2 13th Gen Intel(R) Core(TM) i5-1335U\n", - "\t CPU : 3 13th Gen Intel(R) Core(TM) i5-1335U\n", "\n", "Note: The backend information assumes that the user has 1 process per CPU (node). Using multiple processes per CPU (node) is not the most efficient way to use MPI with purely CPUs.\n", "\n", "\n", - "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", - "Rank 3 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "\n", - "Rank 2 can see [CpuDevice(id=0), CpuDevice(id=1), CpuDevice(id=2), CpuDevice(id=3)]\n", - "\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", "Precomputing transforms\n", "Putting objective force on device 1\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Putting objective force on device 2\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Putting objective force on device 3\n", "------------------------------------------------------------\n", "Rank 0 will run objective(s): ['ForceBalance']\n", "Rank 1 will run objective(s): ['ForceBalance']\n", - "Rank 2 will run objective(s): ['ForceBalance']\n", - "Rank 3 will run objective(s): ['ForceBalance']\n", "------------------------------------------------------------\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", @@ -199,125 +181,148 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "\u001b[32mTimer: Objective build = 949 ms\u001b[0m\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "\u001b[32mTimer: LinearConstraintProjection build = 5.00 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 1.61 sec\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 4.41 sec\u001b[0m\n", "Number of parameters: 551\n", "Number of objectives: 8424\n", - "\u001b[32mTimer: Initializing the optimization = 6.01 sec\u001b[0m\n", + "\u001b[32mTimer: Initializing the optimization = 6.08 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Building objective: force\n", - "Precomputing transforms\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 2.500e+00 1.228e+00 \n", + " 0 1 8.248e-01 3.319e-01 \n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 waiting to gather\n", + " 1 7 6.387e-01 1.861e-01 3.754e-02 2.474e-01 \n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + " 2 9 2.628e-01 3.759e-01 2.842e-02 1.828e-01 \n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", - " 1 2 8.226e-01 1.678e+00 2.256e-01 5.198e-01 \n", + " 3 11 1.516e-01 1.112e-01 2.328e-02 1.405e-01 \n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", - " 2 3 2.473e-02 7.978e-01 1.993e-01 7.096e-02 \n", + " 4 13 8.688e-02 6.468e-02 1.374e-02 1.080e-01 \n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + " 5 14 8.411e-02 2.770e-03 8.472e-03 8.306e-02 \n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + " 6 15 4.776e-02 3.635e-02 7.218e-03 6.471e-02 \n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", + " 7 16 4.141e-02 6.354e-03 3.771e-03 5.071e-02 \n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 0 waiting to gather\n", "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 2 : jvp_scaled_error for objectives ids: [2]\n", "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 3 : jvp_scaled_error for objectives ids: [3]\n", "Rank 0 waiting to gather\n", - " 3 5 3.451e-03 2.128e-02 8.908e-02 3.526e-02 \n", + " 8 17 3.759e-02 3.817e-03 2.504e-03 3.958e-02 \n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + " 9 18 3.601e-02 1.581e-03 2.084e-03 3.107e-02 \n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "Rank 0 : compute_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", + "Rank 0 waiting to gather\n", + " 10 20 3.422e-02 1.791e-03 2.906e-03 2.416e-02 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 3.451e-03\n", - " Total delta_x: 2.505e-01\n", - " Iterations: 3\n", - " Function evaluations: 5\n", - " Jacobian evaluations: 4\n", - "\u001b[32mTimer: Solution time = 28.9 sec\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 7.24 sec\u001b[0m\n", + " Current function value: 3.422e-02\n", + " Total delta_x: 3.460e-02\n", + " Iterations: 10\n", + " Function evaluations: 20\n", + " Jacobian evaluations: 11\n", + "\u001b[32mTimer: Solution time = 1.28 min\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 7.02 sec\u001b[0m\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 2 : compute_scaled_error for objectives ids: [2]\n", - "Rank 3 : compute_scaled_error for objectives ids: [3]\n", "==============================================================================================================\n", " Start --> End\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", "Rank 0 : compute_scaled_error for objectives ids: [0]\n", "Rank 0 waiting to gather\n", - "Total (sum of squares): 2.500e+00 --> 3.451e-03, \n", - "Maximum absolute Force error: 6.794e+04 --> 6.655e+03 (N)\n", - "Minimum absolute Force error: 1.059e-10 --> 1.232e-10 (N)\n", - "Average absolute Force error: 1.304e+04 --> 1.696e+03 (N)\n", - "Maximum absolute Force error: 5.464e-03 --> 5.352e-04 (normalized)\n", - "Minimum absolute Force error: 8.514e-18 --> 9.912e-18 (normalized)\n", - "Average absolute Force error: 1.049e-03 --> 1.364e-04 (normalized)\n", - "Maximum absolute Force error: 2.032e+05 --> 1.704e+04 (N)\n", - "Minimum absolute Force error: 1.502e-10 --> 1.535e-10 (N)\n", - "Average absolute Force error: 4.494e+04 --> 2.436e+03 (N)\n", - "Maximum absolute Force error: 1.634e-02 --> 1.370e-03 (normalized)\n", - "Minimum absolute Force error: 1.208e-17 --> 1.235e-17 (normalized)\n", - "Average absolute Force error: 3.614e-03 --> 1.959e-04 (normalized)\n", - "Maximum absolute Force error: 6.807e+05 --> 2.238e+04 (N)\n", - "Minimum absolute Force error: 8.896e-11 --> 6.677e-11 (N)\n", - "Average absolute Force error: 7.775e+04 --> 2.548e+03 (N)\n", - "Maximum absolute Force error: 5.474e-02 --> 1.800e-03 (normalized)\n", - "Minimum absolute Force error: 7.155e-18 --> 5.370e-18 (normalized)\n", - "Average absolute Force error: 6.253e-03 --> 2.049e-04 (normalized)\n", - "Maximum absolute Force error: 1.149e+07 --> 1.001e+06 (N)\n", - "Minimum absolute Force error: 3.304e-12 --> 6.674e-13 (N)\n", - "Average absolute Force error: 1.493e+05 --> 6.575e+03 (N)\n", - "Maximum absolute Force error: 9.238e-01 --> 8.054e-02 (normalized)\n", - "Minimum absolute Force error: 2.657e-19 --> 5.368e-20 (normalized)\n", - "Average absolute Force error: 1.200e-02 --> 5.288e-04 (normalized)\n", + "Total (sum of squares): 8.248e-01 --> 3.422e-02, \n", + "Maximum absolute Force error: 2.032e+05 --> 4.957e+04 (N)\n", + "Minimum absolute Force error: 1.059e-10 --> 1.061e-10 (N)\n", + "Average absolute Force error: 4.102e+04 --> 1.054e+04 (N)\n", + "Maximum absolute Force error: 1.634e-02 --> 3.986e-03 (normalized)\n", + "Minimum absolute Force error: 8.514e-18 --> 8.534e-18 (normalized)\n", + "Average absolute Force error: 3.299e-03 --> 8.477e-04 (normalized)\n", + "Maximum absolute Force error: 1.149e+07 --> 1.056e+06 (N)\n", + "Minimum absolute Force error: 3.304e-12 --> 1.077e-11 (N)\n", + "Average absolute Force error: 1.029e+05 --> 2.824e+04 (N)\n", + "Maximum absolute Force error: 9.238e-01 --> 8.490e-02 (normalized)\n", + "Minimum absolute Force error: 2.657e-19 --> 8.660e-19 (normalized)\n", + "Average absolute Force error: 8.279e-03 --> 2.272e-03 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", @@ -327,14 +332,12 @@ "==============================================================================================================\n", "\n", "Rank 1 STOPPING\n", - "Rank 2 STOPPING\n", - "Rank 3 STOPPING\n", - "\u001b[0m\u001b[0m\u001b[0m\u001b[0m" + "\u001b[0m\u001b[0m" ] } ], "source": [ - "!mpirun -n 4 python mpi-tutorials/mpi-eq-solve.py" + "!mpirun -n 2 python mpi-tutorials/mpi-eq-solve.py" ] }, { @@ -513,64 +516,64 @@ "name": "stdout", "output_type": "stream", "text": [ + "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", + "\n", "====== TOTAL OF 2 RANKS ======\n", "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+143.gb14571a20.dirty.\n", - "Using JAX backend: jax version=0.5.0, jaxlib version=0.5.0, dtype=float64.\n", - "Using 2 CPUs with 8.90 GB total available memory:\n", + "DESC version=0.14.2+322.g8fa17718f.dirty.\n", + "Using JAX backend: jax version=0.6.2, jaxlib version=0.6.2, dtype=float64.\n", + "Using 2 CPUs with 10.09 GB total available memory:\n", "\t CPU : 0 13th Gen Intel(R) Core(TM) i5-1335U\n", "\t CPU : 1 13th Gen Intel(R) Core(TM) i5-1335U\n", "\n", "Note: The backend information assumes that the user has 1 process per CPU (node). Using multiple processes per CPU (node) is not the most efficient way to use MPI with purely CPUs.\n", "\n", "\n", - "Rank 1 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", - "\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.06 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 1.59 sec\u001b[0m\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.02 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 1.44 sec\u001b[0m\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.00 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 1.38 sec\u001b[0m\n", "------------------------------------------------------------\n", "Rank 0 will run objective(s): ['QuasisymmetryTwoTerm', 'AspectRatio']\n", "Rank 1 will run objective(s): ['QuasisymmetryTwoTerm']\n", "------------------------------------------------------------\n", - "\u001b[32mTimer: Objective build = 3.80 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 5.26 sec\u001b[0m\n", "Building objective: force\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.34 sec\u001b[0m\n", - "\u001b[32mTimer: Objective build = 1.40 sec\u001b[0m\n", - "\u001b[32mTimer: Objective build = 1.70 ms\u001b[0m\n", - "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 3.74 sec\u001b[0m\n", - "\u001b[32mTimer: Proximal projection build = 7.02 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 1.91 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 1.98 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 1.91 ms\u001b[0m\n", + "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 3.63 sec\u001b[0m\n", + "\u001b[32mTimer: Proximal projection build = 7.99 sec\u001b[0m\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "\u001b[32mTimer: Objective build = 555 ms\u001b[0m\n", - "\u001b[32mTimer: LinearConstraintProjection build = 1.34 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 654 ms\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 1.62 sec\u001b[0m\n", "Number of parameters: 8\n", "Number of objectives: 631\n", - "\u001b[32mTimer: Initializing the optimization = 8.97 sec\u001b[0m\n", + "\u001b[32mTimer: Initializing the optimization = 10.3 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", + "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 2.005e+04 1.926e+02 \n", + " 0 1 2.005e+04 1.987e+02 \n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", @@ -583,32 +586,32 @@ "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", - " 1 4 8.123e+03 1.193e+04 4.964e-02 9.847e+01 \n", + " 1 4 9.192e+03 1.086e+04 1.003e-01 1.098e+02 \n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", - "Rank 0 waiting to gather\n", - " 2 5 2.617e+03 5.507e+03 5.877e-02 6.065e+01 \n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", + "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", + "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", + "Rank 0 waiting to gather\n", + " 2 6 5.181e+03 4.011e+03 5.393e-02 7.714e+01 \n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 0 waiting to gather\n", "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", - " 3 7 7.564e+02 1.860e+03 7.212e-02 3.935e+00 \n", + " 3 7 2.311e+03 2.870e+03 3.806e-02 2.626e+01 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 7.564e+02\n", - " Total delta_x: 7.271e-02\n", + " Current function value: 2.311e+03\n", + " Total delta_x: 1.308e-01\n", " Iterations: 3\n", " Function evaluations: 7\n", " Jacobian evaluations: 4\n", - "\u001b[32mTimer: Solution time = 52.3 sec\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 13.0 sec\u001b[0m\n", + "\u001b[32mTimer: Solution time = 1.00 min\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 15.0 sec\u001b[0m\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "Rank 1 : compute_scaled_error for objectives ids: [1]\n", "==============================================================================================================\n", @@ -617,26 +620,26 @@ "Rank 0 waiting to gather\n", "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", "Rank 0 waiting to gather\n", - "Total (sum of squares): 2.005e+04 --> 7.564e+02, \n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.038e-01 --> 1.333e+00 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.569e-04 --> 2.875e-04 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.039e-01 --> 2.474e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.406e-01 --> 1.455e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.803e-04 --> 3.137e-04 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.134e-01 --> 2.699e-01 (normalized)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 9.615e-01 --> 2.043e+00 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 3.670e-04 --> 1.044e-02 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.474e-01 --> 3.819e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.049e+00 --> 2.229e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 4.004e-04 --> 1.139e-02 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.609e-01 --> 4.167e-01 (normalized)\n", - "Aspect ratio: 6.002e+00 --> 7.856e+00 (dimensionless)\n", - "Maximum absolute Force error: 1.435e+05 --> 2.352e+04 (N)\n", - "Minimum absolute Force error: 1.480e+00 --> 6.889e+00 (N)\n", - "Average absolute Force error: 7.215e+03 --> 2.171e+03 (N)\n", - "Maximum absolute Force error: 1.026e-01 --> 1.681e-02 (normalized)\n", - "Minimum absolute Force error: 1.058e-06 --> 4.925e-06 (normalized)\n", - "Average absolute Force error: 5.157e-03 --> 1.552e-03 (normalized)\n", + "Total (sum of squares): 2.005e+04 --> 2.311e+03, \n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.038e-01 --> 1.834e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.569e-04 --> 1.078e-03 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.039e-01 --> 3.498e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.406e-01 --> 2.002e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.803e-04 --> 1.177e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.134e-01 --> 3.817e-01 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 9.615e-01 --> 4.950e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 3.670e-04 --> 5.549e-03 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.474e-01 --> 6.551e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.049e+00 --> 5.402e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 4.004e-04 --> 6.054e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.609e-01 --> 7.148e-01 (normalized)\n", + "Aspect ratio: 6.002e+00 --> 7.723e+00 (dimensionless)\n", + "Maximum absolute Force error: 1.435e+05 --> 3.442e+04 (N)\n", + "Minimum absolute Force error: 1.480e+00 --> 1.546e+01 (N)\n", + "Average absolute Force error: 7.215e+03 --> 3.140e+03 (N)\n", + "Maximum absolute Force error: 1.026e-01 --> 2.460e-02 (normalized)\n", + "Minimum absolute Force error: 1.058e-06 --> 1.105e-05 (normalized)\n", + "Average absolute Force error: 5.157e-03 --> 2.245e-03 (normalized)\n", "R boundary error: 0.000e+00 --> 4.600e-19 (m)\n", "Z boundary error: 0.000e+00 --> 3.469e-18 (m)\n", "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", @@ -702,7 +705,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When using MPI with multiple nodes, each process will see 1 CPU, and if you requested GPUs, only the GPUs connected to that CPU will be visible to your program. With this in mind, for example, if you want to use 2 nodes, and 3 GPUs per nodes with 3 processes per node, you can use 6 objectives in this way.\n", + "When using MPI with multiple nodes, each process will see 1 CPU (with multiple cores), and if you requested GPUs, only the GPUs connected to that CPU will be visible to your program. With this in mind, for example, if you want to use 2 nodes, and 3 GPUs per nodes with 3 processes per node, you can use 6 objectives in this way.\n", "\n", "```python\n", "\n", @@ -723,7 +726,7 @@ "obj3 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid3, device_id=2)\n", "# this will run on node 2, GPU 0 (rank=3)\n", "obj4 = AspectRatio(eq=eq, target=8, weight=100, device_id=0)\n", - "# this will run on node 2, GPU 2 (rank=4)\n", + "# this will run on node 2, GPU 1 (rank=4)\n", "obj5 = Objective(..., device_id=1)\n", "# this will run on node 2, GPU 2 (rank=5)\n", "obj6 = Objective(..., device_id=2)\n", @@ -732,7 +735,13 @@ "# Parallel objective function needs the MPI communicator\n", "objective = ObjectiveFunction(objs, deriv_mode=\"blocked\", mpi=MPI)\n", "\n", - "```" + "```\n", + "\n", + "When you write your script for multiple nodes, the number of devices and the device IDs must be selected as if there is only 1 node and only the local GPUs are visible. Other nodes will be used through `rank` of MPI communicator.\n", + "\n", + "Note: Most clusters have multiple GPUs connected to each node, so before using multiple nodes, use all the GPUs available to that node. Multi-node communication is significantly slower and your script will be easier to write properly.\n", + "\n", + "Note: You should have at least 6 objectives, so at least 1 objective per device. If you want to run multiple objectives on the same device, you can specify the ``rank_per_objective`` in the `ObjectiveFunction` keywords. By default, the initializer will assign different ranks for each sub-objective." ] } ], From 9bbe8b33c99cd1736481a7da758065d137185b8f Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 Aug 2025 22:17:20 +0300 Subject: [PATCH 135/199] clean debugging code --- desc/backend.py | 3 - desc/objectives/objective_funs.py | 95 --------------------- desc/optimize/_constraint_wrappers.py | 15 ---- desc/optimize/least_squares.py | 23 ----- desc/optimize/optimizer.py | 5 -- docs/notebooks/tutorials/multi_device.ipynb | 1 - 6 files changed, 142 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index f85632d43c..8a1e148d43 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -5,7 +5,6 @@ import warnings import numpy as np -import nvtx from packaging.version import Version from termcolor import colored @@ -573,7 +572,6 @@ def pconcat(arrays, mode="concat"): # pragma: no cover """ # we will use either CPU or GPU[0] for the matrix decompositions, so the # array of float64 should fit into single device - rng_pconcat = nvtx.start_range(message="Pconcat", color="blue") size = jnp.array([x.size for x in arrays]) size = jnp.sum(size) if ( @@ -604,7 +602,6 @@ def pconcat(arrays, mode="concat"): # pragma: no cover elif mode == "vstack": out = jnp.vstack([jax.device_put(x, device=device) for x in arrays]) - nvtx.end_range(rng_pconcat) return out diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index b9b5a2fe68..0b395f7bbc 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -4,7 +4,6 @@ from abc import ABC, abstractmethod import numpy as np -import nvtx from desc.backend import ( desc_config, @@ -423,9 +422,7 @@ def _worker_loop(self): # message[1] is the state vector (for compute and jvp's) # message[2] is the tangents (for only jvp's) message = (None, None, None) - rng_wait = nvtx.start_range(message="Wait for message", color="red") message = self.comm.bcast(message, root=0) - nvtx.end_range(rng_wait) obj_idx_rank = self._obj_per_rank[self.rank] objs = [self.objectives[i] for i in obj_idx_rank] @@ -433,10 +430,6 @@ def _worker_loop(self): print(f"Rank {self.rank} STOPPING") break elif "jvp" in message[0] and "proximal" not in message[0]: - print( - f"Rank {self.rank} : {message[0]} for objectives ids: " - + f"{obj_idx_rank}" - ) xs = jax.device_put( message[1], self.objectives[obj_idx_rank[0]]._device ) @@ -446,8 +439,6 @@ def _worker_loop(self): # inputs to jitted functions must live on the same device. Need to # put xi and vi on the same device as the objective - rng_rank = nvtx.start_range(message="Worker Job JVP", color="green") - rng_xv = nvtx.start_range(message="form x and v", color="red") xs = [ [xs[i] for i in self._things_per_objective_idx[idx]] for idx in obj_idx_rank @@ -456,7 +447,6 @@ def _worker_loop(self): [vs[i] for i in self._things_per_objective_idx[idx]] for idx in obj_idx_rank ] - nvtx.end_range(rng_xv) J_rank = jit( jvp_per_process, @@ -467,19 +457,9 @@ def _worker_loop(self): objs, op=message[0], ).block_until_ready() - rng_np = nvtx.start_range(message="numpy", color="red") J_rank = np.asarray(J_rank) - nvtx.end_range(rng_np) - nvtx.end_range(rng_rank) - rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(J_rank, root=0) - nvtx.end_range(rng) elif "compute" in message[0]: - print( - f"Rank {self.rank} : {message[0]} for objectives ids: " - + f"{obj_idx_rank}" - ) - rng_rank = nvtx.start_range(message="Worker Job Compute", color="green") params = jax.device_put( message[1], self.objectives[obj_idx_rank[0]]._device ) @@ -492,18 +472,9 @@ def _worker_loop(self): objs, op=message[0], ).block_until_ready() - rng_np = nvtx.start_range(message="numpy", color="red") f_rank = np.asarray(f_rank) - nvtx.end_range(rng_np) - nvtx.end_range(rng_rank) - rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(f_rank, root=0) - nvtx.end_range(rng) elif "proximal_jvp" in message[0]: - print( - f"Rank {self.rank} : {message[0]} for objectives ids: " - + f"{obj_idx_rank}" - ) op = message[0].replace("proximal_jvp_", "") xs = jax.device_put( message[1], self.objectives[obj_idx_rank[0]]._device @@ -512,10 +483,6 @@ def _worker_loop(self): message[2], self.objectives[obj_idx_rank[0]]._device ) - rng_rank = nvtx.start_range( - message="Worker Job JVP Proximal", color="green" - ) - rng_xv = nvtx.start_range(message="form x and v", color="red") xs = [ [xs[i] for i in self._things_per_objective_idx[idx]] for idx in obj_idx_rank @@ -524,7 +491,6 @@ def _worker_loop(self): [vs[i] for i in self._things_per_objective_idx[idx]] for idx in obj_idx_rank ] - nvtx.end_range(rng_xv) J_rank = jit( jvp_proximal_per_process, static_argnames="op", @@ -534,13 +500,8 @@ def _worker_loop(self): objs, op=op, ).block_until_ready() - rng_np = nvtx.start_range(message="numpy", color="red") J_rank = np.asarray(J_rank) - nvtx.end_range(rng_np) - nvtx.end_range(rng_rank) - rng = nvtx.start_range(message="send to master", color="blue") self.comm.gather(J_rank, root=0) - nvtx.end_range(rng) def _unjit(self): """Remove jit compiled methods.""" @@ -781,9 +742,7 @@ def compute_unscaled(self, x, constants=None): Objective function value(s). """ - rng_unpack = nvtx.start_range(message="unpack state", color="red") params = self.unpack_state(x) - nvtx.end_range(rng_unpack) if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) @@ -796,19 +755,10 @@ def compute_unscaled(self, x, constants=None): ) else: # pragma: no cover if self.rank == 0: - rng_bcast = nvtx.start_range(message="bcast to workers", color="red") message = ("compute_unscaled", params, None) self.comm.bcast(message, root=0) - nvtx.end_range(rng_bcast) obj_idx_rank = self._obj_per_rank[self.rank] - print( - f"Rank {self.rank} : {message[0]} for objectives ids: " - + f"{obj_idx_rank}" - ) - rng = nvtx.start_range( - message="compute_unscaled on master", color="blue" - ) f_rank = jit( compute_per_process, @@ -818,11 +768,7 @@ def compute_unscaled(self, x, constants=None): [self.objectives[i] for i in obj_idx_rank], op=message[0], ).block_until_ready() - nvtx.end_range(rng) - print(f"Rank {self.rank} waiting to gather") - rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) - nvtx.end_range(rng_gather) f = pconcat(fs) return f @@ -843,9 +789,7 @@ def compute_scaled(self, x, constants=None): Objective function value(s). """ - rng_unpack = nvtx.start_range(message="unpack state", color="red") params = self.unpack_state(x) - nvtx.end_range(rng_unpack) if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) @@ -858,17 +802,10 @@ def compute_scaled(self, x, constants=None): ) else: # pragma: no cover if self.rank == 0: - rng_bcast = nvtx.start_range(message="bcast to workers", color="red") message = ("compute_scaled", params, None) self.comm.bcast(message, root=0) - nvtx.end_range(rng_bcast) obj_idx_rank = self._obj_per_rank[self.rank] - print( - f"Rank {self.rank} : {message[0]} for objectives ids: " - + f"{obj_idx_rank}" - ) - rng = nvtx.start_range(message="compute_scaled on master", color="blue") f_rank = jit( compute_per_process, @@ -878,11 +815,7 @@ def compute_scaled(self, x, constants=None): [self.objectives[i] for i in obj_idx_rank], op=message[0], ).block_until_ready() - nvtx.end_range(rng) - print(f"Rank {self.rank} waiting to gather") - rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) - nvtx.end_range(rng_gather) f = pconcat(fs) return f @@ -903,9 +836,7 @@ def compute_scaled_error(self, x, constants=None): Objective function value(s). """ - rng_unpack = nvtx.start_range(message="unpack state", color="red") params = self.unpack_state(x) - nvtx.end_range(rng_unpack) if constants is None: constants = self.constants assert len(params) == len(constants) == len(self.objectives) @@ -918,19 +849,10 @@ def compute_scaled_error(self, x, constants=None): ) else: # pragma: no cover if self.rank == 0: - rng_bcast = nvtx.start_range(message="bcast to workers", color="red") message = ("compute_scaled_error", params, None) self.comm.bcast(message, root=0) - nvtx.end_range(rng_bcast) obj_idx_rank = self._obj_per_rank[self.rank] - print( - f"Rank {self.rank} : {message[0]} for objectives ids: " - + f"{obj_idx_rank}" - ) - rng = nvtx.start_range( - message="compute_scaled_error on master", color="blue" - ) f_rank = jit( compute_per_process, @@ -940,11 +862,7 @@ def compute_scaled_error(self, x, constants=None): [self.objectives[i] for i in obj_idx_rank], op=message[0], ).block_until_ready() - nvtx.end_range(rng) - print(f"Rank {self.rank} waiting to gather") - rng_gather = nvtx.start_range(message="Gather to master", color="red") fs = self.comm.gather(f_rank, root=0) - nvtx.end_range(rng_gather) f = pconcat(fs) return f @@ -1166,7 +1084,6 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): J += [Ji_] else: if self.rank == 0: - rng_unpack = nvtx.start_range(message="precheck", color="red") v = ensure_tuple(v) if len(v) > 1: # using blocked for higher order derivatives is a pain, and only @@ -1180,18 +1097,10 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): xs_splits = np.cumsum([t.dim_x for t in self.things]) xs = jnp.split(x, xs_splits) vs = jnp.split(v[0], xs_splits, axis=-1) - nvtx.end_range(rng_unpack) - rng_bcast = nvtx.start_range(message="bcast to workers", color="red") message = ("jvp_" + op, xs, vs) self.comm.bcast(message, root=0) - nvtx.end_range(rng_bcast) obj_idx_rank = self._obj_per_rank[self.rank] - print( - f"Rank {self.rank} : {message[0]} for objectives ids: " - + f"{obj_idx_rank}" - ) - rng = nvtx.start_range(message="JVP on master", color="blue") J_rank = jit( jvp_per_process, static_argnames="op", @@ -1207,11 +1116,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): [self.objectives[i] for i in obj_idx_rank], op=message[0], ).block_until_ready() - nvtx.end_range(rng) - rng_gather = nvtx.start_range(message="Gather to master", color="red") - print(f"Rank {self.rank} waiting to gather") J = self.comm.gather(J_rank, root=0) - nvtx.end_range(rng_gather) # this is the transpose of the jvp when v is a matrix, for consistency with # jvp_batched diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 7d0fc1fc07..9feb660667 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -3,7 +3,6 @@ import functools import numpy as np -import nvtx from desc.backend import jit, jnp, pconcat, put from desc.batching import batched_vectorize @@ -1381,11 +1380,6 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): objective.comm.bcast(message, root=0) obj_idx_rank = objective._obj_per_rank[objective.rank] - print( - f"Rank {objective.rank} : {message[0]} for objectives ids: {obj_idx_rank}" - ) - rng_rank = nvtx.start_range(message="JVP Proximal on master", color="green") - rng_xv = nvtx.start_range(message="form x and v", color="red") xs = [ [xgs[i] for i in objective._things_per_objective_idx[idx]] for idx in obj_idx_rank @@ -1394,10 +1388,7 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): [vgs[i] for i in objective._things_per_objective_idx[idx]] for idx in obj_idx_rank ] - nvtx.end_range(rng_xv) - rng_obj = nvtx.start_range(message="form objs and constants", color="red") objs = [objective.objectives[i] for i in obj_idx_rank] - nvtx.end_range(rng_obj) J_rank = jit( jvp_proximal_per_process, static_argnames="op", @@ -1407,12 +1398,6 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): objs, op=op, ) - nvtx.end_range(rng_rank) - print(f"Rank {objective.rank} waiting to gather") - rng_gat = nvtx.start_range(message="Gather to master", color="green") J = objective.comm.gather(J_rank, root=0) - nvtx.end_range(rng_gat) - rng_pcat = nvtx.start_range(message="Pconcat", color="blue") J = pconcat(J).T - nvtx.end_range(rng_pcat) return J.block_until_ready() diff --git a/desc/optimize/least_squares.py b/desc/optimize/least_squares.py index e632e03163..3ff7f6f85d 100644 --- a/desc/optimize/least_squares.py +++ b/desc/optimize/least_squares.py @@ -1,6 +1,5 @@ """Function for solving nonlinear least squares problems.""" -import nvtx from scipy.optimize import OptimizeResult from desc.backend import jnp, qr @@ -176,7 +175,6 @@ def lsqtr( # noqa: C901 assert in_bounds(x, lb, ub), "x0 is infeasible" x = make_strictly_feasible(x, lb, ub) - rng_comp = nvtx.start_range(message="First Compute/Jac", color="red") f = fun(x, *args) nfev += 1 cost = 0.5 * jnp.dot(f, f) @@ -185,7 +183,6 @@ def lsqtr( # noqa: C901 J = jac(x, *args).block_until_ready() njev += 1 g = jnp.dot(J.T, f) - nvtx.end_range(rng_comp) maxiter = setdefault(maxiter, n * 100) max_nfev = options.pop("max_nfev", 5 * maxiter + 1) @@ -275,13 +272,10 @@ def lsqtr( # noqa: C901 alpha = 0.0 # "Levenberg-Marquardt" parameter while iteration < maxiter and success is None: - rng = nvtx.start_range(message="ITERATION", color="blue") - # we don't want to factorize the extra stuff if we don't need to J_a = jnp.vstack([J_h, jnp.diag(diag_h**0.5)]) if bounded else J_h f_a = jnp.concatenate([f, jnp.zeros(diag_h.size)]) if bounded else f - rng_qr0 = nvtx.start_range(message="QR Newton", color="green") if tr_method == "svd": U, s, Vt = jnp.linalg.svd(J_a, full_matrices=False) elif tr_method == "cho": @@ -299,7 +293,6 @@ def lsqtr( # noqa: C901 # Trust region solver will solve the augmented system # with a new Q and R del Q, R - nvtx.end_range(rng_qr0) actual_reduction = -1 @@ -311,7 +304,6 @@ def lsqtr( # noqa: C901 # This gives us the proposed step relative to the current position # and it tells us whether the proposed step # has reached the trust region boundary or not. - rng_qr = nvtx.start_range(message="QR subproblem", color="green") if tr_method == "svd": step_h, hits_boundary, alpha = trust_region_step_exact_svd( f_a, U, s, Vt.T, trust_radius, alpha @@ -324,10 +316,8 @@ def lsqtr( # noqa: C901 step_h, hits_boundary, alpha = trust_region_step_exact_qr( p_newton, f_a, J_a, trust_radius, alpha ) - nvtx.end_range(rng_qr) step = d * step_h # Trust-region solution in the original space. - rng_ss = nvtx.start_range(message="Select Step", color="red") step, step_h, predicted_reduction = select_step( x, J_h, @@ -342,17 +332,12 @@ def lsqtr( # noqa: C901 theta, mode="jac", ) - nvtx.end_range(rng_ss) step_h_norm = jnp.linalg.norm(step_h, ord=2) step_norm = jnp.linalg.norm(step, ord=2) - rng_fea = nvtx.start_range(message="Make feasible", color="red") x_new = make_strictly_feasible(x + step, lb, ub, rstep=0) - nvtx.end_range(rng_fea) - rng_fn = nvtx.start_range(message="Fnew", color="red") f_new = fun(x_new, *args) - nvtx.end_range(rng_fn) nfev += 1 cost_new = 0.5 * jnp.dot(f_new, f_new) @@ -402,11 +387,9 @@ def lsqtr( # noqa: C901 allx.append(x) f = f_new cost = cost_new - rng_jac = nvtx.start_range(message="Jac per iter", color="green") J = jac(x, *args) njev += 1 g = jnp.dot(J.T, f) - nvtx.end_range(rng_jac) if jac_scale: scale, scale_inv = compute_jac_scale(J, scale_inv) @@ -441,13 +424,9 @@ def lsqtr( # noqa: C901 iteration += 1 if verbose > 1: - rng_print = nvtx.start_range(message="Print Iter", color="red") print_iteration_nonlinear( iteration, nfev, cost, actual_reduction, step_norm, g_norm ) - nvtx.end_range(rng_print) - - nvtx.end_range(rng) if g_norm < gtol: success, message = True, STATUS_MESSAGES["gtol"] + f" ({gtol=:.2e})" @@ -472,7 +451,6 @@ def lsqtr( # noqa: C901 alltr=alltr, ) if verbose > 0: - rng_print = nvtx.start_range(message="Print Last", color="red") if result["success"]: print(result["message"]) else: @@ -484,6 +462,5 @@ def lsqtr( # noqa: C901 print(" Iterations: {:d}".format(result["nit"])) print(" Function evaluations: {:d}".format(result["nfev"])) print(" Jacobian evaluations: {:d}".format(result["njev"])) - nvtx.end_range(rng_print) return result diff --git a/desc/optimize/optimizer.py b/desc/optimize/optimizer.py index 4d76de32cc..49019bf457 100644 --- a/desc/optimize/optimizer.py +++ b/desc/optimize/optimizer.py @@ -5,7 +5,6 @@ import warnings import numpy as np -import nvtx from termcolor import colored from desc.io import IOAble @@ -276,7 +275,6 @@ def optimize( # noqa: C901 timer.start("Solution time") - rng_opt = nvtx.start_range(message="Actual Optimization", color="red") result = optimizers[method]["fun"]( objective, nonlinear_constraint, @@ -287,9 +285,7 @@ def optimize( # noqa: C901 stoptol, options, ) - nvtx.end_range(rng_opt) - rng_po = nvtx.start_range(message="Post Optimization", color="red") if isinstance(objective, LinearConstraintProjection): # remove wrapper to get at underlying objective result["allx"] = [objective.recover(x) for x in result["allx"]] @@ -342,7 +338,6 @@ def optimize( # noqa: C901 t0.params_dict = final_params return things0, result - nvtx.end_range(rng_po) return things, result diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 137a64d78b..faad2fd726 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -43,7 +43,6 @@ "sys.path.insert(0, os.path.abspath(\".\"))\n", "sys.path.append(os.path.abspath(\"../../../../\"))\n", "\n", - "import nvtx\n", "from mpi4py import MPI\n", "\n", "from desc import set_device, _set_cpu_count\n", From f7eaa4a46029946b740caf69c25c9d992ef4fa1b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 Aug 2025 22:48:20 +0300 Subject: [PATCH 136/199] remove debug utility function get_parallel_forcebalance, it wasn't meant for final use --- desc/objectives/getters.py | 60 ------------------- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 34 ++++++++--- docs/notebooks/tutorials/multi_device.ipynb | 58 ++++++++++++------ 3 files changed, 67 insertions(+), 85 deletions(-) diff --git a/desc/objectives/getters.py b/desc/objectives/getters.py index dd8fa23996..be47a1b0c2 100644 --- a/desc/objectives/getters.py +++ b/desc/objectives/getters.py @@ -367,63 +367,3 @@ def add_if_multiple(constraints, cls): constraints = add_if_multiple(constraints, FixCurveRotation) return constraints - - -def get_parallel_forcebalance(eq, num_device, mpi, grid=None, use_jit=True, verbose=1): - """Get an ObjectiveFunction for parallel computing ForceBalance. - - Parameters - ---------- - eq : Equilibrium - Equilibrium to constrain. - num_device : int - Number of devices to use for parallel computing. - - Returns - ------- - obj : ObjectiveFunction - A built objective function with force balance objectives. Each objective is - computed on a separate device. - """ - from desc.backend import desc_config, jnp - from desc.grid import LinearGrid - - if desc_config["num_device"] < num_device: - raise ValueError( - f"Number of devices in desc_config ({desc_config['num_device']}) " - f"is less than the number of devices in input ({num_device})." - ) - if grid is not None: - if len(grid) != num_device: - raise ValueError( - f"Number of grids and num_device must be the same! Got " - f"{len(grid)=} and {num_device=}." - ) - if eq.L_grid % num_device == 0: - k = eq.L_grid // num_device - L = eq.L_grid - else: - k = eq.L_grid // num_device + 1 - L = k * num_device - - rhos = jnp.linspace(0.01, 1.0, L) - objs = () - for i in range(num_device): - if grid is None: - gridi = LinearGrid( - rho=rhos[i * k : (i + 1) * k], - # kind of experimental way of set giving - # less grid points to inner part, but seems - # to make transforms way slower - # M=int(eq.M_grid * i / num_device), # noqa: E800 - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - ) - else: - gridi = grid[i] - obj = ForceBalance(eq, grid=gridi, device_id=i) - objs += (obj,) - objective = ObjectiveFunction(objs, mpi=mpi, deriv_mode="blocked") - objective.build(use_jit=use_jit, verbose=verbose) - return objective diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 19fb518f72..0811518b77 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -6,6 +6,7 @@ sys.path.append(os.path.abspath("../../../")) sys.path.append(os.path.abspath("../../../../")) +import numpy as np from mpi4py import MPI from desc import _set_cpu_count, set_device @@ -33,10 +34,9 @@ from desc import config as desc_config from desc.backend import jax, print_backend_info from desc.examples import get -from desc.objectives.getters import ( - get_fixed_boundary_constraints, - get_parallel_forcebalance, -) +from desc.grid import LinearGrid +from desc.objectives import ForceBalance, ObjectiveFunction +from desc.objectives.getters import get_fixed_boundary_constraints if __name__ == "__main__": rank = MPI.COMM_WORLD.Get_rank() @@ -68,10 +68,28 @@ # for local testing use lower resolution eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) - # this will create a parallel objective function - # user can create their own parallel objective function as well which will be - # shown in the next example - obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1) + # setup 2 grids for 2 objectives covering different flux surfaces + rhos = np.linspace(0.1, 1.0, eq.L_grid) + grid1 = LinearGrid( + rho=rhos[: rhos.size // 2], + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + ) + grid2 = LinearGrid( + rho=rhos[rhos.size // 2 :], + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + ) + obj = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1, device_id=0), + ForceBalance(eq, grid=grid2, device_id=1), + ], + mpi=MPI, + deriv_mode="blocked", + ) cons = get_fixed_boundary_constraints(eq) # Until this line, the code is performed on all ranks, so it might print some diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index faad2fd726..b43826eb1f 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -41,20 +41,23 @@ "\n", "# Add the path to the parent directory to augment search for module\n", "sys.path.insert(0, os.path.abspath(\".\"))\n", + "sys.path.append(os.path.abspath(\"../../../\"))\n", "sys.path.append(os.path.abspath(\"../../../../\"))\n", "\n", + "import numpy as np\n", "from mpi4py import MPI\n", "\n", - "from desc import set_device, _set_cpu_count\n", + "from desc import _set_cpu_count, set_device\n", "\n", - "# ====== Using CPUs ======\n", + "kind = \"cpu\" # or \"gpu\"\n", "num_device = 2\n", + "# ====== Using CPUs ======\n", "# These will be used for diving the single CPU into multiple virtual CPUs\n", "# such that JAX and XLA thinks there are multiple devices\n", - "\n", - "# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", - "_set_cpu_count(num_device)\n", - "set_device(\"cpu\", num_device=num_device, mpi=MPI)\n", + "if kind == \"cpu\":\n", + " # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", + " _set_cpu_count(num_device)\n", + " set_device(\"cpu\", num_device=num_device, mpi=MPI)\n", "\n", "# ====== Using GPUs ======\n", "# When we have multiple processes using the same devices (for example, 3 processes\n", @@ -62,17 +65,16 @@ "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform`\n", "# such that there is no pre-allocation. This is a bit conservative (and probably there is room\n", "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", - "\n", - "# os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", - "# set_device(\"gpu\", num_device=num_device)\n", + "elif kind == \"gpu\":\n", + " os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", + " set_device(\"gpu\", num_device=num_device)\n", "\n", "from desc import config as desc_config\n", "from desc.backend import jax, print_backend_info\n", "from desc.examples import get\n", - "from desc.objectives.getters import (\n", - " get_fixed_boundary_constraints,\n", - " get_parallel_forcebalance,\n", - ")\n", + "from desc.grid import LinearGrid\n", + "from desc.objectives import ObjectiveFunction, ForceBalance\n", + "from desc.objectives.getters import get_fixed_boundary_constraints\n", "\n", "if __name__ == \"__main__\":\n", " rank = MPI.COMM_WORLD.Get_rank()\n", @@ -100,11 +102,32 @@ " print(\"\\n\")\n", "\n", " eq = get(\"HELIOTRON\")\n", + " if desc_config[\"kind\"] == \"cpu\":\n", + " # for local testing use lower resolution\n", + " eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4)\n", "\n", - " # this will create a parallel objective function\n", - " # user can create their own parallel objective function as well which will be\n", - " # shown in the next example\n", - " obj = get_parallel_forcebalance(eq, num_device=num_device, mpi=MPI, verbose=1)\n", + " # setup 2 grids for 2 objectives covering different flux surfaces\n", + " rhos = np.linspace(0.1, 1.0, eq.L_grid)\n", + " grid1 = LinearGrid(\n", + " rho=rhos[: rhos.size // 2],\n", + " M=eq.M_grid,\n", + " N=eq.N_grid,\n", + " NFP=eq.NFP,\n", + " )\n", + " grid2 = LinearGrid(\n", + " rho=rhos[rhos.size // 2 :],\n", + " M=eq.M_grid,\n", + " N=eq.N_grid,\n", + " NFP=eq.NFP,\n", + " )\n", + " obj = ObjectiveFunction(\n", + " [\n", + " ForceBalance(eq, grid=grid1, device_id=0),\n", + " ForceBalance(eq, grid=grid2, device_id=1),\n", + " ],\n", + " mpi=MPI,\n", + " deriv_mode=\"blocked\",\n", + " )\n", " cons = get_fixed_boundary_constraints(eq)\n", "\n", " # Until this line, the code is performed on all ranks, so it might print some\n", @@ -129,6 +152,7 @@ "\n", " # if you put a code here, it will be performed on all ranks\n", "\n", + "\n", "```" ] }, From b5713d8b9866a018263dfe2a6577b1623bae0378 Mon Sep 17 00:00:00 2001 From: Yigit Gunsur Elmacioglu <102380275+YigitElma@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:40:31 +0300 Subject: [PATCH 137/199] Update desc/objectives/objective_funs.py --- desc/objectives/objective_funs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 0b395f7bbc..33cdcec6d9 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1475,10 +1475,8 @@ class _Objective(IOAble, ABC): "_print_value_fmt", "_scalar", "_units", + "_device", ] - # _device is of type 'jaxlib.xla_extension.Device' which cannot be an argument - # to a jitted function. - _static_attrs = ["_device"] def __init__( self, From 12acdf86d6f17d2ffcadcc48e6655882b9a7e136 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 Aug 2025 23:43:59 +0300 Subject: [PATCH 138/199] fix static args --- desc/objectives/objective_funs.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 33cdcec6d9..c77bd4faac 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -262,6 +262,7 @@ class ObjectiveFunction(IOAble): "_name", "_things_per_objective_idx", "_use_jit", + "_is_mpi", ] def __init__( @@ -357,7 +358,15 @@ def __init__( "There is at least one rank that does not have any objective assigned. " f"Objectives per rank are {self._obj_per_rank}.", ) - self._static_attrs += ["mpi", "comm", "rank", "size"] + self._static_attrs += [ + "mpi", + "comm", + "rank", + "size", + "running", + "_obj_per_rank", + "_rank_per_objective", + ] if self._is_mpi and mpi is None: raise ValueError( From 90927c0ec2acc68bc9bc2095811d9150d8b92790 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 00:33:08 +0300 Subject: [PATCH 139/199] add initial attempt for mpi tests --- .github/workflows/mpi_tests.yml | 88 +++++++++++++++++++++++++++++++ desc/objectives/objective_funs.py | 28 +++++----- devtools/check_unmarked_tests.sh | 2 +- setup.cfg | 1 + tests/test_multidevice.py | 36 +++++++------ 5 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/mpi_tests.yml diff --git a/.github/workflows/mpi_tests.yml b/.github/workflows/mpi_tests.yml new file mode 100644 index 0000000000..683afe549a --- /dev/null +++ b/.github/workflows/mpi_tests.yml @@ -0,0 +1,88 @@ +name: MPI tests + +on: + push: + branches: + - master + - dev + pull_request: + branches: + - master + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + regression_tests: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + + strategy: + matrix: + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Filter changes + id: changes + uses: dorny/paths-filter@v3 + with: + filters: | + has_changes: + - 'desc/**' + - 'tests/test_mpi*' + - 'requirements.txt' + - 'devtools/dev-requirements.txt' + - 'setup.cfg' + - '.github/workflows/mpi_tests.yml' + + - name: Check for relevant changes + id: check_changes + run: echo "has_changes=${{ !contains(github.event.pull_request.labels.*.name, 'only-docs-comments') && steps.changes.outputs.has_changes }}" >> $GITHUB_ENV + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Check full Python version + run: | + python --version + python_version=$(python --version 2>&1 | cut -d' ' -f2) + echo "Python version: $python_version" + echo "version=$python_version" >> $GITHUB_ENV + + - name: Install MPI (OpenMPI + compiler) + if: env.has_changes == 'true' + run: | + sudo apt-get update + sudo apt-get install -y libopenmpi-dev openmpi-bin + + - name: Set up virtual environment + run: | + python -m venv .venv-${{ env.version }} + source .venv-${{ env.version }}/bin/activate + python -m pip install --upgrade pip + pip install -r devtools/dev-requirements.txt + pip install matplotlib==3.9.2 + # install mpi4py after OpenMPI is available + pip install mpi4py + + - name: Action Details + if: env.has_changes == 'true' + run: | + source .venv-${{ env.version }}/bin/activate + which python + python --version + mpirun --version + pip list + + - name: Test with pytest using mpirun + if: env.has_changes == 'true' + run: | + source .venv-${{ env.version }}/bin/activate + python -m pytest -v -m mpi diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index c77bd4faac..ec2b689069 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -605,17 +605,17 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 errorif( any(sub_obj_jac_chunk_sizes_are_ints) and self._deriv_mode == "batched", ValueError, - "'jac_chunk_size' was passed into one or more sub-objectives, but the\n" - "ObjectiveFunction is using 'batched' deriv_mode, so sub-objective \n" - "'jac_chunk_size' will be ignored in favor of the ObjectiveFunction's \n" - f"'jac_chunk_size' of {self._jac_chunk_size}.\n" - "Specify 'blocked' deriv_mode and don't pass `jac_chunk_size` for \n" - "ObjectiveFunction if each sub-objective is desired to have a \n" - "different 'jac_chunk_size' for its Jacobian computation. \n" - "`jac_chunk_size` of sub-objective(s): \n" - f"{sub_obj_chunk_sizes_names}\n" - f"Note: If you didn't specify 'jac_chunk_size' for the sub-objectives, \n" - "it might be that sub-objective has an internal logic to determine the \n" + "'jac_chunk_size' was passed into one or more sub-objectives, but the " + "ObjectiveFunction is using 'batched' deriv_mode, so sub-objective " + "'jac_chunk_size' will be ignored in favor of the ObjectiveFunction's " + f"'jac_chunk_size' of {self._jac_chunk_size}. " + "Specify 'blocked' deriv_mode and don't pass `jac_chunk_size` for " + "ObjectiveFunction if each sub-objective is desired to have a " + "different 'jac_chunk_size' for its Jacobian computation. " + "`jac_chunk_size` of sub-objective(s): " + f"{sub_obj_chunk_sizes_names} " + f"Note: If you didn't specify 'jac_chunk_size' for the sub-objectives, " + "it might be that sub-objective has an internal logic to determine the " "chunk size based on the available memory.", ) @@ -630,9 +630,9 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 warnif( self._is_mpi and self._deriv_mode != "blocked", UserWarning, - "\nWhen using multiple devices, the ObjectiveFunction will run each \n" - "sub-objective on the device specified in the sub-objective. \n" - "Setting the deriv_mode to 'blocked' to ensure that each sub-objective\n" + "When using multiple devices, the ObjectiveFunction will run each " + "sub-objective on the device specified in the sub-objective. " + "Setting the deriv_mode to 'blocked' to ensure that each sub-objective " "runs on the correct device.", ) if self._is_mpi: diff --git a/devtools/check_unmarked_tests.sh b/devtools/check_unmarked_tests.sh index 027411a6f0..f1f00afa03 100755 --- a/devtools/check_unmarked_tests.sh +++ b/devtools/check_unmarked_tests.sh @@ -10,7 +10,7 @@ start_time=$(date +%s) echo "Files to check: $@" # Collect unmarked tests for the specific file and suppress errors -unmarked=$(pytest "$@" --collect-only -m "not unit and not regression and not benchmark and not memory" -q 2> /dev/null | head -n -2) +unmarked=$(pytest "$@" --collect-only -m "not unit and not regression and not benchmark and not memory and not mpi" -q 2> /dev/null | head -n -2) # Count the number of unmarked tests found, ignoring empty lines num_unmarked=$(echo "$unmarked" | sed '/^\s*$/d' | wc -l) diff --git a/setup.cfg b/setup.cfg index 8fda42a781..075dc11859 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ markers= slow: marks tests as slow (deselect with 'pytest -m "not slow"'). fast: mark tests as fast. memory: marks tests that check memory usage + mpi: marks tests that require MPI filterwarnings= error ignore::pytest.PytestUnraisableExceptionWarning diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 603f56a5ea..bb85d7add5 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -1,23 +1,31 @@ """Tests for the multidevice capabilities.""" +import warnings + # This file has to run on a separate process because it changes the number of CPUs from desc import _set_cpu_count, set_device -num_device = 1 -_set_cpu_count(num_device) -set_device(kind="cpu", num_device=num_device) +num_device = 2 +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + _set_cpu_count(num_device) + set_device(kind="cpu", num_device=num_device) import numpy as np import pytest -from desc.backend import jax +try: + from mpi4py import MPI +except ModuleNotFoundError: + print("mpi4py is not installed, skipping MPI tests.") + pytest.skip("mpi4py is not installed, skipping MPI tests.", allow_module_level=True) + from desc.examples import get from desc.grid import LinearGrid from desc.objectives import ForceBalance, ObjectiveFunction -@pytest.mark.xfail(reason="We need to make a new action for these tests.") -@pytest.mark.unit +@pytest.mark.mpi def test_multidevice_jac(): """Test that the Jacobian is the same for a single and multi device.""" eq = get("HELIOTRON") @@ -43,16 +51,14 @@ def test_multidevice_jac(): objective3 = ForceBalance(eq2, grid=grid3, device_id=0) objective4 = ForceBalance(eq2, grid=grid4, device_id=0) - for obj in [objective1, objective2, objective3, objective4]: - obj.build() - obj = jax.device_put(obj, device=obj._device) - objective1.things[0] = eq1 - objective2.things[0] = eq1 - objective3.things[0] = eq2 - objective4.things[0] = eq2 + # need to pass MPI communicator to the ObjectiveFunction + with pytest.raises(ValueError): + # this one is multi-device, and grids have different sizes + obj1 = ObjectiveFunction([objective1, objective2]) - # this one is multi-device, and grids have different sizes - obj1 = ObjectiveFunction([objective1, objective2]) + # deriv_mode will be set to "blocked" automatically + with pytest.warns(UserWarning, match="When using multiple devices"): + obj1 = ObjectiveFunction([objective1, objective2], mpi=MPI) # this one is single device, and grids have different sizes obj2 = ObjectiveFunction([objective3, objective4]) obj1.build() From 566238d3740b0fb2049b665fea8f36be6e285a63 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 01:56:00 +0300 Subject: [PATCH 140/199] I don't know why this worked??? add some extra tests --- .github/workflows/mpi_tests.yml | 4 +-- desc/objectives/objective_funs.py | 2 ++ tests/test_multidevice.py | 60 +++++++++++++++++++++---------- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/.github/workflows/mpi_tests.yml b/.github/workflows/mpi_tests.yml index 683afe549a..490d78d74e 100644 --- a/.github/workflows/mpi_tests.yml +++ b/.github/workflows/mpi_tests.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true jobs: - regression_tests: + mpi_tests: runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} @@ -81,7 +81,7 @@ jobs: mpirun --version pip list - - name: Test with pytest using mpirun + - name: Test with pytest if: env.has_changes == 'true' run: | source .venv-${{ env.version }}/bin/activate diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index ec2b689069..469afe494e 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1485,6 +1485,8 @@ class _Objective(IOAble, ABC): "_scalar", "_units", "_device", + "_device_id", + "_static_attrs", ] def __init__( diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index bb85d7add5..771009c06b 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -20,31 +20,35 @@ print("mpi4py is not installed, skipping MPI tests.") pytest.skip("mpi4py is not installed, skipping MPI tests.", allow_module_level=True) +from desc import config as desc_config from desc.examples import get from desc.grid import LinearGrid from desc.objectives import ForceBalance, ObjectiveFunction @pytest.mark.mpi -def test_multidevice_jac(): - """Test that the Jacobian is the same for a single and multi device.""" +def test_set_cpu_count(): + """Test that _set_cpu_count.""" + # we already called the function, just check the desc_config + assert desc_config["num_device"] == num_device + assert len(desc_config["devices"]) == num_device + + +@pytest.mark.mpi +def test_multidevice_objective(): + """Test that objective function have proper attributes.""" eq = get("HELIOTRON") - eq.change_resolution(6, 6, 3, 12, 12, 6) + with pytest.warns(UserWarning, match="Reducing radial (L) resolution"): + eq.change_resolution(6, 6, 3, 12, 12, 6) eq1 = eq.copy() eq2 = eq.copy() - grid1 = LinearGrid( - M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2]), sym=True - ) - grid2 = LinearGrid( - M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.6, 0.8]), sym=True - ) - grid3 = LinearGrid( - M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.2, 0.6]), sym=True - ) - grid4 = LinearGrid( - M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array([0.4, 0.8, 0.9]), sym=True - ) + gM = eq.M_grid + gN = eq.N_grid + grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) + grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6, 0.8], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2, 0.6], sym=True) + grid4 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.4, 0.8, 0.9], sym=True) objective1 = ForceBalance(eq1, grid=grid1, device_id=0) objective2 = ForceBalance(eq1, grid=grid2, device_id=1) @@ -73,19 +77,37 @@ def test_multidevice_jac(): assert obj1._deriv_mode == "blocked" assert obj2._deriv_mode == "batched" + +@pytest.mark.mpi +def test_multidevice_jac(): + """Test that the Jacobian is the same for a single and multi device.""" + eq = get("HELIOTRON") + with pytest.warns(UserWarning, match="Reducing radial (L) resolution"): + eq.change_resolution(6, 6, 3, 12, 12, 6) + eq1 = eq.copy() + + gM = eq.M_grid + gN = eq.N_grid + grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) + grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6, 0.8], sym=True) + + objective1 = ForceBalance(eq1, grid=grid1, device_id=0) + objective2 = ForceBalance(eq1, grid=grid2, device_id=1) + + # deriv_mode will be set to "blocked" automatically + with pytest.warns(UserWarning, match="When using multiple devices"): + obj1 = ObjectiveFunction([objective1, objective2], mpi=MPI) + obj1.build() + # creating grids like grid3 = [grid1, grid2] doesn't give the same # node, spacing and weight ordering, so we can't compare the Jacobians # or the objective values directly. Instead, we compare the objective # values before and after a single iteration of the solver. This should # always decrease the objective value. error_init1 = obj1.compute_scalar(obj1.x(eq1)) - error_init2 = obj2.compute_scalar(obj2.x(eq2)) eq1.solve(objective=obj1, maxiter=1) - eq2.solve(objective=obj2, maxiter=1) error_final1 = obj1.compute_scalar(obj1.x(eq1)) - error_final2 = obj2.compute_scalar(obj2.x(eq2)) assert error_final1 < error_init1 - assert error_final2 < error_init2 From 22584195a6cd992f7737cb1aebe0819bb774bbfb Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 13:24:04 +0300 Subject: [PATCH 141/199] clean up redundant jits --- desc/objectives/objective_funs.py | 53 +++++++++------------------ desc/optimize/_constraint_wrappers.py | 8 ++-- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 469afe494e..a4a49218cd 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -457,15 +457,12 @@ def _worker_loop(self): for idx in obj_idx_rank ] - J_rank = jit( - jvp_per_process, - static_argnames="op", - )( + J_rank = jvp_per_process( xs, vs, objs, op=message[0], - ).block_until_ready() + ) J_rank = np.asarray(J_rank) self.comm.gather(J_rank, root=0) elif "compute" in message[0]: @@ -473,14 +470,11 @@ def _worker_loop(self): message[1], self.objectives[obj_idx_rank[0]]._device ) - f_rank = jit( - compute_per_process, - static_argnames="op", - )( + f_rank = compute_per_process( [params[i] for i in obj_idx_rank], objs, op=message[0], - ).block_until_ready() + ) f_rank = np.asarray(f_rank) self.comm.gather(f_rank, root=0) elif "proximal_jvp" in message[0]: @@ -500,15 +494,12 @@ def _worker_loop(self): [vs[i] for i in self._things_per_objective_idx[idx]] for idx in obj_idx_rank ] - J_rank = jit( - jvp_proximal_per_process, - static_argnames="op", - )( + J_rank = jvp_proximal_per_process( xs, vs, objs, op=op, - ).block_until_ready() + ) J_rank = np.asarray(J_rank) self.comm.gather(J_rank, root=0) @@ -769,14 +760,12 @@ def compute_unscaled(self, x, constants=None): obj_idx_rank = self._obj_per_rank[self.rank] - f_rank = jit( - compute_per_process, - static_argnames="op", - )( + f_rank = compute_per_process( [params[i] for i in obj_idx_rank], [self.objectives[i] for i in obj_idx_rank], op=message[0], - ).block_until_ready() + ) + f_rank = np.asarray(f_rank) fs = self.comm.gather(f_rank, root=0) f = pconcat(fs) return f @@ -816,14 +805,12 @@ def compute_scaled(self, x, constants=None): obj_idx_rank = self._obj_per_rank[self.rank] - f_rank = jit( - compute_per_process, - static_argnames="op", - )( + f_rank = compute_per_process( [params[i] for i in obj_idx_rank], [self.objectives[i] for i in obj_idx_rank], op=message[0], - ).block_until_ready() + ) + f_rank = np.asarray(f_rank) fs = self.comm.gather(f_rank, root=0) f = pconcat(fs) return f @@ -863,14 +850,12 @@ def compute_scaled_error(self, x, constants=None): obj_idx_rank = self._obj_per_rank[self.rank] - f_rank = jit( - compute_per_process, - static_argnames="op", - )( + f_rank = compute_per_process( [params[i] for i in obj_idx_rank], [self.objectives[i] for i in obj_idx_rank], op=message[0], - ).block_until_ready() + ) + f_rank = np.asarray(f_rank) fs = self.comm.gather(f_rank, root=0) f = pconcat(fs) return f @@ -1110,10 +1095,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): self.comm.bcast(message, root=0) obj_idx_rank = self._obj_per_rank[self.rank] - J_rank = jit( - jvp_per_process, - static_argnames="op", - )( + J_rank = jvp_per_process( [ [xs[i] for i in self._things_per_objective_idx[idx]] for idx in obj_idx_rank @@ -1124,7 +1106,8 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): ], [self.objectives[i] for i in obj_idx_rank], op=message[0], - ).block_until_ready() + ) + J_rank = np.asarray(J_rank) J = self.comm.gather(J_rank, root=0) # this is the transpose of the jvp when v is a matrix, for consistency with diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 9feb660667..5c04fb87ab 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1389,15 +1389,13 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): for idx in obj_idx_rank ] objs = [objective.objectives[i] for i in obj_idx_rank] - J_rank = jit( - jvp_proximal_per_process, - static_argnames="op", - )( + J_rank = jvp_proximal_per_process( xs, vs, objs, op=op, ) + J_rank = np.asarray(J_rank) J = objective.comm.gather(J_rank, root=0) J = pconcat(J).T - return J.block_until_ready() + return J From 26eb98a098c8b4ca0b9cddd8538b069f4a044182 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 13:54:32 +0300 Subject: [PATCH 142/199] fix the failing test, had to build it before and used correct method name --- desc/objectives/objective_funs.py | 2 +- tests/test_optimizer.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index a4a49218cd..f28d9bec63 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -546,7 +546,7 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 Level of output. """ - use_jit_wrapper = True + use_jit_wrapper = self._use_jit if use_jit is not None: self._use_jit = use_jit # use_jit_wrapper is used to determine if we jit the ObjectiveFunction diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 922bd2f805..2093c07bd4 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -408,8 +408,11 @@ def compute(self, params, constants=None): np.random.seed(0) objective = ObjectiveFunction(DummyObjective(things=eq), use_jit=False) + # we need to build before we declare a new method to properly unjit + # the objective methods, so that _static_attrs is set correctly + objective.build() # make gradient super noisy so it stalls - objective.jac_scaled_error = lambda x, *args: objective._jac_scaled_error( + objective.jac_scaled_error = lambda x, *args: objective.jac_scaled_error( x ) + 1e2 * (np.random.random((objective._dim_f, x.size)) - 0.5) From 5ac14acb8a16935f12a88362064c6780cff826e3 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 13:57:06 +0300 Subject: [PATCH 143/199] add back the space --- desc/optimize/least_squares.py | 1 + 1 file changed, 1 insertion(+) diff --git a/desc/optimize/least_squares.py b/desc/optimize/least_squares.py index 3ff7f6f85d..2414068aa3 100644 --- a/desc/optimize/least_squares.py +++ b/desc/optimize/least_squares.py @@ -272,6 +272,7 @@ def lsqtr( # noqa: C901 alpha = 0.0 # "Levenberg-Marquardt" parameter while iteration < maxiter and success is None: + # we don't want to factorize the extra stuff if we don't need to J_a = jnp.vstack([J_h, jnp.diag(diag_h**0.5)]) if bounded else J_h f_a = jnp.concatenate([f, jnp.zeros(diag_h.size)]) if bounded else f From 85283e7d70fce92150704424f7bd43b110560de5 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 14:28:13 +0300 Subject: [PATCH 144/199] I don't know why I need to add _static_attrs to _static_attrs only for this PR but it solves the issue --- desc/objectives/objective_funs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index f28d9bec63..c3eba1e406 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -263,6 +263,7 @@ class ObjectiveFunction(IOAble): "_things_per_objective_idx", "_use_jit", "_is_mpi", + "_static_attrs", ] def __init__( From c08ca9dc0a9d2c71dfe78da5e863dbc0143a44f4 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 15:51:51 +0300 Subject: [PATCH 145/199] make the mpi test work --- .github/workflows/mpi_tests.yml | 10 ++++++-- devtools/check_unmarked_tests.sh | 2 +- setup.cfg | 3 ++- tests/test_multidevice.py | 40 ++++++++++++++++++-------------- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/.github/workflows/mpi_tests.yml b/.github/workflows/mpi_tests.yml index 490d78d74e..f5856c5e7a 100644 --- a/.github/workflows/mpi_tests.yml +++ b/.github/workflows/mpi_tests.yml @@ -81,8 +81,14 @@ jobs: mpirun --version pip list - - name: Test with pytest + - name: Test with pytest (MPI setup) if: env.has_changes == 'true' run: | source .venv-${{ env.version }}/bin/activate - python -m pytest -v -m mpi + python -m pytest -v -m mpi_setup + + - name: Test with pytest (MPI run) + if: env.has_changes == 'true' + run: | + source .venv-${{ env.version }}/bin/activate + mpirun -n 2 python -m pytest -v -m mpi_run diff --git a/devtools/check_unmarked_tests.sh b/devtools/check_unmarked_tests.sh index f1f00afa03..2ddaba5ca6 100755 --- a/devtools/check_unmarked_tests.sh +++ b/devtools/check_unmarked_tests.sh @@ -10,7 +10,7 @@ start_time=$(date +%s) echo "Files to check: $@" # Collect unmarked tests for the specific file and suppress errors -unmarked=$(pytest "$@" --collect-only -m "not unit and not regression and not benchmark and not memory and not mpi" -q 2> /dev/null | head -n -2) +unmarked=$(pytest "$@" --collect-only -m "not unit and not regression and not benchmark and not memory and not mpi_run and not mpi_setup" -q 2> /dev/null | head -n -2) # Count the number of unmarked tests found, ignoring empty lines num_unmarked=$(echo "$unmarked" | sed '/^\s*$/d' | wc -l) diff --git a/setup.cfg b/setup.cfg index 075dc11859..65e4fce715 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,7 +47,8 @@ markers= slow: marks tests as slow (deselect with 'pytest -m "not slow"'). fast: mark tests as fast. memory: marks tests that check memory usage - mpi: marks tests that require MPI + mpi_setup: marks tests that require MPI but not MPI processes + mpi_run: marks tests that require MPI and need MPI processes filterwarnings= error ignore::pytest.PytestUnraisableExceptionWarning diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 771009c06b..bdeba16930 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -26,7 +26,7 @@ from desc.objectives import ForceBalance, ObjectiveFunction -@pytest.mark.mpi +@pytest.mark.mpi_setup def test_set_cpu_count(): """Test that _set_cpu_count.""" # we already called the function, just check the desc_config @@ -34,7 +34,7 @@ def test_set_cpu_count(): assert len(desc_config["devices"]) == num_device -@pytest.mark.mpi +@pytest.mark.mpi_setup def test_multidevice_objective(): """Test that objective function have proper attributes.""" eq = get("HELIOTRON") @@ -63,9 +63,9 @@ def test_multidevice_objective(): # deriv_mode will be set to "blocked" automatically with pytest.warns(UserWarning, match="When using multiple devices"): obj1 = ObjectiveFunction([objective1, objective2], mpi=MPI) + obj1.build() # this one is single device, and grids have different sizes obj2 = ObjectiveFunction([objective3, objective4]) - obj1.build() obj2.build() assert obj1._is_mpi @@ -78,36 +78,42 @@ def test_multidevice_objective(): assert obj2._deriv_mode == "batched" -@pytest.mark.mpi +@pytest.mark.mpi_run def test_multidevice_jac(): """Test that the Jacobian is the same for a single and multi device.""" + rank = MPI.COMM_WORLD.Get_rank() + size = MPI.COMM_WORLD.Get_size() + if rank == 0: + print(f"====== TOTAL OF {size} RANKS ======") + eq = get("HELIOTRON") - with pytest.warns(UserWarning, match="Reducing radial (L) resolution"): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") eq.change_resolution(6, 6, 3, 12, 12, 6) - eq1 = eq.copy() gM = eq.M_grid gN = eq.N_grid grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6, 0.8], sym=True) - objective1 = ForceBalance(eq1, grid=grid1, device_id=0) - objective2 = ForceBalance(eq1, grid=grid2, device_id=1) + objective1 = ForceBalance(eq, grid=grid1, device_id=0) + objective2 = ForceBalance(eq, grid=grid2, device_id=1) # deriv_mode will be set to "blocked" automatically - with pytest.warns(UserWarning, match="When using multiple devices"): - obj1 = ObjectiveFunction([objective1, objective2], mpi=MPI) - obj1.build() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + obj = ObjectiveFunction([objective1, objective2], mpi=MPI) + obj.build() # creating grids like grid3 = [grid1, grid2] doesn't give the same # node, spacing and weight ordering, so we can't compare the Jacobians # or the objective values directly. Instead, we compare the objective # values before and after a single iteration of the solver. This should # always decrease the objective value. - error_init1 = obj1.compute_scalar(obj1.x(eq1)) - - eq1.solve(objective=obj1, maxiter=1) - - error_final1 = obj1.compute_scalar(obj1.x(eq1)) + with obj: + if rank == 0: + f0 = obj.compute_scalar(obj.x(eq)) + eq.solve(objective=obj, maxiter=2, verbose=3) + f1 = obj.compute_scalar(obj.x(eq)) - assert error_final1 < error_init1 + assert f1 < f0 From 3e9ebcaacad2a76c9182d5d549cbc3aa39d9bcd2 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 15:52:09 +0300 Subject: [PATCH 146/199] remove redundant function --- desc/optimize/_constraint_wrappers.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 5c04fb87ab..deddd78533 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1299,24 +1299,6 @@ def __getattr__(self, name): # define these helper functions that are stateless so we can safely jit them -# currently not in use but might be useful later -def jit_if_not_parallel(func): - """Jit a function if not in parallel mode.""" - - @functools.wraps(func) - def wrapper(*args, **kwargs): - obj = args[0] - if not getattr(obj, "_is_multi_device", False): - # Apply jit if jittable - jitted_func = functools.partial(jit, static_argnames=["op"])(func) - return jitted_func(*args, **kwargs) - else: - # Run normally if not jittable - return func(*args, **kwargs) - - return wrapper - - @functools.partial(jit, static_argnames=["op"]) def _proximal_jvp_f_pure(constraint, xf, constants, dc, eq_feasible_tangents, dxdc, op): # Note: This function is called by _get_tangent which is vectorized over v From cfe9329a89bebc6f53e5f29716f2235f651189d8 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 16:26:24 +0300 Subject: [PATCH 147/199] add more tests, remove some no-coverage commands --- .github/workflows/mpi_tests.yml | 42 ++++++++++++- desc/objectives/objective_funs.py | 10 +-- tests/test_multidevice.py | 101 ++++++++++++++++++++++++++++-- 3 files changed, 140 insertions(+), 13 deletions(-) diff --git a/.github/workflows/mpi_tests.yml b/.github/workflows/mpi_tests.yml index f5856c5e7a..e384662b29 100644 --- a/.github/workflows/mpi_tests.yml +++ b/.github/workflows/mpi_tests.yml @@ -85,10 +85,48 @@ jobs: if: env.has_changes == 'true' run: | source .venv-${{ env.version }}/bin/activate - python -m pytest -v -m mpi_setup + python -m pytest -v -m mpi_setup\ + --durations=0 \ + --cov-report xml:cov.xml \ + --cov-config=setup.cfg \ + --cov=desc/ \ + --db ./prof.db - name: Test with pytest (MPI run) if: env.has_changes == 'true' run: | source .venv-${{ env.version }}/bin/activate - mpirun -n 2 python -m pytest -v -m mpi_run + mpirun -n 2 python -m pytest -v -m mpi_run\ + --durations=0 \ + --cov-report xml:cov.xml \ + --cov-config=setup.cfg \ + --cov=desc/ \ + --db ./prof.db + + - name: Run MPI tutorials + if: env.has_changes == 'true' + run: | + source .venv-${{ env.version }}/bin/activate + # Make sure that they just run (without checking for errors) + mpirun -n 2 python -m docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py + mpirun -n 2 python -m docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py + + - name: save coverage file + if: always() && env.has_changes == 'true' + uses: actions/upload-artifact@v4 + with: + name: mpi_test_artifact-${{ matrix.python-version }} + path: | + ./cov.xml + ./mpl_results.html + ./prof.db + + - name: Upload coverage + if: env.has_changes == 'true' + id : codecov + uses: codecov/codecov-action@v5 + with: + name: codecov-umbrella + files: ./cov.xml + fail_ci_if_error: true + verbose: true diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index c3eba1e406..21c97cadcb 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -754,7 +754,7 @@ def compute_unscaled(self, x, constants=None): for par, obj, const in zip(params, self.objectives, constants) ] ) - else: # pragma: no cover + else: if self.rank == 0: message = ("compute_unscaled", params, None) self.comm.bcast(message, root=0) @@ -799,7 +799,7 @@ def compute_scaled(self, x, constants=None): for par, obj, const in zip(params, self.objectives, constants) ] ) - else: # pragma: no cover + else: if self.rank == 0: message = ("compute_scaled", params, None) self.comm.bcast(message, root=0) @@ -844,7 +844,7 @@ def compute_scaled_error(self, x, constants=None): for par, obj, const in zip(params, self.objectives, constants) ] ) - else: # pragma: no cover + else: if self.rank == 0: message = ("compute_scaled_error", params, None) self.comm.bcast(message, root=0) @@ -931,7 +931,7 @@ def print_value(self, x, x0=None, constants=None): for par, par0, obj, const in zip( params, params0, self.objectives, constants ): - if self._is_mpi: # pragma: no cover + if self._is_mpi: par = jax.device_put(par, obj._device) par0 = jax.device_put(par0, obj._device) outi = obj.print_value(par, par0, constants=const) @@ -939,7 +939,7 @@ def print_value(self, x, x0=None, constants=None): out[obj._print_value_fmt].append(outi) else: out[obj._print_value_fmt] = [outi] - else: # pragma: no cover + else: for par, obj, const in zip(params, self.objectives, constants): if self._is_mpi: par = jax.device_put(par, obj._device) diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index bdeba16930..d5b533b6d8 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -23,7 +23,18 @@ from desc import config as desc_config from desc.examples import get from desc.grid import LinearGrid -from desc.objectives import ForceBalance, ObjectiveFunction +from desc.objectives import ( + AspectRatio, + FixBoundaryR, + FixBoundaryZ, + FixCurrent, + FixPressure, + FixPsi, + ForceBalance, + ObjectiveFunction, + QuasisymmetryTwoTerm, +) +from desc.optimize import Optimizer @pytest.mark.mpi_setup @@ -79,12 +90,12 @@ def test_multidevice_objective(): @pytest.mark.mpi_run -def test_multidevice_jac(): - """Test that the Jacobian is the same for a single and multi device.""" +def test_multidevice_eq_solve(): + """Test that eq.solve still reduces force error.""" rank = MPI.COMM_WORLD.Get_rank() size = MPI.COMM_WORLD.Get_size() - if rank == 0: - print(f"====== TOTAL OF {size} RANKS ======") + assert size == 2 + assert rank < 2 eq = get("HELIOTRON") with warnings.catch_warnings(): @@ -112,8 +123,86 @@ def test_multidevice_jac(): # always decrease the objective value. with obj: if rank == 0: - f0 = obj.compute_scalar(obj.x(eq)) + f0 = obj.compute_scalar(obj.x(eq)).block_until_ready() eq.solve(objective=obj, maxiter=2, verbose=3) f1 = obj.compute_scalar(obj.x(eq)) assert f1 < f0 + + +@pytest.mark.mpi_run +def test_multidevice_eq_optimize(): + """Test that eq.optimize still reduces error.""" + rank = MPI.COMM_WORLD.Get_rank() + size = MPI.COMM_WORLD.Get_size() + assert size == 2 + assert rank < 2 + + eq = get("precise_QA") + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) + + # create two grids with different rho values, this will effectively separate + # the quasisymmetry objective into two parts + grid1 = LinearGrid( + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + rho=np.linspace(0.2, 0.5, 2), + sym=True, + ) + grid2 = LinearGrid( + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + rho=np.linspace(0.6, 1.0, 3), + sym=True, + ) + + # when using parallel objectives, the user needs to supply the device_id + obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) + obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) + obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) + objs = [obj1, obj2, obj3] + + objective = ObjectiveFunction( + objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 0]) + ) + objective.build() + + # we will fix some modes as usual + k = 1 + R_modes = np.vstack( + ( + [0, 0, 0], + eq.surface.R_basis.modes[ + np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : + ], + ) + ) + Z_modes = eq.surface.Z_basis.modes[ + np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : + ] + constraints = ( + ForceBalance(eq=eq), + FixBoundaryR(eq=eq, modes=R_modes), + FixBoundaryZ(eq=eq, modes=Z_modes), + FixPressure(eq=eq), + FixPsi(eq=eq), + FixCurrent(eq=eq), + ) + optimizer = Optimizer("proximal-lsq-exact") + + with objective as objective: + if rank == 0: + f0 = objective.compute_scalar(objective.x(eq)) + eq.optimize( + objective=objective, + constraints=constraints, + optimizer=optimizer, + maxiter=1, + verbose=3, + ) + f1 = objective.compute_scalar(objective.x(eq)) + assert f1 < f0 From 054d5b23ec5815792490e5f14f2fe1ecb1cb99a4 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Wed, 20 Aug 2025 19:14:50 +0300 Subject: [PATCH 148/199] add compute and derivatove tests, some clean-up --- desc/__init__.py | 2 +- desc/objectives/objective_funs.py | 66 ++++++------ desc/optimize/_constraint_wrappers.py | 19 ++-- tests/test_multidevice.py | 144 ++++++++++++++++++++++---- 4 files changed, 160 insertions(+), 71 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index d8f6275588..e97ada269d 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -61,7 +61,7 @@ def __getattr__(name): BANNER = colored(_BANNER, "magenta") -config = {"devices": None, "avail_mem": None, "kind": None, "num_device": None} +config = {"devices": None, "avail_mems": None, "kind": None, "num_device": None} def _get_processor_name(): diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 21c97cadcb..68bea67313 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -342,7 +342,7 @@ def __init__( self.size = self.comm.Get_size() self.running = True errorif( - max(self._rank_per_objective) != self.size - 1, + max(self._rank_per_objective) + 1 != self.size, ValueError, "The maximum value of rank_per_objective " f"({max(self._rank_per_objective)+1}, supplied as " @@ -1021,18 +1021,28 @@ def grad(self, x, constants=None): """Compute gradient vector of self.compute_scalar wrt x.""" if constants is None: constants = self.constants - return jnp.atleast_1d( - Derivative(self.compute_scalar, mode="grad")(x, constants).squeeze() - ) + if not self._is_mpi: + return jnp.atleast_1d( + Derivative(self.compute_scalar, mode="grad")(x, constants).squeeze() + ) + else: + raise NotImplementedError( + "Gradient computation is not implemented for MPI ObjectiveFunction." + ) @jit def hess(self, x, constants=None): """Compute Hessian matrix of self.compute_scalar wrt x.""" if constants is None: constants = self.constants - return jnp.atleast_2d( - Derivative(self.compute_scalar, mode="hess")(x, constants).squeeze() - ) + if not self._is_mpi: + return jnp.atleast_2d( + Derivative(self.compute_scalar, mode="hess")(x, constants).squeeze() + ) + else: + raise NotImplementedError( + "Gradient computation is not implemented for MPI ObjectiveFunction." + ) @jit def jac_scaled(self, x, constants=None): @@ -1053,18 +1063,19 @@ def jac_unscaled(self, x, constants=None): return self.jvp_unscaled(v, x, constants).T def _jvp_blocked(self, v, x, constants=None, op="scaled"): - if not self._is_mpi: - v = ensure_tuple(v) - if len(v) > 1: - # using blocked for higher order derivatives is a pain, and only really - # is needed for perturbations. Just pass that to jvp_batched for now - return self._jvp_batched(v, x, constants, op) + v = ensure_tuple(v) + if len(v) > 1: + # using blocked for higher order derivatives is a pain, and only really + # is needed for perturbations. Just pass that to jvp_batched for now + return self._jvp_batched(v, x, constants, op) - if constants is None: - constants = self.constants - xs_splits = np.cumsum([t.dim_x for t in self.things]) - xs = jnp.split(x, xs_splits) - vs = jnp.split(v[0], xs_splits, axis=-1) + if constants is None: + constants = self.constants + + xs_splits = np.cumsum([t.dim_x for t in self.things]) + xs = jnp.split(x, xs_splits) + vs = jnp.split(v[0], xs_splits, axis=-1) + if not self._is_mpi: J = [] assert len(self.objectives) == len(self.constants) # basic idea is we compute the jacobian of each objective wrt each thing @@ -1079,19 +1090,6 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): J += [Ji_] else: if self.rank == 0: - v = ensure_tuple(v) - if len(v) > 1: - # using blocked for higher order derivatives is a pain, and only - # really is needed for perturbations. Just pass that to - # jvp_batched for now - return self._jvp_batched(v, x, constants, op) - - if constants is None: - constants = self.constants - - xs_splits = np.cumsum([t.dim_x for t in self.things]) - xs = jnp.split(x, xs_splits) - vs = jnp.split(v[0], xs_splits, axis=-1) message = ("jvp_" + op, xs, vs) self.comm.bcast(message, root=0) @@ -2131,7 +2129,7 @@ def __call__(self, things): # These will run on workers, and we wan to safely jit them @functools.partial(jit, static_argnames="op") -def compute_per_process(params, objectives, op="compute_scaled_error"): +def compute_per_process(params, objectives, op): """Compute the objective function on each process.""" f_rank = jnp.concatenate( [ @@ -2145,7 +2143,7 @@ def compute_per_process(params, objectives, op="compute_scaled_error"): @functools.partial(jit, static_argnames="op") -def jvp_per_process(x, v, objectives, op="jvp_scaled_error"): +def jvp_per_process(x, v, objectives, op): """Compute the Jacobian-vector product on each process.""" J_rank = jnp.hstack( [getattr(obj, op)(v[idx], x[idx]) for idx, obj in enumerate(objectives)] @@ -2154,7 +2152,7 @@ def jvp_per_process(x, v, objectives, op="jvp_scaled_error"): @functools.partial(jit, static_argnames="op") -def jvp_proximal_per_process(x, v, objectives, op="scaled_error"): +def jvp_proximal_per_process(x, v, objectives, op): """Compute the Jacobian-vector product on each process, for proximal.""" J_rank = [] for idx, obj in enumerate(objectives): diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index deddd78533..e8fa642758 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1220,20 +1220,13 @@ def _jvp(self, v, x, constants=None, op="scaled_error"): if self._objective._deriv_mode == "batched": # objective's method already know about its jac_chunk_size return getattr(self._objective, "jvp_" + op)(tangents, xg, constants[0]) - elif not self._objective._is_mpi: - return _proximal_jvp_blocked_pure( - self._objective, - jnp.split(tangents, np.cumsum(self._dimx_per_thing), axis=-1), - jnp.split(xg, np.cumsum(self._dimx_per_thing)), - op, - ) else: - return _proximal_jvp_blocked_parallel( - self._objective, - jnp.split(tangents, np.cumsum(self._dimx_per_thing), axis=-1), - jnp.split(xg, np.cumsum(self._dimx_per_thing)), - op, - ) + vgs = jnp.split(tangents, np.cumsum(self._dimx_per_thing), axis=-1) + xgs = jnp.split(xg, np.cumsum(self._dimx_per_thing)) + if not self._objective._is_mpi: + return _proximal_jvp_blocked_pure(self._objective, vgs, xgs, op) + else: + return _proximal_jvp_blocked_parallel(self._objective, vgs, xgs, op) def _get_tangent(self, v, xf, constants, op): # Note: This function is vectorized over v. So, v is expected to be 1D array diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index d5b533b6d8..c3c1314b70 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -41,18 +41,123 @@ def test_set_cpu_count(): """Test that _set_cpu_count.""" # we already called the function, just check the desc_config + assert desc_config["kind"] == "cpu" assert desc_config["num_device"] == num_device assert len(desc_config["devices"]) == num_device + assert len(desc_config["avail_mems"]) == num_device -@pytest.mark.mpi_setup +@pytest.mark.mpi_run +def test_multidevice_compute(): + """Test that objective compute gives same results.""" + rank = MPI.COMM_WORLD.Get_rank() + eq = get("precise_QH") + with pytest.warns(UserWarning, match="Setting rotational transform"): + eq.iota = eq.get_profile("iota") + + gM = eq.M_grid + gN = eq.N_grid + grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) + grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2, 0.6], sym=True) + + obj0 = ObjectiveFunction(ForceBalance(eq, grid=grid3)) + obj0.build() + obj1 = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1), + ForceBalance(eq, grid=grid2), + ], + deriv_mode="blocked", + ) + obj1.build() + + # deriv_mode will be set to "blocked" automatically + with pytest.warns(UserWarning, match="When using multiple devices"): + obj2 = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1, device_id=0), + ForceBalance(eq, grid=grid2, device_id=1), + ], + mpi=MPI, + ) + obj2.build() + + f0 = obj0.compute_scalar(obj0.x(eq)) + f1 = obj1.compute_scalar(obj1.x(eq)) + with obj2: + if rank == 0: + f2 = obj2.compute_scalar(obj2.x(eq)) + + np.testing.assert_allclose(f2, f1, atol=1e-8) + np.testing.assert_allclose(f2, f0, atol=1e-7) + + f1 = obj1.compute_scaled(obj1.x(eq)) + f2 = obj2.compute_scaled(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + f1 = obj1.compute_scaled_error(obj1.x(eq)) + f2 = obj2.compute_scaled_error(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + +@pytest.mark.mpi_run +def test_multidevice_derivatives(): + """Test that objective derivatives gives same results.""" + rank = MPI.COMM_WORLD.Get_rank() + eq = get("precise_QH") + with pytest.warns(UserWarning, match="Setting rotational transform"): + eq.iota = eq.get_profile("iota") + + gM = eq.M_grid + gN = eq.N_grid + grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) + grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6], sym=True) + + obj1 = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1), + ForceBalance(eq, grid=grid2), + ], + deriv_mode="blocked", + ) + obj1.build() + + # deriv_mode will be set to "blocked" automatically + with pytest.warns(UserWarning, match="When using multiple devices"): + obj2 = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1, device_id=0), + ForceBalance(eq, grid=grid2, device_id=1), + ], + mpi=MPI, + ) + obj2.build() + + with obj2: + if rank == 0: + with pytest.raises(NotImplementedError): + _ = obj2.grad(obj2.x(eq)) + + f1 = obj1.jac_unscaled(obj1.x(eq)) + f2 = obj2.jac_unscaled(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + # figure out why this fails! One of them doesn't + # apply the scalign properly + f1 = obj1.jac_scaled(obj1.x(eq)) + f2 = obj2.jac_scaled(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + f1 = obj1.jac_scaled_error(obj1.x(eq)) + f2 = obj2.jac_scaled_error(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + +@pytest.mark.mpi_run def test_multidevice_objective(): """Test that objective function have proper attributes.""" eq = get("HELIOTRON") - with pytest.warns(UserWarning, match="Reducing radial (L) resolution"): - eq.change_resolution(6, 6, 3, 12, 12, 6) - eq1 = eq.copy() - eq2 = eq.copy() gM = eq.M_grid gN = eq.N_grid @@ -61,10 +166,10 @@ def test_multidevice_objective(): grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2, 0.6], sym=True) grid4 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.4, 0.8, 0.9], sym=True) - objective1 = ForceBalance(eq1, grid=grid1, device_id=0) - objective2 = ForceBalance(eq1, grid=grid2, device_id=1) - objective3 = ForceBalance(eq2, grid=grid3, device_id=0) - objective4 = ForceBalance(eq2, grid=grid4, device_id=0) + objective1 = ForceBalance(eq, grid=grid1, device_id=0) + objective2 = ForceBalance(eq, grid=grid2, device_id=1) + objective3 = ForceBalance(eq, grid=grid3, device_id=0) + objective4 = ForceBalance(eq, grid=grid4, device_id=0) # need to pass MPI communicator to the ObjectiveFunction with pytest.raises(ValueError): @@ -82,7 +187,7 @@ def test_multidevice_objective(): assert obj1._is_mpi assert not obj2._is_mpi - np.testing.assert_allclose(obj1.x(eq1), obj2.x(eq2)) + np.testing.assert_allclose(obj1.x(eq), obj2.x(eq)) # multi-device objective must be blocked assert obj1._deriv_mode == "blocked" @@ -98,8 +203,7 @@ def test_multidevice_eq_solve(): assert rank < 2 eq = get("HELIOTRON") - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with pytest.warns(UserWarning, match="Reducing radial"): eq.change_resolution(6, 6, 3, 12, 12, 6) gM = eq.M_grid @@ -111,8 +215,7 @@ def test_multidevice_eq_solve(): objective2 = ForceBalance(eq, grid=grid2, device_id=1) # deriv_mode will be set to "blocked" automatically - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with pytest.warns(UserWarning, match="When using multiple devices"): obj = ObjectiveFunction([objective1, objective2], mpi=MPI) obj.build() @@ -139,9 +242,8 @@ def test_multidevice_eq_optimize(): assert rank < 2 eq = get("precise_QA") - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) + eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) + surf = eq.surface # create two grids with different rho values, this will effectively separate # the quasisymmetry objective into two parts @@ -176,14 +278,10 @@ def test_multidevice_eq_optimize(): R_modes = np.vstack( ( [0, 0, 0], - eq.surface.R_basis.modes[ - np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : - ], + surf.R_basis.modes[np.max(np.abs(surf.R_basis.modes), 1) > k, :], ) ) - Z_modes = eq.surface.Z_basis.modes[ - np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : - ] + Z_modes = surf.Z_basis.modes[np.max(np.abs(surf.Z_basis.modes), 1) > k, :] constraints = ( ForceBalance(eq=eq), FixBoundaryR(eq=eq, modes=R_modes), From 6de096cc1d7005ab7cca94298ff5f8544982c074 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 Aug 2025 17:20:41 +0300 Subject: [PATCH 149/199] fix the stupid bug, another reason why #1728 is necessary --- desc/objectives/objective_funs.py | 26 ++++++++------ tests/test_multidevice.py | 56 +++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 68bea67313..d5cfee0b90 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1063,15 +1063,14 @@ def jac_unscaled(self, x, constants=None): return self.jvp_unscaled(v, x, constants).T def _jvp_blocked(self, v, x, constants=None, op="scaled"): + if constants is None: + constants = self.constants v = ensure_tuple(v) if len(v) > 1: # using blocked for higher order derivatives is a pain, and only really # is needed for perturbations. Just pass that to jvp_batched for now return self._jvp_batched(v, x, constants, op) - if constants is None: - constants = self.constants - xs_splits = np.cumsum([t.dim_x for t in self.things]) xs = jnp.split(x, xs_splits) vs = jnp.split(v[0], xs_splits, axis=-1) @@ -1760,6 +1759,8 @@ def jac_unscaled(self, *args, **kwargs): )(*args, **kwargs) def _jvp(self, v, x, constants=None, op="scaled"): + if constants is None: + constants = self.constants v = ensure_tuple(v) x = ensure_tuple(x) assert len(x) == len(v) @@ -2127,15 +2128,13 @@ def __call__(self, things): return unique -# These will run on workers, and we wan to safely jit them +# These will run on workers, and we want to safely jit them @functools.partial(jit, static_argnames="op") def compute_per_process(params, objectives, op): """Compute the objective function on each process.""" f_rank = jnp.concatenate( [ - getattr(obj, op)( - *param, - ) + getattr(obj, op)(*param, constants=obj.constants) for (obj, param) in zip(objectives, params) ] ) @@ -2146,7 +2145,10 @@ def compute_per_process(params, objectives, op): def jvp_per_process(x, v, objectives, op): """Compute the Jacobian-vector product on each process.""" J_rank = jnp.hstack( - [getattr(obj, op)(v[idx], x[idx]) for idx, obj in enumerate(objectives)] + [ + getattr(obj, op)(v[idx], x[idx], constants=obj.constants) + for idx, obj in enumerate(objectives) + ] ) return J_rank @@ -2159,12 +2161,16 @@ def jvp_proximal_per_process(x, v, objectives, op): if obj._deriv_mode == "rev": # obj might not allow fwd mode, so compute full rev mode # jacobian and do matmul manually. This is slightly - # inefficient, but usuallywhen rev mode is used, + # inefficient, but usually when rev mode is used, # dim_f <<< dim_x, so its not too bad. Ji = getattr(obj, "jac_" + op)(*x[idx]) J_rank.append( jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, v[idx])]).sum(axis=0) ) else: - J_rank.append(getattr(obj, "jvp_" + op)([_vi for _vi in v[idx]], x[idx]).T) + J_rank.append( + getattr(obj, "jvp_" + op)( + [_vi for _vi in v[idx]], x[idx], constants=obj.constants + ).T + ) return jnp.vstack(J_rank) diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index c3c1314b70..189ac42e06 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -39,7 +39,7 @@ @pytest.mark.mpi_setup def test_set_cpu_count(): - """Test that _set_cpu_count.""" + """Test that _set_cpu_count works.""" # we already called the function, just check the desc_config assert desc_config["kind"] == "cpu" assert desc_config["num_device"] == num_device @@ -47,13 +47,55 @@ def test_set_cpu_count(): assert len(desc_config["avail_mems"]) == num_device +@pytest.mark.mpi_run +def test_multidevice_objective_attributes(): + """Test that objective attributes are same.""" + eq = get("precise_QH") + with pytest.warns(UserWarning, match="Setting rotational transform"): + eq.iota = eq.get_profile("iota") + + gM = eq.M_grid + gN = eq.N_grid + grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) + grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6], sym=True) + + obj1 = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1), + ForceBalance(eq, grid=grid2), + ], + deriv_mode="blocked", + ) + obj1.build() + + # deriv_mode will be set to "blocked" automatically + with pytest.warns(UserWarning, match="When using multiple devices"): + obj2 = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1, device_id=0), + ForceBalance(eq, grid=grid2, device_id=1), + ], + mpi=MPI, + ) + obj2.build() + + for obj1i, obj2i in zip(obj1.objectives, obj2.objectives): + assert obj1i._loss_function == obj2i._loss_function + np.testing.assert_allclose(obj1i._weight, obj2i._weight) + np.testing.assert_allclose(obj1i._target, obj2i._target) + np.testing.assert_allclose(obj1i._normalization, obj2i._normalization) + np.testing.assert_allclose(obj1i._dim_f, obj2i._dim_f) + key = "quad_weights" + np.testing.assert_allclose( + obj1i._constants[key], obj2i._constants[key], err_msg=key + ) + + @pytest.mark.mpi_run def test_multidevice_compute(): """Test that objective compute gives same results.""" rank = MPI.COMM_WORLD.Get_rank() eq = get("precise_QH") - with pytest.warns(UserWarning, match="Setting rotational transform"): - eq.iota = eq.get_profile("iota") gM = eq.M_grid gN = eq.N_grid @@ -92,6 +134,10 @@ def test_multidevice_compute(): np.testing.assert_allclose(f2, f1, atol=1e-8) np.testing.assert_allclose(f2, f0, atol=1e-7) + f1 = obj1.compute_unscaled(obj1.x(eq)) + f2 = obj2.compute_unscaled(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + f1 = obj1.compute_scaled(obj1.x(eq)) f2 = obj2.compute_scaled(obj2.x(eq)) np.testing.assert_allclose(f2, f1, atol=1e-8) @@ -106,8 +152,6 @@ def test_multidevice_derivatives(): """Test that objective derivatives gives same results.""" rank = MPI.COMM_WORLD.Get_rank() eq = get("precise_QH") - with pytest.warns(UserWarning, match="Setting rotational transform"): - eq.iota = eq.get_profile("iota") gM = eq.M_grid gN = eq.N_grid @@ -144,7 +188,7 @@ def test_multidevice_derivatives(): np.testing.assert_allclose(f2, f1, atol=1e-8) # figure out why this fails! One of them doesn't - # apply the scalign properly + # apply the scaling properly f1 = obj1.jac_scaled(obj1.x(eq)) f2 = obj2.jac_scaled(obj2.x(eq)) np.testing.assert_allclose(f2, f1, atol=1e-8) From b3abedb5583fcc98873016412642d9aa8b3169d5 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 Aug 2025 18:08:26 +0300 Subject: [PATCH 150/199] add tests, check errors/warnings --- .github/workflows/mpi_tests.yml | 2 +- desc/objectives/objective_funs.py | 12 +++++--- tests/test_multidevice.py | 50 +++++++++++++++++++++++-------- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/.github/workflows/mpi_tests.yml b/.github/workflows/mpi_tests.yml index e384662b29..dda0075a92 100644 --- a/.github/workflows/mpi_tests.yml +++ b/.github/workflows/mpi_tests.yml @@ -96,7 +96,7 @@ jobs: if: env.has_changes == 'true' run: | source .venv-${{ env.version }}/bin/activate - mpirun -n 2 python -m pytest -v -m mpi_run\ + mpirun -n 3 python -m pytest -v -m mpi_run\ --durations=0 \ --cov-report xml:cov.xml \ --cov-config=setup.cfg \ diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index d5cfee0b90..22f2f6542b 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -315,10 +315,12 @@ def __init__( else np.arange(len(objectives)) ) errorif( - np.unique(self._rank_per_objective).size == 1, + np.unique(self._rank_per_objective).size < desc_config["num_device"], ValueError, - "There is only one rank. You cannot use MPI for this case. Call " - "ObjectiveFunction with `mpi=None`.", + "Requested number of ranks is less than the number of devices. You " + f"asked for {desc_config['num_device']} devices, but only have " + f" {np.unique(self._rank_per_objective).size} ranks assigned to " + "objectives. There should be at least as many ranks as devices.", ) errorif( ( @@ -329,6 +331,7 @@ def __init__( f"ranks {self._rank_per_objective} and device ids {device_ids} are " "not compatible.", ) + # TODO: should this throw an Error? warnif( max(device_ids) != desc_config["num_device"] - 1, UserWarning, @@ -371,7 +374,8 @@ def __init__( if self._is_mpi and mpi is None: raise ValueError( - "When using multiple devices, MPI communicator must be passed." + "MPI communicator must be passed when objectives are on different " + "devices." ) def __enter__(self): diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 189ac42e06..3ddbc58c3e 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -5,7 +5,7 @@ # This file has to run on a separate process because it changes the number of CPUs from desc import _set_cpu_count, set_device -num_device = 2 +num_device = 3 with warnings.catch_warnings(): warnings.simplefilter("ignore") _set_cpu_count(num_device) @@ -51,18 +51,18 @@ def test_set_cpu_count(): def test_multidevice_objective_attributes(): """Test that objective attributes are same.""" eq = get("precise_QH") - with pytest.warns(UserWarning, match="Setting rotational transform"): - eq.iota = eq.get_profile("iota") gM = eq.M_grid gN = eq.N_grid grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.8], sym=True) obj1 = ObjectiveFunction( [ ForceBalance(eq, grid=grid1), ForceBalance(eq, grid=grid2), + ForceBalance(eq, grid=grid3), ], deriv_mode="blocked", ) @@ -74,6 +74,7 @@ def test_multidevice_objective_attributes(): [ ForceBalance(eq, grid=grid1, device_id=0), ForceBalance(eq, grid=grid2, device_id=1), + ForceBalance(eq, grid=grid3, device_id=2), ], mpi=MPI, ) @@ -101,14 +102,16 @@ def test_multidevice_compute(): gN = eq.N_grid grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6], sym=True) - grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2, 0.6], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.8], sym=True) + grid4 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2, 0.6, 0.8], sym=True) - obj0 = ObjectiveFunction(ForceBalance(eq, grid=grid3)) + obj0 = ObjectiveFunction(ForceBalance(eq, grid=grid4)) obj0.build() obj1 = ObjectiveFunction( [ ForceBalance(eq, grid=grid1), ForceBalance(eq, grid=grid2), + ForceBalance(eq, grid=grid3), ], deriv_mode="blocked", ) @@ -120,6 +123,7 @@ def test_multidevice_compute(): [ ForceBalance(eq, grid=grid1, device_id=0), ForceBalance(eq, grid=grid2, device_id=1), + ForceBalance(eq, grid=grid3, device_id=2), ], mpi=MPI, ) @@ -157,11 +161,13 @@ def test_multidevice_derivatives(): gN = eq.N_grid grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.8], sym=True) obj1 = ObjectiveFunction( [ ForceBalance(eq, grid=grid1), ForceBalance(eq, grid=grid2), + ForceBalance(eq, grid=grid3), ], deriv_mode="blocked", ) @@ -173,6 +179,7 @@ def test_multidevice_derivatives(): [ ForceBalance(eq, grid=grid1, device_id=0), ForceBalance(eq, grid=grid2, device_id=1), + ForceBalance(eq, grid=grid3, device_id=2), ], mpi=MPI, ) @@ -199,8 +206,8 @@ def test_multidevice_derivatives(): @pytest.mark.mpi_run -def test_multidevice_objective(): - """Test that objective function have proper attributes.""" +def test_multidevice_objective_build(): + """Test that objective function build works fine.""" eq = get("HELIOTRON") gM = eq.M_grid @@ -212,20 +219,37 @@ def test_multidevice_objective(): objective1 = ForceBalance(eq, grid=grid1, device_id=0) objective2 = ForceBalance(eq, grid=grid2, device_id=1) - objective3 = ForceBalance(eq, grid=grid3, device_id=0) + objective3 = ForceBalance(eq, grid=grid3, device_id=2) objective4 = ForceBalance(eq, grid=grid4, device_id=0) # need to pass MPI communicator to the ObjectiveFunction - with pytest.raises(ValueError): - # this one is multi-device, and grids have different sizes - obj1 = ObjectiveFunction([objective1, objective2]) + with pytest.raises(ValueError, match="MPI communicator"): + # this one is multi-device + obj1 = ObjectiveFunction([objective1, objective2, objective3]) + + # need to use multiple ranks if using multiple devices + with pytest.raises(ValueError, match="Requested number of ranks is"): + # this one is multi-device + obj1 = ObjectiveFunction( + [objective1, objective2, objective3], mpi=MPI, rank_per_objective=[0, 0, 0] + ) + + # need to have same device for the same rank objectives + with pytest.raises(ValueError, match="Same rank objectives should"): + # this one is multi-device + obj1 = ObjectiveFunction( + [objective1, objective2, objective3, objective4], + mpi=MPI, + rank_per_objective=[0, 1, 2, 2], + ) + obj1 = ObjectiveFunction([objective1, objective2, objective3], mpi=MPI) # deriv_mode will be set to "blocked" automatically with pytest.warns(UserWarning, match="When using multiple devices"): - obj1 = ObjectiveFunction([objective1, objective2], mpi=MPI) obj1.build() + # this one is single device, and grids have different sizes - obj2 = ObjectiveFunction([objective3, objective4]) + obj2 = ObjectiveFunction([objective1, objective4]) obj2.build() assert obj1._is_mpi From eeb84127a214dcb1dcd223a427f8d19b6d43a7cd Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 Aug 2025 18:55:23 +0300 Subject: [PATCH 151/199] try to re-use code as much as possible, add oversubscribe to prevent MPI test fail randomly --- .github/workflows/mpi_tests.yml | 6 +- desc/objectives/objective_funs.py | 156 +++++++++++------------------- 2 files changed, 58 insertions(+), 104 deletions(-) diff --git a/.github/workflows/mpi_tests.yml b/.github/workflows/mpi_tests.yml index dda0075a92..46c5aae7d2 100644 --- a/.github/workflows/mpi_tests.yml +++ b/.github/workflows/mpi_tests.yml @@ -96,7 +96,7 @@ jobs: if: env.has_changes == 'true' run: | source .venv-${{ env.version }}/bin/activate - mpirun -n 3 python -m pytest -v -m mpi_run\ + mpirun -n 3 --oversubscribe python -m pytest -v -m mpi_run\ --durations=0 \ --cov-report xml:cov.xml \ --cov-config=setup.cfg \ @@ -108,8 +108,8 @@ jobs: run: | source .venv-${{ env.version }}/bin/activate # Make sure that they just run (without checking for errors) - mpirun -n 2 python -m docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py - mpirun -n 2 python -m docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py + mpirun -n 2 --oversubscribe python -m docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py + mpirun -n 2 --oversubscribe python -m docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py - name: save coverage file if: always() && env.has_changes == 'true' diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 22f2f6542b..ff953f7911 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -443,54 +443,22 @@ def _worker_loop(self): if message[0] == "STOP": print(f"Rank {self.rank} STOPPING") break - elif "jvp" in message[0] and "proximal" not in message[0]: - xs = jax.device_put( - message[1], self.objectives[obj_idx_rank[0]]._device - ) - vs = jax.device_put( - message[2], self.objectives[obj_idx_rank[0]]._device - ) - - # inputs to jitted functions must live on the same device. Need to - # put xi and vi on the same device as the objective - xs = [ - [xs[i] for i in self._things_per_objective_idx[idx]] - for idx in obj_idx_rank - ] - vs = [ - [vs[i] for i in self._things_per_objective_idx[idx]] - for idx in obj_idx_rank - ] - - J_rank = jvp_per_process( - xs, - vs, - objs, - op=message[0], - ) - J_rank = np.asarray(J_rank) - self.comm.gather(J_rank, root=0) elif "compute" in message[0]: params = jax.device_put( message[1], self.objectives[obj_idx_rank[0]]._device ) - - f_rank = compute_per_process( - [params[i] for i in obj_idx_rank], - objs, - op=message[0], - ) - f_rank = np.asarray(f_rank) - self.comm.gather(f_rank, root=0) - elif "proximal_jvp" in message[0]: - op = message[0].replace("proximal_jvp_", "") + params = [params[i] for i in obj_idx_rank] + out = compute_per_process(params, objs, op=message[0]) + elif "jvp" in message[0]: + # inputs to jitted functions must live on the same device. Need to + # put xi and vi on the same device as the objective xs = jax.device_put( message[1], self.objectives[obj_idx_rank[0]]._device ) vs = jax.device_put( message[2], self.objectives[obj_idx_rank[0]]._device ) - + # only pass the relevant parts of x and v to each objective xs = [ [xs[i] for i in self._things_per_objective_idx[idx]] for idx in obj_idx_rank @@ -499,14 +467,15 @@ def _worker_loop(self): [vs[i] for i in self._things_per_objective_idx[idx]] for idx in obj_idx_rank ] - J_rank = jvp_proximal_per_process( - xs, - vs, - objs, - op=op, - ) - J_rank = np.asarray(J_rank) - self.comm.gather(J_rank, root=0) + if "proximal" not in message[0]: + out = jvp_per_process(xs, vs, objs, op=message[0]) + elif "proximal_jvp" in message[0]: + op = message[0].replace("proximal_jvp_", "") + out = jvp_proximal_per_process(xs, vs, objs, op=op) + + # TODO: CUDA aware MPI may prevent np call + out = np.asarray(out) + self.comm.gather(out, root=0) def _unjit(self): """Remove jit compiled methods.""" @@ -759,20 +728,13 @@ def compute_unscaled(self, x, constants=None): ] ) else: - if self.rank == 0: - message = ("compute_unscaled", params, None) - self.comm.bcast(message, root=0) - - obj_idx_rank = self._obj_per_rank[self.rank] - - f_rank = compute_per_process( - [params[i] for i in obj_idx_rank], - [self.objectives[i] for i in obj_idx_rank], - op=message[0], - ) - f_rank = np.asarray(f_rank) - fs = self.comm.gather(f_rank, root=0) - f = pconcat(fs) + f = _parallel_compute( + params, + self.comm, + self.objectives, + self._obj_per_rank, + "compute_unscaled", + ) return f @jit @@ -804,20 +766,9 @@ def compute_scaled(self, x, constants=None): ] ) else: - if self.rank == 0: - message = ("compute_scaled", params, None) - self.comm.bcast(message, root=0) - - obj_idx_rank = self._obj_per_rank[self.rank] - - f_rank = compute_per_process( - [params[i] for i in obj_idx_rank], - [self.objectives[i] for i in obj_idx_rank], - op=message[0], - ) - f_rank = np.asarray(f_rank) - fs = self.comm.gather(f_rank, root=0) - f = pconcat(fs) + f = _parallel_compute( + params, self.comm, self.objectives, self._obj_per_rank, "compute_scaled" + ) return f @jit @@ -849,20 +800,13 @@ def compute_scaled_error(self, x, constants=None): ] ) else: - if self.rank == 0: - message = ("compute_scaled_error", params, None) - self.comm.bcast(message, root=0) - - obj_idx_rank = self._obj_per_rank[self.rank] - - f_rank = compute_per_process( - [params[i] for i in obj_idx_rank], - [self.objectives[i] for i in obj_idx_rank], - op=message[0], - ) - f_rank = np.asarray(f_rank) - fs = self.comm.gather(f_rank, root=0) - f = pconcat(fs) + f = _parallel_compute( + params, + self.comm, + self.objectives, + self._obj_per_rank, + "compute_scaled_error", + ) return f @jit @@ -1091,6 +1035,8 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): vi = [vs[i] for i in thing_idx] Ji_ = getattr(obj, "jvp_" + op)(vi, xi, constants=const) J += [Ji_] + + return jnp.hstack(J) else: if self.rank == 0: message = ("jvp_" + op, xs, vs) @@ -1112,15 +1058,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): J_rank = np.asarray(J_rank) J = self.comm.gather(J_rank, root=0) - # this is the transpose of the jvp when v is a matrix, for consistency with - # jvp_batched - if not self._is_mpi: - J = jnp.hstack(J) - else: - # this will handle the device placement of the J matrix - J = pconcat(J, mode="hstack") - - return J + return pconcat(J, mode="hstack") def _jvp_batched(self, v, x, constants=None, op="scaled"): v = ensure_tuple(v) @@ -2132,29 +2070,45 @@ def __call__(self, things): return unique +def _parallel_compute(params, comm, objectives, obj_per_rank, op): + rank = comm.Get_rank() + if rank == 0: + message = (op, params, None) + comm.bcast(message, root=0) + obj_idx_rank = obj_per_rank[rank] + + f_rank = compute_per_process( + [params[i] for i in obj_idx_rank], + [objectives[i] for i in obj_idx_rank], + op=message[0], + ) + # TODO: CUDA aware MPI may prevent np call + f_rank = np.asarray(f_rank) + fs = comm.gather(f_rank, root=0) + return pconcat(fs) + + # These will run on workers, and we want to safely jit them @functools.partial(jit, static_argnames="op") def compute_per_process(params, objectives, op): """Compute the objective function on each process.""" - f_rank = jnp.concatenate( + return jnp.concatenate( [ getattr(obj, op)(*param, constants=obj.constants) for (obj, param) in zip(objectives, params) ] ) - return f_rank @functools.partial(jit, static_argnames="op") def jvp_per_process(x, v, objectives, op): """Compute the Jacobian-vector product on each process.""" - J_rank = jnp.hstack( + return jnp.hstack( [ getattr(obj, op)(v[idx], x[idx], constants=obj.constants) for idx, obj in enumerate(objectives) ] ) - return J_rank @functools.partial(jit, static_argnames="op") From dd711c99ce7d235d641f69fe07697bca1a5a4e1a Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 Aug 2025 21:00:03 +0300 Subject: [PATCH 152/199] fix tests --- tests/test_multidevice.py | 56 ++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 3ddbc58c3e..9c57dbfffd 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -136,7 +136,7 @@ def test_multidevice_compute(): f2 = obj2.compute_scalar(obj2.x(eq)) np.testing.assert_allclose(f2, f1, atol=1e-8) - np.testing.assert_allclose(f2, f0, atol=1e-7) + np.testing.assert_allclose(f2, f0, atol=5e-7) f1 = obj1.compute_unscaled(obj1.x(eq)) f2 = obj2.compute_unscaled(obj2.x(eq)) @@ -267,8 +267,8 @@ def test_multidevice_eq_solve(): """Test that eq.solve still reduces force error.""" rank = MPI.COMM_WORLD.Get_rank() size = MPI.COMM_WORLD.Get_size() - assert size == 2 - assert rank < 2 + assert size == num_device + assert rank < num_device eq = get("HELIOTRON") with pytest.warns(UserWarning, match="Reducing radial"): @@ -278,13 +278,15 @@ def test_multidevice_eq_solve(): gN = eq.N_grid grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6, 0.8], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.9], sym=True) - objective1 = ForceBalance(eq, grid=grid1, device_id=0) - objective2 = ForceBalance(eq, grid=grid2, device_id=1) + obj1 = ForceBalance(eq, grid=grid1, device_id=0) + obj2 = ForceBalance(eq, grid=grid2, device_id=1) + obj3 = ForceBalance(eq, grid=grid3, device_id=2) # deriv_mode will be set to "blocked" automatically with pytest.warns(UserWarning, match="When using multiple devices"): - obj = ObjectiveFunction([objective1, objective2], mpi=MPI) + obj = ObjectiveFunction([obj1, obj2, obj3], mpi=MPI) obj.build() # creating grids like grid3 = [grid1, grid2] doesn't give the same @@ -306,50 +308,38 @@ def test_multidevice_eq_optimize(): """Test that eq.optimize still reduces error.""" rank = MPI.COMM_WORLD.Get_rank() size = MPI.COMM_WORLD.Get_size() - assert size == 2 - assert rank < 2 + assert size == num_device + assert rank < num_device eq = get("precise_QA") eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) - surf = eq.surface # create two grids with different rho values, this will effectively separate # the quasisymmetry objective into two parts - grid1 = LinearGrid( - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - rho=np.linspace(0.2, 0.5, 2), - sym=True, - ) - grid2 = LinearGrid( - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - rho=np.linspace(0.6, 1.0, 3), - sym=True, - ) + gM = eq.M_grid + gN = eq.N_grid + grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) + grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6, 0.8], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.9], sym=True) # when using parallel objectives, the user needs to supply the device_id obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) - obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) - objs = [obj1, obj2, obj3] + obj3 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid3, device_id=2) + obj4 = AspectRatio(eq=eq, target=8, weight=100, device_id=0) + objs = [obj1, obj2, obj3, obj4] objective = ObjectiveFunction( - objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 0]) + objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 2, 0]) ) objective.build() # we will fix some modes as usual k = 1 - R_modes = np.vstack( - ( - [0, 0, 0], - surf.R_basis.modes[np.max(np.abs(surf.R_basis.modes), 1) > k, :], - ) - ) - Z_modes = surf.Z_basis.modes[np.max(np.abs(surf.Z_basis.modes), 1) > k, :] + sRm = eq.surface.R_basis.modes + sZm = eq.surface.Z_basis.modes + R_modes = np.vstack(([0, 0, 0], sRm[np.max(np.abs(sRm), 1) > k, :])) + Z_modes = sZm[np.max(np.abs(sZm), 1) > k, :] constraints = ( ForceBalance(eq=eq), FixBoundaryR(eq=eq, modes=R_modes), From a6da955481b4c38fa6d200dc2e11c9136e382c02 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 Aug 2025 22:27:17 +0300 Subject: [PATCH 153/199] try to fix test --- .github/workflows/mpi_tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mpi_tests.yml b/.github/workflows/mpi_tests.yml index 46c5aae7d2..98a5f5e232 100644 --- a/.github/workflows/mpi_tests.yml +++ b/.github/workflows/mpi_tests.yml @@ -108,8 +108,8 @@ jobs: run: | source .venv-${{ env.version }}/bin/activate # Make sure that they just run (without checking for errors) - mpirun -n 2 --oversubscribe python -m docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py - mpirun -n 2 --oversubscribe python -m docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py + mpirun -n 2 --oversubscribe python docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py + mpirun -n 2 --oversubscribe python docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py - name: save coverage file if: always() && env.has_changes == 'true' From c21896a5f8934c872f24c1795116a11c685f7a5d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 Aug 2025 23:03:23 +0300 Subject: [PATCH 154/199] try to fix test --- docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py | 1 + docs/notebooks/tutorials/multi_device.ipynb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 0811518b77..3878427e6a 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -90,6 +90,7 @@ mpi=MPI, deriv_mode="blocked", ) + obj.build() cons = get_fixed_boundary_constraints(eq) # Until this line, the code is performed on all ranks, so it might print some diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index b43826eb1f..e5f7c6e434 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -73,7 +73,7 @@ "from desc.backend import jax, print_backend_info\n", "from desc.examples import get\n", "from desc.grid import LinearGrid\n", - "from desc.objectives import ObjectiveFunction, ForceBalance\n", + "from desc.objectives import ForceBalance, ObjectiveFunction\n", "from desc.objectives.getters import get_fixed_boundary_constraints\n", "\n", "if __name__ == \"__main__\":\n", @@ -128,6 +128,7 @@ " mpi=MPI,\n", " deriv_mode=\"blocked\",\n", " )\n", + " obj.build()\n", " cons = get_fixed_boundary_constraints(eq)\n", "\n", " # Until this line, the code is performed on all ranks, so it might print some\n", From 89399bfdd24693d64deb14b7bf7da0c83098ac72 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 Aug 2025 23:32:25 +0300 Subject: [PATCH 155/199] write coverage to separate files to prevent error --- .github/workflows/mpi_tests.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/mpi_tests.yml b/.github/workflows/mpi_tests.yml index 98a5f5e232..66c2d2514a 100644 --- a/.github/workflows/mpi_tests.yml +++ b/.github/workflows/mpi_tests.yml @@ -96,21 +96,30 @@ jobs: if: env.has_changes == 'true' run: | source .venv-${{ env.version }}/bin/activate - mpirun -n 3 --oversubscribe python -m pytest -v -m mpi_run\ - --durations=0 \ - --cov-report xml:cov.xml \ - --cov-config=setup.cfg \ - --cov=desc/ \ - --db ./prof.db + # ensure each MPI rank writes to a different coverage file + mpirun -n 3 --oversubscribe \ + bash -c 'COVERAGE_FILE=.coverage.$OMPI_COMM_WORLD_RANK \ + python -m pytest -v -m mpi_run \ + --durations=0 \ + --cov=desc/ \ + --cov-config=setup.cfg \ + --cov-append \ + --cov-report=' - name: Run MPI tutorials if: env.has_changes == 'true' run: | source .venv-${{ env.version }}/bin/activate - # Make sure that they just run (without checking for errors) mpirun -n 2 --oversubscribe python docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py mpirun -n 2 --oversubscribe python docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py + - name: Combine coverage files + if: always() && env.has_changes == 'true' + run: | + source .venv-${{ env.version }}/bin/activate + coverage combine + coverage xml -o cov.xml + - name: save coverage file if: always() && env.has_changes == 'true' uses: actions/upload-artifact@v4 From 6c33270c1d5724e4b4edeea72d22169d7f2b873e Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 26 Aug 2025 00:19:01 +0300 Subject: [PATCH 156/199] update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9de24ff97..a9bf3ad01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Changelog ========= +New Features + +- Adds initial support for multiple GPU optimization. This allows to compute derivatives and costs on multiple GPU, and allows more memory intense objectives. Note that, at this phase, the multi-device support is for memory, not speed. + + v0.15.0 ------- @@ -21,7 +26,6 @@ New Features - Parallelized ideal ballooning stability and Newcomb ballooning metrics and [other improvements](https://github.com/PlasmaControl/DESC/pull/1763). - Adds ``FourierXYCoil`` to compatible coils for ``CoilSetArclengthVariance`` objective. - Separated ``gamma_c`` calculation from ``Gamma_c``. User can also plot ``gamma_c`` using the ``plot_gammac`` function. -- Adds initial support for multiple GPU optimization. This allows to compute derivatives and costs on multiple GPU, and allows more memory intense objectives. Note that, at this phase, the multi-device support is for memory, not speed. Bug Fixes From 6c71205a4e762b61df753c6fc0387ad185f87e1c Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 26 Aug 2025 16:18:06 +0300 Subject: [PATCH 157/199] address my own comments --- desc/__init__.py | 6 +- desc/objectives/objective_funs.py | 88 +- desc/optimize/_constraint_wrappers.py | 13 +- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 2 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 2 +- docs/notebooks/tutorials/multi_device.ipynb | 910 ++++++++---------- 6 files changed, 452 insertions(+), 569 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index e97ada269d..1eecc7b1fc 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -157,9 +157,10 @@ def set_device(kind="cpu", gpuid=None, num_device=1, mpi=None): # noqa: C901 try: if mpi is None: warnings.warn( - "To get the fu list of CPUs, provide the MPI communicator.", + "To get the full list of CPUs, provide the MPI communicator.", UserWarning, ) + # return the same device multiple times cpu_names = [ f"{str(i) + ' ' + cpu_info}" for i in range(num_device) ] @@ -168,9 +169,6 @@ def set_device(kind="cpu", gpuid=None, num_device=1, mpi=None): # noqa: C901 rank = comm.Get_rank() cpu_name = f"{str(rank) + ' ' + cpu_info}" cpu_names = comm.allgather(cpu_name) - # These CPUs might not be the same model, but I think slurm will - # always give same model (and getting model of each CPU is not - # straightforward) config["devices"] = [name for name in cpu_names] # This memory is not individual but the total memory config["avail_mems"] = [cpu_mem for _ in range(num_device)] diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index ff953f7911..196c9b6696 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -412,7 +412,7 @@ def _worker_loop(self): This function is called when the ObjectiveFunction is used as a context manager. - with ObjectiveFunction(...) as obj: + with obj: if rank == 0: eq.optimize(objective=obj) @@ -515,7 +515,7 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 use_jit : bool, optional Whether to just-in-time compile the objective and derivatives. If using multiple GPUs, instead of jitting the ObjectiveFunction, the sub-objectives - will be jitted individually, independent of the value of `use_jit`. + will be jitted individually. verbose : int, optional Level of output. @@ -552,6 +552,9 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 f"{objective._device_id}" ) objective = jax.device_put(objective, objective._device) + # same object on different device will have different id + # need to overwrite by original to keep it the same and make + # _set_things work objective._things = obj_things if self._dim_f == 1: self._scalar = True @@ -699,6 +702,25 @@ def _set_things(self, things=None): self._unflatten = _ThingUnflattener(len(unique_), inds_, treedef_) self._flatten = _ThingFlattener(len(flat_), treedef_) + def _compute_op(self, x, constants=None, op="compute_unscaled"): + """Helper function to compute various operations.""" + params = self.unpack_state(x) + if constants is None: + constants = self.constants + assert len(params) == len(constants) == len(self.objectives) + if not self._is_mpi: + f = jnp.concatenate( + [ + getattr(obj, op)(*par, constants=const) + for par, obj, const in zip(params, self.objectives, constants) + ] + ) + else: + f = _parallel_compute( + params, self.comm, self.objectives, self._obj_per_rank, op + ) + return f + @jit def compute_unscaled(self, x, constants=None): """Compute the raw value of the objective function. @@ -716,26 +738,8 @@ def compute_unscaled(self, x, constants=None): Objective function value(s). """ - params = self.unpack_state(x) - if constants is None: - constants = self.constants - assert len(params) == len(constants) == len(self.objectives) - if not self._is_mpi: - f = jnp.concatenate( - [ - obj.compute_unscaled(*par, constants=const) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) - else: - f = _parallel_compute( - params, - self.comm, - self.objectives, - self._obj_per_rank, - "compute_unscaled", - ) - return f + op = "compute_unscaled" + return self._compute_op(x, constants=constants, op=op) @jit def compute_scaled(self, x, constants=None): @@ -754,22 +758,8 @@ def compute_scaled(self, x, constants=None): Objective function value(s). """ - params = self.unpack_state(x) - if constants is None: - constants = self.constants - assert len(params) == len(constants) == len(self.objectives) - if not self._is_mpi: - f = jnp.concatenate( - [ - obj.compute_scaled(*par, constants=const) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) - else: - f = _parallel_compute( - params, self.comm, self.objectives, self._obj_per_rank, "compute_scaled" - ) - return f + op = "compute_scaled" + return self._compute_op(x, constants=constants, op=op) @jit def compute_scaled_error(self, x, constants=None): @@ -788,26 +778,8 @@ def compute_scaled_error(self, x, constants=None): Objective function value(s). """ - params = self.unpack_state(x) - if constants is None: - constants = self.constants - assert len(params) == len(constants) == len(self.objectives) - if not self._is_mpi: - f = jnp.concatenate( - [ - obj.compute_scaled_error(*par, constants=const) - for par, obj, const in zip(params, self.objectives, constants) - ] - ) - else: - f = _parallel_compute( - params, - self.comm, - self.objectives, - self._obj_per_rank, - "compute_scaled_error", - ) - return f + op = "compute_scaled_error" + return self._compute_op(x, constants=constants, op=op) @jit def compute_scalar(self, x, constants=None): diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index e8fa642758..3baefdcfcb 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1208,13 +1208,14 @@ def _jvp(self, v, x, constants=None, op="scaled_error"): )(v) else: # TODO: implement parallel constraint for ProximalProjection + # Note: This would require putting workers into a second infinite loop. + # One way to do this could be to give pre-build objective + # and constraint to the optimizer and use 2 context managers. Also, + # divide workers for force balance constraint loop and objective loop. + # This is probably a rare use case, so not a priority for now. raise NotImplementedError( - "Parallel constraint for ProximalProjection not implemented yet. Note: " - "This would require putting workers into a second infinite loop which " - "break things. One way to do this could be to give pre-build objective " - "and constraint to the optimizer and use 2 context managers. Also, " - "divide workers for force balance constraint loop and objective loop. " - "This is probably a rare use case, so not a priority for now." + "Parallel constraint for ProximalProjection not implemented yet. " + "Please use only one Equilibrium constraint." ) if self._objective._deriv_mode == "batched": diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 3878427e6a..99b3398a0c 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -99,7 +99,7 @@ # this context manager will put the workers in a loop to listen to the master # to compute the objective function and its derivatives - with obj as obj: + with obj: # apart from cost evaluation and derivatives, everything else will be only # performed on the master rank if rank == 0: diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 4f2dfa6bde..0d4b258095 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -142,7 +142,7 @@ # this context manager will put the workers in a loop to listen to the master # to compute the objective function and its derivatives - with objective as objective: + with objective: # apart from cost evaluation and derivatives, everything else will be only # performed on the master rank if rank == 0: diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index e5f7c6e434..77e39f5f38 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -27,160 +27,180 @@ "import os\n", "\n", "sys.path.insert(0, os.path.abspath(\".\"))\n", - "sys.path.append(os.path.abspath(\"../../../\"))" + "sys.path.append(os.path.abspath(\"../../../\"))\n", + "\n", + "from IPython.display import Markdown" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 2, "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "```python\n", + "import os\n", + "import sys\n", + "\n", + "# Add the path to the parent directory to augment search for module\n", + "sys.path.insert(0, os.path.abspath(\".\"))\n", + "sys.path.append(os.path.abspath(\"../../../\"))\n", + "sys.path.append(os.path.abspath(\"../../../../\"))\n", + "\n", + "import numpy as np\n", + "from mpi4py import MPI\n", + "\n", + "from desc import _set_cpu_count, set_device\n", + "\n", + "kind = \"cpu\" # or \"gpu\"\n", + "num_device = 2\n", + "# ====== Using CPUs ======\n", + "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# such that JAX and XLA thinks there are multiple devices\n", + "if kind == \"cpu\":\n", + " # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", + " _set_cpu_count(num_device)\n", + " set_device(\"cpu\", num_device=num_device, mpi=MPI)\n", + "\n", + "# ====== Using GPUs ======\n", + "# When we have multiple processes using the same devices (for example, 3 processes\n", + "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", + "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform`\n", + "# such that there is no pre-allocation. This is a bit conservative (and probably there is room\n", + "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", + "elif kind == \"gpu\":\n", + " os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", + " set_device(\"gpu\", num_device=num_device)\n", + "\n", + "from desc import config as desc_config\n", + "from desc.backend import jax, print_backend_info\n", + "from desc.examples import get\n", + "from desc.grid import LinearGrid\n", + "from desc.objectives import ForceBalance, ObjectiveFunction\n", + "from desc.objectives.getters import get_fixed_boundary_constraints\n", + "\n", + "if __name__ == \"__main__\":\n", + " rank = MPI.COMM_WORLD.Get_rank()\n", + " size = MPI.COMM_WORLD.Get_size()\n", + " if rank == 0:\n", + " print(f\"====== TOTAL OF {size} RANKS ======\")\n", + "\n", + " # see which rank is running on which device\n", + " # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()`\n", + " # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()`\n", + " # will return only the devices that are available to the current process. This is\n", + " # useful when you have multiple processes running on multiple nodes and you want\n", + " # to see which devices are available to each process.\n", + " if desc_config[\"kind\"] == \"gpu\":\n", + " print(\n", + " f\"Rank {rank} can see {jax.local_devices(backend='gpu')} \"\n", + " f\"and {jax.local_devices(backend='cpu')}\\n\"\n", + " )\n", + " else:\n", + " print(f\"Rank {rank} can see {jax.local_devices(backend='cpu')}\\n\")\n", + "\n", + " if rank == 0:\n", + " print(\"====== BACKEND INFO ======\")\n", + " print_backend_info()\n", + " print(\"\\n\")\n", + "\n", + " eq = get(\"HELIOTRON\")\n", + " if desc_config[\"kind\"] == \"cpu\":\n", + " # for local testing use lower resolution\n", + " eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4)\n", + "\n", + " # setup 2 grids for 2 objectives covering different flux surfaces\n", + " rhos = np.linspace(0.1, 1.0, eq.L_grid)\n", + " grid1 = LinearGrid(\n", + " rho=rhos[: rhos.size // 2],\n", + " M=eq.M_grid,\n", + " N=eq.N_grid,\n", + " NFP=eq.NFP,\n", + " )\n", + " grid2 = LinearGrid(\n", + " rho=rhos[rhos.size // 2 :],\n", + " M=eq.M_grid,\n", + " N=eq.N_grid,\n", + " NFP=eq.NFP,\n", + " )\n", + " obj = ObjectiveFunction(\n", + " [\n", + " ForceBalance(eq, grid=grid1, device_id=0),\n", + " ForceBalance(eq, grid=grid2, device_id=1),\n", + " ],\n", + " mpi=MPI,\n", + " deriv_mode=\"blocked\",\n", + " )\n", + " obj.build()\n", + " cons = get_fixed_boundary_constraints(eq)\n", + "\n", + " # Until this line, the code is performed on all ranks, so it might print some\n", + " # information multiple times. The following part will only be performed on the\n", + " # master rank\n", + "\n", + " # this context manager will put the workers in a loop to listen to the master\n", + " # to compute the objective function and its derivatives\n", + " with obj:\n", + " # apart from cost evaluation and derivatives, everything else will be only\n", + " # performed on the master rank\n", + " if rank == 0:\n", + " eq.solve(\n", + " objective=obj,\n", + " constraints=cons,\n", + " maxiter=10,\n", + " ftol=0,\n", + " gtol=0,\n", + " xtol=0,\n", + " verbose=3,\n", + " )\n", + "\n", + " # if you put a code here, it will be performed on all ranks\n", + "\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "```python\n", - "\n", - "import os\n", - "import sys\n", - "\n", - "# Add the path to the parent directory to augment search for module\n", - "sys.path.insert(0, os.path.abspath(\".\"))\n", - "sys.path.append(os.path.abspath(\"../../../\"))\n", - "sys.path.append(os.path.abspath(\"../../../../\"))\n", - "\n", - "import numpy as np\n", - "from mpi4py import MPI\n", - "\n", - "from desc import _set_cpu_count, set_device\n", + "# Display the content of mpi-eq-solve.py\n", + "with open(\"mpi-tutorials/mpi-eq-solve.py\", \"r\") as f:\n", + " code = f.read()\n", "\n", - "kind = \"cpu\" # or \"gpu\"\n", - "num_device = 2\n", - "# ====== Using CPUs ======\n", - "# These will be used for diving the single CPU into multiple virtual CPUs\n", - "# such that JAX and XLA thinks there are multiple devices\n", - "if kind == \"cpu\":\n", - " # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", - " _set_cpu_count(num_device)\n", - " set_device(\"cpu\", num_device=num_device, mpi=MPI)\n", - "\n", - "# ====== Using GPUs ======\n", - "# When we have multiple processes using the same devices (for example, 3 processes\n", - "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", - "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform`\n", - "# such that there is no pre-allocation. This is a bit conservative (and probably there is room\n", - "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", - "elif kind == \"gpu\":\n", - " os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", - " set_device(\"gpu\", num_device=num_device)\n", - "\n", - "from desc import config as desc_config\n", - "from desc.backend import jax, print_backend_info\n", - "from desc.examples import get\n", - "from desc.grid import LinearGrid\n", - "from desc.objectives import ForceBalance, ObjectiveFunction\n", - "from desc.objectives.getters import get_fixed_boundary_constraints\n", - "\n", - "if __name__ == \"__main__\":\n", - " rank = MPI.COMM_WORLD.Get_rank()\n", - " size = MPI.COMM_WORLD.Get_size()\n", - " if rank == 0:\n", - " print(f\"====== TOTAL OF {size} RANKS ======\")\n", - "\n", - " # see which rank is running on which device\n", - " # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()`\n", - " # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()`\n", - " # will return only the devices that are available to the current process. This is\n", - " # useful when you have multiple processes running on multiple nodes and you want\n", - " # to see which devices are available to each process.\n", - " if desc_config[\"kind\"] == \"gpu\":\n", - " print(\n", - " f\"Rank {rank} can see {jax.local_devices(backend='gpu')} \"\n", - " f\"and {jax.local_devices(backend='cpu')}\\n\"\n", - " )\n", - " else:\n", - " print(f\"Rank {rank} can see {jax.local_devices(backend='cpu')}\\n\")\n", - "\n", - " if rank == 0:\n", - " print(\"====== BACKEND INFO ======\")\n", - " print_backend_info()\n", - " print(\"\\n\")\n", - "\n", - " eq = get(\"HELIOTRON\")\n", - " if desc_config[\"kind\"] == \"cpu\":\n", - " # for local testing use lower resolution\n", - " eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4)\n", - "\n", - " # setup 2 grids for 2 objectives covering different flux surfaces\n", - " rhos = np.linspace(0.1, 1.0, eq.L_grid)\n", - " grid1 = LinearGrid(\n", - " rho=rhos[: rhos.size // 2],\n", - " M=eq.M_grid,\n", - " N=eq.N_grid,\n", - " NFP=eq.NFP,\n", - " )\n", - " grid2 = LinearGrid(\n", - " rho=rhos[rhos.size // 2 :],\n", - " M=eq.M_grid,\n", - " N=eq.N_grid,\n", - " NFP=eq.NFP,\n", - " )\n", - " obj = ObjectiveFunction(\n", - " [\n", - " ForceBalance(eq, grid=grid1, device_id=0),\n", - " ForceBalance(eq, grid=grid2, device_id=1),\n", - " ],\n", - " mpi=MPI,\n", - " deriv_mode=\"blocked\",\n", - " )\n", - " obj.build()\n", - " cons = get_fixed_boundary_constraints(eq)\n", - "\n", - " # Until this line, the code is performed on all ranks, so it might print some\n", - " # information multiple times. The following part will only be performed on the\n", - " # master rank\n", - "\n", - " # this context manager will put the workers in a loop to listen to the master\n", - " # to compute the objective function and its derivatives\n", - " with obj as obj:\n", - " # apart from cost evaluation and derivatives, everything else will be only\n", - " # performed on the master rank\n", - " if rank == 0:\n", - " eq.solve(\n", - " objective=obj,\n", - " constraints=cons,\n", - " maxiter=10,\n", - " ftol=0,\n", - " gtol=0,\n", - " xtol=0,\n", - " verbose=3,\n", - " )\n", - "\n", - " # if you put a code here, it will be performed on all ranks\n", - "\n", - "\n", - "```" + "Markdown(f\"```python\\n{code}\\n```\")" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1)]\n", - "\n", "====== TOTAL OF 2 RANKS ======\n", "Rank 0 can see [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+322.g8fa17718f.dirty.\n", + "DESC version=0.15.0+189.g6c33270c1.dirty.\n", "Using JAX backend: jax version=0.6.2, jaxlib version=0.6.2, dtype=float64.\n", - "Using 2 CPUs with 9.89 GB total available memory:\n", - "\t CPU : 0 13th Gen Intel(R) Core(TM) i5-1335U\n", - "\t CPU : 1 13th Gen Intel(R) Core(TM) i5-1335U\n", + "Using 2 CPUs with 19.06 GB total available memory:\n", + "\t CPU : 0 13th Gen Intel(R) Core(TM) i9-13900HX\n", + "\t CPU : 1 13th Gen Intel(R) Core(TM) i9-13900HX\n", "\n", "Note: The backend information assumes that the user has 1 process per CPU (node). Using multiple processes per CPU (node) is not the most efficient way to use MPI with purely CPUs.\n", "\n", "\n", + "Rank 1 can see [CpuDevice(id=0), CpuDevice(id=1)]\n", + "\n", "Building objective: force\n", "Precomputing transforms\n", "Building objective: force\n", @@ -205,148 +225,49 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "\u001b[32mTimer: Objective build = 1.61 sec\u001b[0m\n", - "\u001b[32mTimer: LinearConstraintProjection build = 4.41 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 768 ms\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 2.16 sec\u001b[0m\n", "Number of parameters: 551\n", "Number of objectives: 8424\n", - "\u001b[32mTimer: Initializing the optimization = 6.08 sec\u001b[0m\n", + "\u001b[32mTimer: Initializing the optimization = 2.96 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 8.248e-01 3.319e-01 \n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - " 1 7 6.387e-01 1.861e-01 3.754e-02 2.474e-01 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - " 2 9 2.628e-01 3.759e-01 2.842e-02 1.828e-01 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - " 3 11 1.516e-01 1.112e-01 2.328e-02 1.405e-01 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - " 4 13 8.688e-02 6.468e-02 1.374e-02 1.080e-01 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - " 5 14 8.411e-02 2.770e-03 8.472e-03 8.306e-02 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - " 6 15 4.776e-02 3.635e-02 7.218e-03 6.471e-02 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - " 7 16 4.141e-02 6.354e-03 3.771e-03 5.071e-02 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - " 8 17 3.759e-02 3.817e-03 2.504e-03 3.958e-02 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - " 9 18 3.601e-02 1.581e-03 2.084e-03 3.107e-02 \n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : jvp_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - " 10 20 3.422e-02 1.791e-03 2.906e-03 2.416e-02 \n", + " 0 1 8.421e-01 6.217e-01 \n", + " 1 2 2.599e-01 5.822e-01 3.670e-01 2.179e-01 \n", + " 2 3 1.183e-01 1.416e-01 2.557e-01 2.515e-01 \n", + " 3 4 5.716e-02 6.110e-02 4.079e-01 1.491e-01 \n", + " 4 5 3.971e-02 1.745e-02 3.098e-01 1.289e-01 \n", + " 5 7 2.335e-02 1.636e-02 8.984e-02 1.444e-01 \n", + " 6 9 1.465e-02 8.698e-03 1.920e-02 1.263e-01 \n", + " 7 10 1.058e-02 4.076e-03 2.100e-02 9.828e-02 \n", + " 8 12 7.898e-04 9.785e-03 1.084e-02 1.536e-02 \n", + " 9 13 5.671e-04 2.227e-04 1.678e-02 4.195e-03 \n", + " 10 16 5.368e-04 3.027e-05 3.373e-03 1.381e-03 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 3.422e-02\n", - " Total delta_x: 3.460e-02\n", + " Current function value: 5.368e-04\n", + " Total delta_x: 8.143e-01\n", " Iterations: 10\n", - " Function evaluations: 20\n", + " Function evaluations: 16\n", " Jacobian evaluations: 11\n", - "\u001b[32mTimer: Solution time = 1.28 min\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 7.02 sec\u001b[0m\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "\u001b[32mTimer: Solution time = 23.3 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 2.11 sec\u001b[0m\n", "==============================================================================================================\n", " Start --> End\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : compute_scaled_error for objectives ids: [0]\n", - "Rank 0 waiting to gather\n", - "Total (sum of squares): 8.248e-01 --> 3.422e-02, \n", - "Maximum absolute Force error: 2.032e+05 --> 4.957e+04 (N)\n", - "Minimum absolute Force error: 1.059e-10 --> 1.061e-10 (N)\n", - "Average absolute Force error: 4.102e+04 --> 1.054e+04 (N)\n", - "Maximum absolute Force error: 1.634e-02 --> 3.986e-03 (normalized)\n", - "Minimum absolute Force error: 8.514e-18 --> 8.534e-18 (normalized)\n", - "Average absolute Force error: 3.299e-03 --> 8.477e-04 (normalized)\n", - "Maximum absolute Force error: 1.149e+07 --> 1.056e+06 (N)\n", - "Minimum absolute Force error: 3.304e-12 --> 1.077e-11 (N)\n", - "Average absolute Force error: 1.029e+05 --> 2.824e+04 (N)\n", - "Maximum absolute Force error: 9.238e-01 --> 8.490e-02 (normalized)\n", - "Minimum absolute Force error: 2.657e-19 --> 8.660e-19 (normalized)\n", - "Average absolute Force error: 8.279e-03 --> 2.272e-03 (normalized)\n", + "Total (sum of squares): 8.421e-01 --> 5.368e-04, \n", + "Maximum absolute Force error: 2.169e+05 --> 8.279e+03 (N)\n", + "Minimum absolute Force error: 1.091e-10 --> 1.310e-10 (N)\n", + "Average absolute Force error: 4.139e+04 --> 1.043e+03 (N)\n", + "Maximum absolute Force error: 1.744e-02 --> 6.659e-04 (normalized)\n", + "Minimum absolute Force error: 8.774e-18 --> 1.054e-17 (normalized)\n", + "Average absolute Force error: 3.329e-03 --> 8.390e-05 (normalized)\n", + "Maximum absolute Force error: 1.149e+07 --> 2.085e+05 (N)\n", + "Minimum absolute Force error: 2.439e-12 --> 3.449e-12 (N)\n", + "Average absolute Force error: 1.017e+05 --> 3.559e+03 (N)\n", + "Maximum absolute Force error: 9.238e-01 --> 1.677e-02 (normalized)\n", + "Minimum absolute Force error: 1.962e-19 --> 2.774e-19 (normalized)\n", + "Average absolute Force error: 8.181e-03 --> 2.862e-04 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", @@ -373,167 +294,197 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 4, "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "```python\n", + "import os\n", + "import sys\n", + "\n", + "# Add the path to the parent directory to augment search for module\n", + "sys.path.insert(0, os.path.abspath(\".\"))\n", + "sys.path.append(os.path.abspath(\"../../../\"))\n", + "sys.path.append(os.path.abspath(\"../../../../\"))\n", + "\n", + "from mpi4py import MPI\n", + "\n", + "from desc import _set_cpu_count, set_device\n", + "\n", + "kind = \"cpu\" # or \"gpu\"\n", + "num_device = 2\n", + "# ====== Using CPUs ======\n", + "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# such that JAX and XLA thinks there are multiple devices\n", + "if kind == \"cpu\":\n", + " # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", + " _set_cpu_count(num_device)\n", + " set_device(\"cpu\", num_device=num_device, mpi=MPI)\n", + "\n", + "# ====== Using GPUs ======\n", + "# When we have multiple processes using the same devices (for example, 3 processes\n", + "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", + "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform`\n", + "# such that there is no pre-allocation. This is a bit conservative (and probably there is room\n", + "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", + "elif kind == \"gpu\":\n", + " os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", + " set_device(\"gpu\", num_device=num_device)\n", + "\n", + "\n", + "import numpy as np\n", + "\n", + "from desc import config as desc_config\n", + "from desc.backend import jax, jnp, print_backend_info\n", + "from desc.examples import get\n", + "from desc.grid import LinearGrid\n", + "from desc.objectives import (\n", + " AspectRatio,\n", + " FixBoundaryR,\n", + " FixBoundaryZ,\n", + " FixCurrent,\n", + " FixPressure,\n", + " FixPsi,\n", + " ForceBalance,\n", + " ObjectiveFunction,\n", + " QuasisymmetryTwoTerm,\n", + ")\n", + "from desc.optimize import Optimizer\n", + "\n", + "if __name__ == \"__main__\":\n", + " rank = MPI.COMM_WORLD.Get_rank()\n", + " size = MPI.COMM_WORLD.Get_size()\n", + " if rank == 0:\n", + " print(f\"====== TOTAL OF {size} RANKS ======\")\n", + "\n", + " # see which rank is running on which device\n", + " # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()`\n", + " # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()`\n", + " # will return only the devices that are available to the current process. This is\n", + " # useful when you have multiple processes running on multiple nodes and you want\n", + " # to see which devices are available to each process.\n", + " if desc_config[\"kind\"] == \"gpu\":\n", + " print(\n", + " f\"Rank {rank} is running on {jax.local_devices(backend='gpu')} \"\n", + " f\"and {jax.local_devices(backend='cpu')}\\n\"\n", + " )\n", + " else:\n", + " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\\n\")\n", + "\n", + " if rank == 0:\n", + " print(\"====== BACKEND INFO ======\")\n", + " print_backend_info()\n", + " print(\"\\n\")\n", + "\n", + " eq = get(\"precise_QA\")\n", + " if desc_config[\"kind\"] == \"cpu\":\n", + " eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4)\n", + "\n", + " # create two grids with different rho values, this will effectively separate\n", + " # the quasisymmetry objective into two parts\n", + " grid1 = LinearGrid(\n", + " M=eq.M_grid,\n", + " N=eq.N_grid,\n", + " NFP=eq.NFP,\n", + " rho=jnp.linspace(0.2, 0.5, 4),\n", + " sym=True,\n", + " )\n", + " grid2 = LinearGrid(\n", + " M=eq.M_grid,\n", + " N=eq.N_grid,\n", + " NFP=eq.NFP,\n", + " rho=jnp.linspace(0.6, 1.0, 6),\n", + " sym=True,\n", + " )\n", + "\n", + " # when using parallel objectives, the user needs to supply the device_id\n", + " obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", + " obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", + " obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0)\n", + " objs = [obj1, obj2, obj3]\n", + "\n", + " # Parallel objective function needs the MPI communicator\n", + " # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will\n", + " # automatically switch to `blocked`.\n", + " objective = ObjectiveFunction(\n", + " objs, deriv_mode=\"blocked\", mpi=MPI, rank_per_objective=np.array([0, 1, 0])\n", + " )\n", + " if rank == 0:\n", + " objective.build(verbose=3)\n", + " else:\n", + " objective.build(verbose=0)\n", + "\n", + " # we will fix some modes as usual\n", + " k = 1\n", + " R_modes = np.vstack(\n", + " (\n", + " [0, 0, 0],\n", + " eq.surface.R_basis.modes[\n", + " np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :\n", + " ],\n", + " )\n", + " )\n", + " Z_modes = eq.surface.Z_basis.modes[\n", + " np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :\n", + " ]\n", + " constraints = (\n", + " ForceBalance(eq=eq),\n", + " FixBoundaryR(eq=eq, modes=R_modes),\n", + " FixBoundaryZ(eq=eq, modes=Z_modes),\n", + " FixPressure(eq=eq),\n", + " FixPsi(eq=eq),\n", + " FixCurrent(eq=eq),\n", + " )\n", + " optimizer = Optimizer(\"proximal-lsq-exact\")\n", + "\n", + " # Until this line, the code is performed on all ranks, so it might print some\n", + " # information multiple times. The following part will only be performed on the\n", + " # master rank\n", + "\n", + " # this context manager will put the workers in a loop to listen to the master\n", + " # to compute the objective function and its derivatives\n", + " with objective:\n", + " # apart from cost evaluation and derivatives, everything else will be only\n", + " # performed on the master rank\n", + " if rank == 0:\n", + " eq.optimize(\n", + " objective=objective,\n", + " constraints=constraints,\n", + " optimizer=optimizer,\n", + " maxiter=3,\n", + " verbose=3,\n", + " options={\n", + " \"initial_trust_ratio\": 1.0,\n", + " },\n", + " )\n", + "\n", + " # if you put a code here, it will be performed on all ranks\n", + "\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "```python\n", - "\n", - "import os\n", - "import sys\n", - "\n", - "# Add the path to the parent directory to augment search for module\n", - "sys.path.insert(0, os.path.abspath(\".\"))\n", - "sys.path.append(os.path.abspath(\"../../../\"))\n", - "\n", - "from mpi4py import MPI\n", - "from desc import _set_cpu_count, set_device\n", - "\n", - "# ====== Using CPUs ======\n", - "num_device = 2\n", - "# These will be used for diving the single CPU into multiple virtual CPUs\n", - "# such that JAX and XLA thinks there are multiple devices\n", - "\n", - "# !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", - "_set_cpu_count(num_device)\n", - "set_device(\"cpu\", num_device=num_device, mpi=MPI)\n", + "# Display the content of mpi-proximal.py\n", + "with open(\"mpi-tutorials/mpi-proximal.py\", \"r\") as f:\n", + " code = f.read()\n", "\n", - "# ====== Using GPUs ======\n", - "# When we have multiple processes using the same devices (for example, 3 processes\n", - "# using 3 GPUs), each process will try to pre-allocate 75% of the GPU memory which will\n", - "# cause the memory allocation to fail. To avoid this, we can set the allocator to `platform`\n", - "# such that there is no pre-allocation. This is a bit conservative (and probably there is room\n", - "# for improvement), but if a process needs more memory, it can use more memory on the fly.\n", - "#\n", - "# os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", - "# set_device(\"gpu\", num_device=num_device)\n", - "\n", - "\n", - "import numpy as np\n", - "\n", - "from desc import config as desc_config\n", - "from desc.backend import jax, jnp, print_backend_info\n", - "from desc.examples import get\n", - "from desc.grid import LinearGrid\n", - "from desc.objectives import (\n", - " AspectRatio,\n", - " FixBoundaryR,\n", - " FixBoundaryZ,\n", - " FixCurrent,\n", - " FixPressure,\n", - " FixPsi,\n", - " ForceBalance,\n", - " ObjectiveFunction,\n", - " QuasisymmetryTwoTerm,\n", - ")\n", - "from desc.optimize import Optimizer\n", - "\n", - "if __name__ == \"__main__\":\n", - " rank = MPI.COMM_WORLD.Get_rank()\n", - " size = MPI.COMM_WORLD.Get_size()\n", - " if rank == 0:\n", - " print(f\"====== TOTAL OF {size} RANKS ======\")\n", - "\n", - " # see which rank is running on which device\n", - " # Note: JAX has 2 functions for this: `jax.devices()` and `jax.local_devices()`\n", - " # `jax.devices()` will return all devices available to JAX, while `jax.local_devices()`\n", - " # will return only the devices that are available to the current process. This is\n", - " # useful when you have multiple processes running on multiple nodes and you want\n", - " # to see which devices are available to each process.\n", - " if desc_config[\"kind\"] == \"gpu\":\n", - " print(\n", - " f\"Rank {rank} is running on {jax.local_devices(backend='gpu')} \"\n", - " f\"and {jax.local_devices(backend='cpu')}\\n\"\n", - " )\n", - " else:\n", - " print(f\"Rank {rank} is running on {jax.local_devices(backend='cpu')}\\n\")\n", - "\n", - " if rank == 0:\n", - " print(\"====== BACKEND INFO ======\")\n", - " print_backend_info()\n", - " print(\"\\n\")\n", - "\n", - " eq = get(\"precise_QA\")\n", - " eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4)\n", - "\n", - " # create two grids with different rho values, this will effectively separate\n", - " # the quasisymmetry objective into two parts\n", - " grid1 = LinearGrid(\n", - " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.2, 0.5, 4), sym=True\n", - " )\n", - " grid2 = LinearGrid(\n", - " M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=jnp.linspace(0.6, 1.0, 6), sym=True\n", - " )\n", - "\n", - " # when using parallel objectives, the user needs to supply the device_id\n", - " obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0)\n", - " obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1)\n", - " obj3 = AspectRatio(eq=eq, target=8, weight=100, device_id=0)\n", - " objs = [obj1, obj2, obj3]\n", - "\n", - " # Parallel objective function needs the MPI communicator\n", - " # If you don't specify `deriv_mode=blocked`, you will get a warning and DESC will\n", - " # automatically switch to `blocked`.\n", - " objective = ObjectiveFunction(\n", - " objs, deriv_mode=\"blocked\", mpi=MPI, rank_per_objective=np.array([0, 1, 0])\n", - " )\n", - " if rank == 0:\n", - " objective.build(verbose=3)\n", - " else:\n", - " objective.build(verbose=0)\n", - "\n", - " # we will fix some modes as usual\n", - " k = 1\n", - " R_modes = np.vstack(\n", - " (\n", - " [0, 0, 0],\n", - " eq.surface.R_basis.modes[\n", - " np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :\n", - " ],\n", - " )\n", - " )\n", - " Z_modes = eq.surface.Z_basis.modes[\n", - " np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :\n", - " ]\n", - " constraints = (\n", - " ForceBalance(eq=eq),\n", - " FixBoundaryR(eq=eq, modes=R_modes),\n", - " FixBoundaryZ(eq=eq, modes=Z_modes),\n", - " FixPressure(eq=eq),\n", - " FixPsi(eq=eq),\n", - " FixCurrent(eq=eq),\n", - " )\n", - " optimizer = Optimizer(\"proximal-lsq-exact\")\n", - "\n", - " # Until this line, the code is performed on all ranks, so it might print some\n", - " # information multiple times. The following part will only be performed on the\n", - " # master rank\n", - "\n", - " # this context manager will put the workers in a loop to listen to the master\n", - " # to compute the objective function and its derivatives\n", - " with objective as objective:\n", - " # apart from cost evaluation and derivatives, everything else will be only\n", - " # performed on the master rank\n", - " if rank == 0:\n", - " eq.optimize(\n", - " objective=objective,\n", - " constraints=constraints,\n", - " optimizer=optimizer,\n", - " maxiter=3,\n", - " verbose=3,\n", - " options={\n", - " \"initial_trust_ratio\": 1.0,\n", - " },\n", - " )\n", - "\n", - " # if you put a code here, it will be performed on all ranks\n", - "\n", - " \n", - "```" + "Markdown(f\"```python\\n{code}\\n```\")" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -546,124 +497,85 @@ "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.14.2+322.g8fa17718f.dirty.\n", + "DESC version=0.15.0+189.g6c33270c1.dirty.\n", "Using JAX backend: jax version=0.6.2, jaxlib version=0.6.2, dtype=float64.\n", - "Using 2 CPUs with 10.09 GB total available memory:\n", - "\t CPU : 0 13th Gen Intel(R) Core(TM) i5-1335U\n", - "\t CPU : 1 13th Gen Intel(R) Core(TM) i5-1335U\n", + "Using 2 CPUs with 19.37 GB total available memory:\n", + "\t CPU : 0 13th Gen Intel(R) Core(TM) i9-13900HX\n", + "\t CPU : 1 13th Gen Intel(R) Core(TM) i9-13900HX\n", "\n", "Note: The backend information assumes that the user has 1 process per CPU (node). Using multiple processes per CPU (node) is not the most efficient way to use MPI with purely CPUs.\n", "\n", "\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.59 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 771 ms\u001b[0m\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.44 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 656 ms\u001b[0m\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.38 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 636 ms\u001b[0m\n", "------------------------------------------------------------\n", "Rank 0 will run objective(s): ['QuasisymmetryTwoTerm', 'AspectRatio']\n", "Rank 1 will run objective(s): ['QuasisymmetryTwoTerm']\n", "------------------------------------------------------------\n", - "\u001b[32mTimer: Objective build = 5.26 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 2.52 sec\u001b[0m\n", "Building objective: force\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 1.91 sec\u001b[0m\n", - "\u001b[32mTimer: Objective build = 1.98 sec\u001b[0m\n", - "\u001b[32mTimer: Objective build = 1.91 ms\u001b[0m\n", - "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 3.63 sec\u001b[0m\n", - "\u001b[32mTimer: Proximal projection build = 7.99 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 886 ms\u001b[0m\n", + "\u001b[32mTimer: Objective build = 931 ms\u001b[0m\n", + "\u001b[32mTimer: Objective build = 1.11 ms\u001b[0m\n", + "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 2.18 sec\u001b[0m\n", + "\u001b[32mTimer: Proximal projection build = 4.57 sec\u001b[0m\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "\u001b[32mTimer: Objective build = 654 ms\u001b[0m\n", - "\u001b[32mTimer: LinearConstraintProjection build = 1.62 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 433 ms\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 969 ms\u001b[0m\n", "Number of parameters: 8\n", "Number of objectives: 631\n", - "\u001b[32mTimer: Initializing the optimization = 10.3 sec\u001b[0m\n", + "\u001b[32mTimer: Initializing the optimization = 6.01 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", - "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 2.005e+04 1.987e+02 \n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", - "Rank 0 waiting to gather\n", - " 1 4 9.192e+03 1.086e+04 1.003e-01 1.098e+02 \n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", - "Rank 0 waiting to gather\n", - " 2 6 5.181e+03 4.011e+03 5.393e-02 7.714e+01 \n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 0 waiting to gather\n", - "Rank 1 : proximal_jvp_scaled_error for objectives ids: [1]\n", - "Rank 0 : proximal_jvp_scaled_error for objectives ids: [0 2]\n", - "Rank 0 waiting to gather\n", - " 3 7 2.311e+03 2.870e+03 3.806e-02 2.626e+01 \n", + " 0 1 2.005e+04 1.926e+02 \n", + " 1 4 8.123e+03 1.193e+04 4.964e-02 9.847e+01 \n", + " 2 5 2.617e+03 5.507e+03 5.877e-02 6.065e+01 \n", + " 3 7 7.564e+02 1.860e+03 7.212e-02 3.935e+00 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 2.311e+03\n", - " Total delta_x: 1.308e-01\n", + " Current function value: 7.564e+02\n", + " Total delta_x: 7.271e-02\n", " Iterations: 3\n", " Function evaluations: 7\n", " Jacobian evaluations: 4\n", - "\u001b[32mTimer: Solution time = 1.00 min\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 15.0 sec\u001b[0m\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", - "Rank 1 : compute_scaled_error for objectives ids: [1]\n", + "\u001b[32mTimer: Solution time = 31.5 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 7.89 sec\u001b[0m\n", "==============================================================================================================\n", " Start --> End\n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 0 waiting to gather\n", - "Rank 0 : compute_scaled_error for objectives ids: [0 2]\n", - "Rank 0 waiting to gather\n", - "Total (sum of squares): 2.005e+04 --> 2.311e+03, \n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.038e-01 --> 1.834e+00 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.569e-04 --> 1.078e-03 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.039e-01 --> 3.498e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.406e-01 --> 2.002e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.803e-04 --> 1.177e-03 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.134e-01 --> 3.817e-01 (normalized)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 9.615e-01 --> 4.950e+00 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 3.670e-04 --> 5.549e-03 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.474e-01 --> 6.551e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.049e+00 --> 5.402e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 4.004e-04 --> 6.054e-03 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.609e-01 --> 7.148e-01 (normalized)\n", - "Aspect ratio: 6.002e+00 --> 7.723e+00 (dimensionless)\n", - "Maximum absolute Force error: 1.435e+05 --> 3.442e+04 (N)\n", - "Minimum absolute Force error: 1.480e+00 --> 1.546e+01 (N)\n", - "Average absolute Force error: 7.215e+03 --> 3.140e+03 (N)\n", - "Maximum absolute Force error: 1.026e-01 --> 2.460e-02 (normalized)\n", - "Minimum absolute Force error: 1.058e-06 --> 1.105e-05 (normalized)\n", - "Average absolute Force error: 5.157e-03 --> 2.245e-03 (normalized)\n", + "Total (sum of squares): 2.005e+04 --> 7.564e+02, \n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.038e-01 --> 1.333e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.569e-04 --> 2.875e-04 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.039e-01 --> 2.474e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.406e-01 --> 1.455e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.803e-04 --> 3.137e-04 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.134e-01 --> 2.699e-01 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 9.615e-01 --> 2.043e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 3.670e-04 --> 1.044e-02 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.474e-01 --> 3.819e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.049e+00 --> 2.229e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 4.004e-04 --> 1.139e-02 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.609e-01 --> 4.167e-01 (normalized)\n", + "Aspect ratio: 6.002e+00 --> 7.856e+00 (dimensionless)\n", + "Maximum absolute Force error: 1.435e+05 --> 2.352e+04 (N)\n", + "Minimum absolute Force error: 1.480e+00 --> 6.889e+00 (N)\n", + "Average absolute Force error: 7.215e+03 --> 2.171e+03 (N)\n", + "Maximum absolute Force error: 1.026e-01 --> 1.681e-02 (normalized)\n", + "Minimum absolute Force error: 1.058e-06 --> 4.925e-06 (normalized)\n", + "Average absolute Force error: 5.157e-03 --> 1.552e-03 (normalized)\n", "R boundary error: 0.000e+00 --> 4.600e-19 (m)\n", "Z boundary error: 0.000e+00 --> 3.469e-18 (m)\n", "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", @@ -763,7 +675,7 @@ "\n", "When you write your script for multiple nodes, the number of devices and the device IDs must be selected as if there is only 1 node and only the local GPUs are visible. Other nodes will be used through `rank` of MPI communicator.\n", "\n", - "Note: Most clusters have multiple GPUs connected to each node, so before using multiple nodes, use all the GPUs available to that node. Multi-node communication is significantly slower and your script will be easier to write properly.\n", + "**Note: Most clusters have multiple GPUs connected to each node, so before using multiple nodes, use all the GPUs available to that node. Multi-node communication is significantly slower and your script will be easier to write properly.**\n", "\n", "Note: You should have at least 6 objectives, so at least 1 objective per device. If you want to run multiple objectives on the same device, you can specify the ``rank_per_objective`` in the `ObjectiveFunction` keywords. By default, the initializer will assign different ranks for each sub-objective." ] @@ -785,7 +697,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.12.11" } }, "nbformat": 4, From 974f6290b73af568ad34a5a55c3b4eb7ee1a26dd Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 26 Aug 2025 16:22:26 +0300 Subject: [PATCH 158/199] remove redundant change to make reviewing easier --- desc/objectives/objective_funs.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 196c9b6696..137891a814 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -573,17 +573,17 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 errorif( any(sub_obj_jac_chunk_sizes_are_ints) and self._deriv_mode == "batched", ValueError, - "'jac_chunk_size' was passed into one or more sub-objectives, but the " - "ObjectiveFunction is using 'batched' deriv_mode, so sub-objective " - "'jac_chunk_size' will be ignored in favor of the ObjectiveFunction's " - f"'jac_chunk_size' of {self._jac_chunk_size}. " - "Specify 'blocked' deriv_mode and don't pass `jac_chunk_size` for " - "ObjectiveFunction if each sub-objective is desired to have a " - "different 'jac_chunk_size' for its Jacobian computation. " - "`jac_chunk_size` of sub-objective(s): " - f"{sub_obj_chunk_sizes_names} " - f"Note: If you didn't specify 'jac_chunk_size' for the sub-objectives, " - "it might be that sub-objective has an internal logic to determine the " + "'jac_chunk_size' was passed into one or more sub-objectives, but the\n" + "ObjectiveFunction is using 'batched' deriv_mode, so sub-objective \n" + "'jac_chunk_size' will be ignored in favor of the ObjectiveFunction's \n" + f"'jac_chunk_size' of {self._jac_chunk_size}.\n" + "Specify 'blocked' deriv_mode and don't pass `jac_chunk_size` for \n" + "ObjectiveFunction if each sub-objective is desired to have a \n" + "different 'jac_chunk_size' for its Jacobian computation. \n" + "`jac_chunk_size` of sub-objective(s): \n" + f"{sub_obj_chunk_sizes_names}\n" + f"Note: If you didn't specify 'jac_chunk_size' for the sub-objectives, \n" + "it might be that sub-objective has an internal logic to determine the \n" "chunk size based on the available memory.", ) From 0d6aee4d8926cfae5e79c81a51ef353616aa03db Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 26 Aug 2025 20:55:52 +0300 Subject: [PATCH 159/199] use Bcast for arrays instead of bcast which uses pickling and allegdly has overhead --- desc/backend.py | 31 ++++++++ desc/objectives/objective_funs.py | 107 ++++++++++++++++---------- desc/optimize/_constraint_wrappers.py | 29 +++---- 3 files changed, 112 insertions(+), 55 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 8a1e148d43..b0a7cc94b8 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -604,6 +604,33 @@ def pconcat(arrays, mode="concat"): # pragma: no cover return out + def safe_mpi_Bcast(arr, comm, root=0): + """Safe Bcast function for Jax arrays. + + JAX arrays cannot be directly broadcasted using MPI's Bcast, but numpy + arrays can. If CUDA-aware MPI is available, JAX arrays on GPU can be + broadcasted directly. This function checks the type of the array and + perform the broadcast safely. It assumes that for GPU backend CUDA-aware + MPI is available. + + Parameters + ---------- + arr : jnp.ndarray or np.ndarray + Array to broadcast. Only the root process needs to provide this. + comm : MPI.Comm + MPI communicator. + root : int + Rank of root process. Default is 0. + + Returns + ------- + arr : jnp.ndarray or np.ndarray + Broadcasted array. + """ + if isinstance(arr, jnp.ndarray) and desc_config["kind"] == "cpu": + arr = np.array(arr) + return comm.Bcast(arr, root=root) + # we can't really test the numpy backend stuff in automated testing, so we ignore it # for coverage purposes @@ -1080,3 +1107,7 @@ def pconcat(arrays, mode="concat"): elif mode == "vstack": out = np.vstack(arrays) return out + + def safe_mpi_Bcast(arr, comm, root=0): + """Numpy implementation of desc.backend.safe_mpi_Bcast.""" + return comm.Bcast(arr, root=root) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 137891a814..506c992e29 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -12,6 +12,7 @@ jit, jnp, pconcat, + safe_mpi_Bcast, tree_flatten, tree_map, tree_unflatten, @@ -264,6 +265,7 @@ class ObjectiveFunction(IOAble): "_use_jit", "_is_mpi", "_static_attrs", + "_dim_x_splits", ] def __init__( @@ -430,34 +432,47 @@ def _worker_loop(self): if self.rank == 0: # Root rank won't enter worker loop return + + def alloc_array(shape): + if desc_config["kind"] == "cpu": + return np.empty(shape, dtype=np.float64) + return jnp.empty(shape, dtype=jnp.float64) + while self.running: # The message contains 3 parts, # message[0] is the operation to be performed - # message[1] is the state vector (for compute and jvp's) - # message[2] is the tangents (for only jvp's) + # message[1] is the size of state vector (for compute and jvp's) + # message[2] is the shape of tangents (for only jvp's) message = (None, None, None) message = self.comm.bcast(message, root=0) - obj_idx_rank = self._obj_per_rank[self.rank] - objs = [self.objectives[i] for i in obj_idx_rank] if message[0] == "STOP": print(f"Rank {self.rank} STOPPING") break - elif "compute" in message[0]: + + # get arrays by Bcast which uses buffers and faster than bcast + x = alloc_array(message[1]) + safe_mpi_Bcast(x, self.comm, root=0) + + obj_idx_rank = self._obj_per_rank[self.rank] + objs = [self.objectives[i] for i in obj_idx_rank] + + if "compute" in message[0]: + params = self.unpack_state(x) params = jax.device_put( - message[1], self.objectives[obj_idx_rank[0]]._device + params, self.objectives[obj_idx_rank[0]]._device ) params = [params[i] for i in obj_idx_rank] out = compute_per_process(params, objs, op=message[0]) elif "jvp" in message[0]: - # inputs to jitted functions must live on the same device. Need to + x = jnp.split(x, self._dim_x_splits) + vs = alloc_array(message[2]) + safe_mpi_Bcast(vs, self.comm, root=0) + vs = jnp.split(vs, self._dim_x_splits, axis=-1) + # put xi and vi on the same device as the objective - xs = jax.device_put( - message[1], self.objectives[obj_idx_rank[0]]._device - ) - vs = jax.device_put( - message[2], self.objectives[obj_idx_rank[0]]._device - ) + xs = jax.device_put(x, self.objectives[obj_idx_rank[0]]._device) + vs = jax.device_put(vs, self.objectives[obj_idx_rank[0]]._device) # only pass the relevant parts of x and v to each objective xs = [ [xs[i] for i in self._things_per_objective_idx[idx]] @@ -562,6 +577,7 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 self._scalar = False self._set_things() + self._dim_x_splits = np.cumsum([t.dim_x for t in self.things]) # setting derivative mode and chunking. sub_obj_jac_chunk_sizes_are_ints = [ @@ -704,11 +720,11 @@ def _set_things(self, things=None): def _compute_op(self, x, constants=None, op="compute_unscaled"): """Helper function to compute various operations.""" - params = self.unpack_state(x) if constants is None: constants = self.constants - assert len(params) == len(constants) == len(self.objectives) if not self._is_mpi: + params = self.unpack_state(x) + assert len(params) == len(constants) == len(self.objectives) f = jnp.concatenate( [ getattr(obj, op)(*par, constants=const) @@ -716,11 +732,30 @@ def _compute_op(self, x, constants=None, op="compute_unscaled"): ] ) else: - f = _parallel_compute( - params, self.comm, self.objectives, self._obj_per_rank, op - ) + f = self._parallel_compute(x, op) return f + def _parallel_compute(self, x, op): + """Compute the objective function in parallel using MPI.""" + if self.rank == 0: + message = (op, x.shape, None) + self.comm.bcast(message, root=0) + safe_mpi_Bcast(x, self.comm, root=0) + + params = self.unpack_state(x) + obj_idx_rank = self._obj_per_rank[self.rank] + + f_rank = compute_per_process( + [params[i] for i in obj_idx_rank], + [self.objectives[i] for i in obj_idx_rank], + op=message[0], + ) + # TODO: CUDA aware MPI may prevent np call + # Use Gatherv to improve speed + f_rank = np.asarray(f_rank) + fs = self.comm.gather(f_rank, root=0) + return pconcat(fs) + @jit def compute_unscaled(self, x, constants=None): """Compute the raw value of the objective function. @@ -900,8 +935,7 @@ def unpack_state(self, x, per_objective=True): + f"{self.dim_x} got {x.size}." ) - xs_splits = np.cumsum([t.dim_x for t in self.things]) - xs = jnp.split(x, xs_splits) + xs = jnp.split(x, self._dim_x_splits) xs = xs[: len(self.things)] # jnp.split returns an empty array at the end assert len(xs) == len(self.things) params = [t.unpack_params(xi) for t, xi in zip(self.things, xs)] @@ -991,10 +1025,9 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): # is needed for perturbations. Just pass that to jvp_batched for now return self._jvp_batched(v, x, constants, op) - xs_splits = np.cumsum([t.dim_x for t in self.things]) - xs = jnp.split(x, xs_splits) - vs = jnp.split(v[0], xs_splits, axis=-1) if not self._is_mpi: + xs = jnp.split(x, self._dim_x_splits) + vs = jnp.split(v[0], self._dim_x_splits, axis=-1) J = [] assert len(self.objectives) == len(self.constants) # basic idea is we compute the jacobian of each objective wrt each thing @@ -1011,8 +1044,15 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): return jnp.hstack(J) else: if self.rank == 0: - message = ("jvp_" + op, xs, vs) + # broadcasting x and v as single array is faster than + # boradcasting the list + message = ("jvp_" + op, x.shape, v[0].shape) self.comm.bcast(message, root=0) + safe_mpi_Bcast(x, self.comm, root=0) + safe_mpi_Bcast(v[0], self.comm, root=0) + + xs = jnp.split(x, self._dim_x_splits) + vs = jnp.split(v[0], self._dim_x_splits, axis=-1) obj_idx_rank = self._obj_per_rank[self.rank] J_rank = jvp_per_process( @@ -1027,6 +1067,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): [self.objectives[i] for i in obj_idx_rank], op=message[0], ) + # Use Gatherv to use fast buffer transfer J_rank = np.asarray(J_rank) J = self.comm.gather(J_rank, root=0) @@ -2042,24 +2083,6 @@ def __call__(self, things): return unique -def _parallel_compute(params, comm, objectives, obj_per_rank, op): - rank = comm.Get_rank() - if rank == 0: - message = (op, params, None) - comm.bcast(message, root=0) - obj_idx_rank = obj_per_rank[rank] - - f_rank = compute_per_process( - [params[i] for i in obj_idx_rank], - [objectives[i] for i in obj_idx_rank], - op=message[0], - ) - # TODO: CUDA aware MPI may prevent np call - f_rank = np.asarray(f_rank) - fs = comm.gather(f_rank, root=0) - return pconcat(fs) - - # These will run on workers, and we want to safely jit them @functools.partial(jit, static_argnames="op") def compute_per_process(params, objectives, op): diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 3baefdcfcb..328c79d2ad 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import jit, jnp, pconcat, put +from desc.backend import jit, jnp, pconcat, put, safe_mpi_Bcast from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -753,11 +753,12 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 self._dimc_per_thing[self._eq_idx] = np.sum( [self._eq.dimensions[arg] for arg in self._args] ) + self._dim_x_splits = np.cumsum(self._dimx_per_thing) # equivalent matrix for A[unfixed_idx] @ D @ Z == A @ feasible_tangents self._feasible_tangents = jnp.eye(self._objective.dim_x) self._feasible_tangents = jnp.split( - self._feasible_tangents, np.cumsum(self._dimx_per_thing), axis=-1 + self._feasible_tangents, self._dim_x_splits, axis=-1 ) # dg/dxeq_reduced = dg/dx_eq_unscaled @ dx_eq_unscaled/dxeq_reduced # noqa: E800 # x_eq_unscaled = Deq(xp_eq + Zeq @ xeq_reduced) # noqa: E800 @@ -1222,12 +1223,14 @@ def _jvp(self, v, x, constants=None, op="scaled_error"): # objective's method already know about its jac_chunk_size return getattr(self._objective, "jvp_" + op)(tangents, xg, constants[0]) else: - vgs = jnp.split(tangents, np.cumsum(self._dimx_per_thing), axis=-1) - xgs = jnp.split(xg, np.cumsum(self._dimx_per_thing)) if not self._objective._is_mpi: + vgs = jnp.split(tangents, self._dim_x_splits, axis=-1) + xgs = jnp.split(xg, self._dim_x_splits) return _proximal_jvp_blocked_pure(self._objective, vgs, xgs, op) else: - return _proximal_jvp_blocked_parallel(self._objective, vgs, xgs, op) + return _proximal_jvp_blocked_parallel( + self._objective, tangents, xg, self._dim_x_splits, op + ) def _get_tangent(self, v, xf, constants, op): # Note: This function is vectorized over v. So, v is expected to be 1D array @@ -1350,10 +1353,15 @@ def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): return jnp.concatenate(out).T -def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): +def _proximal_jvp_blocked_parallel(objective, vgs, xgs, splits, op): if objective.rank == 0: - message = ("proximal_jvp_" + op, xgs, vgs) + message = ("proximal_jvp_" + op, xgs.shape, vgs.shape) objective.comm.bcast(message, root=0) + safe_mpi_Bcast(xgs, comm=objective.comm, root=0) + safe_mpi_Bcast(vgs, comm=objective.comm, root=0) + + xgs = jnp.split(xgs, splits) + vgs = jnp.split(vgs, splits, axis=-1) obj_idx_rank = objective._obj_per_rank[objective.rank] xs = [ @@ -1365,12 +1373,7 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, op): for idx in obj_idx_rank ] objs = [objective.objectives[i] for i in obj_idx_rank] - J_rank = jvp_proximal_per_process( - xs, - vs, - objs, - op=op, - ) + J_rank = jvp_proximal_per_process(xs, vs, objs, op=op) J_rank = np.asarray(J_rank) J = objective.comm.gather(J_rank, root=0) J = pconcat(J).T From b78fc8e662bd64509d23e6ea21b260ce7247f708 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 28 Aug 2025 13:34:27 +0300 Subject: [PATCH 160/199] seems like FixParameters is changing dim_x? The tests that use FixParameters were failing when dim_x_splits was static, reverting (also wasn't needed anymore) --- desc/objectives/objective_funs.py | 16 +++++++--------- desc/optimize/_constraint_wrappers.py | 9 ++++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 506c992e29..1bd41f4e17 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -265,7 +265,6 @@ class ObjectiveFunction(IOAble): "_use_jit", "_is_mpi", "_static_attrs", - "_dim_x_splits", ] def __init__( @@ -465,10 +464,10 @@ def alloc_array(shape): params = [params[i] for i in obj_idx_rank] out = compute_per_process(params, objs, op=message[0]) elif "jvp" in message[0]: - x = jnp.split(x, self._dim_x_splits) + x = jnp.split(x, np.cumsum([t.dim_x for t in self.things])) vs = alloc_array(message[2]) safe_mpi_Bcast(vs, self.comm, root=0) - vs = jnp.split(vs, self._dim_x_splits, axis=-1) + vs = jnp.split(vs, np.cumsum([t.dim_x for t in self.things]), axis=-1) # put xi and vi on the same device as the objective xs = jax.device_put(x, self.objectives[obj_idx_rank[0]]._device) @@ -577,7 +576,6 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 self._scalar = False self._set_things() - self._dim_x_splits = np.cumsum([t.dim_x for t in self.things]) # setting derivative mode and chunking. sub_obj_jac_chunk_sizes_are_ints = [ @@ -935,7 +933,7 @@ def unpack_state(self, x, per_objective=True): + f"{self.dim_x} got {x.size}." ) - xs = jnp.split(x, self._dim_x_splits) + xs = jnp.split(x, np.cumsum([t.dim_x for t in self.things])) xs = xs[: len(self.things)] # jnp.split returns an empty array at the end assert len(xs) == len(self.things) params = [t.unpack_params(xi) for t, xi in zip(self.things, xs)] @@ -1026,8 +1024,8 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): return self._jvp_batched(v, x, constants, op) if not self._is_mpi: - xs = jnp.split(x, self._dim_x_splits) - vs = jnp.split(v[0], self._dim_x_splits, axis=-1) + xs = jnp.split(x, np.cumsum([t.dim_x for t in self.things])) + vs = jnp.split(v[0], np.cumsum([t.dim_x for t in self.things]), axis=-1) J = [] assert len(self.objectives) == len(self.constants) # basic idea is we compute the jacobian of each objective wrt each thing @@ -1051,8 +1049,8 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): safe_mpi_Bcast(x, self.comm, root=0) safe_mpi_Bcast(v[0], self.comm, root=0) - xs = jnp.split(x, self._dim_x_splits) - vs = jnp.split(v[0], self._dim_x_splits, axis=-1) + xs = jnp.split(x, np.cumsum([t.dim_x for t in self.things])) + vs = jnp.split(v[0], np.cumsum([t.dim_x for t in self.things]), axis=-1) obj_idx_rank = self._obj_per_rank[self.rank] J_rank = jvp_per_process( diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 328c79d2ad..3f414838f5 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -753,12 +753,11 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 self._dimc_per_thing[self._eq_idx] = np.sum( [self._eq.dimensions[arg] for arg in self._args] ) - self._dim_x_splits = np.cumsum(self._dimx_per_thing) # equivalent matrix for A[unfixed_idx] @ D @ Z == A @ feasible_tangents self._feasible_tangents = jnp.eye(self._objective.dim_x) self._feasible_tangents = jnp.split( - self._feasible_tangents, self._dim_x_splits, axis=-1 + self._feasible_tangents, np.cumsum(self._dimx_per_thing), axis=-1 ) # dg/dxeq_reduced = dg/dx_eq_unscaled @ dx_eq_unscaled/dxeq_reduced # noqa: E800 # x_eq_unscaled = Deq(xp_eq + Zeq @ xeq_reduced) # noqa: E800 @@ -1224,12 +1223,12 @@ def _jvp(self, v, x, constants=None, op="scaled_error"): return getattr(self._objective, "jvp_" + op)(tangents, xg, constants[0]) else: if not self._objective._is_mpi: - vgs = jnp.split(tangents, self._dim_x_splits, axis=-1) - xgs = jnp.split(xg, self._dim_x_splits) + vgs = jnp.split(tangents, np.cumsum(self._dimx_per_thing), axis=-1) + xgs = jnp.split(xg, np.cumsum(self._dimx_per_thing)) return _proximal_jvp_blocked_pure(self._objective, vgs, xgs, op) else: return _proximal_jvp_blocked_parallel( - self._objective, tangents, xg, self._dim_x_splits, op + self._objective, tangents, xg, np.cumsum(self._dimx_per_thing), op ) def _get_tangent(self, v, xf, constants, op): From 26329d1704ed0836b2bda0cd91db6b76efb3cde1 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Thu, 28 Aug 2025 13:58:19 +0300 Subject: [PATCH 161/199] update changelog and docs --- CHANGELOG.md | 5 ++++- docs/notebooks/tutorials/multi_device.ipynb | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bf3ad01e..5214391730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ Changelog New Features -- Adds initial support for multiple GPU optimization. This allows to compute derivatives and costs on multiple GPU, and allows more memory intense objectives. Note that, at this phase, the multi-device support is for memory, not speed. +- Adds initial support for multi-device optimization with MPI. This allows to compute derivatives and costs on multiple devices (GPUs/CPUs), and to split memory usage during these operations across devices. See the [documentation](https://desc-docs.readthedocs.io/en/stable/notebooks/tutorials/multi_device.html) for details. Couple important notes: + - MPI is not a default dependency of DESC, so, to use MPI functionality, the users should verify their MPI installation themselves. + - Using MPI is recommended only for the cases where you get out-of-memory error. If your problem fits to single GPU memory, it's unlikely that MPI will give speed improvement. + - MPI is not implemented for matrix decompositions (i.e. QR/SVD/Cholesky) which default optimizer ``lsq-exact`` uses. For the cases where Jacobian doesn't fit to GPU memory, matrix decompositions will be performed on CPU and will be slow. Feel free to open a PR, if you have knowledge on parallel QR/SVD or Cholesky. v0.15.0 diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 77e39f5f38..6a5ef16507 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -290,7 +290,9 @@ "metadata": {}, "source": [ "## Using other Objectives\n", - "Above we used the convenience function for force balance objective, but we can also use other objectives with this approach." + "Above we used MPI for force balance objective, but we can also use it for general optimization.\n", + "\n", + "**Note:** Currently, if the optimizer solves the equilibrium at each step, this equilibrium solve cannot use MPI." ] }, { From b6ea6c93abb1a44150374e8c7d066445acf236ce Mon Sep 17 00:00:00 2001 From: YigitElma Date: Sat, 30 Aug 2025 23:38:17 +0300 Subject: [PATCH 162/199] use Gatherv for passing Jacobian and compute data --- desc/objectives/objective_funs.py | 82 +++++++++++++++++++++------ desc/optimize/_constraint_wrappers.py | 26 +++++++-- tests/test_multidevice.py | 2 +- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 1bd41f4e17..19a3478e8c 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -11,7 +11,6 @@ jax, jit, jnp, - pconcat, safe_mpi_Bcast, tree_flatten, tree_map, @@ -371,6 +370,8 @@ def __init__( "running", "_obj_per_rank", "_rank_per_objective", + "_f_sizes", + "_f_displs", ] if self._is_mpi and mpi is None: @@ -463,6 +464,11 @@ def alloc_array(shape): ) params = [params[i] for i in obj_idx_rank] out = compute_per_process(params, objs, op=message[0]) + if desc_config["kind"] == "cpu": + out = np.array(out) + self.comm.Gatherv( + out, (None, self._f_sizes, self._f_displs, self.mpi.DOUBLE), root=0 + ) elif "jvp" in message[0]: x = jnp.split(x, np.cumsum([t.dim_x for t in self.things])) vs = alloc_array(message[2]) @@ -485,11 +491,20 @@ def alloc_array(shape): out = jvp_per_process(xs, vs, objs, op=message[0]) elif "proximal_jvp" in message[0]: op = message[0].replace("proximal_jvp_", "") - out = jvp_proximal_per_process(xs, vs, objs, op=op) - - # TODO: CUDA aware MPI may prevent np call - out = np.asarray(out) - self.comm.gather(out, root=0) + out = jvp_proximal_per_process(xs, vs, objs, op=op).T + + if desc_config["kind"] == "cpu": + out = np.array(out) + self.comm.Gatherv( + out, + ( + None, + self._f_sizes * out.shape[0], + self._f_displs * out.shape[0], + self.mpi.DOUBLE, + ), + root=0, + ) def _unjit(self): """Remove jit compiled methods.""" @@ -652,6 +667,18 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 # use the chunk size of the first objective self._jac_chunk_size = self.objectives[0]._jac_chunk_size + if self._is_mpi: + # sizes and displacements for Gatherv + self._f_sizes = np.array( + [ + sum([self.objectives[i].dim_f for i in ids]) + for ids in self._obj_per_rank + ] + ) + self._f_displs = np.array( + [sum(self._f_sizes[:i]) for i in range(self.size)] + ) + if self._is_mpi and verbose > 0: if self.rank == 0: objective_names_per_rank = [ @@ -748,11 +775,19 @@ def _parallel_compute(self, x, op): [self.objectives[i] for i in obj_idx_rank], op=message[0], ) - # TODO: CUDA aware MPI may prevent np call - # Use Gatherv to improve speed - f_rank = np.asarray(f_rank) - fs = self.comm.gather(f_rank, root=0) - return pconcat(fs) + if desc_config["kind"] == "cpu": + f_rank = np.array(f_rank) + recvbuf = np.empty(self.dim_f, dtype=np.float64) + else: + recvbuf = jnp.empty(self.dim_f, dtype=jnp.float64) + self.comm.Gatherv( + f_rank, + (recvbuf, self._f_sizes, self._f_displs, self.mpi.DOUBLE), + root=0, + ) + if desc_config["kind"] == "cpu": + recvbuf = jnp.array(recvbuf) + return recvbuf @jit def compute_unscaled(self, x, constants=None): @@ -1065,11 +1100,26 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): [self.objectives[i] for i in obj_idx_rank], op=message[0], ) - # Use Gatherv to use fast buffer transfer - J_rank = np.asarray(J_rank) - J = self.comm.gather(J_rank, root=0) - - return pconcat(J, mode="hstack") + if desc_config["kind"] == "cpu": + J_rank = np.array(J_rank) + recvbuf = np.empty((J_rank.shape[0], self.dim_f), dtype=np.float64) + else: + recvbuf = jnp.empty( + (J_rank.shape[0], self.dim_f), dtype=jnp.float64 + ) + self.comm.Gatherv( + J_rank, + ( + recvbuf, + self._f_sizes * J_rank.shape[0], + self._f_displs * J_rank.shape[0], + self.mpi.DOUBLE, + ), + root=0, + ) + if desc_config["kind"] == "cpu": + recvbuf = jnp.array(recvbuf) + return recvbuf def _jvp_batched(self, v, x, constants=None, op="scaled"): v = ensure_tuple(v) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 3f414838f5..a2d98c1d92 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import jit, jnp, pconcat, put, safe_mpi_Bcast +from desc.backend import desc_config, jit, jnp, put, safe_mpi_Bcast from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -1372,8 +1372,22 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, splits, op): for idx in obj_idx_rank ] objs = [objective.objectives[i] for i in obj_idx_rank] - J_rank = jvp_proximal_per_process(xs, vs, objs, op=op) - J_rank = np.asarray(J_rank) - J = objective.comm.gather(J_rank, root=0) - J = pconcat(J).T - return J + J_rank = jvp_proximal_per_process(xs, vs, objs, op=op).T + if desc_config["kind"] == "cpu": + J_rank = np.array(J_rank) + recvbuf = np.empty((J_rank.shape[0], objective.dim_f), dtype=np.float64) + else: + recvbuf = jnp.empty((J_rank.shape[0], objective.dim_f), dtype=jnp.float64) + objective.comm.Gatherv( + J_rank, + ( + recvbuf, + objective._f_sizes * J_rank.shape[0], + objective._f_displs * J_rank.shape[0], + objective.mpi.DOUBLE, + ), + root=0, + ) + if desc_config["kind"] == "cpu": + recvbuf = jnp.array(recvbuf) + return recvbuf diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 9c57dbfffd..163ce7f6d6 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -350,7 +350,7 @@ def test_multidevice_eq_optimize(): ) optimizer = Optimizer("proximal-lsq-exact") - with objective as objective: + with objective: if rank == 0: f0 = objective.compute_scalar(objective.x(eq)) eq.optimize( From 8b08ed98eaa403168e0273e9386319711555b5a3 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Sun, 31 Aug 2025 10:55:11 -0400 Subject: [PATCH 163/199] resolve the scrambled array issue, add test for Proximal derivatives --- desc/objectives/objective_funs.py | 27 ++++---- desc/optimize/_constraint_wrappers.py | 18 ++++-- tests/test_multidevice.py | 88 ++++++++++++++++++++++++--- 3 files changed, 106 insertions(+), 27 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 19a3478e8c..2a6ff18d0c 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -488,10 +488,10 @@ def alloc_array(shape): for idx in obj_idx_rank ] if "proximal" not in message[0]: - out = jvp_per_process(xs, vs, objs, op=message[0]) + out = jvp_per_process(xs, vs, objs, op=message[0]).T elif "proximal_jvp" in message[0]: op = message[0].replace("proximal_jvp_", "") - out = jvp_proximal_per_process(xs, vs, objs, op=op).T + out = jvp_proximal_per_process(xs, vs, objs, op=op) if desc_config["kind"] == "cpu": out = np.array(out) @@ -499,8 +499,8 @@ def alloc_array(shape): out, ( None, - self._f_sizes * out.shape[0], - self._f_displs * out.shape[0], + self._f_sizes * message[2][1], + self._f_displs * message[2][1], self.mpi.DOUBLE, ), root=0, @@ -1088,6 +1088,11 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): vs = jnp.split(v[0], np.cumsum([t.dim_x for t in self.things]), axis=-1) obj_idx_rank = self._obj_per_rank[self.rank] + # jvp_per_process returns the Jacobian in a transposed way which is + # hard to stack vertically by MPI (colums get scrambled), that's why + # we will do multiple transpose operations. The first one is to be able + # to stack the Jacobian parts vertically, the second one is to return + # the Jacobian in the expected way by other functions. J_rank = jvp_per_process( [ [xs[i] for i in self._things_per_objective_idx[idx]] @@ -1099,27 +1104,25 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): ], [self.objectives[i] for i in obj_idx_rank], op=message[0], - ) + ).T if desc_config["kind"] == "cpu": J_rank = np.array(J_rank) - recvbuf = np.empty((J_rank.shape[0], self.dim_f), dtype=np.float64) + recvbuf = np.empty((self.dim_f, message[2][1]), dtype=np.float64) else: - recvbuf = jnp.empty( - (J_rank.shape[0], self.dim_f), dtype=jnp.float64 - ) + recvbuf = jnp.empty((self.dim_f, message[2][1]), dtype=jnp.float64) self.comm.Gatherv( J_rank, ( recvbuf, - self._f_sizes * J_rank.shape[0], - self._f_displs * J_rank.shape[0], + self._f_sizes * message[2][1], + self._f_displs * message[2][1], self.mpi.DOUBLE, ), root=0, ) if desc_config["kind"] == "cpu": recvbuf = jnp.array(recvbuf) - return recvbuf + return recvbuf.T def _jvp_batched(self, v, x, constants=None, op="scaled"): v = ensure_tuple(v) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index a2d98c1d92..59f82f11c6 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1349,6 +1349,7 @@ def _proximal_jvp_blocked_pure(objective, vgs, xgs, op): else: outi = getattr(obj, "jvp_" + op)([_vi for _vi in vi], xi, constants=const).T out.append(outi) + return jnp.concatenate(out).T @@ -1372,22 +1373,27 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, splits, op): for idx in obj_idx_rank ] objs = [objective.objectives[i] for i in obj_idx_rank] - J_rank = jvp_proximal_per_process(xs, vs, objs, op=op).T + J_rank = jvp_proximal_per_process(xs, vs, objs, op=op) if desc_config["kind"] == "cpu": J_rank = np.array(J_rank) - recvbuf = np.empty((J_rank.shape[0], objective.dim_f), dtype=np.float64) + recvbuf = np.empty((objective.dim_f, J_rank.shape[1]), dtype=np.float64) else: - recvbuf = jnp.empty((J_rank.shape[0], objective.dim_f), dtype=jnp.float64) + recvbuf = jnp.empty((objective.dim_f, J_rank.shape[1]), dtype=jnp.float64) objective.comm.Gatherv( J_rank, ( recvbuf, - objective._f_sizes * J_rank.shape[0], - objective._f_displs * J_rank.shape[0], + objective._f_sizes * J_rank.shape[1], + objective._f_displs * J_rank.shape[1], objective.mpi.DOUBLE, ), root=0, ) if desc_config["kind"] == "cpu": recvbuf = jnp.array(recvbuf) - return recvbuf + + # we collected the Jacobian in the proper way above, but as a convention + # the _jvp methods return the transpose of the Jacobian. For example, + # _jac methods always take the transpose of the returned quantity by _jvp. + # To be consistent with that, we return the transpose here. + return recvbuf.T diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 163ce7f6d6..7097f15142 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -34,7 +34,7 @@ ObjectiveFunction, QuasisymmetryTwoTerm, ) -from desc.optimize import Optimizer +from desc.optimize import Optimizer, ProximalProjection @pytest.mark.mpi_setup @@ -103,10 +103,7 @@ def test_multidevice_compute(): grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6], sym=True) grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.8], sym=True) - grid4 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2, 0.6, 0.8], sym=True) - obj0 = ObjectiveFunction(ForceBalance(eq, grid=grid4)) - obj0.build() obj1 = ObjectiveFunction( [ ForceBalance(eq, grid=grid1), @@ -129,14 +126,11 @@ def test_multidevice_compute(): ) obj2.build() - f0 = obj0.compute_scalar(obj0.x(eq)) f1 = obj1.compute_scalar(obj1.x(eq)) with obj2: if rank == 0: f2 = obj2.compute_scalar(obj2.x(eq)) - np.testing.assert_allclose(f2, f1, atol=1e-8) - np.testing.assert_allclose(f2, f0, atol=5e-7) f1 = obj1.compute_unscaled(obj1.x(eq)) f2 = obj2.compute_unscaled(obj2.x(eq)) @@ -194,8 +188,6 @@ def test_multidevice_derivatives(): f2 = obj2.jac_unscaled(obj2.x(eq)) np.testing.assert_allclose(f2, f1, atol=1e-8) - # figure out why this fails! One of them doesn't - # apply the scaling properly f1 = obj1.jac_scaled(obj1.x(eq)) f2 = obj2.jac_scaled(obj2.x(eq)) np.testing.assert_allclose(f2, f1, atol=1e-8) @@ -205,6 +197,84 @@ def test_multidevice_derivatives(): np.testing.assert_allclose(f2, f1, atol=1e-8) +@pytest.mark.mpi_run +def test_multidevice_proximal_derivatives(): + """Test that proximal derivatives gives same results.""" + rank = MPI.COMM_WORLD.Get_rank() + eq = get("precise_QH") + with pytest.warns(UserWarning, match="Reducing radial"): + eq.change_resolution(1, 1, 1, 2, 2, 2) + + eq1 = eq.copy() + eq2 = eq.copy() + + gM = eq.M_grid + gN = eq.N_grid + grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) + grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6, 0.8], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.9], sym=True) + + obj1 = QuasisymmetryTwoTerm(eq=eq1, helicity=(1, eq.NFP), grid=grid1) + obj2 = QuasisymmetryTwoTerm(eq=eq1, helicity=(1, eq.NFP), grid=grid2) + obj3 = QuasisymmetryTwoTerm(eq=eq1, helicity=(1, eq.NFP), grid=grid3) + objs = [obj1, obj2, obj3] + + objective1 = ObjectiveFunction(objs, deriv_mode="blocked") + objective1.build(verbose=0) + + con1 = ObjectiveFunction(ForceBalance(eq1)) + con1.build(verbose=0) + + obj1 = QuasisymmetryTwoTerm(eq=eq2, helicity=(1, eq.NFP), grid=grid1, device_id=0) + obj2 = QuasisymmetryTwoTerm(eq=eq2, helicity=(1, eq.NFP), grid=grid2, device_id=1) + obj3 = QuasisymmetryTwoTerm(eq=eq2, helicity=(1, eq.NFP), grid=grid3, device_id=2) + objs = [obj1, obj2, obj3] + + objective2 = ObjectiveFunction( + objs, deriv_mode="blocked", mpi=MPI, rank_per_objective=np.array([0, 1, 2]) + ) + objective2.build(verbose=0) + con2 = ObjectiveFunction(ForceBalance(eq2)) + con2.build(verbose=0) + + perturb_options = {"order": 1} + solve_options = {"maxiter": 1} + prox1 = ProximalProjection( + objective=objective1, + constraint=con1, + eq=eq1, + solve_options=solve_options, + perturb_options=perturb_options, + ) + prox2 = ProximalProjection( + objective=objective2, + constraint=con2, + eq=eq2, + solve_options=solve_options, + perturb_options=perturb_options, + ) + prox1.build() + prox2.build() + + with objective2: + if rank == 0: + f1 = prox1.grad(prox1.x(eq1)) + f2 = prox2.grad(prox2.x(eq2)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + f1 = prox1.jac_unscaled(prox1.x(eq1)) + f2 = prox2.jac_unscaled(prox2.x(eq2)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + f1 = prox1.jac_scaled(prox1.x(eq1)) + f2 = prox2.jac_scaled(prox2.x(eq2)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + f1 = prox1.jac_scaled_error(prox1.x(eq1)) + f2 = prox2.jac_scaled_error(prox2.x(eq2)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + @pytest.mark.mpi_run def test_multidevice_objective_build(): """Test that objective function build works fine.""" From e6698c87d8d9f7f9b81f97f9421c7722a1262cb2 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Sun, 31 Aug 2025 10:56:11 -0400 Subject: [PATCH 164/199] remove pconcat since it is not needed anymore --- desc/backend.py | 60 ------------------------------------------------- 1 file changed, 60 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index b0a7cc94b8..c6d4855f74 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -554,56 +554,6 @@ def bodyfun(state): x = jax.lax.custom_root(res, x0, solve, _tangent_solve, has_aux=False) return x - def pconcat(arrays, mode="concat"): # pragma: no cover - """Concatenate arrays that live on different devices. - - Parameters - ---------- - arrays : list of jnp.ndarray - Arrays to concatenate. - mode : str - "concat:, "hstack" or "vstack. Default is "concat" - - Returns - ------- - out : jnp.ndarray - Concatenated array that lives on GPU[id=0]. If thre is not enough memory - the array will be stored on CPU. - """ - # we will use either CPU or GPU[0] for the matrix decompositions, so the - # array of float64 should fit into single device - size = jnp.array([x.size for x in arrays]) - size = jnp.sum(size) - if ( - size * 8 / (1024**3) > desc_config["avail_mems"][0] - or desc_config["kind"] == "cpu" - ): - if ( - getattr(desc_config, "SUPPRESS_CPU_WARNING", False) - and desc_config["kind"] == "gpu" - ): - warnings.warn( - "The total size of the arrays exceeds the available memory of the " - "GPU[id=0]. Moving the array to CPU. This may cause performance " - "degredation. To suppress this warning, use " - "`from desc import config as desc_config` \n" - "`desc_config['SUPPRESS_CPU_WARNING'] = True`" - ) - device = jax.devices("cpu")[0] - else: - device = jax.devices("gpu")[0] - - if mode == "concat": - out = jnp.concatenate([jax.device_put(x, device=device) for x in arrays]) - elif mode == "hstack": - out = jnp.hstack( - [jnp.atleast_2d(jax.device_put(x, device=device)) for x in arrays] - ) - elif mode == "vstack": - out = jnp.vstack([jax.device_put(x, device=device) for x in arrays]) - - return out - def safe_mpi_Bcast(arr, comm, root=0): """Safe Bcast function for Jax arrays. @@ -1098,16 +1048,6 @@ def take( out = np.take(a, indices, axis, out, mode) return out - def pconcat(arrays, mode="concat"): - """Numpy implementation of desc.backend.pconcat.""" - if mode == "concat": - out = np.concatenate(arrays) - elif mode == "hstack": - out = np.hstack(arrays) - elif mode == "vstack": - out = np.vstack(arrays) - return out - def safe_mpi_Bcast(arr, comm, root=0): """Numpy implementation of desc.backend.safe_mpi_Bcast.""" return comm.Bcast(arr, root=root) From 2599588ef635818993876e25411e9138fe669d00 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Sun, 31 Aug 2025 11:28:01 -0400 Subject: [PATCH 165/199] fix dimensional issue, add linear constraint projection test too --- desc/objectives/objective_funs.py | 12 +++--- tests/test_multidevice.py | 62 ++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 2a6ff18d0c..c4fe7cc7e9 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -499,8 +499,8 @@ def alloc_array(shape): out, ( None, - self._f_sizes * message[2][1], - self._f_displs * message[2][1], + self._f_sizes * message[2][0], + self._f_displs * message[2][0], self.mpi.DOUBLE, ), root=0, @@ -1107,15 +1107,15 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): ).T if desc_config["kind"] == "cpu": J_rank = np.array(J_rank) - recvbuf = np.empty((self.dim_f, message[2][1]), dtype=np.float64) + recvbuf = np.empty((self.dim_f, message[2][0]), dtype=np.float64) else: - recvbuf = jnp.empty((self.dim_f, message[2][1]), dtype=jnp.float64) + recvbuf = jnp.empty((self.dim_f, message[2][0]), dtype=jnp.float64) self.comm.Gatherv( J_rank, ( recvbuf, - self._f_sizes * message[2][1], - self._f_displs * message[2][1], + self._f_sizes * message[2][0], + self._f_displs * message[2][0], self.mpi.DOUBLE, ), root=0, diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 7097f15142..a28de6aab6 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -33,8 +33,9 @@ ForceBalance, ObjectiveFunction, QuasisymmetryTwoTerm, + get_fixed_boundary_constraints, ) -from desc.optimize import Optimizer, ProximalProjection +from desc.optimize import LinearConstraintProjection, Optimizer, ProximalProjection @pytest.mark.mpi_setup @@ -197,6 +198,65 @@ def test_multidevice_derivatives(): np.testing.assert_allclose(f2, f1, atol=1e-8) +@pytest.mark.mpi_run +def test_multidevice_linear_proj_derivatives(): + """Test that linear projection derivatives gives same results.""" + rank = MPI.COMM_WORLD.Get_rank() + eq = get("precise_QH") + + gM = eq.M_grid + gN = eq.N_grid + grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) + grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6], sym=True) + grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.8], sym=True) + + objf1 = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1), + ForceBalance(eq, grid=grid2), + ForceBalance(eq, grid=grid3), + ], + deriv_mode="blocked", + ) + objf1.build() + + # deriv_mode will be set to "blocked" automatically + with pytest.warns(UserWarning, match="When using multiple devices"): + objf2 = ObjectiveFunction( + [ + ForceBalance(eq, grid=grid1, device_id=0), + ForceBalance(eq, grid=grid2, device_id=1), + ForceBalance(eq, grid=grid3, device_id=2), + ], + mpi=MPI, + ) + objf2.build() + + cons = get_fixed_boundary_constraints(eq) + cons = ObjectiveFunction(cons) + obj1 = LinearConstraintProjection(objective=objf1, constraint=cons) + obj2 = LinearConstraintProjection(objective=objf2, constraint=cons) + obj1.build() + obj2.build() + + with objf2: + if rank == 0: + with pytest.raises(NotImplementedError): + _ = obj2.grad(obj2.x(eq)) + + f1 = obj1.jac_unscaled(obj1.x(eq)) + f2 = obj2.jac_unscaled(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + f1 = obj1.jac_scaled(obj1.x(eq)) + f2 = obj2.jac_scaled(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + f1 = obj1.jac_scaled_error(obj1.x(eq)) + f2 = obj2.jac_scaled_error(obj2.x(eq)) + np.testing.assert_allclose(f2, f1, atol=1e-8) + + @pytest.mark.mpi_run def test_multidevice_proximal_derivatives(): """Test that proximal derivatives gives same results.""" From 0acc8a6a753d73affd0abe61d3d41df9e9d6f7d5 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 2 Sep 2025 11:16:00 -0400 Subject: [PATCH 166/199] trying to fix CUDA aware MPI --- desc/backend.py | 2 +- desc/objectives/objective_funs.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index c6d4855f74..a6816d9942 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -577,7 +577,7 @@ def safe_mpi_Bcast(arr, comm, root=0): arr : jnp.ndarray or np.ndarray Broadcasted array. """ - if isinstance(arr, jnp.ndarray) and desc_config["kind"] == "cpu": + if desc_config["kind"] == "cpu": arr = np.array(arr) return comm.Bcast(arr, root=root) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index c4fe7cc7e9..844d91643b 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -433,10 +433,10 @@ def _worker_loop(self): # Root rank won't enter worker loop return - def alloc_array(shape): + def alloc_array(shape, device=None): if desc_config["kind"] == "cpu": return np.empty(shape, dtype=np.float64) - return jnp.empty(shape, dtype=jnp.float64) + return jnp.empty(shape, dtype=jnp.float64, device=device) while self.running: # The message contains 3 parts, @@ -450,13 +450,13 @@ def alloc_array(shape): print(f"Rank {self.rank} STOPPING") break - # get arrays by Bcast which uses buffers and faster than bcast - x = alloc_array(message[1]) - safe_mpi_Bcast(x, self.comm, root=0) - obj_idx_rank = self._obj_per_rank[self.rank] objs = [self.objectives[i] for i in obj_idx_rank] + # get arrays by Bcast which uses buffers and faster than bcast + x = alloc_array(message[1], device=self.objectives[obj_idx_rank[0]]._device) + safe_mpi_Bcast(x, self.comm, root=0) + if "compute" in message[0]: params = self.unpack_state(x) params = jax.device_put( @@ -471,7 +471,9 @@ def alloc_array(shape): ) elif "jvp" in message[0]: x = jnp.split(x, np.cumsum([t.dim_x for t in self.things])) - vs = alloc_array(message[2]) + vs = alloc_array( + message[2], device=self.objectives[obj_idx_rank[0]]._device + ) safe_mpi_Bcast(vs, self.comm, root=0) vs = jnp.split(vs, np.cumsum([t.dim_x for t in self.things]), axis=-1) From d067a5cd095125b267db45f2638ea65c1f689417 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 2 Sep 2025 11:23:38 -0400 Subject: [PATCH 167/199] trying to fix CUDA aware MPI, add block until ready --- desc/backend.py | 1 + desc/objectives/objective_funs.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/desc/backend.py b/desc/backend.py index a6816d9942..78a8f9bcf8 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -577,6 +577,7 @@ def safe_mpi_Bcast(arr, comm, root=0): arr : jnp.ndarray or np.ndarray Broadcasted array. """ + arr.block_until_ready() if desc_config["kind"] == "cpu": arr = np.array(arr) return comm.Bcast(arr, root=root) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 844d91643b..fc04b8a5fe 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -436,7 +436,9 @@ def _worker_loop(self): def alloc_array(shape, device=None): if desc_config["kind"] == "cpu": return np.empty(shape, dtype=np.float64) - return jnp.empty(shape, dtype=jnp.float64, device=device) + return jnp.empty( + shape, dtype=jnp.float64, device=device + ).block_until_ready() while self.running: # The message contains 3 parts, From 349f283a5ffa8bffd170cdebd8c25ef94ddeae71 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 2 Sep 2025 12:38:48 -0400 Subject: [PATCH 168/199] make the device transfer based on desc_config[mpi-cuda] which is False until we make it CUDA-aware compatible --- CHANGELOG.md | 1 + desc/__init__.py | 8 +++++++- desc/backend.py | 2 +- desc/objectives/objective_funs.py | 17 +++++++---------- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5214391730..81d6275c5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ New Features - MPI is not a default dependency of DESC, so, to use MPI functionality, the users should verify their MPI installation themselves. - Using MPI is recommended only for the cases where you get out-of-memory error. If your problem fits to single GPU memory, it's unlikely that MPI will give speed improvement. - MPI is not implemented for matrix decompositions (i.e. QR/SVD/Cholesky) which default optimizer ``lsq-exact`` uses. For the cases where Jacobian doesn't fit to GPU memory, matrix decompositions will be performed on CPU and will be slow. Feel free to open a PR, if you have knowledge on parallel QR/SVD or Cholesky. + - CUDA-aware MPI is not supported yet. v0.15.0 diff --git a/desc/__init__.py b/desc/__init__.py index 1eecc7b1fc..54fdfbcf78 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -61,7 +61,13 @@ def __getattr__(name): BANNER = colored(_BANNER, "magenta") -config = {"devices": None, "avail_mems": None, "kind": None, "num_device": None} +config = { + "devices": None, + "avail_mems": None, + "kind": None, + "num_device": None, + "mpi-cuda": False, +} def _get_processor_name(): diff --git a/desc/backend.py b/desc/backend.py index 78a8f9bcf8..6c086036b4 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -578,7 +578,7 @@ def safe_mpi_Bcast(arr, comm, root=0): Broadcasted array. """ arr.block_until_ready() - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: arr = np.array(arr) return comm.Bcast(arr, root=root) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index fc04b8a5fe..c042ecf56f 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -434,7 +434,7 @@ def _worker_loop(self): return def alloc_array(shape, device=None): - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: return np.empty(shape, dtype=np.float64) return jnp.empty( shape, dtype=jnp.float64, device=device @@ -466,7 +466,7 @@ def alloc_array(shape, device=None): ) params = [params[i] for i in obj_idx_rank] out = compute_per_process(params, objs, op=message[0]) - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: out = np.array(out) self.comm.Gatherv( out, (None, self._f_sizes, self._f_displs, self.mpi.DOUBLE), root=0 @@ -497,7 +497,7 @@ def alloc_array(shape, device=None): op = message[0].replace("proximal_jvp_", "") out = jvp_proximal_per_process(xs, vs, objs, op=op) - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: out = np.array(out) self.comm.Gatherv( out, @@ -779,7 +779,7 @@ def _parallel_compute(self, x, op): [self.objectives[i] for i in obj_idx_rank], op=message[0], ) - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: f_rank = np.array(f_rank) recvbuf = np.empty(self.dim_f, dtype=np.float64) else: @@ -789,7 +789,7 @@ def _parallel_compute(self, x, op): (recvbuf, self._f_sizes, self._f_displs, self.mpi.DOUBLE), root=0, ) - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: recvbuf = jnp.array(recvbuf) return recvbuf @@ -1109,7 +1109,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): [self.objectives[i] for i in obj_idx_rank], op=message[0], ).T - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: J_rank = np.array(J_rank) recvbuf = np.empty((self.dim_f, message[2][0]), dtype=np.float64) else: @@ -1124,7 +1124,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): ), root=0, ) - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: recvbuf = jnp.array(recvbuf) return recvbuf.T @@ -1635,9 +1635,6 @@ def build(self, use_jit=True, verbose=1): self._unjit() self._built = True - # put the constants to device as jax arrays - if desc_config["kind"] == "gpu": - self._constants = jax.device_put(self.constants, self._device) @abstractmethod def compute(self, *args, **kwargs): From 09b245582e9ffe766f1dc086ab1bd3418b081aa6 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 2 Sep 2025 12:39:56 -0400 Subject: [PATCH 169/199] add comment --- desc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/__init__.py b/desc/__init__.py index 54fdfbcf78..2e85c43e1e 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -60,7 +60,7 @@ def __getattr__(name): BANNER = colored(_BANNER, "magenta") - +# mpi-cuda = True is not supported yet config = { "devices": None, "avail_mems": None, From 8c049f2d267e10af4ee595c0830d1730fdaa961e Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 2 Sep 2025 12:46:22 -0400 Subject: [PATCH 170/199] remove some block until ready s --- desc/backend.py | 1 - desc/objectives/objective_funs.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index 6c086036b4..f39b9755d9 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -577,7 +577,6 @@ def safe_mpi_Bcast(arr, comm, root=0): arr : jnp.ndarray or np.ndarray Broadcasted array. """ - arr.block_until_ready() if not desc_config["mpi-cuda"]: arr = np.array(arr) return comm.Bcast(arr, root=root) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index c042ecf56f..d0089f4af5 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -436,9 +436,7 @@ def _worker_loop(self): def alloc_array(shape, device=None): if not desc_config["mpi-cuda"]: return np.empty(shape, dtype=np.float64) - return jnp.empty( - shape, dtype=jnp.float64, device=device - ).block_until_ready() + return jnp.empty(shape, dtype=jnp.float64, device=device) while self.running: # The message contains 3 parts, From c56b1967fc6855c398f602bbbf8c6c23c962ae11 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 2 Sep 2025 22:22:06 -0400 Subject: [PATCH 171/199] fix the unintended copy by returning the new stuff and overwriting the original by the new --- desc/backend.py | 10 ++++++---- desc/objectives/objective_funs.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index f39b9755d9..b371e23e03 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -560,13 +560,12 @@ def safe_mpi_Bcast(arr, comm, root=0): JAX arrays cannot be directly broadcasted using MPI's Bcast, but numpy arrays can. If CUDA-aware MPI is available, JAX arrays on GPU can be broadcasted directly. This function checks the type of the array and - perform the broadcast safely. It assumes that for GPU backend CUDA-aware - MPI is available. + perform the broadcast safely. Parameters ---------- arr : jnp.ndarray or np.ndarray - Array to broadcast. Only the root process needs to provide this. + Array to broadcast. comm : MPI.Comm MPI communicator. root : int @@ -579,7 +578,10 @@ def safe_mpi_Bcast(arr, comm, root=0): """ if not desc_config["mpi-cuda"]: arr = np.array(arr) - return comm.Bcast(arr, root=root) + comm.Bcast(arr, root=root) + # don't use this returned value for root == rank, as it will replace the jax + # array with a numpy array, which is not ideal + return arr # we can't really test the numpy backend stuff in automated testing, so we ignore it diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index d0089f4af5..70ebfad925 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -455,7 +455,7 @@ def alloc_array(shape, device=None): # get arrays by Bcast which uses buffers and faster than bcast x = alloc_array(message[1], device=self.objectives[obj_idx_rank[0]]._device) - safe_mpi_Bcast(x, self.comm, root=0) + x = safe_mpi_Bcast(x, self.comm, root=0) if "compute" in message[0]: params = self.unpack_state(x) @@ -474,7 +474,7 @@ def alloc_array(shape, device=None): vs = alloc_array( message[2], device=self.objectives[obj_idx_rank[0]]._device ) - safe_mpi_Bcast(vs, self.comm, root=0) + vs = safe_mpi_Bcast(vs, self.comm, root=0) vs = jnp.split(vs, np.cumsum([t.dim_x for t in self.things]), axis=-1) # put xi and vi on the same device as the objective From 3aae65841b34e3192f1f23cb8f9722952a6074e1 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 2 Feb 2026 20:02:17 -0500 Subject: [PATCH 172/199] gather arrays on CPU, need to add a size check later --- desc/objectives/objective_funs.py | 4 ++-- desc/optimize/_constraint_wrappers.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 3491f23945..fe8ff5d63d 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -788,7 +788,7 @@ def _parallel_compute(self, x, op): root=0, ) if not desc_config["mpi-cuda"]: - recvbuf = jnp.array(recvbuf) + recvbuf = jnp.array(recvbuf, device=jax.devices("cpu")[0]) return recvbuf @jit @@ -1123,7 +1123,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): root=0, ) if not desc_config["mpi-cuda"]: - recvbuf = jnp.array(recvbuf) + recvbuf = jnp.array(recvbuf, device=jax.devices("cpu")[0]) return recvbuf.T def _jvp_batched(self, v, x, constants=None, op="scaled"): diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index d1470a9050..5ab92e15c9 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,7 @@ import numpy as np -from desc.backend import desc_config, jit, jnp, put, safe_mpi_Bcast +from desc.backend import desc_config, jax, jit, jnp, put, safe_mpi_Bcast from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -1404,7 +1404,7 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, splits, op): ] objs = [objective.objectives[i] for i in obj_idx_rank] J_rank = jvp_proximal_per_process(xs, vs, objs, op=op) - if desc_config["kind"] == "cpu": + if not desc_config["mpi-cuda"]: J_rank = np.array(J_rank) recvbuf = np.empty((objective.dim_f, J_rank.shape[1]), dtype=np.float64) else: @@ -1419,8 +1419,8 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, splits, op): ), root=0, ) - if desc_config["kind"] == "cpu": - recvbuf = jnp.array(recvbuf) + if not desc_config["mpi-cuda"]: + recvbuf = jnp.array(recvbuf, device=jax.devices("cpu")[0]) # we collected the Jacobian in the proper way above, but as a convention # the _jvp methods return the transpose of the Jacobian. For example, From 493b96baa69eb89675e90643abfa7b80e4073d02 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 2 Feb 2026 20:50:23 -0500 Subject: [PATCH 173/199] add size check for the transfer --- desc/backend.py | 27 +++++++++++++++++++++++++++ desc/objectives/objective_funs.py | 7 +++---- desc/optimize/_constraint_wrappers.py | 12 +++++++++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/desc/backend.py b/desc/backend.py index c45d8923b9..b3abc59098 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -583,6 +583,29 @@ def safe_mpi_Bcast(arr, comm, root=0): # array with a numpy array, which is not ideal return arr + def safe_transfer_to_device(arr): + """Safely transfer array to device. + + Handles the final array device if the array is too big for GPU, + or the backend is CPU. + + Parameters + ---------- + arr : jnp.ndarray or np.ndarray + Array to transfer. + + Returns + ------- + arr : jnp.ndarray + Array on the target device. + """ + size_gb = arr.nbytes / 1024**3 + + # this can still fail if arr is big even for CPU + if desc_config["kind"] == "cpu" or size_gb > desc_config["avail_mems"][0] * 0.9: + return jnp.array(arr, device=jax.devices("cpu")[0]) + return jax.array(arr) + # we can't really test the numpy backend stuff in automated testing, so we ignore it # for coverage purposes @@ -1057,3 +1080,7 @@ def take( def safe_mpi_Bcast(arr, comm, root=0): """Numpy implementation of desc.backend.safe_mpi_Bcast.""" return comm.Bcast(arr, root=root) + + def safe_transfer_to_device(arr): + """Numpy implementation of desc.backend.safe_transfer_to_device.""" + return arr diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index fe8ff5d63d..57ca54f53f 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -12,6 +12,7 @@ jit, jnp, safe_mpi_Bcast, + safe_transfer_to_device, tree_flatten, tree_map, tree_unflatten, @@ -787,8 +788,7 @@ def _parallel_compute(self, x, op): (recvbuf, self._f_sizes, self._f_displs, self.mpi.DOUBLE), root=0, ) - if not desc_config["mpi-cuda"]: - recvbuf = jnp.array(recvbuf, device=jax.devices("cpu")[0]) + recvbuf = safe_transfer_to_device(recvbuf) return recvbuf @jit @@ -1122,8 +1122,7 @@ def _jvp_blocked(self, v, x, constants=None, op="scaled"): ), root=0, ) - if not desc_config["mpi-cuda"]: - recvbuf = jnp.array(recvbuf, device=jax.devices("cpu")[0]) + recvbuf = safe_transfer_to_device(recvbuf) return recvbuf.T def _jvp_batched(self, v, x, constants=None, op="scaled"): diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index 5ab92e15c9..ecd465e1ac 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -4,7 +4,14 @@ import numpy as np -from desc.backend import desc_config, jax, jit, jnp, put, safe_mpi_Bcast +from desc.backend import ( + desc_config, + jit, + jnp, + put, + safe_mpi_Bcast, + safe_transfer_to_device, +) from desc.batching import batched_vectorize from desc.objectives import ( BoundaryRSelfConsistency, @@ -1419,8 +1426,7 @@ def _proximal_jvp_blocked_parallel(objective, vgs, xgs, splits, op): ), root=0, ) - if not desc_config["mpi-cuda"]: - recvbuf = jnp.array(recvbuf, device=jax.devices("cpu")[0]) + recvbuf = safe_transfer_to_device(recvbuf) # we collected the Jacobian in the proper way above, but as a convention # the _jvp methods return the transpose of the Jacobian. For example, From 36244aa11954c325faf0b26795caf08aba29fcc3 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Fri, 6 Feb 2026 15:59:22 -0500 Subject: [PATCH 174/199] resurrect memory warning --- desc/__init__.py | 3 +++ desc/backend.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index d2f005c33f..e439d1f108 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -67,7 +67,10 @@ def __getattr__(name): "avail_mems": None, "kind": None, "num_device": None, + # Set to True if CUDA-aware MPI is installed "mpi-cuda": False, + # Suppress the warning in `desc.backend.safe_transfer_to_device` + "SUPPRESS_GPU_MEMORY_WARNING": False, } diff --git a/desc/backend.py b/desc/backend.py index b3abc59098..a1d5df7210 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -603,8 +603,19 @@ def safe_transfer_to_device(arr): # this can still fail if arr is big even for CPU if desc_config["kind"] == "cpu" or size_gb > desc_config["avail_mems"][0] * 0.9: - return jnp.array(arr, device=jax.devices("cpu")[0]) - return jax.array(arr) + if ( + not desc_config["SUPPRESS_GPU_MEMORY_WARNING"] + and desc_config["kind"] == "gpu" + ): + warnings.warn( + "The total size of the arrays exceeds the available memory of the " + "GPU[id=0]. Moving the array to CPU. This may cause performance " + "degredation. To suppress this warning, use \n" + "`from desc import config as desc_config` \n" + "`desc_config['SUPPRESS_GPU_MEMORY_WARNING'] = True`" + ) + return jnp.asarray(arr, device=jax.devices("cpu")[0]) + return jnp.asarray(arr) # we can't really test the numpy backend stuff in automated testing, so we ignore it From 601a82e46cd4b6c4683b64c697f8d6d6b0755901 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 9 Feb 2026 17:38:10 -0500 Subject: [PATCH 175/199] fix the proximal grad problem, must implement proper vjp later --- desc/optimize/_constraint_wrappers.py | 30 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/desc/optimize/_constraint_wrappers.py b/desc/optimize/_constraint_wrappers.py index ecd465e1ac..cf64004277 100644 --- a/desc/optimize/_constraint_wrappers.py +++ b/desc/optimize/_constraint_wrappers.py @@ -1075,15 +1075,27 @@ def grad(self, x, constants=None): v = jnp.eye(x.shape[0]) constants = setdefault(constants, self.constants) xg, xf = self._update_equilibrium(x, store=True) - jvpfun = lambda u: self._get_tangent(u, xf, constants, op="scaled_error") - tangents = batched_vectorize( - jvpfun, - signature="(n)->(k)", - chunk_size=self._constraint._jac_chunk_size, - )(v) - g = self._objective.compute_scaled_error(xg, constants[0]) - g_vjp = self._objective.vjp_scaled_error(g, xg, constants[0]) - return tangents @ g_vjp + if not (self._constraint._is_mpi or self._objective._is_mpi): + jvpfun = lambda u: self._get_tangent(u, xf, constants, op="scaled_error") + tangents = batched_vectorize( + jvpfun, + signature="(n)->(k)", + chunk_size=self._constraint._jac_chunk_size, + )(v) + g = self._objective.compute_scaled_error(xg, constants[0]) + g_vjp = self._objective.vjp_scaled_error(g, xg, constants[0]) + return tangents @ g_vjp + elif self._constraint._is_mpi: + # TODO: implement parallel constraint for ProximalProjection + raise NotImplementedError( + "Parallel constraint for ProximalProjection not implemented yet. " + "Please use only one Equilibrium constraint." + ) + else: + # TODO: apply vjp for multidevice similar to #2030 + f = jnp.atleast_1d(self.compute_scaled_error(x, constants)) + J = self.jac_scaled_error(x, constants) + return f.T @ J def hess(self, x, constants=None): """Compute Hessian of self.compute_scalar. From 5c7a7496988ad299e313b97953c2013b589d72a8 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 9 Feb 2026 18:39:54 -0500 Subject: [PATCH 176/199] add some possible fix to memory issue, not pleasant yet --- desc/optimize/least_squares.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/desc/optimize/least_squares.py b/desc/optimize/least_squares.py index 2d4473600e..f8fefa7a17 100644 --- a/desc/optimize/least_squares.py +++ b/desc/optimize/least_squares.py @@ -2,7 +2,7 @@ from scipy.optimize import OptimizeResult -from desc.backend import jnp, qr +from desc.backend import execute_on_cpu, jax, jnp, qr from desc.utils import errorif, safediv, setdefault from .bound_utils import ( @@ -181,6 +181,19 @@ def lsqtr( # noqa: C901 # block is needed for jaxify util which uses jax functions inside # jax.pure_callback and gets stuck due to async dispatch J = jac(x, *args).block_until_ready() + if J.device == jax.devices("cpu")[0]: + f = jnp.asarray(f, device=jax.devices("cpu")[0]) + tr_cho = execute_on_cpu(trust_region_step_exact_cho) + tr_qr = execute_on_cpu(trust_region_step_exact_qr) + tr_svd = execute_on_cpu(trust_region_step_exact_svd) + select_step_device = execute_on_cpu(select_step) + compute_jac_scale_device = execute_on_cpu(compute_jac_scale) + else: + tr_cho = trust_region_step_exact_cho + tr_qr = trust_region_step_exact_qr + tr_svd = trust_region_step_exact_svd + select_step_device = select_step + compute_jac_scale_device = compute_jac_scale njev += 1 g = jnp.dot(J.T, f) @@ -191,7 +204,7 @@ def lsqtr( # noqa: C901 jac_scale = isinstance(x_scale, str) and x_scale in ["jac", "auto"] if jac_scale: - scale, scale_inv = compute_jac_scale(J) + scale, scale_inv = compute_jac_scale_device(J) else: x_scale = jnp.broadcast_to(x_scale, x.shape) scale, scale_inv = x_scale, 1 / x_scale @@ -322,20 +335,18 @@ def lsqtr( # noqa: C901 # and it tells us whether the proposed step # has reached the trust region boundary or not. if tr_method == "svd": - step_h, hits_boundary, alpha = trust_region_step_exact_svd( + step_h, hits_boundary, alpha = tr_svd( f_a, U, s, Vt.T, trust_radius, alpha ) elif tr_method == "cho": - step_h, hits_boundary, alpha = trust_region_step_exact_cho( - g_h, B_h, trust_radius, alpha - ) + step_h, hits_boundary, alpha = tr_cho(g_h, B_h, trust_radius, alpha) elif tr_method == "qr": - step_h, hits_boundary, alpha = trust_region_step_exact_qr( + step_h, hits_boundary, alpha = tr_qr( p_newton, f_a, J_a, trust_radius, alpha ) step = d * step_h # Trust-region solution in the original space. - step, step_h, predicted_reduction = select_step( + step, step_h, predicted_reduction = select_step_device( x, J_h, diag_h, @@ -405,11 +416,13 @@ def lsqtr( # noqa: C901 f = f_new cost = cost_new J = jac(x, *args) + if J.device == jax.devices("cpu")[0]: + f = jnp.asarray(f, device=jax.devices("cpu")[0]) njev += 1 g = jnp.dot(J.T, f) if jac_scale: - scale, scale_inv = compute_jac_scale(J, scale_inv) + scale, scale_inv = compute_jac_scale_device(J, scale_inv) v, dv = cl_scaling_vector(x, g, lb, ub) v = jnp.where(dv != 0, v * scale_inv, v) From d888d9dad70c994c485a6491aa5351fd36b7a00f Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 9 Feb 2026 18:52:28 -0500 Subject: [PATCH 177/199] ok, since J is commited to a device, computation should follow where J is, see jax discussion #34942 --- desc/optimize/least_squares.py | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/desc/optimize/least_squares.py b/desc/optimize/least_squares.py index f8fefa7a17..2d4473600e 100644 --- a/desc/optimize/least_squares.py +++ b/desc/optimize/least_squares.py @@ -2,7 +2,7 @@ from scipy.optimize import OptimizeResult -from desc.backend import execute_on_cpu, jax, jnp, qr +from desc.backend import jnp, qr from desc.utils import errorif, safediv, setdefault from .bound_utils import ( @@ -181,19 +181,6 @@ def lsqtr( # noqa: C901 # block is needed for jaxify util which uses jax functions inside # jax.pure_callback and gets stuck due to async dispatch J = jac(x, *args).block_until_ready() - if J.device == jax.devices("cpu")[0]: - f = jnp.asarray(f, device=jax.devices("cpu")[0]) - tr_cho = execute_on_cpu(trust_region_step_exact_cho) - tr_qr = execute_on_cpu(trust_region_step_exact_qr) - tr_svd = execute_on_cpu(trust_region_step_exact_svd) - select_step_device = execute_on_cpu(select_step) - compute_jac_scale_device = execute_on_cpu(compute_jac_scale) - else: - tr_cho = trust_region_step_exact_cho - tr_qr = trust_region_step_exact_qr - tr_svd = trust_region_step_exact_svd - select_step_device = select_step - compute_jac_scale_device = compute_jac_scale njev += 1 g = jnp.dot(J.T, f) @@ -204,7 +191,7 @@ def lsqtr( # noqa: C901 jac_scale = isinstance(x_scale, str) and x_scale in ["jac", "auto"] if jac_scale: - scale, scale_inv = compute_jac_scale_device(J) + scale, scale_inv = compute_jac_scale(J) else: x_scale = jnp.broadcast_to(x_scale, x.shape) scale, scale_inv = x_scale, 1 / x_scale @@ -335,18 +322,20 @@ def lsqtr( # noqa: C901 # and it tells us whether the proposed step # has reached the trust region boundary or not. if tr_method == "svd": - step_h, hits_boundary, alpha = tr_svd( + step_h, hits_boundary, alpha = trust_region_step_exact_svd( f_a, U, s, Vt.T, trust_radius, alpha ) elif tr_method == "cho": - step_h, hits_boundary, alpha = tr_cho(g_h, B_h, trust_radius, alpha) + step_h, hits_boundary, alpha = trust_region_step_exact_cho( + g_h, B_h, trust_radius, alpha + ) elif tr_method == "qr": - step_h, hits_boundary, alpha = tr_qr( + step_h, hits_boundary, alpha = trust_region_step_exact_qr( p_newton, f_a, J_a, trust_radius, alpha ) step = d * step_h # Trust-region solution in the original space. - step, step_h, predicted_reduction = select_step_device( + step, step_h, predicted_reduction = select_step( x, J_h, diag_h, @@ -416,13 +405,11 @@ def lsqtr( # noqa: C901 f = f_new cost = cost_new J = jac(x, *args) - if J.device == jax.devices("cpu")[0]: - f = jnp.asarray(f, device=jax.devices("cpu")[0]) njev += 1 g = jnp.dot(J.T, f) if jac_scale: - scale, scale_inv = compute_jac_scale_device(J, scale_inv) + scale, scale_inv = compute_jac_scale(J, scale_inv) v, dv = cl_scaling_vector(x, g, lb, ub) v = jnp.where(dv != 0, v * scale_inv, v) From 7969a96f29a129d59ae8af654af75a57a7b845ef Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 10 Feb 2026 11:01:56 -0500 Subject: [PATCH 178/199] reduce xtol, CI runner has a different processor which fails, cannot reproduce locally --- tests/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 4f27069fe2..35d36748c1 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -173,7 +173,7 @@ def test_solve_bounds(): obj = ObjectiveFunction( ForceBalance(normalize=False, normalize_target=False, bounds=(-1e3, 1e3), eq=eq) ) - eq.solve(objective=obj, ftol=1e-16, xtol=1e-16, maxiter=200, verbose=3) + eq.solve(objective=obj, ftol=1e-16, xtol=0, maxiter=200, verbose=3) # check that all errors are nearly 0, since residual values are within target bounds f = obj.compute_scaled_error(obj.x(eq)) From 1435648bb464ef0d72fbd38a2f0b2643ec3ecaa1 Mon Sep 17 00:00:00 2001 From: daniel-dudt Date: Fri, 10 Apr 2026 17:42:44 -0400 Subject: [PATCH 179/199] add device_id to PlasmaCoilSetMinDistance --- desc/objectives/_coils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index d99b16602c..33f5f4d385 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1407,6 +1407,7 @@ def __init__( use_softmin=False, softmin_alpha=1.0, dist_chunk_size=None, + device_id=0, ): if target is None and bounds is None: bounds = (1, np.inf) @@ -1430,6 +1431,7 @@ def __init__( use_softmin=use_softmin, softmin_alpha=softmin_alpha, dist_chunk_size=dist_chunk_size, + device_id=device_id, ) From f5a6a760c50b0badf26edce7d5fc1ec4f25c08e0 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 14 Apr 2026 16:12:52 -0400 Subject: [PATCH 180/199] resolve the todo, do not allow linear constraints to have different device, update changelog --- CHANGELOG.md | 11 +++++------ desc/objectives/_generic.py | 2 -- desc/objectives/objective_funs.py | 5 +++-- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 480f3cdced..583b09b288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Changelog New Features - Adds ``num_neighbors`` parameter to ``CoilSetMinDistance`` that limits the pairwise distance computation to the nearest neighbors per coil, reducing memory useage for large coilsets. +- Adds initial support for multi-device optimization with MPI. This allows to compute derivatives and costs on multiple devices (GPUs/CPUs), and to split memory usage during these operations across devices. See the [documentation](https://desc-docs.readthedocs.io/en/stable/notebooks/tutorials/multi_device.html) for details. Couple important notes: + - MPI is not a default dependency of DESC, so, to use MPI functionality, the users should verify their MPI installation themselves. + - Using MPI is recommended only for the cases where you get out-of-memory error. If your problem fits to single GPU memory, it's unlikely that MPI will give speed improvement. + - MPI is not implemented for matrix decompositions (i.e. QR/SVD/Cholesky) which default optimizer ``lsq-exact`` uses. For the cases where Jacobian doesn't fit to GPU memory, matrix decompositions will be performed on CPU and will be slow. Feel free to open a PR, if you have knowledge on parallel QR/SVD or Cholesky. + - CUDA-aware MPI is not supported yet. Bug Fixes @@ -84,11 +89,6 @@ New Features - Adds support for optimization targeting individual coils in a coilset. - Coil objectives accept pytree inputs for `target`, `bounds`, and `weight`. - Able to set weights to zero, excluding certain coils from the objective. -- Adds initial support for multi-device optimization with MPI. This allows to compute derivatives and costs on multiple devices (GPUs/CPUs), and to split memory usage during these operations across devices. See the [documentation](https://desc-docs.readthedocs.io/en/stable/notebooks/tutorials/multi_device.html) for details. Couple important notes: - - MPI is not a default dependency of DESC, so, to use MPI functionality, the users should verify their MPI installation themselves. - - Using MPI is recommended only for the cases where you get out-of-memory error. If your problem fits to single GPU memory, it's unlikely that MPI will give speed improvement. - - MPI is not implemented for matrix decompositions (i.e. QR/SVD/Cholesky) which default optimizer ``lsq-exact`` uses. For the cases where Jacobian doesn't fit to GPU memory, matrix decompositions will be performed on CPU and will be slow. Feel free to open a PR, if you have knowledge on parallel QR/SVD or Cholesky. - - CUDA-aware MPI is not supported yet. Bug Fixes @@ -116,7 +116,6 @@ Performance Improvements - Non-uniform FFTs (NUFFTS) are now used by default for computing bounce integrals, see #1834. NUFFT functionality is added through `jax-finufft` package. If the GPU installation fails, users can fall back to the older (much slower) implementation by setting `nufft_eps=0` in the computation of the related quantities. - v0.15.0 ------- diff --git a/desc/objectives/_generic.py b/desc/objectives/_generic.py index 0d6e5573db..f7879b6719 100644 --- a/desc/objectives/_generic.py +++ b/desc/objectives/_generic.py @@ -412,7 +412,6 @@ def __init__( normalize_target=False, name="custom linear", jac_chunk_size=None, - device_id=0, ): if target is None and bounds is None: target = 0 @@ -426,7 +425,6 @@ def __init__( normalize_target=normalize_target, name=name, jac_chunk_size=jac_chunk_size, - device_id=device_id, ) def build(self, use_jit=False, verbose=1): diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index e4104bf05e..9852cfea22 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1536,8 +1536,9 @@ def __init__( self._jac_chunk_size = jac_chunk_size self._device_id = device_id # if we have multiple GPU devices, this will help the data placement - # TODO: figure out why we cannot use it with constraints? Computation - # gets stuck! + # linear objectives have a separate ObjectiveFunction hence cannot make + # use of the "with" context manager of the main objective, they should run + # on the default device to avoid staling code. if ( desc_config["num_device"] != 1 and desc_config["kind"] == "gpu" From 7fa6b933151292de2e0d2c8346ac48513d68522f Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 14 Apr 2026 16:21:42 -0400 Subject: [PATCH 181/199] remove redundant changes --- desc/objectives/utils.py | 1 - tests/test_examples.py | 2 +- tests/test_optimizer.py | 4 +--- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 5222870ef0..5b8e07302f 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -143,7 +143,6 @@ def factorize_linear_constraints(objective, constraint, x_scale="auto"): # noqa xp = put(xp, unfixed_idx, A_inv @ b) xp = put(xp, fixed_idx, ((1 / D) * xp)[fixed_idx]) # cast to jnp arrays - # TODO: might consider sharding these xp = jnp.asarray(xp) A = jnp.asarray(A) b = jnp.asarray(b) diff --git a/tests/test_examples.py b/tests/test_examples.py index fce97952e9..3ba575cd66 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -173,7 +173,7 @@ def test_solve_bounds(): obj = ObjectiveFunction( ForceBalance(normalize=False, normalize_target=False, bounds=(-3e3, 3e3), eq=eq) ) - eq.solve(objective=obj, ftol=1e-16, xtol=0, maxiter=200, verbose=3) + eq.solve(objective=obj, ftol=1e-16, xtol=1e-16, maxiter=200, verbose=3) # check that all errors are nearly 0, since residual values are within target bounds f = obj.compute_scaled_error(obj.x(eq)) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index f763eafa83..ee4275a70f 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -485,10 +485,8 @@ def compute(self, params, constants=None): np.random.seed(0) objective = ObjectiveFunction(DummyObjective(things=eq), use_jit=False) - # we need to build before we declare a new method to properly unjit - # the objective methods, so that _static_attrs is set correctly - objective.build() # make gradient super noisy so it stalls + objective.build() objective.jac_scaled_error = lambda x, *args: objective.jac_scaled_error( x ) + 1e2 * (np.random.random((objective._dim_f, x.size)) - 0.5) From 179487c60a0ad464e8db0ec580a79cf7513d19ad Mon Sep 17 00:00:00 2001 From: YigitElma Date: Sun, 3 May 2026 17:39:22 -0400 Subject: [PATCH 182/199] formatting after merge conflict --- desc/objectives/objective_funs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index b5f92ba708..62d0e1f6c6 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -583,7 +583,6 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 Level of output. """ - use_jit_wrapper = self._use_jit if use_jit is not None: self._use_jit = use_jit @@ -598,12 +597,12 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 f"{[o.__class__.__name__ for o in self.objectives if not o._use_jit]}", ) self._use_jit = False - + device_ids = [obj._device_id for obj in self._objectives] is_multi_device = len(set(device_ids)) > 1 if self._is_mpi and is_multi_device: self._use_jit = False - + timer = Timer() timer.start("Objective build") From c09ff3c358d09956094a89a7eca23fd8ef0f5f72 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 4 May 2026 19:52:58 -0400 Subject: [PATCH 183/199] fix test --- tests/test_multidevice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index a28de6aab6..e20aef0d82 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -378,6 +378,11 @@ def test_multidevice_objective_build(): with pytest.warns(UserWarning, match="When using multiple devices"): obj1.build() + # reset objectives + for o in [objective1, objective2, objective3]: + o._built = False + o._use_jit = True + # this one is single device, and grids have different sizes obj2 = ObjectiveFunction([objective1, objective4]) obj2.build() From 23cee26e417dae4ec8338b72ad54548d525593db Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 4 May 2026 19:56:24 -0400 Subject: [PATCH 184/199] update pytest marks --- devtools/check_unmarked_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/check_unmarked_tests.py b/devtools/check_unmarked_tests.py index 150a67d2cb..019e6618fe 100644 --- a/devtools/check_unmarked_tests.py +++ b/devtools/check_unmarked_tests.py @@ -8,7 +8,7 @@ import ast import sys -REQUIRED_MARKS = {"unit", "regression", "benchmark", "memory"} +REQUIRED_MARKS = {"unit", "regression", "benchmark", "memory", "mpi_run", "mpi_setup"} def _pytest_marks(decorators): From 40b5e9ea10a7e8d5fd755da4dab430d194315681 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 4 May 2026 20:04:03 -0400 Subject: [PATCH 185/199] remove some unnecessary changes --- desc/objectives/objective_funs.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 1c37911fad..1e7f52f332 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -665,9 +665,7 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 Parameters ---------- use_jit : bool, optional - Whether to just-in-time compile the objective and derivatives. If using - multiple GPUs, instead of jitting the ObjectiveFunction, the sub-objectives - will be jitted individually. + Whether to just-in-time compile the objective and derivatives. verbose : int, optional Level of output. @@ -687,6 +685,7 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 ) self._use_jit = False + # under jit, we cannot use different devices, unjit to allow for that device_ids = [obj._device_id for obj in self._objectives] is_multi_device = len(set(device_ids)) > 1 if self._is_mpi and is_multi_device: @@ -775,9 +774,9 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 # Heuristic estimates of fwd mode Jacobian memory usage, # slightly conservative, based on using ForceBalance as the objective estimated_memory_usage = 2.4e-7 * self.dim_f * self.dim_x + 1 # in GB - mem_avail = desc_config["avail_mems"][0] # in GB + avail_mem = desc_config["avail_mems"][0] # in GB max_chunk_size = round( - (mem_avail / estimated_memory_usage - 0.22) / 0.85 * self.dim_x + (avail_mem / estimated_memory_usage - 0.22) / 0.85 * self.dim_x ) self._jac_chunk_size = max([1, max_chunk_size]) From 4c1d99e4dcf3239633e8efc56c46ec62b8ad60b0 Mon Sep 17 00:00:00 2001 From: daniel-dudt Date: Thu, 7 May 2026 15:05:11 -0400 Subject: [PATCH 186/199] clean up comments --- desc/objectives/objective_funs.py | 39 +++++++++---------- .../tutorials/mpi-tutorials/mpi-eq-solve.py | 2 +- .../tutorials/mpi-tutorials/mpi-proximal.py | 11 ++---- docs/notebooks/tutorials/multi_device.ipynb | 6 +-- tests/test_multidevice.py | 2 +- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 1e7f52f332..f785388901 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -426,9 +426,9 @@ def __init__( device_ids = [obj._device_id for obj in objectives] self._is_mpi = len(set(device_ids)) > 1 if mpi is not None: - # for multiple node cases, each process sees 1 CPU, for those cases, - # we cannot put objectives to different devices. Instead, we will - # run each objective on the given rank. + # for multiple node cases, each process sees 1 CPU + # for those cases we cannot put objectives on different devices + # instead we will run each objective on the given rank self._is_mpi = True self._rank_per_objective = ( rank_per_objective @@ -514,8 +514,7 @@ def __enter__(self): "ObjectiveFunction must be parallel to be used as a context manager.", ) # when entering the context manager, we start the worker loop - # this will allow the root rank to send messages to the workers - # to compute and to stop + # this allows the root rank to send messages to the workers to compute and stop self._worker_loop() return self @@ -524,8 +523,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): # we send a stop message to the workers if self.rank == 0: # only the root rank can send the stop message - # in general, the message contains 3 parts, but for the stop message - # we only need the first part + # in general the message contains 3 parts + # but for the stop message we only need the first part message = ("STOP", None, None) self.comm.bcast(message, root=0) self.running = False @@ -545,8 +544,8 @@ def _worker_loop(self): will then broadcast the results back to the root rank. Once the context manager exits, the loop will be terminated by the root rank. - This way, we can still use MPI parallelization with the ObjectiveFunction, but - prevent execution of redundant calculations multiple times on different ranks. + Therefore we can use MPI parallelization with the ObjectiveFunction while + preventing execution of redundant calculations on different ranks. This is very similar to the strategy used in Simsopt. """ @@ -560,7 +559,7 @@ def alloc_array(shape, device=None): return jnp.empty(shape, dtype=jnp.float64, device=device) while self.running: - # The message contains 3 parts, + # The message contains 3 parts: # message[0] is the operation to be performed # message[1] is the size of state vector (for compute and jvp's) # message[2] is the shape of tangents (for only jvp's) @@ -685,7 +684,7 @@ def build(self, use_jit=None, verbose=1): # noqa: C901 ) self._use_jit = False - # under jit, we cannot use different devices, unjit to allow for that + # cannot use different devices under jit, unjit to allow for that device_ids = [obj._device_id for obj in self._objectives] is_multi_device = len(set(device_ids)) > 1 if self._is_mpi and is_multi_device: @@ -1629,10 +1628,10 @@ def __init__( self._jac_chunk_size = jac_chunk_size self._device_id = device_id - # if we have multiple GPU devices, this will help the data placement - # linear objectives have a separate ObjectiveFunction hence cannot make - # use of the "with" context manager of the main objective, they should run - # on the default device to avoid staling code. + # This will help the data placement if we have multiple GPU devices. + # Linear objectives have a separate ObjectiveFunction, hence they cannot use the + # "with" context manager of the main objective. + # They should run on the default device to avoid staling code. if ( desc_config["num_device"] != 1 and desc_config["kind"] == "gpu" @@ -1640,8 +1639,7 @@ def __init__( ): self._device = jax.devices("gpu")[device_id] else: - # we won't transfer data for multiple CPUs because their rank should - # already have that data. + # multiple CPUs should already have the data on their rank self._device = None self._target = target @@ -2288,10 +2286,9 @@ def jvp_proximal_per_process(x, v, objectives, op): J_rank = [] for idx, obj in enumerate(objectives): if obj._deriv_mode == "rev": - # obj might not allow fwd mode, so compute full rev mode - # jacobian and do matmul manually. This is slightly - # inefficient, but usually when rev mode is used, - # dim_f <<< dim_x, so its not too bad. + # obj might not allow fwd mode, so compute full rev mode Jacobian and do + # matmul manually. This is slightly inefficient, but usually with rev mode + # dim_f << dim_x so it is not too bad. Ji = getattr(obj, "jac_" + op)(*x[idx]) J_rank.append( jnp.array([Jii @ vii.T for Jii, vii in zip(Ji, v[idx])]).sum(axis=0) diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py index 99b3398a0c..16f35ebb2b 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-eq-solve.py @@ -14,7 +14,7 @@ kind = "cpu" # or "gpu" num_device = 2 # ====== Using CPUs ====== -# These will be used for diving the single CPU into multiple virtual CPUs +# These will be used for dividing the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices if kind == "cpu": # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! diff --git a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py index 0d4b258095..bf50de5e38 100644 --- a/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py +++ b/docs/notebooks/tutorials/mpi-tutorials/mpi-proximal.py @@ -13,7 +13,7 @@ kind = "cpu" # or "gpu" num_device = 2 # ====== Using CPUs ====== -# These will be used for diving the single CPU into multiple virtual CPUs +# These will be used for dividing the single CPU into multiple virtual CPUs # such that JAX and XLA thinks there are multiple devices if kind == "cpu": # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!! @@ -114,17 +114,16 @@ objective.build(verbose=0) # we will fix some modes as usual - k = 1 R_modes = np.vstack( ( [0, 0, 0], eq.surface.R_basis.modes[ - np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : + np.max(np.abs(eq.surface.R_basis.modes), 1) > 1, : ], ) ) Z_modes = eq.surface.Z_basis.modes[ - np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : + np.max(np.abs(eq.surface.Z_basis.modes), 1) > 1, : ] constraints = ( ForceBalance(eq=eq), @@ -152,9 +151,7 @@ optimizer=optimizer, maxiter=3, verbose=3, - options={ - "initial_trust_ratio": 1.0, - }, + options={"initial_trust_ratio": 1.0}, ) # if you put a code here, it will be performed on all ranks diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 6a5ef16507..0d06097491 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -604,7 +604,7 @@ "\n", "**Note :** For more details, one can check Princeton University Research Computing page [here](https://researchcomputing.princeton.edu/support/knowledge-base/slurm#Multinode--Multithreaded-Jobs).\n", "\n", - "One needs to use proper slurm script to run parallel code on a cluster. Here, we will give an example in which we use 2 nodes, 8 processes per node and 4 CPU cores per process. *Node* means the actual CPU chip, so we will have 2 CPUs or you can think of it as, we will have 2 computers that are connected to each other. We will have 16 processes and 64 CPU cores in total. Additionally, you can specify number of GPUs per node." + "One needs to use proper slurm script to run parallel code on a cluster. Here, we will give an example in which we use 2 nodes, 8 processes per node and 4 CPU cores per process. *Node* means the actual CPU chip, so we will have 2 CPUs (you can think of it as having 2 computers that are connected to each other). We will have 16 processes and 64 CPU cores in total. Additionally, you can specify the number of GPUs per node." ] }, { @@ -614,7 +614,7 @@ "```bash\n", "\n", "#!/bin/bash\n", - "#SBATCH --job-name=mpi-example # create a short name for your job\n", + "#SBATCH --job-name=mpi-example # create a short name for your job\n", "#SBATCH --nodes=2 # node count\n", "#SBATCH --ntasks-per-node=8 # total number of tasks per node\n", "#SBATCH --cpus-per-task=4 # cpu-cores per task (>1 if multi-threaded tasks)\n", @@ -643,7 +643,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When using MPI with multiple nodes, each process will see 1 CPU (with multiple cores), and if you requested GPUs, only the GPUs connected to that CPU will be visible to your program. With this in mind, for example, if you want to use 2 nodes, and 3 GPUs per nodes with 3 processes per node, you can use 6 objectives in this way.\n", + "When using MPI with multiple nodes, each process will see 1 CPU (with multiple cores), and if you requested GPUs, only the GPUs connected to that CPU will be visible to your program. For example, if you want to use 2 nodes with 3 GPUs and 3 processes per node, you can use 6 objectives each on an independent device.\n", "\n", "```python\n", "\n", diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index e20aef0d82..91638c301f 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -127,9 +127,9 @@ def test_multidevice_compute(): ) obj2.build() - f1 = obj1.compute_scalar(obj1.x(eq)) with obj2: if rank == 0: + f1 = obj1.compute_scalar(obj1.x(eq)) f2 = obj2.compute_scalar(obj2.x(eq)) np.testing.assert_allclose(f2, f1, atol=1e-8) From 09e1991221eaa56fd44357a1979155783357b342 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 May 2026 13:10:06 -0400 Subject: [PATCH 187/199] remove indent --- .github/workflows/notebook_tests.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/notebook_tests.yml b/.github/workflows/notebook_tests.yml index 666fa89bb5..ee90f34b4b 100644 --- a/.github/workflows/notebook_tests.yml +++ b/.github/workflows/notebook_tests.yml @@ -16,13 +16,12 @@ concurrency: jobs: notebook_tests: - runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} strategy: matrix: - python-version: ['3.10'] + python-version: ["3.10"] group: [1, 2, 3] steps: @@ -45,7 +44,6 @@ jobs: id: check_changes run: echo "has_changes=${{ !contains(github.event.pull_request.labels.*.name, 'only-docs-comments') && steps.changes.outputs.has_changes }}" >> $GITHUB_ENV - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: @@ -92,9 +90,9 @@ jobs: source .venv-${{ env.version }}/bin/activate export PYTHONPATH=$(pwd) pytest -v --nbmake "./docs/notebooks" \ - --nbmake-timeout=2000 \ - --ignore=./docs/notebooks/zernike_eval.ipynb \ - --ignore=./docs/notebooks/tutorials/multi_device.ipynb \ - --splits 3 \ - --group ${{ matrix.group }} \ - --splitting-algorithm least_duration + --nbmake-timeout=2000 \ + --ignore=./docs/notebooks/zernike_eval.ipynb \ + --ignore=./docs/notebooks/tutorials/multi_device.ipynb \ + --splits 3 \ + --group ${{ matrix.group }} \ + --splitting-algorithm least_duration From 80d2d2954e19415dc7dddff3799c16c7c01ce7fb Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 May 2026 13:23:49 -0400 Subject: [PATCH 188/199] allow array-like inputs for rank_per_objective --- desc/objectives/objective_funs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index d90a134adb..9fb4490808 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -360,7 +360,7 @@ class ObjectiveFunction(IOAble): to manually choose a chunk_size if an OOM error is experienced in this case. mpi : MPI object, optional MPI communicator. Required when using multiple devices. - rank_per_objective : ndarray, optional + rank_per_objective : array-like of int, optional Specifies which rank each objective should run on. This will allow for multiple objectives to run on the same rank. By default, each objective will be assigned to different ranks. @@ -435,6 +435,7 @@ def __init__( if rank_per_objective is not None else np.arange(len(objectives)) ) + self._rank_per_objective = np.asarray(self._rank_per_objective) errorif( np.unique(self._rank_per_objective).size < desc_config["num_device"], ValueError, @@ -452,7 +453,6 @@ def __init__( f"ranks {self._rank_per_objective} and device ids {device_ids} are " "not compatible.", ) - # TODO: should this throw an Error? warnif( max(device_ids) != desc_config["num_device"] - 1, UserWarning, From 5b46eec9aa134da8099f8c7bf5f24c420169a7b1 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 May 2026 13:48:38 -0400 Subject: [PATCH 189/199] update tutorial --- docs/notebooks/tutorials/multi_device.ipynb | 251 +++++++++++++------- 1 file changed, 159 insertions(+), 92 deletions(-) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 0d06097491..20155c2720 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -57,7 +57,7 @@ "kind = \"cpu\" # or \"gpu\"\n", "num_device = 2\n", "# ====== Using CPUs ======\n", - "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# These will be used for dividing the single CPU into multiple virtual CPUs\n", "# such that JAX and XLA thinks there are multiple devices\n", "if kind == \"cpu\":\n", " # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", @@ -190,9 +190,9 @@ "Rank 0 can see [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.15.0+189.g6c33270c1.dirty.\n", - "Using JAX backend: jax version=0.6.2, jaxlib version=0.6.2, dtype=float64.\n", - "Using 2 CPUs with 19.06 GB total available memory:\n", + "DESC version=0.17.1+264.g80d2d2954.\n", + "Using JAX backend: jax version=0.9.2, jaxlib version=0.9.2, dtype=float64.\n", + "Using 2 CPUs with 18.31 GB total available memory:\n", "\t CPU : 0 13th Gen Intel(R) Core(TM) i9-13900HX\n", "\t CPU : 1 13th Gen Intel(R) Core(TM) i9-13900HX\n", "\n", @@ -225,49 +225,64 @@ "Building objective: lambda gauge\n", "Building objective: axis R self consistency\n", "Building objective: axis Z self consistency\n", - "\u001b[32mTimer: Objective build = 768 ms\u001b[0m\n", - "\u001b[32mTimer: LinearConstraintProjection build = 2.16 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 842 ms\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 2.52 sec\u001b[0m\n", "Number of parameters: 551\n", "Number of objectives: 8424\n", - "\u001b[32mTimer: Initializing the optimization = 2.96 sec\u001b[0m\n", + "\u001b[32mTimer: Initializing the optimization = 3.39 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: lsq-exact\n", + "Solver options:\n", + "------------------------------------------------------------\n", + "Maximum Function Evaluations : 51\n", + "Maximum Allowed Total Δx Norm : inf\n", + "Scaled Termination : True\n", + "Trust Region Method : qr\n", + "Initial Trust Radius : 5.583e+03\n", + "Maximum Trust Radius : inf\n", + "Minimum Trust Radius : 2.220e-16\n", + "Trust Radius Increase Ratio : 2.000e+00\n", + "Trust Radius Decrease Ratio : 2.500e-01\n", + "Trust Radius Increase Threshold : 7.500e-01\n", + "Trust Radius Decrease Threshold : 2.500e-01\n", + "------------------------------------------------------------ \n", + "\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 8.421e-01 6.217e-01 \n", - " 1 2 2.599e-01 5.822e-01 3.670e-01 2.179e-01 \n", - " 2 3 1.183e-01 1.416e-01 2.557e-01 2.515e-01 \n", - " 3 4 5.716e-02 6.110e-02 4.079e-01 1.491e-01 \n", - " 4 5 3.971e-02 1.745e-02 3.098e-01 1.289e-01 \n", - " 5 7 2.335e-02 1.636e-02 8.984e-02 1.444e-01 \n", - " 6 9 1.465e-02 8.698e-03 1.920e-02 1.263e-01 \n", - " 7 10 1.058e-02 4.076e-03 2.100e-02 9.828e-02 \n", - " 8 12 7.898e-04 9.785e-03 1.084e-02 1.536e-02 \n", - " 9 13 5.671e-04 2.227e-04 1.678e-02 4.195e-03 \n", - " 10 16 5.368e-04 3.027e-05 3.373e-03 1.381e-03 \n", + " 0 1 1.540e+00 1.132e+00 \n", + " 1 2 4.303e-01 1.110e+00 4.473e-01 3.546e-01 \n", + " 2 3 9.374e-02 3.365e-01 2.949e-01 1.240e-01 \n", + " 3 5 2.716e-02 6.658e-02 1.387e-01 1.557e-01 \n", + " 4 7 7.589e-03 1.957e-02 6.497e-02 7.389e-02 \n", + " 5 8 1.072e-03 6.517e-03 3.394e-02 2.471e-02 \n", + " 6 11 6.304e-04 4.417e-04 6.563e-03 5.988e-03 \n", + " 7 12 6.040e-04 2.638e-05 6.114e-03 6.742e-04 \n", + " 8 14 6.034e-04 6.960e-07 3.717e-03 1.056e-03 \n", + " 9 15 6.024e-04 9.979e-07 3.879e-03 1.004e-03 \n", + " 10 17 6.010e-04 1.326e-06 1.098e-03 2.785e-04 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 5.368e-04\n", - " Total delta_x: 8.143e-01\n", + " Current function value: 6.010e-04\n", + " Total delta_x: 3.276e-01\n", " Iterations: 10\n", - " Function evaluations: 16\n", + " Function evaluations: 17\n", " Jacobian evaluations: 11\n", - "\u001b[32mTimer: Solution time = 23.3 sec\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 2.11 sec\u001b[0m\n", + "\u001b[32mTimer: Solution time = 22.9 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 2.08 sec\u001b[0m\n", "==============================================================================================================\n", " Start --> End\n", - "Total (sum of squares): 8.421e-01 --> 5.368e-04, \n", - "Maximum absolute Force error: 2.169e+05 --> 8.279e+03 (N)\n", - "Minimum absolute Force error: 1.091e-10 --> 1.310e-10 (N)\n", - "Average absolute Force error: 4.139e+04 --> 1.043e+03 (N)\n", - "Maximum absolute Force error: 1.744e-02 --> 6.659e-04 (normalized)\n", - "Minimum absolute Force error: 8.774e-18 --> 1.054e-17 (normalized)\n", - "Average absolute Force error: 3.329e-03 --> 8.390e-05 (normalized)\n", - "Maximum absolute Force error: 1.149e+07 --> 2.085e+05 (N)\n", - "Minimum absolute Force error: 2.439e-12 --> 3.449e-12 (N)\n", - "Average absolute Force error: 1.017e+05 --> 3.559e+03 (N)\n", - "Maximum absolute Force error: 9.238e-01 --> 1.677e-02 (normalized)\n", - "Minimum absolute Force error: 1.962e-19 --> 2.774e-19 (normalized)\n", - "Average absolute Force error: 8.181e-03 --> 2.862e-04 (normalized)\n", + "Total (sum of squares): 1.540e+00 --> 6.010e-04, \n", + "Maximum absolute Force error: 2.530e+05 --> 9.751e+03 (N)\n", + "Minimum absolute Force error: 1.089e-10 --> 1.311e-10 (N)\n", + "Average absolute Force error: 5.001e+04 --> 1.075e+03 (N)\n", + "Maximum absolute Force error: 2.035e-02 --> 7.842e-04 (normalized)\n", + "Minimum absolute Force error: 8.759e-18 --> 1.054e-17 (normalized)\n", + "Average absolute Force error: 4.022e-03 --> 8.648e-05 (normalized)\n", + "Maximum absolute Force error: 1.231e+07 --> 2.136e+05 (N)\n", + "Minimum absolute Force error: 2.182e-12 --> 3.323e-14 (N)\n", + "Average absolute Force error: 1.467e+05 --> 3.861e+03 (N)\n", + "Maximum absolute Force error: 9.903e-01 --> 1.718e-02 (normalized)\n", + "Minimum absolute Force error: 1.755e-19 --> 2.672e-21 (normalized)\n", + "Average absolute Force error: 1.180e-02 --> 3.105e-04 (normalized)\n", "R boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Z boundary error: 0.000e+00 --> 0.000e+00 (m)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", @@ -319,7 +334,7 @@ "kind = \"cpu\" # or \"gpu\"\n", "num_device = 2\n", "# ====== Using CPUs ======\n", - "# These will be used for diving the single CPU into multiple virtual CPUs\n", + "# These will be used for dividing the single CPU into multiple virtual CPUs\n", "# such that JAX and XLA thinks there are multiple devices\n", "if kind == \"cpu\":\n", " # !!! If you have multiple CPUs, you shouldn't call `_set_cpu_count` !!!\n", @@ -420,17 +435,16 @@ " objective.build(verbose=0)\n", "\n", " # we will fix some modes as usual\n", - " k = 1\n", " R_modes = np.vstack(\n", " (\n", " [0, 0, 0],\n", " eq.surface.R_basis.modes[\n", - " np.max(np.abs(eq.surface.R_basis.modes), 1) > k, :\n", + " np.max(np.abs(eq.surface.R_basis.modes), 1) > 1, :\n", " ],\n", " )\n", " )\n", " Z_modes = eq.surface.Z_basis.modes[\n", - " np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, :\n", + " np.max(np.abs(eq.surface.Z_basis.modes), 1) > 1, :\n", " ]\n", " constraints = (\n", " ForceBalance(eq=eq),\n", @@ -458,9 +472,7 @@ " optimizer=optimizer,\n", " maxiter=3,\n", " verbose=3,\n", - " options={\n", - " \"initial_trust_ratio\": 1.0,\n", - " },\n", + " options={\"initial_trust_ratio\": 1.0},\n", " )\n", "\n", " # if you put a code here, it will be performed on all ranks\n", @@ -499,9 +511,9 @@ "Rank 0 is running on [CpuDevice(id=0), CpuDevice(id=1)]\n", "\n", "====== BACKEND INFO ======\n", - "DESC version=0.15.0+189.g6c33270c1.dirty.\n", - "Using JAX backend: jax version=0.6.2, jaxlib version=0.6.2, dtype=float64.\n", - "Using 2 CPUs with 19.37 GB total available memory:\n", + "DESC version=0.17.1+264.g80d2d2954.\n", + "Using JAX backend: jax version=0.9.2, jaxlib version=0.9.2, dtype=float64.\n", + "Using 2 CPUs with 18.32 GB total available memory:\n", "\t CPU : 0 13th Gen Intel(R) Core(TM) i9-13900HX\n", "\t CPU : 1 13th Gen Intel(R) Core(TM) i9-13900HX\n", "\n", @@ -510,76 +522,91 @@ "\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 771 ms\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 990 ms\u001b[0m\n", "Building objective: QS two-term\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 656 ms\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 955 ms\u001b[0m\n", "Putting objective QS two-term on device 1\n", "Building objective: aspect ratio\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 636 ms\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 919 ms\u001b[0m\n", "------------------------------------------------------------\n", "Rank 0 will run objective(s): ['QuasisymmetryTwoTerm', 'AspectRatio']\n", "Rank 1 will run objective(s): ['QuasisymmetryTwoTerm']\n", "------------------------------------------------------------\n", - "\u001b[32mTimer: Objective build = 2.52 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 3.67 sec\u001b[0m\n", "Building objective: force\n", "Precomputing transforms\n", - "\u001b[32mTimer: Precomputing transforms = 886 ms\u001b[0m\n", - "\u001b[32mTimer: Objective build = 931 ms\u001b[0m\n", - "\u001b[32mTimer: Objective build = 1.11 ms\u001b[0m\n", - "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 2.18 sec\u001b[0m\n", - "\u001b[32mTimer: Proximal projection build = 4.57 sec\u001b[0m\n", + "\u001b[32mTimer: Precomputing transforms = 1.28 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 1.33 sec\u001b[0m\n", + "\u001b[32mTimer: Objective build = 999 us\u001b[0m\n", + "\u001b[32mTimer: Eq Update LinearConstraintProjection build = 2.58 sec\u001b[0m\n", + "\u001b[32mTimer: Proximal projection build = 13.1 sec\u001b[0m\n", "Building objective: lcfs R\n", "Building objective: lcfs Z\n", "Building objective: fixed pressure\n", "Building objective: fixed Psi\n", "Building objective: fixed current\n", - "\u001b[32mTimer: Objective build = 433 ms\u001b[0m\n", - "\u001b[32mTimer: LinearConstraintProjection build = 969 ms\u001b[0m\n", + "\u001b[32mTimer: Objective build = 666 ms\u001b[0m\n", + "\u001b[32mTimer: LinearConstraintProjection build = 1.20 sec\u001b[0m\n", "Number of parameters: 8\n", "Number of objectives: 631\n", - "\u001b[32mTimer: Initializing the optimization = 6.01 sec\u001b[0m\n", + "\u001b[32mTimer: Initializing the optimization = 15.0 sec\u001b[0m\n", "\n", "Starting optimization\n", "Using method: proximal-lsq-exact\n", + "Solver options:\n", + "------------------------------------------------------------\n", + "Maximum Function Evaluations : 16\n", + "Maximum Allowed Total Δx Norm : inf\n", + "Scaled Termination : True\n", + "Trust Region Method : qr\n", + "Initial Trust Radius : 6.219e+02\n", + "Maximum Trust Radius : inf\n", + "Minimum Trust Radius : 2.220e-16\n", + "Trust Radius Increase Ratio : 2.000e+00\n", + "Trust Radius Decrease Ratio : 2.500e-01\n", + "Trust Radius Increase Threshold : 7.500e-01\n", + "Trust Radius Decrease Threshold : 2.500e-01\n", + "------------------------------------------------------------ \n", + "\n", " Iteration Total nfev Cost Cost reduction Step norm Optimality \n", - " 0 1 2.005e+04 1.926e+02 \n", - " 1 4 8.123e+03 1.193e+04 4.964e-02 9.847e+01 \n", - " 2 5 2.617e+03 5.507e+03 5.877e-02 6.065e+01 \n", - " 3 7 7.564e+02 1.860e+03 7.212e-02 3.935e+00 \n", + " 0 1 2.001e+04 1.870e+02 \n", + " 1 4 8.742e+03 1.126e+04 3.689e-02 8.655e+01 \n", + " 2 5 3.984e+03 4.758e+03 6.537e-02 6.133e+01 \n", + " 3 6 2.205e+03 1.779e+03 8.708e-02 1.535e+01 \n", "Warning: Maximum number of iterations has been exceeded.\n", - " Current function value: 7.564e+02\n", - " Total delta_x: 7.271e-02\n", + " Current function value: 2.205e+03\n", + " Total delta_x: 1.233e-01\n", " Iterations: 3\n", - " Function evaluations: 7\n", + " Function evaluations: 6\n", " Jacobian evaluations: 4\n", - "\u001b[32mTimer: Solution time = 31.5 sec\u001b[0m\n", - "\u001b[32mTimer: Avg time per step = 7.89 sec\u001b[0m\n", + "\u001b[32mTimer: Solution time = 37.0 sec\u001b[0m\n", + "\u001b[32mTimer: Avg time per step = 9.25 sec\u001b[0m\n", "==============================================================================================================\n", " Start --> End\n", - "Total (sum of squares): 2.005e+04 --> 7.564e+02, \n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.038e-01 --> 1.333e+00 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.569e-04 --> 2.875e-04 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.039e-01 --> 2.474e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 4.406e-01 --> 1.455e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.803e-04 --> 3.137e-04 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.134e-01 --> 2.699e-01 (normalized)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 9.615e-01 --> 2.043e+00 (T^3)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 3.670e-04 --> 1.044e-02 (T^3)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.474e-01 --> 3.819e-01 (T^3)\n", - "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.049e+00 --> 2.229e+00 (normalized)\n", - "Minimum absolute Quasi-symmetry (1,2) two-term error: 4.004e-04 --> 1.139e-02 (normalized)\n", - "Average absolute Quasi-symmetry (1,2) two-term error: 1.609e-01 --> 4.167e-01 (normalized)\n", - "Aspect ratio: 6.002e+00 --> 7.856e+00 (dimensionless)\n", - "Maximum absolute Force error: 1.435e+05 --> 2.352e+04 (N)\n", - "Minimum absolute Force error: 1.480e+00 --> 6.889e+00 (N)\n", - "Average absolute Force error: 7.215e+03 --> 2.171e+03 (N)\n", - "Maximum absolute Force error: 1.026e-01 --> 1.681e-02 (normalized)\n", - "Minimum absolute Force error: 1.058e-06 --> 4.925e-06 (normalized)\n", - "Average absolute Force error: 5.157e-03 --> 1.552e-03 (normalized)\n", - "R boundary error: 0.000e+00 --> 4.600e-19 (m)\n", - "Z boundary error: 0.000e+00 --> 3.469e-18 (m)\n", + "Total (sum of squares): 2.001e+04 --> 2.205e+03, \n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.910e-01 --> 9.784e-01 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 2.766e-07 --> 1.949e-03 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 6.521e-02 --> 2.375e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 3.429e-01 --> 1.757e+00 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 4.966e-07 --> 3.500e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 1.171e-01 --> 4.265e-01 (normalized)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 1.425e+01 --> 7.592e+00 (T^3)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 1.678e-03 --> 8.196e-04 (T^3)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 2.460e-01 --> 3.879e-01 (T^3)\n", + "Maximum absolute Quasi-symmetry (1,2) two-term error: 2.558e+01 --> 1.363e+01 (normalized)\n", + "Minimum absolute Quasi-symmetry (1,2) two-term error: 3.012e-03 --> 1.472e-03 (normalized)\n", + "Average absolute Quasi-symmetry (1,2) two-term error: 4.417e-01 --> 6.965e-01 (normalized)\n", + "Aspect ratio: 8.000e+00 --> 8.000e+00 (dimensionless)\n", + "Maximum absolute Force error: 5.997e+05 --> 2.364e+04 (N)\n", + "Minimum absolute Force error: 3.370e+00 --> 5.342e+00 (N)\n", + "Average absolute Force error: 1.335e+04 --> 2.503e+03 (N)\n", + "Maximum absolute Force error: 5.499e-01 --> 2.168e-02 (normalized)\n", + "Minimum absolute Force error: 3.090e-06 --> 4.898e-06 (normalized)\n", + "Average absolute Force error: 1.224e-02 --> 2.295e-03 (normalized)\n", + "R boundary error: 5.081e-18 --> 3.981e-18 (m)\n", + "Z boundary error: 2.877e-18 --> 2.453e-18 (m)\n", "Fixed pressure profile error: 0.000e+00 --> 0.000e+00 (Pa)\n", "Fixed Psi error: 0.000e+00 --> 0.000e+00 (Wb)\n", "Fixed current profile error: 0.000e+00 --> 0.000e+00 (A)\n", @@ -643,7 +670,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When using MPI with multiple nodes, each process will see 1 CPU (with multiple cores), and if you requested GPUs, only the GPUs connected to that CPU will be visible to your program. For example, if you want to use 2 nodes with 3 GPUs and 3 processes per node, you can use 6 objectives each on an independent device.\n", + "When using MPI with multiple nodes, each process will see 1 CPU (with multiple cores), and if you requested GPUs, only the GPUs connected to that CPU will be visible to your program. For example, if you want to use 2 nodes with 3 GPUs and 3 processes per node, you can have 6 objectives each on an independent device.\n", "\n", "```python\n", "\n", @@ -677,9 +704,49 @@ "\n", "When you write your script for multiple nodes, the number of devices and the device IDs must be selected as if there is only 1 node and only the local GPUs are visible. Other nodes will be used through `rank` of MPI communicator.\n", "\n", - "**Note: Most clusters have multiple GPUs connected to each node, so before using multiple nodes, use all the GPUs available to that node. Multi-node communication is significantly slower and your script will be easier to write properly.**\n", + "**Note: Most clusters have multiple GPUs connected to each node, so before using multiple nodes, use all the GPUs available to that node. Multi-node communication is slower and your script will be easier to write properly.**\n", + "\n", + "Note: You should have at least 6 objectives, so at least 1 objective per device. If you want to run multiple objectives on the same device, you can specify the ``rank_per_objective`` in the `ObjectiveFunction` keywords. By default, the initializer will assign different ranks for each sub-objective. For example,\n", + "\n", + "```python\n", + "\n", + "# each node will see 3 GPUs\n", + "num_device = 3\n", + "os.environ[\"XLA_PYTHON_CLIENT_ALLOCATOR\"] = \"platform\"\n", + "set_device(\"gpu\", num_device=num_device)\n", + "\n", + "\n", + "...\n", + "\n", + "\n", + "# this will run on node 1, GPU 0 (rank=0)\n", + "obj1 = SomeObjective(..., device_id=0)\n", + "# this will run on node 1, GPU 1 (rank=1)\n", + "obj2 = SomeObjective(..., device_id=1)\n", + "# this will run on node 1, GPU 1 (rank=1)\n", + "obj3 = SomeObjective(..., device_id=1)\n", + "# this will run on node 1, GPU 2 (rank=2)\n", + "obj4 = SomeObjective(..., device_id=2)\n", + "# this will run on node 2, GPU 0 (rank=3)\n", + "obj5 = SomeObjective(..., device_id=0)\n", + "# this will run on node 2, GPU 0 (rank=3)\n", + "obj6 = SomeObjective(..., device_id=0)\n", + "# this will run on node 2, GPU 1 (rank=4)\n", + "obj7 = SomeObjective(..., device_id=1)\n", + "# this will run on node 2, GPU 2 (rank=5)\n", + "obj8 = SomeObjective(..., device_id=2)\n", + "# this will run on node 2, GPU 2 (rank=5)\n", + "obj9 = SomeObjective(..., device_id=2)\n", + "objs = [obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9]\n", + "\n", + "objective = ObjectiveFunction(\n", + " objs, \n", + " deriv_mode=\"blocked\", \n", + " mpi=MPI, \n", + " rank_per_objective=[0, 1, 1, 2, 3, 3, 4, 5, 5]\n", + ")\n", "\n", - "Note: You should have at least 6 objectives, so at least 1 objective per device. If you want to run multiple objectives on the same device, you can specify the ``rank_per_objective`` in the `ObjectiveFunction` keywords. By default, the initializer will assign different ranks for each sub-objective." + "```\n" ] } ], From 85e47f074ada669550990725a98e165a30516ed6 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 May 2026 14:18:34 -0400 Subject: [PATCH 190/199] update the test to compare actual optimization results --- tests/test_multidevice.py | 65 +++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 91638c301f..b7dde05170 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -448,15 +448,62 @@ def test_multidevice_eq_optimize(): eq = get("precise_QA") eq.change_resolution(M=3, N=2, M_grid=6, N_grid=4) + eq_no_mpi = eq.copy() # create two grids with different rho values, this will effectively separate # the quasisymmetry objective into two parts - gM = eq.M_grid - gN = eq.N_grid + gM = 2 + gN = 2 grid1 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.2], sym=True) grid2 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.6, 0.8], sym=True) grid3 = LinearGrid(M=gM, N=gN, NFP=eq.NFP, rho=[0.9], sym=True) + # we will fix some modes as usual + k = 1 + sRm = eq.surface.R_basis.modes + sZm = eq.surface.Z_basis.modes + R_modes = np.vstack(([0, 0, 0], sRm[np.max(np.abs(sRm), 1) > k, :])) + Z_modes = sZm[np.max(np.abs(sZm), 1) > k, :] + + verbose = 3 if rank == 0 else 0 + maxiter = 3 + + ### Single device optimization + + # when using parallel objectives, the user needs to supply the device_id + obj1 = QuasisymmetryTwoTerm(eq=eq_no_mpi, helicity=(1, eq.NFP), grid=grid1) + obj2 = QuasisymmetryTwoTerm(eq=eq_no_mpi, helicity=(1, eq.NFP), grid=grid2) + obj3 = QuasisymmetryTwoTerm(eq=eq_no_mpi, helicity=(1, eq.NFP), grid=grid3) + obj4 = AspectRatio(eq=eq_no_mpi, target=8, weight=100) + objs = [obj1, obj2, obj3, obj4] + + objective = ObjectiveFunction(objs, deriv_mode="blocked") + objective.build(verbose=verbose) + + constraints = ( + ForceBalance(eq=eq_no_mpi), + FixBoundaryR(eq=eq_no_mpi, modes=R_modes), + FixBoundaryZ(eq=eq_no_mpi, modes=Z_modes), + FixPressure(eq=eq_no_mpi), + FixPsi(eq=eq_no_mpi), + FixCurrent(eq=eq_no_mpi), + ) + optimizer = Optimizer("proximal-lsq-exact") + eq_no_mpi.optimize( + objective=objective, + constraints=constraints, + optimizer=optimizer, + maxiter=maxiter, + verbose=verbose, + ) + x1_no_mpi = objective.x(eq_no_mpi) + f1_no_mpi = objective.compute_scalar(x1_no_mpi) + + ### Multidevice optimization + + # Wait for everyone to finish their work before proceeding + MPI.COMM_WORLD.Barrier() + # when using parallel objectives, the user needs to supply the device_id obj1 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid1, device_id=0) obj2 = QuasisymmetryTwoTerm(eq=eq, helicity=(1, eq.NFP), grid=grid2, device_id=1) @@ -469,12 +516,6 @@ def test_multidevice_eq_optimize(): ) objective.build() - # we will fix some modes as usual - k = 1 - sRm = eq.surface.R_basis.modes - sZm = eq.surface.Z_basis.modes - R_modes = np.vstack(([0, 0, 0], sRm[np.max(np.abs(sRm), 1) > k, :])) - Z_modes = sZm[np.max(np.abs(sZm), 1) > k, :] constraints = ( ForceBalance(eq=eq), FixBoundaryR(eq=eq, modes=R_modes), @@ -492,8 +533,12 @@ def test_multidevice_eq_optimize(): objective=objective, constraints=constraints, optimizer=optimizer, - maxiter=1, + maxiter=maxiter, verbose=3, ) - f1 = objective.compute_scalar(objective.x(eq)) + x1 = objective.x(eq) + f1 = objective.compute_scalar(x1) assert f1 < f0 + + np.testing.assert_allclose(x1_no_mpi, x1, atol=1e-8, rtol=1e-8) + np.testing.assert_allclose(f1_no_mpi, f1, atol=1e-8, rtol=1e-8) From 396a736abb1bb3d84676eee7c034f096c59eb12c Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 May 2026 14:23:21 -0400 Subject: [PATCH 191/199] update the tutorial --- docs/notebooks/tutorials/multi_device.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index 20155c2720..b16b193861 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -706,7 +706,7 @@ "\n", "**Note: Most clusters have multiple GPUs connected to each node, so before using multiple nodes, use all the GPUs available to that node. Multi-node communication is slower and your script will be easier to write properly.**\n", "\n", - "Note: You should have at least 6 objectives, so at least 1 objective per device. If you want to run multiple objectives on the same device, you can specify the ``rank_per_objective`` in the `ObjectiveFunction` keywords. By default, the initializer will assign different ranks for each sub-objective. For example,\n", + "Note: You should have at least 6 objectives, so at least 1 objective per device. If you want to run multiple objectives on the same device, you can specify the ``rank_per_objective`` in the `ObjectiveFunction` keywords. By default, the initializer will assign different ranks for each sub-objective, in above example it defaults to `np.arange(len(objs))`. An example optimization setup where each rank has 1 or more objectives,\n", "\n", "```python\n", "\n", From 321ee3be30fa428bf73c5524a456a948b1fc664d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 19 May 2026 14:36:01 -0400 Subject: [PATCH 192/199] update docstring for set_device --- desc/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/desc/__init__.py b/desc/__init__.py index cca20aeb5e..8f32db4aee 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -147,6 +147,12 @@ def set_device(kind="cpu", gpuid=None, num_device=1, mpi=None): # noqa: C901 GPU id to use. Default is None. Supported only when num_device is 1. num_device : int number of devices to use. Default is 1. + mpi : MPI object, optional + MPI communicator. Used to get distinct CPU information for multi-node + jobs where each rank runs on different node. Communicator is not used + if the backend is ``'gpu'``. Supplying communicator doesn't + change the computations, it can only change the output of + ``desc.backend.print_backend_info()``. """ config["kind"] = kind From bb7f57b3298d25d06fb2e5eee0d5b9b557ad28bb Mon Sep 17 00:00:00 2001 From: Yigit Gunsur Elmacioglu <102380275+YigitElma@users.noreply.github.com> Date: Thu, 21 May 2026 15:37:33 -0400 Subject: [PATCH 193/199] Update desc/objectives/objective_funs.py --- desc/objectives/objective_funs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 563cbfd6cb..c21b563ac2 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1952,7 +1952,7 @@ def jac_unscaled(self, *args, **kwargs): def _jvp(self, v, x, constants=None, op="scaled"): if constants is None: - constants = self.constants + constants = self._get_deprecated_constants(constants) v = ensure_tuple(v) x = ensure_tuple(x) assert len(x) == len(v) From edabdf178eac102b382045933ae322ad8052f29d Mon Sep 17 00:00:00 2001 From: Yigit Gunsur Elmacioglu <102380275+YigitElma@users.noreply.github.com> Date: Thu, 21 May 2026 15:37:41 -0400 Subject: [PATCH 194/199] Update desc/objectives/objective_funs.py --- desc/objectives/objective_funs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index c21b563ac2..b11abbbc9d 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -2431,7 +2431,7 @@ def jvp_proximal_per_process(x, v, objectives, op): else: J_rank.append( getattr(obj, "jvp_" + op)( - [_vi for _vi in v[idx]], x[idx], constants=obj.constants + [_vi for _vi in v[idx]], x[idx], constants=None ).T ) return jnp.vstack(J_rank) From 42987a150f965846d15f77b4c957def884a0e671 Mon Sep 17 00:00:00 2001 From: Yigit Gunsur Elmacioglu <102380275+YigitElma@users.noreply.github.com> Date: Thu, 21 May 2026 15:37:49 -0400 Subject: [PATCH 195/199] Update desc/objectives/objective_funs.py --- desc/objectives/objective_funs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index b11abbbc9d..2d4de3d021 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -2409,7 +2409,7 @@ def jvp_per_process(x, v, objectives, op): """Compute the Jacobian-vector product on each process.""" return jnp.hstack( [ - getattr(obj, op)(v[idx], x[idx], constants=obj.constants) + getattr(obj, op)(v[idx], x[idx], constants=None) for idx, obj in enumerate(objectives) ] ) From 9abc3c709d6a082f3ad02ac758ba72add354c8ab Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 May 2026 13:28:37 +0300 Subject: [PATCH 196/199] fix constants issue --- desc/objectives/objective_funs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 2d4de3d021..1cd27e897a 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1951,8 +1951,6 @@ def jac_unscaled(self, *args, **kwargs): )(*args, **kwargs) def _jvp(self, v, x, constants=None, op="scaled"): - if constants is None: - constants = self._get_deprecated_constants(constants) v = ensure_tuple(v) x = ensure_tuple(x) assert len(x) == len(v) @@ -2398,7 +2396,7 @@ def compute_per_process(params, objectives, op): """Compute the objective function on each process.""" return jnp.concatenate( [ - getattr(obj, op)(*param, constants=obj.constants) + getattr(obj, op)(*param, constants=None) for (obj, param) in zip(objectives, params) ] ) From b928d91fb1667ccec5e1398aaf57463f053421de Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 25 May 2026 16:26:20 +0300 Subject: [PATCH 197/199] clean-up multidevice set_device and print backend, update some error messages --- desc/__init__.py | 71 +++++++++++++------------------ desc/backend.py | 42 +++++++++--------- desc/objectives/objective_funs.py | 27 ++++++++---- tests/test_multidevice.py | 2 +- 4 files changed, 70 insertions(+), 72 deletions(-) diff --git a/desc/__init__.py b/desc/__init__.py index 8f32db4aee..48f012b474 100644 --- a/desc/__init__.py +++ b/desc/__init__.py @@ -129,10 +129,9 @@ def set_device(kind="cpu", gpuid=None, num_device=1, mpi=None): # noqa: C901 """Sets the device to use for computation. If kind==``'gpu'`` and a gpuid is specified, uses the specified GPU. If - gpuid==``None`` or a wrong GPU id is given, checks available GPUs and selects the - one with the most available memory. - Respects environment variable CUDA_VISIBLE_DEVICES for selecting from multiple - available GPUs. + gpuid==``None`` or a wrong GPU id is given, checks available GPUs and selects + the one with the most available memory. Respects environment variable + `CUDA_VISIBLE_DEVICES` for selecting from multiple available GPUs. Notes ----- @@ -142,11 +141,13 @@ def set_device(kind="cpu", gpuid=None, num_device=1, mpi=None): # noqa: C901 Parameters ---------- kind : {``'cpu'``, ``'gpu'``} - whether to use CPU or GPU. + Whether to use CPU or GPU. gpuid : int, optional GPU id to use. Default is None. Supported only when num_device is 1. - num_device : int - number of devices to use. Default is 1. + num_device : int, optional + Number of devices to use. For `cpu`, this is the number of nodes. + For `gpu`, this is equal to the number of GPUs connected to a single node. + Default is 1. mpi : MPI object, optional MPI communicator. Used to get distinct CPU information for multi-node jobs where each rank runs on different node. Communicator is not used @@ -170,29 +171,21 @@ def set_device(kind="cpu", gpuid=None, num_device=1, mpi=None): # noqa: C901 config["devices"] = [f"{cpu_info} CPU"] config["avail_mems"] = [cpu_mem] else: - try: - if mpi is None: - warnings.warn( - "To get the full list of CPUs, provide the MPI communicator.", - UserWarning, - ) - # return the same device multiple times - cpu_names = [ - f"{str(i) + ' ' + cpu_info}" for i in range(num_device) - ] - else: - comm = mpi.COMM_WORLD - rank = comm.Get_rank() - cpu_name = f"{str(rank) + ' ' + cpu_info}" - cpu_names = comm.allgather(cpu_name) - config["devices"] = [name for name in cpu_names] - # This memory is not individual but the total memory - config["avail_mems"] = [cpu_mem for _ in range(num_device)] - except ModuleNotFoundError: - raise ValueError( - "JAX not installed. Please install JAX to use multiple CPUs." - "Alternatively, set num_device=1 to use a single CPU." + if mpi is None: + warnings.warn( + "To get the full list of CPUs, provide the MPI communicator.", + UserWarning, ) + # return the same device multiple times + cpu_names = [f"{i} {cpu_info}" for i in range(num_device)] + else: + comm = mpi.COMM_WORLD + rank = comm.Get_rank() + cpu_name = f"{rank} {cpu_info}" + cpu_names = comm.allgather(cpu_name) + config["devices"] = cpu_names + # This memory is not individual but the total memory + config["avail_mems"] = [cpu_mem] * num_device elif kind == "gpu": os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" @@ -265,6 +258,9 @@ def _gpu_info(): memories = {dev["index"]: dev["mem_total"] - dev["mem_used"] for dev in devices} if num_device == 1: + selected_gpu = max( + devices, key=lambda dev: dev["mem_total"] - dev["mem_used"] + ) if gpuid is not None: if str(gpuid) in gpu_ids: selected_gpu = next( @@ -278,10 +274,6 @@ def _gpu_info(): "yellow", ) ) - else: - selected_gpu = max( - devices, key=lambda dev: dev["mem_total"] - dev["mem_used"] - ) devices = [selected_gpu] else: @@ -293,12 +285,7 @@ def _gpu_info(): # TODO: implement multiple GPU selection raise ValueError("Cannot specify `gpuid` when requesting multiple GPUs") - config["avail_mems"] = [ - memories[dev["index"]] / 1024 for dev in devices[:num_device] - ] # in GB - config["devices"] = [ - f"{dev['type']} (id={dev['index']})" for dev in devices[:num_device] - ] - os.environ["CUDA_VISIBLE_DEVICES"] = ",".join( - str(dev["index"]) for dev in devices[:num_device] - ) + devs = devices[:num_device] + config["avail_mems"] = [memories[dev["index"]] / 1024 for dev in devs] # in GB + config["devices"] = [f"{dev['type']} (id={dev['index']})" for dev in devs] + os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(str(dev["index"]) for dev in devs) diff --git a/desc/backend.py b/desc/backend.py index bef819f619..e8a1a0f1e5 100644 --- a/desc/backend.py +++ b/desc/backend.py @@ -59,29 +59,29 @@ def print_backend_info(): else: print(f"Using NumPy backend: version={np.__version__}, dtype={y.dtype}.") - if desc_config["num_device"] == 1: - print( - f"CPU Info: {desc_config['cpu_info']} with {desc_config['cpu_mem']:.2f} " - "GB available memory" - ) - elif desc_config["kind"] == "cpu": - print( - f"Using {desc_config['num_device']} CPUs with " - + f"{desc_config['avail_mems'][0]:.2f} GB total available memory:" - ) - for i, dev in enumerate(desc_config["devices"]): - print(f"\t CPU : {dev}") - - print( - "\nNote: The backend information assumes that the user has 1 " - "process per CPU (node). Using multiple processes per CPU (node) is " - "not the most efficient way to use MPI with purely CPUs." - ) + if desc_config["kind"] == "cpu": + if desc_config["num_device"] == 1: + print( + f"CPU Info: {desc_config['cpu_info']} with " + f"{desc_config['cpu_mem']:.2f} GB available memory" + ) + else: + print( + f"Using {desc_config['num_device']} CPUs with " + + f"{desc_config['avail_mems'][0]:.2f} GB total available memory:" + ) + for dev in desc_config["devices"]: + print(f"\t CPU : {dev}") - if desc_config["kind"] == "gpu": + print( + "\nNote: The backend information assumes that the user has 1 " + "process per CPU (node). Using multiple processes per CPU (node) is " + "not the most efficient way to use MPI with purely CPUs." + ) + elif desc_config["kind"] == "gpu": print( - f"CPU Info: {desc_config['cpu_info']} with {desc_config['cpu_mem']:.2f} " - "GB available memory" + f"CPU Info: {desc_config['cpu_info']} with " + f"{desc_config['cpu_mem']:.2f} GB available memory" ) print(f"Using {desc_config['num_device']} device:") for i, dev in enumerate(desc_config["devices"]): diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 1cd27e897a..4e706a61af 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -436,14 +436,18 @@ def __init__( else np.arange(len(objectives)) ) self._rank_per_objective = np.asarray(self._rank_per_objective) + assert len(objectives) == len(self._rank_per_objective) errorif( np.unique(self._rank_per_objective).size < desc_config["num_device"], ValueError, "Requested number of ranks is less than the number of devices. You " f"asked for {desc_config['num_device']} devices, but only have " - f" {np.unique(self._rank_per_objective).size} ranks assigned to " + f"{np.unique(self._rank_per_objective).size} ranks assigned to " "objectives. There should be at least as many ranks as devices.", ) + # here, we guess the number of devices per node by max(device_ids) + 1 + # device id can be same for different devices on different nodes, these will + # have different ranks, for the check, we take the mod for mapping errorif( ( np.mod(self._rank_per_objective, max(device_ids) + 1) != device_ids @@ -465,20 +469,27 @@ def __init__( self.rank = self.comm.Get_rank() self.size = self.comm.Get_size() self.running = True - errorif( - max(self._rank_per_objective) + 1 != self.size, - ValueError, + msg = ( "The maximum value of rank_per_objective " f"({max(self._rank_per_objective)+1}, supplied as " - f"({max(self._rank_per_objective)} in the array) " - f"is not equal to the number of ranks ({self.size}). There " - "should be at least 1 objective per rank.", + f"{max(self._rank_per_objective)} in the array) " + f"is not equal to the number of ranks ({self.size}). " + ) + errorif( + max(self._rank_per_objective) + 1 < self.size, + ValueError, + f"{msg} There should be at least 1 objective per rank.", + ) + errorif( + max(self._rank_per_objective) + 1 > self.size, + ValueError, + f"{msg} Some objectives are assigned to a rank that doesn't exist.", ) self._obj_per_rank = [ np.where(self._rank_per_objective == i)[0] for i in range(self.size) ] errorif( - np.array([foo.size == 0 for foo in self._obj_per_rank]).any(), + any(foo.size == 0 for foo in self._obj_per_rank), ValueError, "There is at least one rank that does not have any objective assigned. " f"Objectives per rank are {self._obj_per_rank}.", diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index b7dde05170..f8ba4e2101 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -378,7 +378,7 @@ def test_multidevice_objective_build(): with pytest.warns(UserWarning, match="When using multiple devices"): obj1.build() - # reset objectives + # reset objectives that are built for o in [objective1, objective2, objective3]: o._built = False o._use_jit = True From 3318793410fda5b9624306e232483bb37f22b58b Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 26 May 2026 22:29:42 +0300 Subject: [PATCH 198/199] update error/warning messages, update notebook --- desc/objectives/objective_funs.py | 53 +++++++++++---------- docs/notebooks/tutorials/multi_device.ipynb | 2 +- tests/test_multidevice.py | 4 +- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 4e706a61af..805d4bff8d 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -436,14 +436,10 @@ def __init__( else np.arange(len(objectives)) ) self._rank_per_objective = np.asarray(self._rank_per_objective) - assert len(objectives) == len(self._rank_per_objective) - errorif( - np.unique(self._rank_per_objective).size < desc_config["num_device"], - ValueError, - "Requested number of ranks is less than the number of devices. You " - f"asked for {desc_config['num_device']} devices, but only have " - f"{np.unique(self._rank_per_objective).size} ranks assigned to " - "objectives. There should be at least as many ranks as devices.", + assert len(objectives) == len(self._rank_per_objective), ( + "rank_per_objective must have one entry per objective. Got " + f"{len(self._rank_per_objective)} entries for " + f"{len(objectives)} objectives." ) # here, we guess the number of devices per node by max(device_ids) + 1 # device id can be same for different devices on different nodes, these will @@ -453,37 +449,44 @@ def __init__( np.mod(self._rank_per_objective, max(device_ids) + 1) != device_ids ).any(), ValueError, - "Same rank objectives should also have the same device id. Supplied " - f"ranks {self._rank_per_objective} and device ids {device_ids} are " - "not compatible.", + "Some objective's rank and device id are inconsistent. The device id " + "of an objective must equal its rank modulo the number of devices per " + f"node ({max(device_ids) + 1}). Got rank_per_objective=" + f"{self._rank_per_objective} and device_ids={device_ids}.", ) warnif( max(device_ids) != desc_config["num_device"] - 1, UserWarning, - "You are not using all the devices available. You asked for " - f"{desc_config['num_device']} devices, but the maximum device id is " - f"{max(device_ids)}. This means that some devices are not being used.", + f"Not all available devices are being used. {desc_config['num_device']}" + f" device(s) are available, but the highest device id assigned to an " + f"objective is {max(device_ids)}.", ) self.mpi = mpi self.comm = self.mpi.COMM_WORLD self.rank = self.comm.Get_rank() self.size = self.comm.Get_size() self.running = True + # rank_per_objective is 0-indexed, so the number of ranks it expects + # is its maximum value + 1. This must match the number of MPI ranks. + n_ranks_needed = max(self._rank_per_objective) + 1 msg = ( - "The maximum value of rank_per_objective " - f"({max(self._rank_per_objective)+1}, supplied as " - f"{max(self._rank_per_objective)} in the array) " - f"is not equal to the number of ranks ({self.size}). " + f"rank_per_objective uses {n_ranks_needed} rank(s) (highest rank " + f"index is {max(self._rank_per_objective)}), but {self.size} MPI " + f"rank(s) are running. These must match. " ) errorif( - max(self._rank_per_objective) + 1 < self.size, + n_ranks_needed < self.size, ValueError, - f"{msg} There should be at least 1 objective per rank.", + f"{msg}You are running more MPI ranks than rank_per_objective uses, " + "so some ranks would have no objective assigned. Either reduce the " + "number of MPI ranks or assign objectives to every rank.", ) errorif( - max(self._rank_per_objective) + 1 > self.size, + n_ranks_needed > self.size, ValueError, - f"{msg} Some objectives are assigned to a rank that doesn't exist.", + f"{msg}Some objectives are assigned to a rank index that is too large " + "for the number of MPI ranks running. Either run more MPI ranks or " + "lower the rank indices in rank_per_objective.", ) self._obj_per_rank = [ np.where(self._rank_per_objective == i)[0] for i in range(self.size) @@ -491,8 +494,10 @@ def __init__( errorif( any(foo.size == 0 for foo in self._obj_per_rank), ValueError, - "There is at least one rank that does not have any objective assigned. " - f"Objectives per rank are {self._obj_per_rank}.", + "Every rank must have at least one objective assigned, but the " + "following ranks have none: " + f"{[i for i, foo in enumerate(self._obj_per_rank) if foo.size == 0]}. " + f"Objective indices per rank are {self._obj_per_rank}.", ) self._static_attrs += [ "mpi", diff --git a/docs/notebooks/tutorials/multi_device.ipynb b/docs/notebooks/tutorials/multi_device.ipynb index b16b193861..0d8713839b 100644 --- a/docs/notebooks/tutorials/multi_device.ipynb +++ b/docs/notebooks/tutorials/multi_device.ipynb @@ -706,7 +706,7 @@ "\n", "**Note: Most clusters have multiple GPUs connected to each node, so before using multiple nodes, use all the GPUs available to that node. Multi-node communication is slower and your script will be easier to write properly.**\n", "\n", - "Note: You should have at least 6 objectives, so at least 1 objective per device. If you want to run multiple objectives on the same device, you can specify the ``rank_per_objective`` in the `ObjectiveFunction` keywords. By default, the initializer will assign different ranks for each sub-objective, in above example it defaults to `np.arange(len(objs))`. An example optimization setup where each rank has 1 or more objectives,\n", + "Note: You should have at least 6 objectives, so at least 1 objective per rank. If you want to have less objectives than number of devices, you need to adjust the number of ranks. If you want to run multiple objectives on the same device, you can specify the ``rank_per_objective`` in the `ObjectiveFunction` keywords. By default, the initializer will assign different ranks for each sub-objective, in above example it defaults to `np.arange(len(objs))`. An example optimization setup where each rank has 1 or more objectives,\n", "\n", "```python\n", "\n", diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index f8ba4e2101..98b1aa6309 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -358,14 +358,14 @@ def test_multidevice_objective_build(): obj1 = ObjectiveFunction([objective1, objective2, objective3]) # need to use multiple ranks if using multiple devices - with pytest.raises(ValueError, match="Requested number of ranks is"): + with pytest.raises(ValueError, match="uses fewer distinct ranks"): # this one is multi-device obj1 = ObjectiveFunction( [objective1, objective2, objective3], mpi=MPI, rank_per_objective=[0, 0, 0] ) # need to have same device for the same rank objectives - with pytest.raises(ValueError, match="Same rank objectives should"): + with pytest.raises(ValueError, match="rank and device id are inconsistent"): # this one is multi-device obj1 = ObjectiveFunction( [objective1, objective2, objective3, objective4], From 40268f171ccde156520c5d27fd556a6a61422e6d Mon Sep 17 00:00:00 2001 From: YigitElma Date: Mon, 1 Jun 2026 22:05:19 +0300 Subject: [PATCH 199/199] update the message in test --- tests/test_multidevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_multidevice.py b/tests/test_multidevice.py index 98b1aa6309..dd961cff72 100644 --- a/tests/test_multidevice.py +++ b/tests/test_multidevice.py @@ -358,7 +358,7 @@ def test_multidevice_objective_build(): obj1 = ObjectiveFunction([objective1, objective2, objective3]) # need to use multiple ranks if using multiple devices - with pytest.raises(ValueError, match="uses fewer distinct ranks"): + with pytest.raises(ValueError, match="rank and device id are inconsistent"): # this one is multi-device obj1 = ObjectiveFunction( [objective1, objective2, objective3], mpi=MPI, rank_per_objective=[0, 0, 0]