Coverage for jaxquantum/utils/utils.py: 94%
115 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-27 22:28 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-27 22:28 +0000
1"""
2JAX Utils
3"""
5from numbers import Number
6from typing import Dict
8from jax import lax, Array, device_put, config
9from jax._src.scipy.special import gammaln
10import jax.numpy as jnp
11import numpy as np
13from typing import Literal
15config.update("jax_enable_x64", True)
18def device_put_params(params: Dict, non_device_params=None):
19 non_device_params = [] if non_device_params is None else non_device_params
20 for param, value in params.items():
21 if param in non_device_params:
22 continue
23 if isinstance(value, (Number, np.ndarray)):
24 params[param] = device_put(value)
25 return params
28def comb(N, k):
29 """
30 NCk
32 #TODO: replace with jsp.special.comb once issue is closed:
33 https://github.com/google/jax/issues/9709
35 Args:
36 N: total items
37 k: # of items to choose
39 Returns:
40 NCk: N choose k
41 """
42 one = 1
43 N_plus_1 = lax.add(N, one)
44 k_plus_1 = lax.add(k, one)
45 return lax.exp(
46 lax.sub(
47 gammaln(N_plus_1), lax.add(gammaln(k_plus_1), gammaln(lax.sub(N_plus_1, k)))
48 )
49 )
52def complex_to_real_iso_matrix(A):
53 return jnp.block([[jnp.real(A), -jnp.imag(A)], [jnp.imag(A), jnp.real(A)]])
56def real_to_complex_iso_matrix(A):
57 N = A.shape[0]
58 return A[: N // 2, : N // 2] + 1j * A[N // 2 :, : N // 2]
61def complex_to_real_iso_vector(v):
62 return jnp.block([[jnp.real(v)], [jnp.imag(v)]])
65def real_to_complex_iso_vector(v):
66 N = v.shape[0]
67 return v[: N // 2, :] + 1j * v[N // 2 :, :]
70def imag_times_iso_vector(v):
71 N = v.shape[0]
72 return jnp.block([[-v[N // 2 :, :]], [v[: N // 2, :]]])
75def imag_times_iso_matrix(A):
76 N = A.shape[0]
77 Ar = A[: N // 2, : N // 2]
78 Ai = A[N // 2 :, : N // 2]
79 return jnp.block([[-Ai, -Ar], [Ar, -Ai]])
82def conj_transpose_iso_matrix(A):
83 N = A.shape[0]
84 Ar = A[: N // 2, : N // 2].T
85 Ai = A[N // 2 :, : N // 2].T
86 return jnp.block([[Ar, Ai], [-Ai, Ar]])
89def robust_isscalar(val):
90 is_scalar = isinstance(val, Number) or np.isscalar(val)
91 if isinstance(val, Array):
92 is_scalar = is_scalar or (len(val.shape) == 0)
93 return is_scalar
96# =====================================================
98# Precision
100def set_precision(precision: Literal["single", "double"]):
101 """
102 Set the precision of JAX operations.
104 Args:
105 precision: 'single' or 'double'
107 Raises:
108 ValueError: if precision is not 'single' or 'double'
109 """
110 if precision == "single":
111 config.update("jax_enable_x64", False)
112 elif precision == "double":
113 config.update("jax_enable_x64", True)
114 else:
115 raise ValueError("precision must be 'single' or 'double'")
118# =====================================================
120# Sharding
123def set_default_sharding(sharding):
124 """Configure the global default ``Sharding`` applied to every Qarray.
126 Once set, every ``DenseImpl`` and ``SparseDiaImpl`` construction routes
127 its underlying ``jnp.ndarray`` through ``jax.lax.with_sharding_constraint``
128 using *sharding*. ``SparseBCOOImpl`` is unsupported under sharding and
129 will raise from ``Qarray.create(..., implementation=SPARSE_BCOO)``.
131 Args:
132 sharding: Either a ``jax.sharding.Sharding`` (typically
133 ``NamedSharding(mesh, PartitionSpec(...))``) applied to every
134 array regardless of rank, or a callable ``(arr) -> Sharding``
135 for rank-adaptive partitioning. Pass ``None`` to disable
136 (equivalent to ``clear_default_sharding()``).
137 """
138 from jaxquantum.core.settings import SETTINGS
139 SETTINGS["default_sharding"] = sharding
142def get_default_sharding():
143 """Return the configured default sharding, or ``None`` if unset."""
144 from jaxquantum.core.settings import SETTINGS
145 return SETTINGS["default_sharding"]
148def clear_default_sharding():
149 """Disable default sharding (return to single-device behaviour)."""
150 from jaxquantum.core.settings import SETTINGS
151 SETTINGS["default_sharding"] = None
154def set_device_mesh(shape, axis_names, partition_spec=None, devices=None):
155 """Configure default sharding from a high-level mesh description.
157 Convenience wrapper around :func:`set_default_sharding` that builds a
158 ``Mesh`` and ``NamedSharding`` for you. Mirrors the pattern used in
159 ``experiments/distributed/1-demo.ipynb``.
161 Args:
162 shape: Tuple of mesh dimensions, e.g. ``(2,)`` for a 2-device 1D mesh
163 or ``(2, 4)`` for a 2x4 2D mesh.
164 axis_names: Tuple of mesh axis names, same length as *shape*, e.g.
165 ``('dp',)`` or ``('dp', 'mp')``.
166 partition_spec: Optional ``jax.sharding.PartitionSpec``. If ``None``,
167 stores a rank-adaptive callable that picks the partition for
168 each array based on the *name* of each mesh axis:
170 * Names starting with ``'dp'`` / ``'data'`` → data-parallel:
171 prefer the leading batch axes ``[0, 1, ..., rank-3]``, then
172 fall through to matrix axes ``[-2, -1]``.
173 * Names starting with ``'mp'`` / ``'model'`` → model-parallel:
174 prefer matrix axes ``[-2, -1]``, then fall through to leading
175 batch axes.
176 * Anything else behaves like ``'mp'``.
178 Each mesh axis is greedy-bound to the first un-claimed array
179 axis (in priority order) whose size is divisible by the mesh
180 axis size. Mesh axes that find no binding are unused (the array
181 replicates along them). This produces e.g.
183 * ``('dp',)`` on ``(B, N, N)`` → ``P('dp', None, None)``
184 (parameter sweep — each device gets a slice of B).
185 * ``('mp',)`` on ``(N, N)`` → ``P('mp', None)`` (single large
186 system, matrix-row sharded).
187 * ``('dp', 'mp')`` on ``(B, N, N)`` → ``P('dp', 'mp', None)``
188 (both modes simultaneously).
189 devices: Optional explicit list of devices. Defaults to
190 ``jax.devices()``.
192 Raises:
193 ValueError: if ``len(shape) != len(axis_names)``.
194 """
195 if len(shape) != len(axis_names):
196 raise ValueError(
197 f"shape ({shape}) and axis_names ({axis_names}) must have the "
198 "same length"
199 )
201 import jax
202 from math import prod
203 from jax.experimental import mesh_utils
204 from jax.sharding import Mesh, NamedSharding, PartitionSpec
206 if devices is None:
207 # Slice to the first prod(shape) devices so a 1D mesh works on a
208 # host with extra devices (e.g. tests run with XLA_FLAGS spoofing 8
209 # CPUs but the user wants a (2,) mesh).
210 all_devices = jax.devices()
211 needed = prod(shape)
212 if len(all_devices) < needed:
213 raise ValueError(
214 f"set_device_mesh(shape={shape}) needs {needed} devices but "
215 f"only {len(all_devices)} are available."
216 )
217 devices = all_devices[:needed]
219 mesh_devices = mesh_utils.create_device_mesh(shape, devices=devices)
220 mesh = Mesh(mesh_devices, axis_names)
222 if partition_spec is None:
223 # Rank-adaptive: pick partition per array based on each mesh axis's
224 # name. 'dp'/'data' prefers leading batch axes; 'mp'/'model' (and
225 # anything else) prefers matrix axes. Greedy first-fit binding;
226 # unbound mesh axes leave the array replicated along them. See the
227 # docstring for the full priority table.
228 mesh_axis_priorities = [
229 (name, _array_axis_priority(name)) for name in axis_names
230 ]
232 def _adaptive(arr):
233 rank = arr.ndim
234 if rank == 0:
235 return NamedSharding(mesh, PartitionSpec())
236 spec_parts = [None] * rank
237 used_array_axes: set = set()
238 for mesh_axis, priority_fn in mesh_axis_priorities:
239 mesh_size = mesh.shape[mesh_axis]
240 for array_axis in priority_fn(arr.shape):
241 if array_axis in used_array_axes:
242 continue
243 if arr.shape[array_axis] % mesh_size != 0:
244 continue
245 spec_parts[array_axis] = mesh_axis
246 used_array_axes.add(array_axis)
247 break
248 return NamedSharding(mesh, PartitionSpec(*spec_parts))
250 set_default_sharding(_adaptive)
251 else:
252 set_default_sharding(NamedSharding(mesh, partition_spec))
255def _array_axis_priority(mesh_axis_name: str):
256 """Return a function ``shape -> list[int]`` giving the array-axis
257 priority order for the given mesh-axis name.
259 'dp'/'data' → batch dims first (leading axes), then space dims.
260 'mp'/'model' (or anything else) → space dims first, then batch.
262 The space-vs-batch split is shape-aware so that 1‑D ``(N,)`` and batched
263 ``(B, N)`` state vectors shard their Hilbert-space axis under model-parallel
264 rather than their batch axis:
266 * rank ≤ 1 → the sole axis is the space axis.
267 * ``shape[-2] == shape[-1]`` → operator-like; the last two axes are the
268 matrix (space) dims, the rest are batch.
269 * otherwise → batched vector; the last axis is the space axis, the rest are
270 batch.
272 The only residual ambiguity is a *square* batched vector (``B == N``), which
273 is indistinguishable from an operator by shape alone and is treated as an
274 operator. This is rare and never incorrect (sharding stays valid).
275 """
276 name = mesh_axis_name.lower()
277 is_dp = name.startswith(("dp", "data"))
279 def priority(shape) -> list:
280 rank = len(shape)
281 if rank <= 1:
282 space = list(range(rank))
283 batch = []
284 elif shape[-2] == shape[-1]:
285 space = [rank - 2, rank - 1]
286 batch = list(range(rank - 2))
287 else:
288 space = [rank - 1]
289 batch = list(range(rank - 1))
290 return (batch + space) if is_dp else (space + batch)
292 return priority