Phase 06 / Data & checkpoint recovery
Recover data position and resume
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 110 minutes · CPU practice
Persist model/optimizer/key and Grain iterator position at one boundary.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: Save model, optimizer, and random state
Hardware: CPU
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/06-recovery/04-recover-data-position-and-resume/code/main.py.\.venv\Scripts\python.exe phases/06-recovery/04-recover-data-position-and-resume/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 recovery-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: The next four ID batches and losses match; parameters, Adam state, continuation key and step agree at step six. One epoch boundary is crossed.
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
- Persist model/optimizer/key and Grain iterator position at one boundary.
- Replay across an epoch transition and compare every future batch.
- Diagnose reader-only and model-only restores.
- Reject changed pipeline configuration and state the tested recovery limits.
The problem
Let’s bring model recovery and data recovery together. We’ll interrupt training after two batches, save both the model state and the next-read position, then continue. Comparing the remaining batches with an uninterrupted run will show whether we really resumed at the same point, including across an epoch boundary.
The idea
Exact recovery joins numerical state and input-pipeline state at one completed update. Mixing boundaries can repeat or skip examples while producing a plausible training curve.
- Before update
model + optimizer + key + data positionIdentify the state needed to choose and process the next batch.
- Completed snapshot
save these values togetherWait for the local checkpoint operation to finish.
- After restore
same next batch → same next transitionCompare the restored transition with uninterrupted training.
This illustrates the lesson’s completed local snapshot boundary. It is verified separately from power-loss durability, asynchronous failure recovery or multi-host coordination.
Conceptual illustration. Verify the computation in the lesson’s Python experiments.
Align model time with data time
If saved weights include the C/D update but the iterator still points to C/D, recovery trains on that batch again. If the iterator is one batch too far ahead, it skips data. Both runs can continue without a shape error.
Draw one vertical boundary after an update and align parameters, optimizer, key and cursor with it. The next restored arrow must consume the same examples as the uninterrupted run.
When comparing loss trajectories, align measurement conventions too. A loss before an update and a loss after it describe different states. Compare IDs and state values alongside loss, because similar scalar losses can conceal different training histories.
One checkpoint, one completed-step boundary
Predict: Why reject a resume even when its first loss nearly matches the reference?
Read down the middle column: all state describes the same completed update. Across each row is its next use. An older cursor repeats data and a newer cursor skips it. The table shows logical state, not wall-clock timing.
Commit one logical boundary
consume retrieves a batch, computes an update and returns the updated training state. After two calls, the model step is two and the iterator points at batch three. We pack that iterator snapshot in the same Orbax payload as the model, Adam memory, random stream and contract.
A real prefetcher may prepare data ahead of consumption; the snapshot semantics must refer to the iterator boundary supported by its implementation. This tutorial uses a serial Grain loader and synchronous checkpointing, so no speculative workers or asynchronous outstanding writes complicate the boundary. Do not extrapolate this protocol to an arbitrary data queue without checking its restore contract.
read batch 1 → update 1
read batch 2 → update 2 → save {state_2,next_read_3}
restore → read batch 3 → update 3Persist opaque iterator bytes without editing them
Grain get_state returns bytes. We store them as a fixed-size uint8 buffer plus length so the Orbax restore target has a predictable shape. set_state receives the original byte sequence recovered from that buffer. The application does not inspect or patch Grain internal counters.
The buffer capacity is bytes and over-capacity snapshots fail. It is a teaching representation, not a general replacement for Grain checkpoint integration handlers. Restore is supported for this tested source/sampler/worker configuration. Changing those settings may invalidate a snapshot, so our contract also records seed, batch size, record count and source revision.
Audit the future, including an epoch transition
There are twelve records in each of two epochs and three four-example batches per epoch. We checkpoint after two batches. The remaining four batches are the final batch of epoch zero followed by all three batches of epoch one. Compare each future ID vector and each future loss in order, then compare every final state leaf.
Comparing only final loss can miss skipped or repeated observations, particularly on an easy dataset. Comparing only IDs can miss an optimizer/key reset. Comparing only weights can miss a count or random-state error that changes a subsequent step. The combined check targets all three failure classes.
epoch 0: batch 1 | batch 2 | checkpoint | batch 3
epoch 1: batch 4 | batch 5 | batch 6
replay compares IDs + losses + params + Adam + key + stepDefine the limits of this recovery claim
The payload contract contains dataset SHA256, source revision, counts, ordering configuration, update configuration, PRNG implementation and package versions. Refuse a mismatch rather than silently announcing a resumed run. This is intentionally stricter than claiming compatibility between arbitrary versions.
The local directory is temporary and the writer is synchronous. The lesson verifies readable files and CPU replay after reconstructing objects in the same process; it does not simulate power loss, a separate machine, multi-host orchestration, async interruption or remote storage behavior. Your portfolio report should state those limits and identify the next failure mode to test. Production use should adopt the library-supported combined handlers/manager and test its completion/recovery boundaries.
Loss trajectory parity: uninterrupted vs. checkpoint-resumed run
Predict before looking: Predict whether resuming training at step 2 with restored optimizer, iterator, and PRNG state matches an uninterrupted trajectory.
Read the figure
The uninterrupted and resumed loss curves overlap to machine precision across steps 3 through 6.
Connect it to the computation
Saving and restoring the step counter, PRNG key, data iterator state, and optimizer moments alongside model weights ensures exact continuation.
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/06-recovery/04-recover-data-position-and-resume/code/main.py (or downloaded exercises/recovery-04.py / exercises/recovery-04.ipynb) and execute from the repository root with python3 phases/06-recovery/04-recover-data-position-and-resume/code/main.py.
1 Prepare the data and state
Create main.py in your course workspace. Use the tested course environment and add this block first.
# Step 1 — Prepare the data and state: This setup makes the dataset and software assumptions explicit.
import jax
# Update state in place with the new values.
jax.config.update("jax_platforms","cpu")
import jax.numpy as jnp
import numpy as np
import optax
import orbax.checkpoint as ocp
import tempfile
import json
import hashlib
from pathlib import Path
# Configure or step the Optax optimizer state (`tx`).
tx = optax.adam(0.05)
def initial_state():
params = {"weight":jnp.array(0.,jnp.float32),"bias":jnp.array(0.,jnp.float32)}
return {"params":params,"optimizer":tx.init(params),
"key_data":jax.random.key_data(jax.random.key(7,impl="threefry2x32")),
"step":jnp.array(0,jnp.int32)}
def pack_bytes(raw,capacity=4096):
# Guard input contract (`len(raw) > capacity`) and fail fast if violated.
if len(raw)>capacity:
raise ValueError("Snapshot exceeds tutorial fixed-byte capacity")
# Allocate initialized array `buffer` with the specified shape and dtype.
buffer = np.zeros(capacity,dtype=np.uint8)
buffer[:len(raw)] = np.frombuffer(raw,dtype=np.uint8)
return {"buffer":buffer,"length":np.array(len(raw),dtype=np.int32)}
def unpack_bytes(packed):
length = int(packed["length"])
# Guard input contract (`not 0 <= length <= len(packed['buffer'])`) and fail fast if violated.
if not 0 <= length <= len(packed["buffer"]):
raise ValueError("Invalid snapshot length")
return np.asarray(packed["buffer"],dtype=np.uint8)[:length].tobytes()
def same_tree(left,right):
assert jax.tree.structure(left)==jax.tree.structure(right)
for a,b in zip(jax.tree.leaves(left),jax.tree.leaves(right)):
# Convert `(host_a, host_b)` to a host NumPy array for inspection or verification.
host_a,host_b = np.asarray(a),np.asarray(b)
# Check tensor shape invariant: `host_a.shape==host_b.shape and host_a.dtype==host_b.dtype`
assert host_a.shape==host_b.shape and host_a.dtype==host_b.dtype
if np.issubdtype(host_a.dtype,np.integer):
np.testing.assert_array_equal(host_a,host_b)
else:
np.testing.assert_allclose(host_a,host_b,rtol=1e-6,atol=1e-7)
# Define `step(state, x, y)` to evaluate the objective and its automatic derivatives:
def step(state,x,y):
# Sample deterministic random values into `key` using an explicit PRNG key.
key = jax.random.wrap_key_data(state["key_data"],impl="threefry2x32")
# Create or split explicit PRNG key(s) (`(next_key, sample_key)`) for reproducible randomness.
next_key,sample_key = jax.random.split(key)
# Sample deterministic random values into `target_noise` using an explicit PRNG key.
target_noise = 0.01*jax.random.normal(sample_key,y.shape)
def objective(params):
residual = params["weight"]*x+params["bias"]-(y+target_noise)
return jnp.mean(residual**2)
# Evaluate both scalar loss and parameter gradients in one pass (`(value, gradient)`).
value,gradient = jax.value_and_grad(objective)(state["params"])
updates,opt_state = tx.update(gradient,state["optimizer"],state["params"])
# Initialize explicit deterministic PRNG key `next_state`.
next_state = {"params":optax.apply_updates(state["params"],updates),
"optimizer":opt_state,"key_data":jax.random.key_data(next_key),
"step":state["step"]+1}
return next_state,value
def package_versions():
import importlib.metadata
return {name:importlib.metadata.version(name) for name in ("jax","numpy","optax","orbax-checkpoint")}
def encode_contract(contract):
return pack_bytes(json.dumps(contract,sort_keys=True).encode())
def validate_contract(saved,expected):
actual = json.loads(unpack_bytes(saved))
# Guard input contract (`actual != expected`) and fail fast if violated.
if actual != expected:
raise ValueError("Checkpoint contract differs: verify dataset, optimizer configuration, key implementation and package versions")
workspace = tempfile.TemporaryDirectory()
root = Path(workspace.name)
# Import required JAX, NumPy, and standard-library modules.
import numpy as np
import grain.python as grain
# Module/container class `TutorialSource` encapsulating model parameters and forward pass:
class TutorialSource:
def __init__(self,count=12):
self.count = count
def __len__(self):
return self.count
def __getitem__(self,index):
value = np.float32(index/10)
return {"id":np.int64(index),"x":value,"y":np.float32(2*value-1)}
def __repr__(self):
return f"TutorialSource(v1,n={self.count})"
def make_loader(seed=42,batch_size=4,count=12,drop_remainder=False):
return grain.DataLoader(
data_source=TutorialSource(count),
sampler=grain.IndexSampler(num_records=count,num_epochs=2,shuffle=True,seed=seed),
operations=[grain.Batch(batch_size,drop_remainder=drop_remainder)],
worker_count=0)Code building blocks in this step
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.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.
This setup makes the dataset and software assumptions explicit. No external dataset or accelerator is required.
2 Build the pipeline or state transition
Append this block to the same file; follow the named state objects through each function.
# Step 2 — Build the pipeline or state transition: The function boundaries expose which inputs determine the next...
def data_contract(seed=42):
source = TutorialSource()
values = np.array([[source[i]["x"],source[i]["y"]] for i in range(len(source))],dtype=np.float32)
return {"schema":1,"seed":seed,"batch_size":4,"count":12,"source":"TutorialSource(v1,n=12)",
"sha256":hashlib.sha256(values.tobytes()).hexdigest(),
"learning_rate":0.05,"optimizer":"adam","key_impl":"threefry2x32",
"versions":dict(package_versions(),grain=__import__("importlib.metadata",fromlist=["version"]).version("grain"))}
def consume(state,iterator,count):
losses=[]
orders=[]
for _ in range(count):
batch = next(iterator)
orders.append(batch["id"].copy())
# Create device-backed JAX array `(state, value)`.
state,value = step(state,jnp.asarray(batch["x"]),jnp.asarray(batch["y"]))
losses.append(float(value))
return state,np.array(losses),orders
contract = data_contract()
reference_iterator = iter(make_loader())
reference_state,_,_ = consume(initial_state(),reference_iterator,2)
payload = {"training":reference_state,"pipeline":pack_bytes(reference_iterator.get_state()),"contract":encode_contract(contract)}
resume_path = root/"completed_step_2"
# Enter `ocp.StandardCheckpointer()` context block:
with ocp.StandardCheckpointer() as cp:
cp.save(resume_path,payload)
cp.wait_until_finished()
restored_payload = cp.restore(resume_path,target={"training":initial_state(),"pipeline":pack_bytes(b""),"contract":encode_contract(contract)})
validate_contract(restored_payload["contract"],contract)
resumed_iterator = iter(make_loader())
resumed_iterator.set_state(unpack_bytes(restored_payload["pipeline"]))Code building blocks in this step
data_contract(...)— Calldata_contractwith your updated parameters or inputs from this lesson's workspace.TutorialSource(...)— CallTutorialSourcewith your updated parameters or inputs from this lesson's workspace.
The function boundaries expose which inputs determine the next output and which state must be preserved.
3 Run the comparison
Append the checks, save main.py and run python main.py. Predict what should agree before running.
# Step 3 — Run the comparison: The comparison checks the next behavior, not merely whether a save...
reference_final,reference_losses,reference_ids = consume(reference_state,reference_iterator,4)
resumed_final,resumed_losses,resumed_ids = consume(restored_payload["training"],resumed_iterator,4)
for a,b in zip(reference_ids,resumed_ids):np.testing.assert_array_equal(a,b)
np.testing.assert_allclose(reference_losses,resumed_losses,rtol=1e-6,atol=1e-7)
same_tree(reference_final,resumed_final)
assert int(resumed_final["step"])==6
print("Restored next four ID batches:",[a.tolist() for a in resumed_ids])
print("Restored next four losses:",resumed_losses.tolist())
print("Full state agrees at step six")Code building blocks in this step
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
The comparison checks the next behavior, not merely whether a save call succeeded.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — Prepare the data and state: This setup makes the dataset and software assumptions explicit.
import jax
# Update state in place with the new values.
jax.config.update("jax_platforms","cpu")
import jax.numpy as jnp
import numpy as np
import optax
import orbax.checkpoint as ocp
import tempfile
import json
import hashlib
from pathlib import Path
# Configure or step the Optax optimizer state (`tx`).
tx = optax.adam(0.05)
def initial_state():
params = {"weight":jnp.array(0.,jnp.float32),"bias":jnp.array(0.,jnp.float32)}
return {"params":params,"optimizer":tx.init(params),
"key_data":jax.random.key_data(jax.random.key(7,impl="threefry2x32")),
"step":jnp.array(0,jnp.int32)}
def pack_bytes(raw,capacity=4096):
# Guard input contract (`len(raw) > capacity`) and fail fast if violated.
if len(raw)>capacity:
raise ValueError("Snapshot exceeds tutorial fixed-byte capacity")
# Allocate initialized array `buffer` with the specified shape and dtype.
buffer = np.zeros(capacity,dtype=np.uint8)
buffer[:len(raw)] = np.frombuffer(raw,dtype=np.uint8)
return {"buffer":buffer,"length":np.array(len(raw),dtype=np.int32)}
def unpack_bytes(packed):
length = int(packed["length"])
# Guard input contract (`not 0 <= length <= len(packed['buffer'])`) and fail fast if violated.
if not 0 <= length <= len(packed["buffer"]):
raise ValueError("Invalid snapshot length")
return np.asarray(packed["buffer"],dtype=np.uint8)[:length].tobytes()
def same_tree(left,right):
assert jax.tree.structure(left)==jax.tree.structure(right)
for a,b in zip(jax.tree.leaves(left),jax.tree.leaves(right)):
# Convert `(host_a, host_b)` to a host NumPy array for inspection or verification.
host_a,host_b = np.asarray(a),np.asarray(b)
# Check tensor shape invariant: `host_a.shape==host_b.shape and host_a.dtype==host_b.dtype`
assert host_a.shape==host_b.shape and host_a.dtype==host_b.dtype
if np.issubdtype(host_a.dtype,np.integer):
np.testing.assert_array_equal(host_a,host_b)
else:
np.testing.assert_allclose(host_a,host_b,rtol=1e-6,atol=1e-7)
# Define `step(state, x, y)` to evaluate the objective and its automatic derivatives:
def step(state,x,y):
# Sample deterministic random values into `key` using an explicit PRNG key.
key = jax.random.wrap_key_data(state["key_data"],impl="threefry2x32")
# Create or split explicit PRNG key(s) (`(next_key, sample_key)`) for reproducible randomness.
next_key,sample_key = jax.random.split(key)
# Sample deterministic random values into `target_noise` using an explicit PRNG key.
target_noise = 0.01*jax.random.normal(sample_key,y.shape)
def objective(params):
residual = params["weight"]*x+params["bias"]-(y+target_noise)
return jnp.mean(residual**2)
# Evaluate both scalar loss and parameter gradients in one pass (`(value, gradient)`).
value,gradient = jax.value_and_grad(objective)(state["params"])
updates,opt_state = tx.update(gradient,state["optimizer"],state["params"])
# Initialize explicit deterministic PRNG key `next_state`.
next_state = {"params":optax.apply_updates(state["params"],updates),
"optimizer":opt_state,"key_data":jax.random.key_data(next_key),
"step":state["step"]+1}
return next_state,value
def package_versions():
import importlib.metadata
return {name:importlib.metadata.version(name) for name in ("jax","numpy","optax","orbax-checkpoint")}
def encode_contract(contract):
return pack_bytes(json.dumps(contract,sort_keys=True).encode())
def validate_contract(saved,expected):
actual = json.loads(unpack_bytes(saved))
# Guard input contract (`actual != expected`) and fail fast if violated.
if actual != expected:
raise ValueError("Checkpoint contract differs: verify dataset, optimizer configuration, key implementation and package versions")
workspace = tempfile.TemporaryDirectory()
root = Path(workspace.name)
# Import required JAX, NumPy, and standard-library modules.
import numpy as np
import grain.python as grain
# Module/container class `TutorialSource` encapsulating model parameters and forward pass:
class TutorialSource:
def __init__(self,count=12):
self.count = count
def __len__(self):
return self.count
def __getitem__(self,index):
value = np.float32(index/10)
return {"id":np.int64(index),"x":value,"y":np.float32(2*value-1)}
def __repr__(self):
return f"TutorialSource(v1,n={self.count})"
def make_loader(seed=42,batch_size=4,count=12,drop_remainder=False):
return grain.DataLoader(
data_source=TutorialSource(count),
sampler=grain.IndexSampler(num_records=count,num_epochs=2,shuffle=True,seed=seed),
operations=[grain.Batch(batch_size,drop_remainder=drop_remainder)],
worker_count=0)
# Step 2 — Build the pipeline or state transition: The function boundaries expose which inputs determine the next...
def data_contract(seed=42):
source = TutorialSource()
values = np.array([[source[i]["x"],source[i]["y"]] for i in range(len(source))],dtype=np.float32)
return {"schema":1,"seed":seed,"batch_size":4,"count":12,"source":"TutorialSource(v1,n=12)",
"sha256":hashlib.sha256(values.tobytes()).hexdigest(),
"learning_rate":0.05,"optimizer":"adam","key_impl":"threefry2x32",
"versions":dict(package_versions(),grain=__import__("importlib.metadata",fromlist=["version"]).version("grain"))}
def consume(state,iterator,count):
losses=[]
orders=[]
for _ in range(count):
batch = next(iterator)
orders.append(batch["id"].copy())
# Create device-backed JAX array `(state, value)`.
state,value = step(state,jnp.asarray(batch["x"]),jnp.asarray(batch["y"]))
losses.append(float(value))
return state,np.array(losses),orders
contract = data_contract()
reference_iterator = iter(make_loader())
reference_state,_,_ = consume(initial_state(),reference_iterator,2)
payload = {"training":reference_state,"pipeline":pack_bytes(reference_iterator.get_state()),"contract":encode_contract(contract)}
resume_path = root/"completed_step_2"
# Enter `ocp.StandardCheckpointer()` context block:
with ocp.StandardCheckpointer() as cp:
cp.save(resume_path,payload)
cp.wait_until_finished()
restored_payload = cp.restore(resume_path,target={"training":initial_state(),"pipeline":pack_bytes(b""),"contract":encode_contract(contract)})
validate_contract(restored_payload["contract"],contract)
resumed_iterator = iter(make_loader())
resumed_iterator.set_state(unpack_bytes(restored_payload["pipeline"]))
# Step 3 — Run the comparison: The comparison checks the next behavior, not merely whether a save...
reference_final,reference_losses,reference_ids = consume(reference_state,reference_iterator,4)
resumed_final,resumed_losses,resumed_ids = consume(restored_payload["training"],resumed_iterator,4)
for a,b in zip(reference_ids,resumed_ids):np.testing.assert_array_equal(a,b)
np.testing.assert_allclose(reference_losses,resumed_losses,rtol=1e-6,atol=1e-7)
same_tree(reference_final,resumed_final)
assert int(resumed_final["step"])==6
print("Restored next four ID batches:",[a.tolist() for a in resumed_ids])
print("Restored next four losses:",resumed_losses.tolist())
print("Full state agrees at step six")Run the example
python3 phases/06-recovery/04-recover-data-position-and-resume/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 — Prepare the data and state: This setup makes the dataset and software assumptions explicit.
import jax
# Update state in place with the new values.
jax.config.update("jax_platforms","cpu")
import jax.numpy as jnp
import numpy as np
import optax
import orbax.checkpoint as ocp
import tempfile
import json
import hashlib
from pathlib import Path
# Configure or step the Optax optimizer state (`tx`).
tx = optax.adam(0.05)
def initial_state():
params = {"weight":jnp.array(0.,jnp.float32),"bias":jnp.array(0.,jnp.float32)}
return {"params":params,"optimizer":tx.init(params),
"key_data":jax.random.key_data(jax.random.key(7,impl="threefry2x32")),
"step":jnp.array(0,jnp.int32)}
def pack_bytes(raw,capacity=4096):
# Guard input contract (`len(raw) > capacity`) and fail fast if violated.
if len(raw)>capacity:
raise ValueError("Snapshot exceeds tutorial fixed-byte capacity")
# Allocate initialized array `buffer` with the specified shape and dtype.
buffer = np.zeros(capacity,dtype=np.uint8)
buffer[:len(raw)] = np.frombuffer(raw,dtype=np.uint8)
return {"buffer":buffer,"length":np.array(len(raw),dtype=np.int32)}
def unpack_bytes(packed):
length = int(packed["length"])
# Guard input contract (`not 0 <= length <= len(packed['buffer'])`) and fail fast if violated.
if not 0 <= length <= len(packed["buffer"]):
raise ValueError("Invalid snapshot length")
return np.asarray(packed["buffer"],dtype=np.uint8)[:length].tobytes()
def same_tree(left,right):
assert jax.tree.structure(left)==jax.tree.structure(right)
for a,b in zip(jax.tree.leaves(left),jax.tree.leaves(right)):
# Convert `(host_a, host_b)` to a host NumPy array for inspection or verification.
host_a,host_b = np.asarray(a),np.asarray(b)
# Check tensor shape invariant: `host_a.shape==host_b.shape and host_a.dtype==host_b.dtype`
assert host_a.shape==host_b.shape and host_a.dtype==host_b.dtype
if np.issubdtype(host_a.dtype,np.integer):
np.testing.assert_array_equal(host_a,host_b)
else:
np.testing.assert_allclose(host_a,host_b,rtol=1e-6,atol=1e-7)
# Define `step(state, x, y)` to evaluate the objective and its automatic derivatives:
def step(state,x,y):
# Sample deterministic random values into `key` using an explicit PRNG key.
key = jax.random.wrap_key_data(state["key_data"],impl="threefry2x32")
# Create or split explicit PRNG key(s) (`(next_key, sample_key)`) for reproducible randomness.
next_key,sample_key = jax.random.split(key)
# Sample deterministic random values into `target_noise` using an explicit PRNG key.
target_noise = 0.01*jax.random.normal(sample_key,y.shape)
def objective(params):
residual = params["weight"]*x+params["bias"]-(y+target_noise)
return jnp.mean(residual**2)
# Evaluate both scalar loss and parameter gradients in one pass (`(value, gradient)`).
value,gradient = jax.value_and_grad(objective)(state["params"])
updates,opt_state = tx.update(gradient,state["optimizer"],state["params"])
# Initialize explicit deterministic PRNG key `next_state`.
next_state = {"params":optax.apply_updates(state["params"],updates),
"optimizer":opt_state,"key_data":jax.random.key_data(next_key),
"step":state["step"]+1}
return next_state,value
def package_versions():
import importlib.metadata
return {name:importlib.metadata.version(name) for name in ("jax","numpy","optax","orbax-checkpoint")}
def encode_contract(contract):
return pack_bytes(json.dumps(contract,sort_keys=True).encode())
def validate_contract(saved,expected):
actual = json.loads(unpack_bytes(saved))
# Guard input contract (`actual != expected`) and fail fast if violated.
if actual != expected:
raise ValueError("Checkpoint contract differs: verify dataset, optimizer configuration, key implementation and package versions")
workspace = tempfile.TemporaryDirectory()
root = Path(workspace.name)
# Import required JAX, NumPy, and standard-library modules.
import numpy as np
import grain.python as grain
# Module/container class `TutorialSource` encapsulating model parameters and forward pass:
class TutorialSource:
def __init__(self,count=12):
self.count = count
def __len__(self):
return self.count
def __getitem__(self,index):
value = np.float32(index/10)
return {"id":np.int64(index),"x":value,"y":np.float32(2*value-1)}
def __repr__(self):
return f"TutorialSource(v1,n={self.count})"
def make_loader(seed=42,batch_size=4,count=12,drop_remainder=False):
return grain.DataLoader(
data_source=TutorialSource(count),
sampler=grain.IndexSampler(num_records=count,num_epochs=2,shuffle=True,seed=seed),
operations=[grain.Batch(batch_size,drop_remainder=drop_remainder)],
worker_count=0)
# Step 2 — Build the pipeline or state transition: The function boundaries expose which inputs determine the next...
def data_contract(seed=42):
source = TutorialSource()
values = np.array([[source[i]["x"],source[i]["y"]] for i in range(len(source))],dtype=np.float32)
return {"schema":1,"seed":seed,"batch_size":4,"count":12,"source":"TutorialSource(v1,n=12)",
"sha256":hashlib.sha256(values.tobytes()).hexdigest(),
"learning_rate":0.05,"optimizer":"adam","key_impl":"threefry2x32",
"versions":dict(package_versions(),grain=__import__("importlib.metadata",fromlist=["version"]).version("grain"))}
def consume(state,iterator,count):
losses=[]
orders=[]
for _ in range(count):
batch = next(iterator)
orders.append(batch["id"].copy())
# Create device-backed JAX array `(state, value)`.
state,value = step(state,jnp.asarray(batch["x"]),jnp.asarray(batch["y"]))
losses.append(float(value))
return state,np.array(losses),orders
contract = data_contract()
reference_iterator = iter(make_loader())
reference_state,_,_ = consume(initial_state(),reference_iterator,2)
payload = {"training":reference_state,"pipeline":pack_bytes(reference_iterator.get_state()),"contract":encode_contract(contract)}
resume_path = root/"completed_step_2"
# Enter `ocp.StandardCheckpointer()` context block:
with ocp.StandardCheckpointer() as cp:
cp.save(resume_path,payload)
cp.wait_until_finished()
restored_payload = cp.restore(resume_path,target={"training":initial_state(),"pipeline":pack_bytes(b""),"contract":encode_contract(contract)})
validate_contract(restored_payload["contract"],contract)
resumed_iterator = iter(make_loader())
resumed_iterator.set_state(unpack_bytes(restored_payload["pipeline"]))
# Step 3 — Run the comparison: The comparison checks the next behavior, not merely whether a save...
reference_final,reference_losses,reference_ids = consume(reference_state,reference_iterator,4)
resumed_final,resumed_losses,resumed_ids = consume(restored_payload["training"],resumed_iterator,4)
for a,b in zip(reference_ids,resumed_ids):np.testing.assert_array_equal(a,b)
np.testing.assert_allclose(reference_losses,resumed_losses,rtol=1e-6,atol=1e-7)
same_tree(reference_final,resumed_final)
assert int(resumed_final["step"])==6
print("Restored next four ID batches:",[a.tolist() for a in resumed_ids])
print("Restored next four losses:",resumed_losses.tolist())
print("Full state agrees at step six")Expected result: The next four ID batches and losses match; parameters, Adam state, continuation key and step agree at step six. One epoch boundary is crossed.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
Restored next four ID batches: [[4, 10, 11, 3], [2, 0, 11, 1], [5, 7, 6, 8], [4, 3, 10, 9]]
Restored next four losses: [0.5496272444725037, 0.8390463590621948, 0.07169045507907867, 0.32427603006362915]
Full state agrees at step six
Restored next four ID batches: [[4, 10, 11, 3], [2, 0, 11, 1], [5, 7, 6, 8], [4, 3, 10, 9]]
Restored next four losses: [0.5496272444725037, 0.8390463590621948, 0.07169045507907867, 0.32427603006362915]
Full state agrees at step six
Missing iterator restore repeats earlier IDs and changes the next loss
Correct IDs alone do not restore the training transition
Rejected incompatible seed
Rejected incompatible sha256
Original run contract remains valid.
Expected batch-configuration mismatch
PASS: recovery-04
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Restart only the reader
# Experiment — Restart only the reader: A model step counter does not automatically seek a data iterator.
fresh = iter(make_loader())
wrong_state,wrong_losses,wrong_ids = consume(restored_payload["training"],fresh,1)
assert not np.array_equal(wrong_ids[0],reference_ids[0])
assert not np.isclose(wrong_losses[0],reference_losses[0],rtol=1e-6,atol=1e-7)
print("Missing iterator restore repeats earlier IDs and changes the next loss")Expected: The first batch repeats instead of the saved next batch; the next loss differs.
A model step counter does not automatically seek a data iterator.
Restore only the reader
# Experiment — Restore only the reader: Input replay and model-state replay are complementary...
reader_only = iter(make_loader())
reader_only.set_state(unpack_bytes(restored_payload["pipeline"]))
wrong_model,wrong_model_losses,correct_ids = consume(initial_state(),reader_only,1)
# Execute `np.testing.assert_array_equal(correct_ids[0],reference_ids[0`
np.testing.assert_array_equal(correct_ids[0],reference_ids[0])
assert not np.isclose(wrong_model_losses[0],reference_losses[0],rtol=1e-6,atol=1e-7)
assert int(wrong_model["step"])==1
print("Correct IDs alone do not restore the training transition")Expected: IDs agree but loss and step differ.
Input replay and model-state replay are complementary requirements; either alone can look superficially successful.
Make it yours
Where to put your code: Append your solution at the bottom of phases/06-recovery/04-recover-data-position-and-resume/code/main.py (or inside the Make it yours cell of exercises/recovery-04.ipynb) and run python3 phases/06-recovery/04-recover-data-position-and-resume/code/main.py to verify your prediction before opening the reference solution.
Repeat the protocol after one batch and compare the next five batches through step six. Save to a distinct directory.
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:
- Enter
ocp.StandardCheckpointer()context block:
# Exercise solution: Repeat the protocol after one batch and compare the next five batches...
other_iterator = iter(...) # TODO: compute other_iterator
other_state,_,_ = consume(...) # TODO: compute other_state,_,_
other_payload = ... # TODO: compute other_payload
# Enter `ocp.StandardCheckpointer()` context block:
with ocp.StandardCheckpointer() as cp:
other_path = Path(...) # TODO: compute other_path
cp.save(other_path,other_payload)
cp.wait_until_finished()
other_saved = cp.restore(...) # TODO: compute other_saved
other_resumed = iter(...) # TODO: compute other_resumed
other_resumed.set_state(unpack_bytes(other_saved["pipeline"]))
a,loss_a,ids_a = consume(...) # TODO: compute a,loss_a,ids_a
b,loss_b,ids_b = consume(...) # TODO: compute b,loss_b,ids_b
for l,r in zip(ids_a,ids_b):np.testing.assert_array_equal(l,r)
np.testing.assert_allclose(loss_a,loss_b,rtol = ... # TODO: compute np.testing.assert_allclose(loss_a,loss_b,rtol
same_tree(a,b)
assert int(b["step"]) # TODO: complete assertion check
Reveal the reference solution
# Exercise solution: Repeat the protocol after one batch and compare the next five batches...
other_iterator = iter(make_loader())
other_state,_,_ = consume(initial_state(),other_iterator,1)
other_payload = {"training":other_state,"pipeline":pack_bytes(other_iterator.get_state()),"contract":encode_contract(contract)}
# Enter `ocp.StandardCheckpointer()` context block:
with ocp.StandardCheckpointer() as cp:
other_path = Path(tempfile.mkdtemp(dir=root))/"completed_step_1"
cp.save(other_path,other_payload)
cp.wait_until_finished()
other_saved = cp.restore(other_path,target={"training":initial_state(),"pipeline":pack_bytes(b""),"contract":encode_contract(contract)})
other_resumed = iter(make_loader())
other_resumed.set_state(unpack_bytes(other_saved["pipeline"]))
a,loss_a,ids_a = consume(other_state,other_iterator,5)
b,loss_b,ids_b = consume(other_saved["training"],other_resumed,5)
for l,r in zip(ids_a,ids_b):np.testing.assert_array_equal(l,r)
np.testing.assert_allclose(loss_a,loss_b,rtol=1e-6,atol=1e-7)
same_tree(a,b)
assert int(b["step"])==6Reject an incompatible resume before consuming data
Transfer
Try the saved contract with a changed sampler seed and then a changed dataset fingerprint. Both must fail before training resumes. Verify that the unchanged contract still passes.
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:
data(...)— Calldatawith your updated parameters or inputs from this lesson's workspace.validate_contract(...)— Callvalidate_contractwith your updated parameters or inputs from this lesson's workspace.
2. Step-by-step implementation plan:
- Run the boundary check and catch the expected exception:
# Reject an incompatible resume before consuming data (Transfer): Exact state replay only makes sense under a compatible data...
for field,value in [('seed',43),('sha256','different-dataset')]:
incompatible = dict(...) # TODO: compute incompatible
# Run the boundary check and catch the expected exception:
try:validate_contract(restored_payload['contract'],incompatible)
except ValueError:print('Rejected incompatible',field)
else:raise AssertionError('Incompatible resume was accepted')
validate_contract(restored_payload['contract'],contract)
print('Original run contract remains valid.')
Show a hint
Use validate_contract on copies of the expected contract; do not rewrite the saved payload.
Reveal reference solution and reasoning
# Reject an incompatible resume before consuming data (Transfer): Exact state replay only makes sense under a compatible data...
for field,value in [('seed',43),('sha256','different-dataset')]:
incompatible=dict(contract,**{field:value})
# Run the boundary check and catch the expected exception:
try:validate_contract(restored_payload['contract'],incompatible)
except ValueError:print('Rejected incompatible',field)
else:raise AssertionError('Incompatible resume was accepted')
validate_contract(restored_payload['contract'],contract)
print('Original run contract remains valid.')Exact state replay only makes sense under a compatible data and update contract. Rejecting a mismatch before consuming a batch keeps an invalid resume from looking like a successful run.
Reject changed batch size before consuming
Challenge
Pretend the resumed application now uses batch size three. Reject the saved contract before a reader is used, then restore the original configuration and check the next IDs.
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:
consuming(...)— Callconsumingwith your updated parameters or inputs from this lesson's workspace.validate_contract(...)— Callvalidate_contractwith your updated parameters or inputs from this lesson's workspace.
2. Step-by-step implementation plan:
- Run the boundary check and catch the expected exception:
- Execute
np.testing.assert_array_equal(next(fixed_reader)["id"],refer
# Reject changed batch size before consuming (Challenge): Changing batching changes consumption semantics.
changed = dict(...) # TODO: compute changed
# Run the boundary check and catch the expected exception:
try:
validate_contract(restored_payload["contract"],changed)
except ValueError:
print("Expected batch-configuration mismatch")
else:
raise AssertionError("Expected incompatible restore rejection")
validate_contract(restored_payload["contract"],contract)
fixed_reader = iter(...) # TODO: compute fixed_reader
fixed_reader.set_state(unpack_bytes(restored_payload["pipeline"]))
# Execute `np.testing.assert_array_equal(next(fixed_reader)["id"],refer`
np.testing.assert_array_equal(next(fixed_reader)["id"],reference_ids[0])
Show a hint
The stored cursor is interpreted in a particular pipeline configuration.
Reveal reference solution and reasoning
# Reject changed batch size before consuming (Challenge): Changing batching changes consumption semantics.
changed = dict(contract,batch_size=3)
# Run the boundary check and catch the expected exception:
try:
validate_contract(restored_payload["contract"],changed)
except ValueError:
print("Expected batch-configuration mismatch")
else:
raise AssertionError("Expected incompatible restore rejection")
validate_contract(restored_payload["contract"],contract)
fixed_reader = iter(make_loader())
fixed_reader.set_state(unpack_bytes(restored_payload["pipeline"]))
# Execute `np.testing.assert_array_equal(next(fixed_reader)["id"],refer`
np.testing.assert_array_equal(next(fixed_reader)["id"],reference_ids[0])Changing batching changes consumption semantics. Refuse the mismatched configuration instead of assuming that training step two corresponds to the same next examples.
Diagnose the result
Changing batching changes consumption semantics. Refuse the mismatched configuration instead of assuming that training step two corresponds to the same next examples.
Carry forward
- Commit one logical boundary
- Persist opaque iterator bytes without editing them
- Audit the future, including an epoch transition
- Define the limits of this recovery claim
Keep your evidence
Keep the committed model/optimizer/key/iterator snapshot, uninterrupted and resumed batch-ID ledger across the epoch boundary, exact keys/counts, declared floating tolerances and inconsistent model-only/reader-only restart failures.
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).