SPMD & Distributed Sharding¶
Sharding describes how an array is distributed across devices. You still write operations on the whole array; JAX executes the computation using its shards and handles the required communication.
Taktiny uses JAX’s Mesh, PartitionSpec, and NamedSharding. Logical axis
names let a model describe its dimensions without hard-coding a device layout
into every layer.
Mesh axes and array dimensions¶
Object |
Meaning |
Example |
|---|---|---|
|
An arrangement of devices with named axes. |
A 2 × 2 mesh named |
|
Which mesh axes partition each array dimension. |
|
|
A spec attached to a particular mesh. |
|
|
Logical names for a parameter’s dimensions. |
|
In a spec, None leaves that array dimension unpartitioned. Mesh axes unused
by the entire spec replicate the array. For example, P(None, "model")
partitions a matrix’s columns along model and replicates it along data.
P() requests full replication.
The spec describes array dimensions, not individual devices. Each partitioned dimension must be divisible by the product of the sizes of its assigned mesh axes.
A complete four-device example¶
To try a 2 × 2 mesh without an accelerator, save the Python block below as
sharding_example.py and run:
JAX_PLATFORMS=cpu XLA_FLAGS='--xla_force_host_platform_device_count=4' uv run python sharding_example.py
Set the environment before JAX initializes its backend. These are four logical CPU devices for testing, not four physical accelerators.
import jax
import jax.numpy as jnp
import numpy as np
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
from taktiny import nn
assert len(jax.devices()) == 4, "Run this example with four JAX devices"
mesh = Mesh(np.array(jax.devices()).reshape(2, 2), ("data", "model"))
with jax.set_mesh(mesh):
layer = nn.Linear(
8, 12,
partition_spec=P(None, "model"),
rngs=nn.Rngs(0),
)
x = jax.device_put(
np.ones((4, 8), dtype=np.float32),
NamedSharding(mesh, P("data", None)),
)
forward = jax.jit(
lambda model, inputs: model(inputs),
out_shardings=NamedSharding(mesh, P("data", "model")),
)
y = forward(layer, x)
assert layer.kernel.value.shape == (8, 12)
assert layer.kernel.value.sharding.spec == P(None, "model")
assert layer.bias.value.sharding.spec == P("model")
assert y.shape == (4, 12)
assert y.sharding.spec == P("data", "model")
The kernel’s output dimension and the bias are partitioned across model.
Inputs are partitioned across data. The requested output layout uses both:
each device holds part of the batch and part of the output features.
The remaining Python blocks build on this example’s imports and mesh.
Name dimensions, then choose their placement¶
Use map_logical_axis_names to map model-level names to mesh axes:
from taktiny.utils.spmd import map_logical_axis_names
with jax.set_mesh(mesh), map_logical_axis_names({
"input": None,
"output": "model",
}):
logical_layer = nn.Linear(
8, 12,
axis_names=("input", "output"),
rngs=nn.Rngs(1),
)
assert logical_layer.kernel.axis_names == ("input", "output")
assert logical_layer.kernel.partition_spec == P(None, "model")
assert logical_layer.kernel.value.sharding.spec == P(None, "model")
Taktiny preserves the original axis_names and stores the resolved mesh spec
separately. For Linear, the bias uses the trailing output axis names and
spec entries.
The mapping applies when the initializer or parameter is created. Leaving the context restores previous rules; it does not move existing arrays. Changing rules later does not automatically reshard an existing model either.
When both arguments are supplied¶
axis_names takes precedence over partition_spec. The explicit spec is not
a fallback for individual unmapped dimensions:
with jax.set_mesh(mesh), map_logical_axis_names({"output": "model"}):
overridden = nn.Linear(
8, 12,
axis_names=("input", "output"),
partition_spec=P("data", None),
rngs=nn.Rngs(2),
)
assert overridden.kernel.partition_spec == P(None, "model")
Here input is unmapped, so it resolves to None, not the explicit spec’s
"data". If no logical names match any rules, every dimension resolves to
None and the parameter is replicated on the active mesh.
To use an explicit spec, omit axis_names. To request replication while keeping
logical names, map those names to None when creating the parameter.
Rule order and scope¶
Inspect a mapping without creating an array using logical_to_mesh_axes:
from taktiny.utils.spmd import logical_to_mesh_axes
assert logical_to_mesh_axes(
("batch", "feature"),
rules=(("batch", "data"), ("feature", "model")),
) == P("data", "model")
assert logical_to_mesh_axes(
("input", "output"),
rules=(("output", "model"), ("input", "model")),
) == P(None, "model")
Rules are considered in order. A mesh axis cannot partition two dimensions of
the same array, so the first available assignment wins. In the second example,
output claims model, leaving input unpartitioned. Repeating a non-None
logical name within one array raises an error.
A logical name can map to a tuple of mesh axes. For example,
{"feature": ("data", "model")} partitions one dimension across both axes;
its size must be divisible by four on this mesh.
Prefer with map_logical_axis_names(...) for temporary rules. Nested blocks
prepend their rules, giving them priority over outer rules, and restore the
previous rules on exit.
For longer-lived configuration, set_logical_axis_rules(rules) replaces the
current rule sequence; get_logical_axis_rules() reads it. The storage is
thread-local, not shared across threads or processes.
Calling map_logical_axis_names(...) without with also changes the rules
immediately, but prepends them instead of replacing them. Merely constructing
that context object has this effect; it does not wait until context entry.
When initialization is sharded¶
Layers such as Linear wrap their initializers with with_logical_partitioning
when sharding arguments are supplied. With an active jax.set_mesh(mesh)
context and a resolved spec, the wrapper compiles the initializer with
jax.jit(..., out_shardings=spec). Its returned array is already sharded;
you do not need a separate post-initialization placement step.
This describes the output layout, not every temporary allocation inside the initializer. Invalid specs, incompatible dimensions, and other compilation errors from this wrapper are not silently converted into replication.
The wrapper has a fallback for a missing active mesh: it calls the original
initializer. A parameter can therefore retain sharding metadata without its
array having that layout. Construct parameters inside the mesh context when
you want placement at initialization, and check the array’s .sharding, not
only its metadata.
You can wrap a custom initializer directly:
from taktiny.utils.spmd import with_logical_partitioning
initialize = with_logical_partitioning(
jax.nn.initializers.normal(),
axis_names=("input", "output"),
)
with jax.set_mesh(mesh), map_logical_axis_names({"output": "model"}):
weight = initialize(jax.random.key(3), (8, 12), jnp.float32)
assert weight.sharding.spec == P(None, "model")
Use the usual initializer signature (key, shape, dtype). Logical rules are
resolved when the wrapped initializer is called, not when it is wrapped.
Match names to the parameter shape¶
A parameter needs one logical axis entry per array dimension. A feature shape with several dimensions therefore needs several entries, not one name:
with jax.set_mesh(mesh), map_logical_axis_names({"output": "model"}):
nd_layer = nn.Linear(
(2, 4), (3, 4),
axis_names=("input_group", "input_feature", "output_group", "output"),
rngs=nn.Rngs(4),
)
assert nd_layer.kernel.value.shape == (2, 4, 3, 4)
assert nd_layer.kernel.partition_spec == P(None, None, None, "model")
assert nd_layer.bias.value.shape == (3, 4)
assert nd_layer.bias.partition_spec == P(None, "model")
Check each layer’s parameter layout before copying a spec: convolution kernels also have spatial dimensions, and bilinear kernels have dimensions for both inputs before the output dimensions.
Inspect placement and keep data loading separate¶
For an ordinary array-backed parameter, parameter.value.sharding reports its
actual placement. array.addressable_shards exposes the shards accessible
from the current process, including their global indices and local arrays.
Replication can mean several devices hold the same global slice.
Use jax.device_put(array, NamedSharding(mesh, spec)) to explicitly place or
reshard an existing array. Updating parameter metadata alone does not move its
value.
Parameter specs do not automatically assign layouts to input batches. Place batches explicitly, as in the first example, and choose output layouts at the JIT boundary when your application requires them.
Likewise, DataLoader(shard_index=..., shard_count=...) partitions source
records between processes; it does not produce device-sharded JAX arrays.
These examples use one process with four devices. Multi-process execution also
requires JAX distributed initialization and correct construction of global
arrays from process-local data; logical-axis rules do not perform that setup.
See the SPMD API reference for mapping helpers and the data guide for input-pipeline sharding.