C.W.K.
Stream
Lesson 05 of 05 · published

Practical Example: Data-Parallel Training Pattern

~12 min · pmap, jax, tutorial

Level 0Curious
0 XP0/73 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

Let's build a complete data-parallel training example that works on any number of devices. We'll show both the pmap approach and the modern sharding approach for comparison.

Approach 1: pmap-based Data Parallel Training

import jax
import jax.numpy as jnp

# Model: simple 2-layer network
def init_params(key, in_dim, hidden_dim, out_dim):
    k1, k2 = jax.random.split(key)
    return {
        'w1': jax.random.normal(k1, (in_dim, hidden_dim)) * 0.1,
        'b1': jnp.zeros(hidden_dim),
        'w2': jax.random.normal(k2, (hidden_dim, out_dim)) * 0.1,
        'b2': jnp.zeros(out_dim),
    }

def forward(params, x):
    h = jnp.tanh(x @ params['w1'] + params['b1'])
    return h @ params['w2'] + params['b2']

def loss_fn(params, x, y):
    preds = forward(params, x)
    return jnp.mean((preds - y) ** 2)

# Data-parallel training step
def train_step(params, x, y):
    loss, grads = jax.value_and_grad(loss_fn)(params, x, y)
    # Synchronize across devices
    grads = jax.lax.pmean(grads, axis_name='i')
    loss = jax.lax.pmean(loss, axis_name='i')
    # SGD update
    params = jax.tree.map(lambda p, g: p - 0.01 * g, params, grads)
    return params, loss

p_train_step = jax.pmap(train_step, axis_name='i')

# Initialize
n_devices = jax.device_count()
params = init_params(jax.random.PRNGKey(0), 10, 32, 1)
replicated_params = jax.tree.map(lambda x: jnp.stack([x] * n_devices), params)

# Generate data (sharded across devices)
key = jax.random.PRNGKey(1)
X = jax.random.normal(key, (n_devices * 64, 10))
y = jax.random.normal(jax.random.PRNGKey(2), (n_devices * 64, 1))
X = X.reshape(n_devices, 64, 10)
y = y.reshape(n_devices, 64, 1)

# Train
for step in range(50):
    replicated_params, losses = p_train_step(replicated_params, X, y)
    if step % 10 == 0:
        print(f"Step {step}: Loss = {losses[0]:.6f}")

Approach 2: Modern Sharding (Recommended for New Code)

import jax
import jax.numpy as jnp

# Create mesh and shardings
mesh = jax.make_mesh((jax.device_count(),), ('dp',))
data_sharding = jax.NamedSharding(mesh, jax.P('dp'))
replicated_sharding = jax.NamedSharding(mesh, jax.P())

# Initialize params (replicated across all devices)
params = init_params(jax.random.PRNGKey(0), 10, 32, 1)
params = jax.device_put(params, replicated_sharding)

# Shard data across the data-parallel axis
X = jax.random.normal(jax.random.PRNGKey(1), (256, 10))
y = jax.random.normal(jax.random.PRNGKey(2), (256, 1))
X = jax.device_put(X, data_sharding)
y = jax.device_put(y, data_sharding)

# Training step — just use jit! The sharding handles distribution.
@jax.jit
def train_step_sharded(params, x, y):
    loss, grads = jax.value_and_grad(loss_fn)(params, x, y)
    params = jax.tree.map(lambda p, g: p - 0.01 * g, params, grads)
    return params, loss

# Train — XLA handles the all-reduce automatically
for step in range(50):
    params, loss = train_step_sharded(params, X, y)
    if step % 10 == 0:
        print(f"Step {step}: Loss = {loss:.6f}")

Notice how much simpler the sharding version is: no manual reshaping, no explicit pmean, no replication boilerplate. You specify how data and params are distributed, and XLA's compiler figures out the communication.

💡 Why This Matters

The modern sharding approach represents JAX's direction: let the compiler handle communication. By declaring what you want (this data sharded here, these params replicated there), you let XLA figure out how to make it happen efficiently. This approach scales more naturally to complex parallelism strategies (tensor parallelism, pipeline parallelism, expert parallelism) that would be very difficult to express with pmap alone.

For production distributed training in the JAX ecosystem, libraries like Flax NNX and training cookbooks provide higher-level abstractions over these sharding primitives, but they all build on the same Mesh + PartitionSpec + NamedSharding foundation.

Code

import jax
import jax.numpy as jnp

# Model: simple 2-layer network
def init_params(key, in_dim, hidden_dim, out_dim):
    k1, k2 = jax.random.split(key)
    return {
        'w1': jax.random.normal(k1, (in_dim, hidden_dim)) * 0.1,
        'b1': jnp.zeros(hidden_dim),
        'w2': jax.random.normal(k2, (hidden_dim, out_dim)) * 0.1,
        'b2': jnp.zeros(out_dim),
    }

def forward(params, x):
    h = jnp.tanh(x @ params['w1'] + params['b1'])
    return h @ params['w2'] + params['b2']

def loss_fn(params, x, y):
    preds = forward(params, x)
    return jnp.mean((preds - y) ** 2)

# Data-parallel training step
def train_step(params, x, y):
    loss, grads = jax.value_and_grad(loss_fn)(params, x, y)
    # Synchronize across devices
    grads = jax.lax.pmean(grads, axis_name='i')
    loss = jax.lax.pmean(loss, axis_name='i')
    # SGD update
    params = jax.tree.map(lambda p, g: p - 0.01 * g, params, grads)
    return params, loss

p_train_step = jax.pmap(train_step, axis_name='i')

# Initialize
n_devices = jax.device_count()
params = init_params(jax.random.PRNGKey(0), 10, 32, 1)
replicated_params = jax.tree.map(lambda x: jnp.stack([x] * n_devices), params)

# Generate data (sharded across devices)
key = jax.random.PRNGKey(1)
X = jax.random.normal(key, (n_devices * 64, 10))
y = jax.random.normal(jax.random.PRNGKey(2), (n_devices * 64, 1))
X = X.reshape(n_devices, 64, 10)
y = y.reshape(n_devices, 64, 1)

# Train
for step in range(50):
    replicated_params, losses = p_train_step(replicated_params, X, y)
    if step % 10 == 0:
        print(f"Step {step}: Loss = {losses[0]:.6f}")
import jax
import jax.numpy as jnp

# Create mesh and shardings
mesh = jax.make_mesh((jax.device_count(),), ('dp',))
data_sharding = jax.NamedSharding(mesh, jax.P('dp'))
replicated_sharding = jax.NamedSharding(mesh, jax.P())

# Initialize params (replicated across all devices)
params = init_params(jax.random.PRNGKey(0), 10, 32, 1)
params = jax.device_put(params, replicated_sharding)

# Shard data across the data-parallel axis
X = jax.random.normal(jax.random.PRNGKey(1), (256, 10))
y = jax.random.normal(jax.random.PRNGKey(2), (256, 1))
X = jax.device_put(X, data_sharding)
y = jax.device_put(y, data_sharding)

# Training step — just use jit! The sharding handles distribution.
@jax.jit
def train_step_sharded(params, x, y):
    loss, grads = jax.value_and_grad(loss_fn)(params, x, y)
    params = jax.tree.map(lambda p, g: p - 0.01 * g, params, grads)
    return params, loss

# Train — XLA handles the all-reduce automatically
for step in range(50):
    params, loss = train_step_sharded(params, X, y)
    if step % 10 == 0:
        print(f"Step {step}: Loss = {loss:.6f}")

External links

Exercise

Wire up a complete data-parallel example: model + loss + optimizer + train loop, sharded across all available devices. Train for 200 steps on synthetic data. Save the final checkpoint with Orbax. The full pipeline is the deliverable.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.