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

Debugging Under JIT and Sharding

~11 min · advanced, jax, tutorial

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

One of the most frustrating aspects of JAX is debugging JIT-compiled functions. Print statements inside @jax.jit only execute during tracing (once), not during actual computation. JAX provides special debugging tools to help.

jax.debug.print

import jax
import jax.numpy as jnp

@jax.jit
def buggy_function(x):
    # Regular print: only runs during tracing!
    print("This prints ONCE during tracing, not during execution")

    # jax.debug.print: runs during execution
    jax.debug.print("x = {x}", x=x)

    y = x ** 2
    jax.debug.print("y = {y}", y=y)

    # You can print inside conditions, scans, etc.
    z = jax.lax.cond(
        x.sum() > 0,
        lambda: (jax.debug.print("positive branch"), x * 2)[1],
        lambda: (jax.debug.print("negative branch"), x * -1)[1],
    )
    return z

result = buggy_function(jnp.array([1.0, -2.0, 3.0]))
# Prints:
# x = [1. -2. 3.]
# y = [1. 4. 9.]
# positive branch

jax.debug.breakpoint

@jax.jit
def function_with_breakpoint(x):
    y = jnp.sin(x)
    jax.debug.breakpoint()  # drops into pdb during execution
    z = jnp.cos(y)
    return z

# When this runs, you'll get an interactive pdb prompt
# where you can inspect y, x, etc.
# result = function_with_breakpoint(jnp.array(1.0))

Sharding and Multi-Device: Mesh, PartitionSpec, NamedSharding

For multi-device training, JAX uses a sharding system based on device meshes:

import jax
from jax.sharding import Mesh, PartitionSpec as P, NamedSharding
from jax.experimental import mesh_utils
import jax.numpy as jnp

# Create a mesh of devices
devices = jax.devices()  # all available devices
mesh = Mesh(devices, axis_names=('data',))

# Or for multi-dimensional parallelism:
# devices = mesh_utils.create_device_mesh((2, 4))
# mesh = Mesh(devices, axis_names=('data', 'model'))

# Shard data across the 'data' axis
data_sharding = NamedSharding(mesh, P('data'))

# Replicate across all devices
replicated = NamedSharding(mesh, P())

# Note: jax.P is a convenient alias for PartitionSpec (since JAX 0.7.0)
data_sharding_v2 = NamedSharding(mesh, jax.P('data'))

# Place data on devices with sharding
x = jax.device_put(jnp.ones((1024, 256)), data_sharding)

# JIT with output sharding
@jax.jit
def forward(params, x):
    return x @ params

# Shard params: replicate across data axis
params = jax.device_put(jnp.ones((256, 128)), replicated)
output = forward(params, x)
print(output.sharding)  # shows how output is distributed

💡 Why This Matters

Sharding is how JAX scales to multi-GPU and multi-TPU training. The Mesh abstraction maps logical parallelism axes (like "data" and "model") to physical devices. PartitionSpec describes how each array dimension maps to mesh axes. Unlike PyTorch's DistributedDataParallel, JAX's sharding is declarative — you say how you want data distributed, and the XLA compiler figures out the communication. JAX 0.7.0 migrated from GSPMD to Shardy for sharding implementation.

# Mixed precision is straightforward in JAX
@jax.jit
def train_step_bf16(params, x, y):
    def loss_fn(params):
        # Cast to bfloat16 for the forward pass
        x_bf16 = x.astype(jnp.bfloat16)
        logits = forward_bf16(params, x_bf16)
        return jnp.mean((logits.astype(jnp.float32) - y) ** 2)

    loss, grads = jax.value_and_grad(loss_fn)(params)
    # Gradients are in float32 — optimizer updates in full precision
    return loss, grads

Code

import jax
import jax.numpy as jnp

@jax.jit
def buggy_function(x):
    # Regular print: only runs during tracing!
    print("This prints ONCE during tracing, not during execution")

    # jax.debug.print: runs during execution
    jax.debug.print("x = {x}", x=x)

    y = x ** 2
    jax.debug.print("y = {y}", y=y)

    # You can print inside conditions, scans, etc.
    z = jax.lax.cond(
        x.sum() > 0,
        lambda: (jax.debug.print("positive branch"), x * 2)[1],
        lambda: (jax.debug.print("negative branch"), x * -1)[1],
    )
    return z

result = buggy_function(jnp.array([1.0, -2.0, 3.0]))
# Prints:
# x = [1. -2. 3.]
# y = [1. 4. 9.]
# positive branch
@jax.jit
def function_with_breakpoint(x):
    y = jnp.sin(x)
    jax.debug.breakpoint()  # drops into pdb during execution
    z = jnp.cos(y)
    return z

# When this runs, you'll get an interactive pdb prompt
# where you can inspect y, x, etc.
# result = function_with_breakpoint(jnp.array(1.0))
import jax
from jax.sharding import Mesh, PartitionSpec as P, NamedSharding
from jax.experimental import mesh_utils
import jax.numpy as jnp

# Create a mesh of devices
devices = jax.devices()  # all available devices
mesh = Mesh(devices, axis_names=('data',))

# Or for multi-dimensional parallelism:
# devices = mesh_utils.create_device_mesh((2, 4))
# mesh = Mesh(devices, axis_names=('data', 'model'))

# Shard data across the 'data' axis
data_sharding = NamedSharding(mesh, P('data'))

# Replicate across all devices
replicated = NamedSharding(mesh, P())

# Note: jax.P is a convenient alias for PartitionSpec (since JAX 0.7.0)
data_sharding_v2 = NamedSharding(mesh, jax.P('data'))

# Place data on devices with sharding
x = jax.device_put(jnp.ones((1024, 256)), data_sharding)

# JIT with output sharding
@jax.jit
def forward(params, x):
    return x @ params

# Shard params: replicate across data axis
params = jax.device_put(jnp.ones((256, 128)), replicated)
output = forward(params, x)
print(output.sharding)  # shows how output is distributed
# Mixed precision is straightforward in JAX
@jax.jit
def train_step_bf16(params, x, y):
    def loss_fn(params):
        # Cast to bfloat16 for the forward pass
        x_bf16 = x.astype(jnp.bfloat16)
        logits = forward_bf16(params, x_bf16)
        return jnp.mean((logits.astype(jnp.float32) - y) ** 2)

    loss, grads = jax.value_and_grad(loss_fn)(params)
    # Gradients are in float32 — optimizer updates in full precision
    return loss, grads

External links

Exercise

Take a jit'd function with a subtle bug (off-by-one in indexing). Use jax.debug.print to inspect intermediate shapes/values without breaking the trace. Then set jax.debug.breakpoint() and step through. The debugging story under jit is its own skill.

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.