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

Scan for Efficient Training and Checkpointing with Orbax

~11 min · training, jax, tutorial

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

jax.lax.scan is JAX's way of writing efficient loops that compile to a single XLA operation. Instead of unrolling a Python loop (which creates one copy of the computation per iteration), scan reuses the same compiled body. This is especially useful for training loops that run inside JIT.

import jax
import jax.numpy as jnp

# Instead of a Python loop for multiple gradient steps:
# for _ in range(100):
#     params, loss = train_step(params, batch)

# Use scan for a compiled loop:
def scan_train(params, opt_state, batches):
    """Run multiple training steps as a single compiled scan."""
    def step_fn(carry, batch):
        params, opt_state = carry
        x, y = batch

        def loss_fn(p):
            logits = forward(p, x)
            return jnp.mean(optax.softmax_cross_entropy_with_integer_labels(
                logits, y))

        loss, grads = jax.value_and_grad(loss_fn)(params)
        updates, opt_state = optimizer.update(grads, opt_state, params)
        params = optax.apply_updates(params, updates)
        return (params, opt_state), loss

    (final_params, final_opt_state), losses = jax.lax.scan(
        step_fn,
        init=(params, opt_state),
        xs=batches,  # stacked batch arrays: (num_steps, batch_size, ...)
    )
    return final_params, final_opt_state, losses

# This compiles to a SINGLE XLA while loop — much faster than
# running train_step in a Python for loop inside jit

Checkpointing with Orbax

Saving and restoring model parameters is essential for long training runs. Orbax is the official checkpointing library for JAX:

import orbax.checkpoint as ocp

# Save a checkpoint
checkpointer = ocp.StandardCheckpointer()

# Save parameters (works with any pytree)
save_path = '/tmp/my_model/step_1000'
checkpointer.save(save_path, params)

# Restore parameters
restored_params = checkpointer.restore(save_path)

# For Flax NNX models, save the state:
state = nnx.state(model)
checkpointer.save('/tmp/my_model/step_2000', state)

# Restore:
restored_state = checkpointer.restore('/tmp/my_model/step_2000')
nnx.update(model, restored_state)

# CheckpointManager for managing multiple checkpoints
manager = ocp.CheckpointManager(
    '/tmp/checkpoints',
    options=ocp.CheckpointManagerOptions(
        max_to_keep=3,           # keep only 3 most recent
        save_interval_steps=500, # save every 500 steps
    ),
)

# In training loop:
for step in range(10000):
    params, loss = train_step(params, batch)
    manager.save(step, args=ocp.args.StandardSave(params))

# Restore latest:
step = manager.latest_step()
params = manager.restore(step)

💡 Why This Matters

Scan-based training loops can be 2-10x faster than Python-loop-based ones for small models, because the entire loop compiles into a single XLA program with no Python overhead between steps. For large models, the difference is smaller (GPU computation dominates), but scan still eliminates host-device synchronization overhead. Orbax checkpointing is essential for any real training job — it supports async saving, sharded checkpoints for multi-device training, and automatic checkpoint management.

⚠️ Pure Function Check

When using jax.lax.scan, the loop body must be a pure function. You can't use Python side effects (printing, logging, mutating external lists) inside scan. Use jax.debug.print for debugging inside scan, and collect metrics as part of the scan output (the ys return value).

Code

import jax
import jax.numpy as jnp

# Instead of a Python loop for multiple gradient steps:
# for _ in range(100):
#     params, loss = train_step(params, batch)

# Use scan for a compiled loop:
def scan_train(params, opt_state, batches):
    """Run multiple training steps as a single compiled scan."""
    def step_fn(carry, batch):
        params, opt_state = carry
        x, y = batch

        def loss_fn(p):
            logits = forward(p, x)
            return jnp.mean(optax.softmax_cross_entropy_with_integer_labels(
                logits, y))

        loss, grads = jax.value_and_grad(loss_fn)(params)
        updates, opt_state = optimizer.update(grads, opt_state, params)
        params = optax.apply_updates(params, updates)
        return (params, opt_state), loss

    (final_params, final_opt_state), losses = jax.lax.scan(
        step_fn,
        init=(params, opt_state),
        xs=batches,  # stacked batch arrays: (num_steps, batch_size, ...)
    )
    return final_params, final_opt_state, losses

# This compiles to a SINGLE XLA while loop — much faster than
# running train_step in a Python for loop inside jit
import orbax.checkpoint as ocp

# Save a checkpoint
checkpointer = ocp.StandardCheckpointer()

# Save parameters (works with any pytree)
save_path = '/tmp/my_model/step_1000'
checkpointer.save(save_path, params)

# Restore parameters
restored_params = checkpointer.restore(save_path)

# For Flax NNX models, save the state:
state = nnx.state(model)
checkpointer.save('/tmp/my_model/step_2000', state)

# Restore:
restored_state = checkpointer.restore('/tmp/my_model/step_2000')
nnx.update(model, restored_state)

# CheckpointManager for managing multiple checkpoints
manager = ocp.CheckpointManager(
    '/tmp/checkpoints',
    options=ocp.CheckpointManagerOptions(
        max_to_keep=3,           # keep only 3 most recent
        save_interval_steps=500, # save every 500 steps
    ),
)

# In training loop:
for step in range(10000):
    params, loss = train_step(params, batch)
    manager.save(step, args=ocp.args.StandardSave(params))

# Restore latest:
step = manager.latest_step()
params = manager.restore(step)

External links

Exercise

Replace your Python for-loop trainer with jax.lax.scan over batches. Time before vs after for 1000 steps. Then wire Orbax to checkpoint params every 100 steps. Resume from a checkpoint and confirm continuity. The combination is what separates research scripts from production trainers.

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.