Phase 09 / Distributed training
Resilient distributed training
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 105 minutes · Logical CPU devices
Restore a complete training state with explicit target sharding. Compare next sample IDs, optimizer state and losses across an epoch boundary.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: Communication-efficient algorithms
Hardware: 4 logical CPU devices; target cluster execution unverified
Setup help: Python, notebook, or Cloud TPU VM
Use the tested CPU packages (the recorded environment uses Python 3.14.3). Other Python versions and accelerator environments require separate checks. The optional Cloud TPU specialization explains the target-specific execution boundaries. Logical CPU devices practice partitioning; they do not simulate TPU performance.
Download the repository ZIP or use your existing checkout. Extract it and open a terminal in the folder containing requirements-cpu.txt. The beginner ZIP is separate; these lessons use the full repository folder.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-cpu.txtpy -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-cpu.txtAfter activation, run the complete worked example from that same folder:
python phases/09-distributed/04-resilient-distributed-training/code/main.py.\.venv\Scripts\python.exe phases/09-distributed/04-resilient-distributed-training/code/main.pyFor an incremental build, create main.py in your own practice folder, append each build block in order, and run python main.py with this environment active. Keep that file separate from the downloaded reference solution.
Download this lesson’s notebook. Open it with a Python kernel using the course environment. Run cells from the top after restarting the kernel; experiments depend on earlier definitions.
For local Jupyter, first install the course requirements using the Local Python steps, then use your environment’s Python to start Jupyter:
python -m pip install jupyterlab
python -m jupyter lab.\.venv\Scripts\python.exe -m pip install jupyterlab
.\.venv\Scripts\python.exe -m jupyter labSelect the downloaded distributed-04.ipynb file in JupyterLab. The notebook contains reference solutions; make your prediction and attempt each practice task before opening them. Restart the kernel before changing logical device setup.
The worked reference for this lesson is CPU-validated. Some references explicitly configure logical CPU devices; setting JAX_PLATFORMS does not adapt them to a TPU. Follow the execution matrix and reviewed multi-host example before attempting a target run.
For accelerator preparation, study TPU setup, observed runtime receipts, checkpointed jobs and precision and retained traces. These target paths remain unqualified on physical TPU hardware until an actual receipt is recorded. Cloud provisioning is optional for CPU learning.
Check: On the four-device CPU fixture, restoration starts at completed step 1 with cursor 8. The next three sample-ID lists and complete states agree with uninterrupted execution. Reference losses are approximately 0.5070, 0.1964 and 0.1798. The real checkpoint path is printed.
Revisit the guided CPU setup · Open 19: TPU Setup. Package instructions are CPU instructions; Windows and accelerator environments need their own validation.
What you will be able to do
- Restore a complete training state with explicit target sharding.
- Compare next sample IDs, optimizer state and losses across an epoch boundary.
- Explain single-controller evidence and the additional requirements of a real multi-controller restart.
The problem
A restarted sharded run loads the right weights, yet its next update differs. Which missing state could explain that? We will checkpoint a real sharded training step, restore its parameters, momentum, random key and data position, then compare three subsequent updates across an epoch boundary.
The idea
Distributed recovery must reconstruct a consistent global state. Participants cannot independently load arbitrary local snapshots and assume the pieces describe one completed update. Checkpoint identity and step meaning must agree.
Agree on one global recovery boundary
Two local files from different steps can each load successfully while forming no valid global checkpoint. A shared manifest should identify which pieces belong together and what happens if a required piece is missing.
After restore, compare the next global computation with an uninterrupted reference, including input position and random-state ownership. Silent partial recovery changes the computation instead of continuing it.
The CPU replay checks this bounded fixture's continuation. It is separate from multi-host failure handling, storage durability or accelerator networking. Those require additional operational evidence when deploying the design.
Name the values that determine the next transition
The state contains weights , momentum , a random key, the current example permutation, the cursor into that permutation and the completed step. A batch is selected from the saved order, then a global mean gradient updates momentum and weights. After the final batch in an epoch, a new split key creates the next order. Saving only the current permutation can therefore reproduce the first resumed batch but fail at the next epoch.
Checkpoint one completed global step
The example partitions examples across four logical CPU devices and keeps its small parameter state replicated. It waits for the step loss, saves the entire state with a real Orbax StandardCheckpointer, waits for saving to finish and restores against a target whose arrays carry the intended mesh and sharding. Every restored leaf is compared with the saved snapshot.
The dataset is a deterministic tiny NumPy fixture. Every controller can reconstruct it from the same declared seed, dimensions and target rule. Real input systems need data identity, preprocessing configuration and iterator recovery, as covered in the recovery phase; matching a seed alone is not enough for a changing dataset. Changing the number of devices here changes fixture dimensions and is deliberately outside this restart contract.
Prove the next updates, including the next epoch
After restoring step one, run the uninterrupted and resumed states through three transitions. Compare sample IDs before comparing losses. Then compare every state leaf after each transition. The first resumed batch finishes the existing epoch, and the next transition draws a new permutation; this exercises saved random state as well as the current data cursor.
Each update also checks the global gradient with a separate NumPy formula. Agreement between two runs would not detect an identical bug in both runs. Independent arithmetic establishes the update; replay establishes the restore boundary. Float32 reduction order can vary, so numerical arrays use a stated tolerance while sample order must match exactly.
To check a real new process on CPU, save the assembled program as main.py, run it once and copy the printed checkpoint parent folder. Run COURSE_RESUME=1 COURSE_CHECKPOINT_DIR=/your/printed/folder python main.py from a new terminal process. This loads the existing file; it does not save a replacement. An incorrect path is rejected. Keep both reports.
Move the protocol to multiple controllers explicitly
The default run has one controller and four logical CPU devices. It does not kill a worker or test cross-host storage failure. The optional COURSE_MULTIPROCESS branch calls jax.distributed.initialize before any device query and builds a global mesh. Every controller must run the same script, take the same branches and call save and restore in the same order. The tiny fixture is intentionally duplicated on each host; the callback supplies only the slices required by each addressable shard.
On a supported existing multi-host TPU environment, give every worker the same new shared checkpoint directory and run the command below on every worker. The launcher must supply the environment that JAX uses for initialization. A path local to only one worker is not shared storage. For a fresh-process restart of this drill, run every controller again with COURSE_RESUME=1 and the same checkpoint directory. Resume skips saving and restores the existing accepted step-one snapshot; the script recomputes the deterministic step-one baseline only to verify this teaching fixture. A general training job must select and validate its own accepted step. Restart the whole worker group according to your job manager; this lesson does not implement elastic membership or automatic worker replacement. This target branch is executable but has not been run on a multi-host cluster here.
Distribute the script and launch across every worker of a multi-host TPU slice
# Distribute the script and launch across every worker of a multi-host TPU slice
gcloud compute tpus tpu-vm scp exercises/distributed-04.py $TPU_NAME:~/jax-tpu-lab/ --zone=$ZONE --worker=all
gcloud compute tpus tpu-vm ssh $TPU_NAME --zone=$ZONE --worker=all \
--command="COURSE_MULTIPROCESS=1 JAX_PLATFORMS=tpu COURSE_CHECKPOINT_DIR=/shared/new-recovery-run ~/jax-tpu-lab/.venv/bin/python ~/jax-tpu-lab/distributed-04.py"Expected:
All workers initialize distributed JAX, coordinate, save and restore through the same shared path, and report tpu with a process count greater than one. This command needs a configured multi-host TPU slice and shared storage.
Resume the same accepted checkpoint across every TPU slice worker
# Resume the same accepted checkpoint across every TPU slice worker
gcloud compute tpus tpu-vm ssh $TPU_NAME --zone=$ZONE --worker=all \
--command="COURSE_MULTIPROCESS=1 COURSE_RESUME=1 JAX_PLATFORMS=tpu COURSE_CHECKPOINT_DIR=/shared/new-recovery-run ~/jax-tpu-lab/.venv/bin/python ~/jax-tpu-lab/distributed-04.py"Expected:
The worker group loads its existing step-one checkpoint without overwriting it, then verifies subsequent updates across the epoch boundary.
Reject an inconsistent recovery point
An existing checkpoint directory is rejected to avoid overwriting evidence from a previous experiment. Use a new directory for each drill. In a production system, the accepted checkpoint also needs a matching code/configuration version and data manifest. A readable checkpoint with the wrong schema, data stream or step counter must not become a successful recovery report.
The controlled failures below replace only momentum or only the random key. The first changes the very next update; the second can remain hidden until an epoch transition. These are fault-injected restore experiments, not claims that a live multi-host process failure was tested. The operations project separately exercises real local worker termination and a restart runbook.
Restored updates reproduce the reference trajectory
Predict before looking: Where might two apparently equal restarts first diverge if the next epoch needs an unsaved random key?
Read the figure
The horizontal axis counts the three updates after the checkpoint, starting at . The vertical axis is the batch mean squared loss before each update. The uninterrupted and restored lines overlap at about , and on the recorded CPU fixture. These are different mini-batches, so the downward sequence should not be interpreted as a full-data convergence guarantee.
Connect it to the computation
The second plotted update begins a newly shuffled epoch. Its agreement exercises the restored key in addition to the saved order and cursor. The code checks sample IDs and complete state; line overlap alone would not establish those facts. The wrong-key practice deliberately reproduces the first batch and then diverges at the next shuffle. No device throughput or multi-host failure timing is shown.
Open full-size figureDownload data and provenance
The notebook contains the data experiment, plotting code, and recorded figure output. Rerun it to test your changes.
Where to put & run this code: Work in phases/09-distributed/04-resilient-distributed-training/code/main.py (or downloaded exercises/distributed-04.py / exercises/distributed-04.ipynb) and execute from the repository root with python3 phases/09-distributed/04-resilient-distributed-training/code/main.py.
1 Initialize the runtime before device access
Add this block after the preceding block in main.py and run the assembled file in a fresh process.
# Step 1 — Initialize the runtime before device access: Every controller must use consistent state, data and collective...
import os
import tempfile
from pathlib import Path
import jax
# Read `multi` from environment configuration (with a default fallback).
multi = os.environ.get('COURSE_MULTIPROCESS') == '1'
# Read `resume` from environment configuration (with a default fallback).
resume = os.environ.get('COURSE_RESUME') == '1'
if multi:
# Every controller runs this before querying devices or creating arrays.
jax.distributed.initialize()
else:
jax.config.update('jax_platforms', 'cpu')
jax.config.update('jax_num_cpu_devices', 4)
import jax.numpy as jnp
import numpy as np
import orbax.checkpoint as ocp
from jax.sharding import Mesh, NamedSharding, PartitionSpec as PCode building blocks in this step
Mesh + PartitionSpec + NamedSharding— Maps logical tensor axes onto physical device mesh axes for SPMD data, tensor, or pipeline parallelism.
Every controller must use consistent state, data and collective order; the default exercise runs on four logical CPU devices.
2 Define the global fixture and placements
Add this block after the preceding block in main.py and run the assembled file in a fresh process.
# Step 2 — Define the global fixture and placements: Every controller must use consistent state, data and collective...
count = jax.device_count()
assert count >= 2, 'Use at least two devices for the placement exercise'
# Configure multi-device placement / sharding specification (`mesh`).
mesh = Mesh(np.asarray(jax.devices()), ('data',))
# Configure multi-device placement / sharding specification (`rep`).
rep = NamedSharding(mesh, P())
# Configure multi-device placement / sharding specification (`row`).
row = NamedSharding(mesh, P('data', None))
# Configure multi-device placement / sharding specification (`label`).
label = NamedSharding(mesh, P('data'))
batch_size, size, width = 2 * count, 4 * count, 2 * count
# Identical tiny synthetic fixture on each controller; not distributed data ingestion.
data = np.random.default_rng(41).normal(size=(size, width)).astype(np.float32)
truth = np.linspace(-0.3, 0.4, width, dtype=np.float32)
# Perform matrix / vector contraction (`@`) to compute `targets`.
targets = data @ truthCode building blocks in this step
Mesh + PartitionSpec + NamedSharding— Maps logical tensor axes onto physical device mesh axes for SPMD data, tensor, or pipeline parallelism.
Every controller must use consistent state, data and collective order; the default exercise runs on four logical CPU devices.
3 Carry complete transition state
Add this block after the preceding block in main.py and run the assembled file in a fresh process.
# Step 3 — Carry complete transition state: Every controller must use consistent state, data and collective...
def placed(value, sharding=rep):
# Convert `a` to a host NumPy array for inspection or verification.
a = np.asarray(value)
return jax.make_array_from_callback(a.shape, sharding, lambda index: a[index])Code building blocks in this step
placed(...)— Callplacedwith your updated parameters or inputs from this lesson's workspace.np.asarray(...)— Callnp.asarraywith your updated parameters or inputs from this lesson's workspace.
Every controller must use consistent state, data and collective order; the default exercise runs on four logical CPU devices.
4 Checkpoint and verify replay
Add this block after the preceding block in main.py and run the assembled file in a fresh process.
# Step 4 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
def initial():
# Create or split explicit PRNG key(s) (`(key, order_key)`) for reproducible randomness.
key, order_key = jax.random.split(jax.random.PRNGKey(17))
return { 'weights': placed(np.zeros(width, np.float32)),
'momentum': placed(np.zeros(width, np.float32)),
'key': placed(np.asarray(key)),
'order': placed(np.asarray(jax.random.permutation(order_key, size))),
'cursor': placed(np.int32(0)), 'step': placed(np.int32(0)) }Code building blocks in this step
jax.random.PRNGKey(seed) & jax.random.split(key)— Manages explicit, stateless PRNG keys—always split a key before passing subkeys into independent random draws.
Every controller must use consistent state, data and collective order; the default exercise runs on four logical CPU devices.
5 Checkpoint and verify replay
Add this block after the preceding block in main.py and run the assembled file in a fresh process.
# Step 5 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
# Define and JIT-compile `update(w, velocity, x, y)` so XLA traces and fuses the operations:
@jax.jit
def update(w, velocity, x, y):
# Differentiate the objective to obtain `(loss, grad)` via automatic differentiation.
loss, grad = jax.value_and_grad(lambda a: jnp.mean((x @ a - y)**2))(w)
v = 0.8 * velocity + grad
return w - 0.04 * v, v, lossCode building blocks in this step
x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).jax.value_and_grad(loss_fn)(params, ...)— Evaluates both the scalar loss and its gradient PyTree(loss_val, grads)in a single forward+backward pass.jax.jit(fn) / @jax.jit— Tracesfnwith abstract shapes and compiles a fused XLA executable cached by input shape and dtype.
Every controller must use consistent state, data and collective order; the default exercise runs on four logical CPU devices.
6 Checkpoint and verify replay
Add this block after the preceding block in main.py and run the assembled file in a fresh process.
# Step 6 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
def transition(state):
cursor = int(state['cursor'])
# Convert `order` to a host NumPy array for inspection or verification.
order = np.asarray(state['order'])
key = state['key']
if cursor == size:
key, shuffle_key = jax.random.split(key)
order = np.asarray(jax.random.permutation(shuffle_key, size))
cursor = 0
ids = order[cursor:cursor + batch_size]
x, y = placed(data[ids], row), placed(targets[ids], label)
w, v, loss = update(state['weights'], state['momentum'], x, y)
# Synchronize host execution until asynchronous device computation completes.
loss.block_until_ready()
# Independent global derivative and momentum update on the host.
old_w, old_v = np.asarray(state['weights']), np.asarray(state['momentum'])
g = 2 * data[ids].T @ (data[ids] @ old_w - targets[ids]) / batch_size
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(v), 0.8 * old_v + g, rtol=2e-5, atol=2e-6)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(w), old_w - 0.04 * np.asarray(v), rtol=2e-5, atol=2e-6)
# Convert `new` to a host NumPy array for inspection or verification.
new = dict(weights=w, momentum=v, key=placed(np.asarray(key)),
order=placed(order), cursor=placed(np.int32(cursor+batch_size)),
step=placed(np.int32(int(state['step'])+1)))
return new, ids.copy(), float(loss)Code building blocks in this step
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jax.random.PRNGKey(seed) & jax.random.split(key)— Manages explicit, stateless PRNG keys—always split a key before passing subkeys into independent random draws.jax.block_until_ready(output)— Synchronizes with the accelerator/CPU device so asynchronous dispatch finishes before wall-clock timing.
Every controller must use consistent state, data and collective order; the default exercise runs on four logical CPU devices.
7 Checkpoint and verify replay
Add this block after the preceding block in main.py and run the assembled file in a fresh process.
# Step 7 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
state = initial()
state, first_ids, _ = transition(state)
# Shared path is mandatory for real multi-controller execution.
if multi:
root = Path(os.environ['COURSE_CHECKPOINT_DIR']).resolve()
else:
root = Path(os.environ['COURSE_CHECKPOINT_DIR']).resolve() if os.environ.get('COURSE_CHECKPOINT_DIR') else Path(tempfile.mkdtemp(prefix='jax-distributed-recovery-'))
checkpoint_path = root / 'step-one'
# Guard input contract (`resume and (not checkpoint_path.exists())`) and fail fast if violated.
if resume and not checkpoint_path.exists():
raise FileNotFoundError('Resume needs an existing step-one checkpoint')
# Guard input contract (`not resume and checkpoint_path.exists()`) and fail fast if violated.
if not resume and checkpoint_path.exists():
raise FileExistsError('Use a new folder or set COURSE_RESUME=1 for the accepted checkpoint')
# Enter `ocp.StandardCheckpointer()` context block:
with ocp.StandardCheckpointer() as checkpointer:
if not resume:
checkpointer.save(checkpoint_path, state)
checkpointer.wait_until_finished()
# Target carries the current mesh/sharding, rather than inferring it from old metadata.
restored = checkpointer.restore(checkpoint_path, target=initial())
for name in state:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_array_equal(np.asarray(restored[name]), np.asarray(state[name]))
baseline, resumed = state, restored
baseline_losses, resumed_losses, replay_ids = [], [], []
for _ in range(3):
baseline, ids_a, loss_a = transition(baseline)
resumed, ids_b, loss_b = transition(resumed)
# Execute `np.testing.assert_array_equal(ids_a, ids_b)`
np.testing.assert_array_equal(ids_a, ids_b)
for name in baseline:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(baseline[name]), np.asarray(resumed[name]), rtol=2e-5, atol=2e-6)
baseline_losses.append(loss_a)
resumed_losses.append(loss_b)
replay_ids.append(ids_a.tolist())
print('Recorded backend/processes/devices:', jax.default_backend(), jax.process_count(), count)
print('Restored step/cursor:', int(restored['step']), int(restored['cursor']))
print('Next sample IDs across epoch boundary:', replay_ids)
print('Uninterrupted losses:', baseline_losses)
print('Restored losses:', resumed_losses)
print('Checkpoint:', checkpoint_path)Code building blocks in this step
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
Every controller must use consistent state, data and collective order; the default exercise runs on four logical CPU devices.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — Initialize the runtime before device access: Every controller must use consistent state, data and collective...
import os
import tempfile
from pathlib import Path
import jax
# Read `multi` from environment configuration (with a default fallback).
multi = os.environ.get('COURSE_MULTIPROCESS') == '1'
# Read `resume` from environment configuration (with a default fallback).
resume = os.environ.get('COURSE_RESUME') == '1'
if multi:
# Every controller runs this before querying devices or creating arrays.
jax.distributed.initialize()
else:
jax.config.update('jax_platforms', 'cpu')
jax.config.update('jax_num_cpu_devices', 4)
import jax.numpy as jnp
import numpy as np
import orbax.checkpoint as ocp
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
# Step 2 — Define the global fixture and placements: Every controller must use consistent state, data and collective...
count = jax.device_count()
assert count >= 2, 'Use at least two devices for the placement exercise'
# Configure multi-device placement / sharding specification (`mesh`).
mesh = Mesh(np.asarray(jax.devices()), ('data',))
# Configure multi-device placement / sharding specification (`rep`).
rep = NamedSharding(mesh, P())
# Configure multi-device placement / sharding specification (`row`).
row = NamedSharding(mesh, P('data', None))
# Configure multi-device placement / sharding specification (`label`).
label = NamedSharding(mesh, P('data'))
batch_size, size, width = 2 * count, 4 * count, 2 * count
# Identical tiny synthetic fixture on each controller; not distributed data ingestion.
data = np.random.default_rng(41).normal(size=(size, width)).astype(np.float32)
truth = np.linspace(-0.3, 0.4, width, dtype=np.float32)
# Perform matrix / vector contraction (`@`) to compute `targets`.
targets = data @ truth
# Step 3 — Carry complete transition state: Every controller must use consistent state, data and collective...
def placed(value, sharding=rep):
# Convert `a` to a host NumPy array for inspection or verification.
a = np.asarray(value)
return jax.make_array_from_callback(a.shape, sharding, lambda index: a[index])
# Step 4 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
def initial():
# Create or split explicit PRNG key(s) (`(key, order_key)`) for reproducible randomness.
key, order_key = jax.random.split(jax.random.PRNGKey(17))
return { 'weights': placed(np.zeros(width, np.float32)),
'momentum': placed(np.zeros(width, np.float32)),
'key': placed(np.asarray(key)),
'order': placed(np.asarray(jax.random.permutation(order_key, size))),
'cursor': placed(np.int32(0)), 'step': placed(np.int32(0)) }
# Step 5 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
# Define and JIT-compile `update(w, velocity, x, y)` so XLA traces and fuses the operations:
@jax.jit
def update(w, velocity, x, y):
# Differentiate the objective to obtain `(loss, grad)` via automatic differentiation.
loss, grad = jax.value_and_grad(lambda a: jnp.mean((x @ a - y)**2))(w)
v = 0.8 * velocity + grad
return w - 0.04 * v, v, loss
# Step 6 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
def transition(state):
cursor = int(state['cursor'])
# Convert `order` to a host NumPy array for inspection or verification.
order = np.asarray(state['order'])
key = state['key']
if cursor == size:
key, shuffle_key = jax.random.split(key)
order = np.asarray(jax.random.permutation(shuffle_key, size))
cursor = 0
ids = order[cursor:cursor + batch_size]
x, y = placed(data[ids], row), placed(targets[ids], label)
w, v, loss = update(state['weights'], state['momentum'], x, y)
# Synchronize host execution until asynchronous device computation completes.
loss.block_until_ready()
# Independent global derivative and momentum update on the host.
old_w, old_v = np.asarray(state['weights']), np.asarray(state['momentum'])
g = 2 * data[ids].T @ (data[ids] @ old_w - targets[ids]) / batch_size
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(v), 0.8 * old_v + g, rtol=2e-5, atol=2e-6)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(w), old_w - 0.04 * np.asarray(v), rtol=2e-5, atol=2e-6)
# Convert `new` to a host NumPy array for inspection or verification.
new = dict(weights=w, momentum=v, key=placed(np.asarray(key)),
order=placed(order), cursor=placed(np.int32(cursor+batch_size)),
step=placed(np.int32(int(state['step'])+1)))
return new, ids.copy(), float(loss)
# Step 7 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
state = initial()
state, first_ids, _ = transition(state)
# Shared path is mandatory for real multi-controller execution.
if multi:
root = Path(os.environ['COURSE_CHECKPOINT_DIR']).resolve()
else:
root = Path(os.environ['COURSE_CHECKPOINT_DIR']).resolve() if os.environ.get('COURSE_CHECKPOINT_DIR') else Path(tempfile.mkdtemp(prefix='jax-distributed-recovery-'))
checkpoint_path = root / 'step-one'
# Guard input contract (`resume and (not checkpoint_path.exists())`) and fail fast if violated.
if resume and not checkpoint_path.exists():
raise FileNotFoundError('Resume needs an existing step-one checkpoint')
# Guard input contract (`not resume and checkpoint_path.exists()`) and fail fast if violated.
if not resume and checkpoint_path.exists():
raise FileExistsError('Use a new folder or set COURSE_RESUME=1 for the accepted checkpoint')
# Enter `ocp.StandardCheckpointer()` context block:
with ocp.StandardCheckpointer() as checkpointer:
if not resume:
checkpointer.save(checkpoint_path, state)
checkpointer.wait_until_finished()
# Target carries the current mesh/sharding, rather than inferring it from old metadata.
restored = checkpointer.restore(checkpoint_path, target=initial())
for name in state:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_array_equal(np.asarray(restored[name]), np.asarray(state[name]))
baseline, resumed = state, restored
baseline_losses, resumed_losses, replay_ids = [], [], []
for _ in range(3):
baseline, ids_a, loss_a = transition(baseline)
resumed, ids_b, loss_b = transition(resumed)
# Execute `np.testing.assert_array_equal(ids_a, ids_b)`
np.testing.assert_array_equal(ids_a, ids_b)
for name in baseline:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(baseline[name]), np.asarray(resumed[name]), rtol=2e-5, atol=2e-6)
baseline_losses.append(loss_a)
resumed_losses.append(loss_b)
replay_ids.append(ids_a.tolist())
print('Recorded backend/processes/devices:', jax.default_backend(), jax.process_count(), count)
print('Restored step/cursor:', int(restored['step']), int(restored['cursor']))
print('Next sample IDs across epoch boundary:', replay_ids)
print('Uninterrupted losses:', baseline_losses)
print('Restored losses:', resumed_losses)
print('Checkpoint:', checkpoint_path)Run the example
python3 phases/09-distributed/04-resilient-distributed-training/code/main.pyAbout this command
This shortcut uses an activated macOS/Linux course environment in the repository folder. For Windows, use the explicit interpreter command in “Setup help” above. The script includes worked examples and reference solutions.
Inspect the complete assembled example
# Step 1 — Initialize the runtime before device access: Every controller must use consistent state, data and collective...
import os
import tempfile
from pathlib import Path
import jax
# Read `multi` from environment configuration (with a default fallback).
multi = os.environ.get('COURSE_MULTIPROCESS') == '1'
# Read `resume` from environment configuration (with a default fallback).
resume = os.environ.get('COURSE_RESUME') == '1'
if multi:
# Every controller runs this before querying devices or creating arrays.
jax.distributed.initialize()
else:
jax.config.update('jax_platforms', 'cpu')
jax.config.update('jax_num_cpu_devices', 4)
import jax.numpy as jnp
import numpy as np
import orbax.checkpoint as ocp
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P
# Step 2 — Define the global fixture and placements: Every controller must use consistent state, data and collective...
count = jax.device_count()
assert count >= 2, 'Use at least two devices for the placement exercise'
# Configure multi-device placement / sharding specification (`mesh`).
mesh = Mesh(np.asarray(jax.devices()), ('data',))
# Configure multi-device placement / sharding specification (`rep`).
rep = NamedSharding(mesh, P())
# Configure multi-device placement / sharding specification (`row`).
row = NamedSharding(mesh, P('data', None))
# Configure multi-device placement / sharding specification (`label`).
label = NamedSharding(mesh, P('data'))
batch_size, size, width = 2 * count, 4 * count, 2 * count
# Identical tiny synthetic fixture on each controller; not distributed data ingestion.
data = np.random.default_rng(41).normal(size=(size, width)).astype(np.float32)
truth = np.linspace(-0.3, 0.4, width, dtype=np.float32)
# Perform matrix / vector contraction (`@`) to compute `targets`.
targets = data @ truth
# Step 3 — Carry complete transition state: Every controller must use consistent state, data and collective...
def placed(value, sharding=rep):
# Convert `a` to a host NumPy array for inspection or verification.
a = np.asarray(value)
return jax.make_array_from_callback(a.shape, sharding, lambda index: a[index])
# Step 4 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
def initial():
# Create or split explicit PRNG key(s) (`(key, order_key)`) for reproducible randomness.
key, order_key = jax.random.split(jax.random.PRNGKey(17))
return { 'weights': placed(np.zeros(width, np.float32)),
'momentum': placed(np.zeros(width, np.float32)),
'key': placed(np.asarray(key)),
'order': placed(np.asarray(jax.random.permutation(order_key, size))),
'cursor': placed(np.int32(0)), 'step': placed(np.int32(0)) }
# Step 5 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
# Define and JIT-compile `update(w, velocity, x, y)` so XLA traces and fuses the operations:
@jax.jit
def update(w, velocity, x, y):
# Differentiate the objective to obtain `(loss, grad)` via automatic differentiation.
loss, grad = jax.value_and_grad(lambda a: jnp.mean((x @ a - y)**2))(w)
v = 0.8 * velocity + grad
return w - 0.04 * v, v, loss
# Step 6 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
def transition(state):
cursor = int(state['cursor'])
# Convert `order` to a host NumPy array for inspection or verification.
order = np.asarray(state['order'])
key = state['key']
if cursor == size:
key, shuffle_key = jax.random.split(key)
order = np.asarray(jax.random.permutation(shuffle_key, size))
cursor = 0
ids = order[cursor:cursor + batch_size]
x, y = placed(data[ids], row), placed(targets[ids], label)
w, v, loss = update(state['weights'], state['momentum'], x, y)
# Synchronize host execution until asynchronous device computation completes.
loss.block_until_ready()
# Independent global derivative and momentum update on the host.
old_w, old_v = np.asarray(state['weights']), np.asarray(state['momentum'])
g = 2 * data[ids].T @ (data[ids] @ old_w - targets[ids]) / batch_size
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(v), 0.8 * old_v + g, rtol=2e-5, atol=2e-6)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(w), old_w - 0.04 * np.asarray(v), rtol=2e-5, atol=2e-6)
# Convert `new` to a host NumPy array for inspection or verification.
new = dict(weights=w, momentum=v, key=placed(np.asarray(key)),
order=placed(order), cursor=placed(np.int32(cursor+batch_size)),
step=placed(np.int32(int(state['step'])+1)))
return new, ids.copy(), float(loss)
# Step 7 — Checkpoint and verify replay: Every controller must use consistent state, data and collective...
state = initial()
state, first_ids, _ = transition(state)
# Shared path is mandatory for real multi-controller execution.
if multi:
root = Path(os.environ['COURSE_CHECKPOINT_DIR']).resolve()
else:
root = Path(os.environ['COURSE_CHECKPOINT_DIR']).resolve() if os.environ.get('COURSE_CHECKPOINT_DIR') else Path(tempfile.mkdtemp(prefix='jax-distributed-recovery-'))
checkpoint_path = root / 'step-one'
# Guard input contract (`resume and (not checkpoint_path.exists())`) and fail fast if violated.
if resume and not checkpoint_path.exists():
raise FileNotFoundError('Resume needs an existing step-one checkpoint')
# Guard input contract (`not resume and checkpoint_path.exists()`) and fail fast if violated.
if not resume and checkpoint_path.exists():
raise FileExistsError('Use a new folder or set COURSE_RESUME=1 for the accepted checkpoint')
# Enter `ocp.StandardCheckpointer()` context block:
with ocp.StandardCheckpointer() as checkpointer:
if not resume:
checkpointer.save(checkpoint_path, state)
checkpointer.wait_until_finished()
# Target carries the current mesh/sharding, rather than inferring it from old metadata.
restored = checkpointer.restore(checkpoint_path, target=initial())
for name in state:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_array_equal(np.asarray(restored[name]), np.asarray(state[name]))
baseline, resumed = state, restored
baseline_losses, resumed_losses, replay_ids = [], [], []
for _ in range(3):
baseline, ids_a, loss_a = transition(baseline)
resumed, ids_b, loss_b = transition(resumed)
# Execute `np.testing.assert_array_equal(ids_a, ids_b)`
np.testing.assert_array_equal(ids_a, ids_b)
for name in baseline:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(baseline[name]), np.asarray(resumed[name]), rtol=2e-5, atol=2e-6)
baseline_losses.append(loss_a)
resumed_losses.append(loss_b)
replay_ids.append(ids_a.tolist())
print('Recorded backend/processes/devices:', jax.default_backend(), jax.process_count(), count)
print('Restored step/cursor:', int(restored['step']), int(restored['cursor']))
print('Next sample IDs across epoch boundary:', replay_ids)
print('Uninterrupted losses:', baseline_losses)
print('Restored losses:', resumed_losses)
print('Checkpoint:', checkpoint_path)Expected result: On the four-device CPU fixture, restoration starts at completed step 1 with cursor 8. The next three sample-ID lists and complete states agree with uninterrupted execution. Reference losses are approximately 0.5070, 0.1964 and 0.1798. The real checkpoint path is printed.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
Recorded backend/processes/devices: cpu 1 4
Restored step/cursor: 1 8
Next sample IDs across epoch boundary: [[12, 5, 4, 1, 8, 13, 2, 11], [9, 0, 7, 15, 11, 13, 8, 14], [1, 6, 5, 4, 3, 12, 2, 10]]
Uninterrupted losses: [0.5069587826728821, 0.19637340307235718, 0.1798383891582489]
Restored losses: [0.5069587826728821, 0.19637340307235718, 0.1798383891582489]
Checkpoint: /var/folders/mp/_mq7srqx2y10p5nhjz9m7hj40000gn/T/jax-distributed-recovery-8l8pqueq/step-one
Recorded backend/processes/devices: cpu 1 4
Restored step/cursor: 1 8
Next sample IDs across epoch boundary: [[12, 5, 4, 1, 8, 13, 2, 11], [9, 0, 7, 15, 11, 13, 8, 14], [1, 6, 5, 4, 3, 12, 2, 10]]
Uninterrupted losses: [0.5069587826728821, 0.19637340307235718, 0.1798383891582489]
Restored losses: [0.5069587826728821, 0.19637340307235718, 0.1798383891582489]
Checkpoint: /var/folders/mp/_mq7srqx2y10p5nhjz9m7hj40000gn/T/jax-distributed-recovery-9h7rtdxi/step-one
Same next sample IDs, different weights after missing momentum: 0.0195157527923584
Five resumed transitions and all addressable parameter replicas agree.
Current-epoch batch matches; next-epoch batch exposes the wrong key.
PASS: distributed-04
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Keep weights but discard momentum
# Experiment — Keep weights but discard momentum: The data stream is correct in this injected failure.
incomplete = {**restored, 'momentum': placed(np.zeros(width, np.float32))}
wrong_next, wrong_ids, _ = transition(incomplete)
right_next, right_ids, _ = transition(restored)
# Execute `np.testing.assert_array_equal(wrong_ids, right_ids)`
np.testing.assert_array_equal(wrong_ids, right_ids)
# Aggregate array values to compute `weight_gap`.
weight_gap = float(np.max(np.abs(np.asarray(wrong_next['weights']) - np.asarray(right_next['weights']))))
assert weight_gap > 1e-5
print('Same next sample IDs, different weights after missing momentum:', weight_gap)Expected: The next sample IDs match but the updated weights differ.
The data stream is correct in this injected failure. The missing optimizer memory explains the different update, so restarting the data loader would not repair it.
Make it yours
Where to put your code: Append your solution at the bottom of phases/09-distributed/04-resilient-distributed-training/code/main.py (or inside the Make it yours cell of exercises/distributed-04.ipynb) and run python3 phases/09-distributed/04-resilient-distributed-training/code/main.py to verify your prediction before opening the reference solution.
Extend uninterrupted and restored execution by two additional steps. Verify sample IDs, complete state and every local parameter replica. Explain which comparison would fail if a single replica held a different parameter vector.
Show a writing guide and starter scaffold
How to write this exercise code: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
2. Step-by-step implementation plan:
- Execute
np.testing.assert_array_equal(ids_a, ids_b) - Convert
to a host NumPy array for inspection or verification. - Convert
to a host NumPy array for inspection or verification.
# Exercise solution: Extend uninterrupted and restored execution by two additional steps.
for _ in range(2):
baseline, ids_a, _ = transition(...) # TODO: compute baseline, ids_a, _
resumed, ids_b, _ = transition(...) # TODO: compute resumed, ids_b, _
# Execute `np.testing.assert_array_equal(ids_a, ids_b)`
np.testing.assert_array_equal(ids_a, ids_b)
for name in baseline:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(baseline[name]), np.asarray(resumed[name]), rtol = ... # TODO: compute np.testing.assert_allclose(np.asarray(baseline[name]), np.asarray(resumed[name]), rtol
for shard in resumed['weights'].addressable_shards:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(shard.data), np.asarray(baseline['weights']), rtol = ... # TODO: compute np.testing.assert_allclose(np.asarray(shard.data), np.asarray(baseline['weights']), rtol
assert int(resumed['step']) # TODO: complete assertion check
print('Five resumed transitions and all addressable parameter replicas agree.')
Reveal the reference solution
# Exercise solution: Extend uninterrupted and restored execution by two additional steps.
for _ in range(2):
baseline, ids_a, _ = transition(baseline)
resumed, ids_b, _ = transition(resumed)
# Execute `np.testing.assert_array_equal(ids_a, ids_b)`
np.testing.assert_array_equal(ids_a, ids_b)
for name in baseline:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(baseline[name]), np.asarray(resumed[name]), rtol=2e-5, atol=2e-6)
for shard in resumed['weights'].addressable_shards:
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(shard.data), np.asarray(baseline['weights']), rtol=2e-5, atol=2e-6)
assert int(resumed['step']) == 6
print('Five resumed transitions and all addressable parameter replicas agree.')Find a delayed random-state failure
Transfer
Restore the saved order and cursor but replace the saved key with a different key. Compare the first resumed batch and the following epoch’s first batch. Predict where replay will first fail.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jax.random.PRNGKey(seed) & jax.random.split(key)— Manages explicit, stateless PRNG keys—always split a key before passing subkeys into independent random draws.
2. Step-by-step implementation plan:
- Create or split explicit PRNG key(s) (
wrong_key_state) for reproducible randomness. - Execute
np.testing.assert_array_equal(ids_a, ids_b) - Convert
to a host NumPy array for inspection or verification.
# Find a delayed random-state failure (Transfer): A successful first resumed update is separate from future...
# Create or split explicit PRNG key(s) (`wrong_key_state`) for reproducible randomness.
wrong_key_state = ... # TODO: compute wrong_key_state
correct_a, ids_a, _ = transition(...) # TODO: compute correct_a, ids_a, _
wrong_a, ids_b, _ = transition(...) # TODO: compute wrong_a, ids_b, _
# Execute `np.testing.assert_array_equal(ids_a, ids_b)`
np.testing.assert_array_equal(ids_a, ids_b)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(correct_a['weights']), np.asarray(wrong_a['weights']), rtol = ... # TODO: compute np.testing.assert_allclose(np.asarray(correct_a['weights']), np.asarray(wrong_a['weights']), rtol
correct_b, ids_c, _ = transition(...) # TODO: compute correct_b, ids_c, _
wrong_b, ids_d, _ = transition(...) # TODO: compute wrong_b, ids_d, _
assert not np.array_equal(ids_c, ids_d) # TODO: complete assertion check
print('Current-epoch batch matches; next-epoch batch exposes the wrong key.')
Show a hint
The saved order supplies the remaining current-epoch batch. The key is consumed when the next permutation is generated.
Reveal reference solution and reasoning
# Find a delayed random-state failure (Transfer): A successful first resumed update is separate from future...
# Create or split explicit PRNG key(s) (`wrong_key_state`) for reproducible randomness.
wrong_key_state = {**restored, 'key': placed(np.asarray(jax.random.PRNGKey(999)))}
correct_a, ids_a, _ = transition(restored)
wrong_a, ids_b, _ = transition(wrong_key_state)
# Execute `np.testing.assert_array_equal(ids_a, ids_b)`
np.testing.assert_array_equal(ids_a, ids_b)
# Convert `` to a host NumPy array for inspection or verification.
np.testing.assert_allclose(np.asarray(correct_a['weights']), np.asarray(wrong_a['weights']), rtol=2e-5, atol=2e-6)
correct_b, ids_c, _ = transition(correct_a)
wrong_b, ids_d, _ = transition(wrong_a)
assert not np.array_equal(ids_c, ids_d)
print('Current-epoch batch matches; next-epoch batch exposes the wrong key.')A successful first resumed update is separate from future replay when later branches consume state that was not yet used. Crossing the epoch boundary reveals the changed key.
Diagnose the result
Matching IDs with differing weights suggests parameter or optimizer-state mismatch. Differing IDs suggests order, cursor, data identity or shuffle-key mismatch. On multiple hosts also inspect consistent configuration, shared checkpoint access and collective call order; a local CPU pass is distinct from those conditions.
Carry forward
- Restore parameters, optimizer, random and input-position state together.
- Compare more than one resumed update and cross a state-consuming boundary.
- Keep single-host correctness evidence separate from multi-host fault-tolerance evidence.
Keep your evidence
Keep the checkpoint path, saved/restored full state, subsequent sample IDs and losses through a new epoch. Diagnose missing momentum and a changed shuffle key; state the untested multi-host boundary.
Save your modified code, its output, and an explanation of your prediction. Checkpoint answers and exercise evidence are tracked separately.
Saved in this browser. Export or back up your portfolio anytime in My learning.
Verified on local CPU. Stage to a Cloud TPU VM via Cloud TPU setup to record accelerator execution receipts (TODO: add automated Cloud TPU CI execution receipts).
Primary references
Phase 09 Integration Project
Bring this phase together: A resilient sharded training experiment
Checkpoint to prove: Explain partitioning, communication costs, and recovery behavior with measured evidence.
This connected project also uses performance and distributed lessons. Work through its prerequisites before the full integration check; return here with the completed evidence. Copy projects/sharded-training/starter/model.py to projects/sharded-training/my_model.py and write your code in projects/sharded-training/my_model.py. Run python3 projects/sharded-training/tests/check.py --implementation projects/sharded-training/my_model.py --stage 1 from the top-level folder to verify stages 1, 2, 3, 4.