Coverage for jaxquantum/core/settings.py: 94%
31 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"""Core settings.
3Holds global runtime configuration read by the rest of ``jaxquantum``.
5How sharding works
6------------------
8A single global ``SETTINGS["default_sharding"]`` (either a
9``jax.sharding.Sharding`` *or* a callable ``arr -> Sharding``) is consumed
10by every ``QarrayImpl._make`` classmethod via :func:`_maybe_shard`. When
11the entry is ``None`` (the default), every ``_make`` call is a pure
12pass-through and behaviour matches the single-device codebase exactly.
14Why a callable: lets the rank-adaptive default produced by
15``set_device_mesh`` return a different ``NamedSharding`` per array shape
16(matrices vs kets vs batched arrays vs SparseDIA ``_diags``).
18Two parallelism modes are first-class, picked by mesh-axis name:
20* **Data-parallel** — name a mesh axis ``'dp'`` or ``'data'``. Shards the
21 leading batch dim of ``(B, *)`` arrays (parameter sweeps, vmap'd
22 trajectories).
23* **Model-parallel** — name a mesh axis ``'mp'`` or ``'model'`` (or
24 anything else). Shards the matrix dim of ``(*, n, n)`` operators.
25* **Both** — use a 2D mesh ``axis_names=('dp', 'mp')``.
27Chokepoint: every impl construction goes through ``_make`` →
28``_maybe_shard`` → ``jax.lax.with_sharding_constraint``. The constraint is
29a no-op when the array is already correctly sharded, so it's safe to apply
30on every op (matmul, kron, conversions). Pure JAX ops between calls
31propagate the sharding through XLA without further help.
33Not sharded: ``SparseBCOO`` (variable nnz per shard — raises in
34``from_data``); ``_offsets`` on ``SparseDIA`` (static Python metadata,
35not a JAX array).
37User knobs (in :mod:`jaxquantum.utils.utils`, next to ``set_precision``):
38``set_device_mesh``, ``set_default_sharding``, ``get_default_sharding``,
39``clear_default_sharding``.
40"""
42from __future__ import annotations
44from typing import Any, Optional
47SETTINGS: dict = {
48 "auto_tidyup_atol": 1e-14,
49 # Optional[jax.sharding.Sharding | Callable[[Array], jax.sharding.Sharding]]
50 "default_sharding": None,
51}
54def _maybe_shard(arr: Any) -> Any:
55 """Apply ``SETTINGS['default_sharding']`` to *arr* if one is configured.
57 Uses ``jax.lax.with_sharding_constraint`` (works inside and outside
58 ``jit``; near-free no-op when the array is already correctly sharded).
59 Short-circuits and returns *arr* unchanged when no default sharding is
60 set — this is the common single-device path and must stay zero-cost.
61 """
62 s = SETTINGS["default_sharding"]
63 if s is None:
64 return arr
66 # Lazy imports keep this module import-cheap.
67 from jax.lax import with_sharding_constraint
69 sharding = s(arr) if callable(s) else s
70 _validate_shape(arr, sharding)
71 return with_sharding_constraint(arr, sharding)
74def _validate_shape(arr: Any, sharding: Any) -> None:
75 """Raise ``ValueError`` if *arr*'s shape can't be partitioned per *sharding*.
77 Walks the ``PartitionSpec`` against ``arr.shape`` and checks that each
78 sharded axis is divisible by the product of the named mesh-axis sizes.
79 Pure-Python on shape tuples — runs only when sharding is enabled.
81 Only attempts validation for ``NamedSharding`` (the common case from
82 ``set_device_mesh``). Other ``Sharding`` types are forwarded to JAX
83 untouched and any mismatch surfaces from JAX itself.
84 """
85 try:
86 from jax.sharding import NamedSharding
87 except Exception: # noqa: BLE001 # pragma: no cover
88 return
90 if not isinstance(sharding, NamedSharding):
91 return
93 spec = sharding.spec
94 mesh = sharding.mesh
95 shape = getattr(arr, "shape", None)
96 if shape is None:
97 return
99 for axis_idx, axis_spec in enumerate(spec):
100 if axis_spec is None or axis_idx >= len(shape):
101 continue
102 names = axis_spec if isinstance(axis_spec, tuple) else (axis_spec,)
103 mesh_size = 1
104 for name in names:
105 mesh_size *= mesh.shape[name]
106 dim = shape[axis_idx]
107 if dim % mesh_size != 0:
108 raise ValueError(
109 f"Cannot apply default sharding {sharding} to array of "
110 f"shape {tuple(shape)}: axis {axis_idx} (size {dim}) is not "
111 f"divisible by mesh axis {names} (size {mesh_size}). "
112 "Pass an explicit `sharding=` per call, or call "
113 "`jqt.clear_default_sharding()` to disable."
114 )