Sharding across devices¶
Sharding places different parts of an array on different devices. Write operations on the whole array; JAX runs them across its shards and handles communication between devices.
Taktiny uses JAX’s Mesh, PartitionSpec, and NamedSharding. Logical axis
names let a model describe its dimensions independently of a device layout.
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.
Each entry in a spec describes one array dimension. Its size must be divisible by the product of the sizes of its assigned mesh axes.
A complete four-device example¶
Try the following example on four CPU devices. Save the Python block 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. The flag exposes four logical CPU devices for testing.
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 rules apply when the initializer or parameter is created. The with block
restores the previous rules on exit. To move an existing array to a new layout,
use jax.device_put with the desired sharding.
When both arguments are supplied¶
axis_names takes precedence over partition_spec: the resolved logical
names replace the entire explicit spec, including 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. If no logical names match
any rules, every dimension resolves to None and the parameter is replicated
on the active mesh.
For an explicit layout, pass partition_spec on its own. To replicate a
parameter while keeping logical names, map those names to None when creating
it.
Rule order and scope¶
Preview a mapping with 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. Each mesh axis can partition one dimension of
an array, so the first available assignment wins. In the second example,
output claims model, leaving input unpartitioned. Give each named array
dimension a distinct logical name.
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. Set rules in each
thread or process that constructs parameters.
Calling map_logical_axis_names(...) without with prepends rules immediately.
Use set_logical_axis_rules(...) when you want to replace the full sequence.
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 uses
jax.jit(..., out_shardings=spec). The initialized parameter already has its
requested layout. The output layout describes the returned array; the
initializer may still use temporary storage while it runs.
Construct parameters inside the mesh context for placement at initialization.
Outside that context, the initializer returns an ordinary array while the
parameter can still retain its sharding metadata. Check parameter.value.sharding
to inspect the actual layout.
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.
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:
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 when choosing names: convolution kernels also have spatial dimensions, and bilinear kernels have dimensions for both inputs before the output dimensions.
Inspect and place arrays¶
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. For input batches, either use jax.device_put as
in the first example or configure DataLoader with axis_names or
partition_spec to place completed batches. Choose output layouts at the JIT
boundary when your application needs a particular layout.
DataLoader(shard_index=..., shard_count=...) assigns source records to
processes; its output-placement arguments control the resulting JAX arrays.
These examples use one process with four devices. For multi-process execution,
initialize JAX distributed execution and construct global arrays from the
process-local data.
See the SPMD API reference for mapping helpers and the data guide for input-pipeline sharding.