SPMD¤
substrax.spmd is the data-parallel training path on top of substrax.mesh: place a
batch on the data axis, run one step, reduce metrics across devices.
import jax
from flax import nnx
from substrax.mesh import DeviceMeshManager
from substrax.spmd import create_data_parallel_sharding, place_batch_on_shards, spmd_train_step
mesh = DeviceMeshManager.create_device_mesh({"data": jax.device_count()})
sharding = create_data_parallel_sharding(mesh)
@nnx.jit
def train_step(model, optimizer, batch):
return spmd_train_step(model, optimizer, loss_fn, batch)
with jax.set_mesh(mesh):
loss = train_step(model, optimizer, place_batch_on_shards(batch, sharding))
place_batch_on_shards also takes a pytree prefix of the batch whose leaves are shardings, one
per subtree, for a batch whose fields are not all split along the batch axis: rows on the data
axis and a batch-level value, which has no batch axis, replicated.
from jax.sharding import NamedSharding, PartitionSpec
replicated = NamedSharding(mesh, PartitionSpec())
batch = {"rows": {"x": x, "y": y}, "batch_state": {"lam": lam}}
placed = place_batch_on_shards(batch, {"rows": sharding, "batch_state": replicated})
On several processes each passes the rows it loaded and jax assembles the global batch; a replicated leaf must be equal on every process. A leaf with fewer dimensions than its sharding splits, such as a scalar under the data-axis sharding, is refused with its path.
spmd_train_step differentiates with nnx.value_and_grad and updates the
nnx.Optimizer; XLA inserts the gradient all-reduce when the parameters are sharded
under jax.set_mesh. The reductions (reduce_mean, reduce_sum, reduce_max,
reduce_min) and the collectives (reduce_mean_collective, reduce_sum_collective,
all_gather) reduce every array leaf of the tree they are given, at any depth, so a gradient tree
beside scalar metrics is reduced leaf by leaf, and return a tree of the same structure; any other
leaf passes through. reduce_custom and collect_from_devices take a flat
mapping from metric name to value.
substrax.spmd
¤
Data-parallel placement, the SPMD training step and collectives.
A data-parallel gradient needs no reduction after differentiating. Under jax.jit with the
batch sharded over a mesh axis, the gradient of the batch's loss is already the full gradient:
the compiler inserts the all-reduce (spmd_train_step relies on this). Inside
jax.shard_map each device sees its shard, and the loss is averaged over the axis before
differentiating, jax.grad(lambda w: jax.lax.pmean(loss(w, shard), axis)); the gradient of a
replicated parameter then comes back as the full one. Averaging the gradient afterwards is wrong
by the axis size, because the gradient of a replicated parameter already arrives summed over
the axis. reduce_mean_collective and reduce_sum_collective average or sum per-device
metrics inside shard_map.
all_gather(metrics, axis_name='batch')
¤
Gather metrics from all devices.
Only valid inside a pmap or shard_map context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
T
|
The metrics to gather: a mapping, or any pytree of them. |
required |
axis_name
|
str
|
The name of the axis to gather across. |
'batch'
|
Returns:
| Type | Description |
|---|---|
T
|
|
collect_from_devices(metrics)
¤
Collect metrics from all devices.
Call outside of a pmapped function to split per-device values from the leading device axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
Mapping[str, V]
|
The metrics from all devices, with the first dimension corresponding to the device axis. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, list[Array] | V]
|
A dictionary of metrics, with array values split into per-device lists. |
reduce_custom(metrics, reduce_fn=None)
¤
Apply custom reduction operations to metrics.
Uses standard JAX operations. Works in SPMD contexts. Each named value is reduced by its
operation when it is an array; without reduce_fn this is reduce_mean.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
Mapping[str, V]
|
The metrics to reduce, by name. |
required |
reduce_fn
|
Mapping[str, str | None] | None
|
A dictionary mapping metric names to reduction operations. Each operation should be one of {"mean", "sum", "max", "min"}. If None, defaults to "mean" for all metrics. |
None
|
Returns:
| Type | Description |
|---|---|
Mapping[str, V | Array]
|
A dictionary of reduced metrics. |
reduce_max(metrics)
¤
Compute the maximum of metrics using standard JAX operations.
Works with global arrays in SPMD contexts (nnx.jit + mesh). Every array leaf with at least one
dimension is reduced, at any depth of metrics; every other leaf passes through unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
T
|
The metrics to reduce: a mapping, or any pytree of them. |
required |
Returns:
| Type | Description |
|---|---|
T
|
|
reduce_mean(metrics)
¤
Compute the mean of metrics using standard JAX operations.
Works with global arrays in SPMD contexts (nnx.jit + mesh). Every array leaf with at least one
dimension is reduced, at any depth of metrics; every other leaf passes through unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
T
|
The metrics to reduce: a mapping, or any pytree of them. |
required |
Returns:
| Type | Description |
|---|---|
T
|
|
reduce_mean_collective(metrics, axis_name='batch')
¤
Compute the mean of metrics using collective operations.
Only valid inside a pmap or shard_map context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
T
|
The metrics to reduce: a mapping, or any pytree of them. |
required |
axis_name
|
str
|
The name of the axis to reduce across. |
'batch'
|
Returns:
| Type | Description |
|---|---|
T
|
|
reduce_min(metrics)
¤
Compute the minimum of metrics using standard JAX operations.
Works with global arrays in SPMD contexts (nnx.jit + mesh). Every array leaf with at least one
dimension is reduced, at any depth of metrics; every other leaf passes through unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
T
|
The metrics to reduce: a mapping, or any pytree of them. |
required |
Returns:
| Type | Description |
|---|---|
T
|
|
reduce_sum(metrics)
¤
Compute the sum of metrics using standard JAX operations.
Works with global arrays in SPMD contexts (nnx.jit + mesh). Every array leaf with at least one
dimension is reduced, at any depth of metrics; every other leaf passes through unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
T
|
The metrics to reduce: a mapping, or any pytree of them. |
required |
Returns:
| Type | Description |
|---|---|
T
|
|
reduce_sum_collective(metrics, axis_name='batch')
¤
Compute the sum of metrics using collective operations.
Only valid inside a pmap or shard_map context.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metrics
|
T
|
The metrics to reduce: a mapping, or any pytree of them. |
required |
axis_name
|
str
|
The name of the axis to reduce across. |
'batch'
|
Returns:
| Type | Description |
|---|---|
T
|
|
create_data_parallel_sharding(mesh, data_axis='data')
¤
Create the sharding that splits a batch's leading axis over a mesh axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mesh
|
Mesh
|
The device mesh to use for sharding. |
required |
data_axis
|
str
|
The name of the mesh axis to use for data parallelism. |
'data'
|
Returns:
| Type | Description |
|---|---|
NamedSharding
|
A |
place_batch_on_shards(batch, sharding)
¤
Turn this process's batch into global arrays, each subtree on its sharding.
Array leaves, whether jax.Array, a NumPy array as a host data loader yields it, or a NumPy
scalar, go to jax.make_array_from_process_local_data in one call. On a single process that
is one batched jax.device_put. On several processes each process passes the slice of the
global batch it loaded, and jax stitches the slices into one global array; a leaf on a
replicated sharding must be equal on every process. Any other leaf, such as a string, is
returned as it is. Call it on the host batches a loader yields, outside jax.jit: inside a
traced function the result is not placed on sharding.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
PyTree
|
The batch this process loaded. |
required |
sharding
|
Sharding | PyTree
|
One sharding for every array leaf, or a pytree prefix of |
required |
Returns:
| Type | Description |
|---|---|
PyTree
|
The batch with every array leaf replaced by the global array on its sharding. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a leaf has fewer dimensions than its sharding partitions (a scalar under a sharding that splits the batch axis); the message names the leaf's path. |
place_nnx_state_on_shards(state, mesh, filter_sharding)
¤
Shard a Flax NNX state tree using current NNX sharding helpers.
spmd_train_step(model, optimizer, loss_fn, batch)
¤
Execute a data-parallel training step using SPMD.
Uses nnx.value_and_grad for automatic differentiation. The XLA compiler handles gradient AllReduce automatically when model parameters are sharded across devices via jax.set_mesh or explicit NamedSharding.
This function should be called inside an @nnx.jit decorated function with a mesh context active.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
M
|
The NNX model to train. |
required |
optimizer
|
Optimizer[M]
|
The NNX optimizer wrapping the model. |
required |
loss_fn
|
Callable[[M, PyTree], Array]
|
Function (model, batch) -> loss scalar. |
required |
batch
|
PyTree
|
The training batch (should be pre-sharded). |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The loss value. |
Example
mesh = DeviceMeshManager.create_device_mesh({"data": 4}) rules = data_parallel_rules()
@nnx.jit def train_step(model, optimizer, batch): return spmd_train_step(model, optimizer, my_loss_fn, batch)
with jax.set_mesh(mesh): loss = train_step(model, optimizer, batch)