Phase 07 / Transformers & language models
Train, checkpoint, and generate
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 120 minutes · CPU practice
Train a tiny causal model, restore it, and generate from a fixed seed.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: A Transformer block and mixed precision
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/07-transformers/04-train-checkpoint-and-generate/code/main.py.\.venv\Scripts\python.exe phases/07-transformers/04-train-checkpoint-and-generate/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 transformers-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: Held-out loss below and accuracy above for the periodic fixture. Snapshot next-step replay agrees. Greedy continuation: abcabcabcabc. Exact loss depends on the tested backend.
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
- Assemble the token-to-logit contract
- Separate a learning experiment from a benchmark
- Checkpoint the continuation state and its schema
- Generate with an explicit decoding policy
- Use failures to test the experiment boundary
The problem
Let’s connect the pieces into a tiny next-token model. We’ll use a repeating three-character sequence, so the targets and expected continuations are easy to inspect. You’ll train, save, resume one matching step, and generate tokens. Success on this controlled sequence does not demonstrate general language ability.
The idea
Training provides known shifted targets. Generation predicts a next-token distribution and applies a decoding rule, then feeds the chosen token back as context. These two procedures use the model for different jobs.
Separate teacher-forced targets from generated context
For probabilities , greedy decoding selects the first token. Sampling may select another nonzero-probability token. That different choice changes the context and therefore the distribution at the following step.
Read each row of the probability figure before interpreting the generated string. Confidence on the fixture's target shows behavior on this small task; it is separate from broad language ability.
Keep tokenizer identity, context limit and termination rules with the checkpoint. Correct weights with a changed token-to-ID mapping define a different system. The text harness adds explicit cache behavior later; do not infer that this lesson's simple generation loop already implements it.
Assemble the token-to-logit contract
Each token ID indexes one row of a embedding table. Add a position row, apply the single-head causal block, normalize and project to three vocabulary logits. vmap applies this per-sequence function over the batch. The loss averages token cross entropy over both example and position axes.
For a uniform three-class prediction, negative log probability is log(3), about , regardless of the target class. Zeroing the head provides a known check of that objective. This reduction assumes all positions are valid; padding and packing would require the masks and eligible-token reduction taught in the preceding lesson.
IDs (B, T) → embeddings (B, T, 8)
+ positions → causal block → logits (B, T, 3)
shifted targets (B, T) → mean cross entropy → scalarSeparate a learning experiment from a benchmark
The training string repeats abc; the held-out string begins with bca. Splitting strings makes the evaluation call distinct, but repeated periodic contexts mean that the split is separate from unseen-context generalization. The expected greedy sequence follows a known cycle, so it is a correctness fixture.
Record initial loss, final held-out loss and accuracy, and inspect target alignment by hand. A model could learn this rule with a simpler architecture. This exercise demonstrates the Transformer training plumbing; it does not demonstrate that attention is necessary for the task or that the model understands language.
Checkpoint the continuation state and its schema
Adam carries count and moment estimates. Saving only parameters loses those values; even an equal initial loss after restart cannot prove that the next update will match. Save parameter and optimizer leaves, step, vocabulary and context. Recreate the expected tree structure from the same model and optimizer schema when loading, and reject mismatched leaf counts, shapes or dtypes.
The example writes numeric NPZ arrays with pickle disabled and a JSON manifest in a temporary directory, then checks the next transition against the uninterrupted state. The files are deliberately cleaned up after this smoke exercise. To keep learner evidence, change the directory to a persistent experiment folder. This two-file format is not atomic crash recovery; the data/recovery phase teaches stronger failure boundaries.
Generate with an explicit decoding policy
For greedy generation, take the last token’s logits, choose argmax, append that token and repeat. Keep at most four tokens as the next context. There is no sampling key because this decoding policy is deterministic. Ties follow the argmax implementation; temperature and stochastic sampling would change the policy.
Recomputing the whole window is simple but inefficient for a large model. This lesson does not implement a key/value cache or serving scheduler. Unknown characters and empty prompts must fail clearly instead of silently becoming another vocabulary ID.
Use failures to test the experiment boundary
Zero logits should give log(3); a future-token perturbation should not affect earlier logits; a mismatched checkpoint vocabulary should be rejected; and an empty prompt should fail. These cases test different contracts, so passing one is verified separately from the others.
Keep the corpus, vocabulary, context, model seed, optimizer settings, package versions and numeric outputs with your work. The deterministic fixture has no shuffled data position or dropout key after initialization; a production resume must preserve those additional states when present.
Inspect next-token probabilities before greedy decoding
Predict before looking: Which token should follow each position in the held-out context?
Read the figure
Each row is a position in the held-out context, and columns are the possible next tokens: a, b, and c. The cells contain next-token probabilities, with darker purple indicating larger probability. Each row sums to approximately ; displayed numbers are rounded.
Read the darkest column in each row: c, a, b, then c. Their probabilities are all above in this run, while the other entries are close to zero. For the context b, c, a, b, these choices continue the learned repeating sequence.
Connect it to the computation
Greedy decoding takes the largest-probability token from the relevant row. The final context position therefore chooses c as the next token. The earlier rows show predictions for earlier prefixes; they are not four independently generated continuations from the final position.
These probabilities describe the trained model’s output on this tiny task. High probability and success on a repeating pattern are separate from general language ability or calibrated confidence. Check the row labels and the context before interpreting a bright cell as a correct prediction.
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/07-transformers/04-train-checkpoint-and-generate/code/main.py (or downloaded exercises/transformers-04.py / exercises/transformers-04.ipynb) and execute from the repository root with python3 phases/07-transformers/04-train-checkpoint-and-generate/code/main.py.
1 Prepare packages and the vocabulary
Create main.py in the CPU environment. Add these imports and the vocabulary/context contract. Integer IDs must stay consistent with the output head.
# Step 1 — 1. Prepare packages and the vocabulary: Vocabulary size is three, model width is eight, and the context...
import jax
import jax.numpy as jnp
import numpy as np
import optax
import json
from pathlib import Path
from tempfile import TemporaryDirectory
VOCAB = {"a":0, "b":1, "c":2}
CONTEXT = 4Vocabulary size is three, model width is eight, and the context limit is four.
2 Reuse the verified causal block
Append the normalization, projection initialization and causal block from the previous lesson. Identify where the attention mask prevents future reads.
# Step 2 — 2. Reuse the verified causal block: The block returns one model-width vector per token; it has no...
def layer_norm(x):
# Reduce along axis=-1 to compute `mean`.
mean = jnp.mean(x, axis=-1, keepdims=True)
# Reduce along axis=-1 to compute `variance`.
variance = jnp.mean((x-mean)**2, axis=-1, keepdims=True)
return (x-mean) / jnp.sqrt(variance + 1e-5)
def init_block(key, width=8):
# Create or split explicit PRNG key(s) (`keys`) for reproducible randomness.
keys = jax.random.split(key, 6)
shapes = [(width,width)]*4 + [(width,2*width),(2*width,width)]
return {name: jax.random.normal(k,s)*0.1 for name,k,s in zip(
["q","k","v","o","up","down"], keys, shapes)}
def block_forward(p, x):
h = layer_norm(x)
q, k, v = h@p["q"], h@p["k"], h@p["v"]
scores = q@k.T / jnp.sqrt(x.shape[-1])
# Create evenly spaced index values in `allowed`.
allowed = jnp.arange(x.shape[0])[:,None] >= jnp.arange(x.shape[0])[None,:]
# Apply nonlinear activation or probability normalization to compute `weights`.
weights = jax.nn.softmax(jnp.where(allowed, scores, -jnp.inf), axis=-1)
residual = x + (weights@v)@p["o"]
return residual + jax.nn.gelu(layer_norm(residual)@p["up"])@p["down"]Code building blocks in this step
jnp.arange(n, dtype=...)— Creates a 1-D JAX array of evenly spaced values[0, 1, ..., n-1]on the target device.x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).jax.random.PRNGKey(seed) & jax.random.split(key)— Manages explicit, stateless PRNG keys—always split a key before passing subkeys into independent random draws.
The block returns one model-width vector per token; it has no dropout or running statistics.
3 Add embeddings and the vocabulary head
Append model initialization, token-to-logit mapping and example construction. Write the input and target IDs for the first abcab window before proceeding.
# Step 3 — 3. Add embeddings and the vocabulary head: The head has three columns; make_examples shifts each window by...
def init_lm(key):
# Create or split explicit PRNG key(s) (`(embed_key, block_key, head_key)`) for reproducible randomness.
embed_key, block_key, head_key = jax.random.split(key, 3)
return {"embed":jax.random.normal(embed_key,(3,8))*0.1,
"position":jnp.arange(CONTEXT*8,dtype=jnp.float32).reshape(CONTEXT,8)*0.001,
"block":init_block(block_key),
"head":jax.random.normal(head_key,(8,3))*0.1}
def logits(p, tokens):
h = p["embed"][tokens] + p["position"][:tokens.shape[0]]
return layer_norm(block_forward(p["block"],h))@p["head"]
def make_examples(text):
ids = np.array([VOCAB[c] for c in text],dtype=np.int32)
# Combine or mask array elements to form `rows`.
rows = np.stack([ids[i:i+CONTEXT+1] for i in range(len(ids)-CONTEXT)])
return jnp.array(rows[:,:-1]), jnp.array(rows[:,1:])Code building blocks in this step
jnp.arange(n, dtype=...)— Creates a 1-D JAX array of evenly spaced values[0, 1, ..., n-1]on the target device.jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.array.reshape(new_shape)— Reorganizes tensor axes without changing the total element count (array.size).jax.random.PRNGKey(seed) & jax.random.split(key)— Manages explicit, stateless PRNG keys—always split a key before passing subkeys into independent random draws.
The head has three columns; make_examples shifts each window by one token for its targets.
4 Define the loss and compiled state transition
Append the dataset calls, cross-entropy reduction, Adam transformation and compiled step. Which values are returned for the next update?
# Step 4 — 4. Define the loss and compiled state transition: train_step returns new parameters, optimizer state and the...
train_x, train_y = make_examples("abc"*12)
held_x, held_y = make_examples("bca"*6)
def lm_loss(p, x, y):
# Vectorize across the batch dimension with `jax.vmap` (`predictions`).
predictions = jax.vmap(logits,in_axes=(None,0))(p,x)
return jnp.mean(optax.softmax_cross_entropy_with_integer_labels(predictions,y))
# Configure or step the Optax optimizer state (`optimizer`).
optimizer = optax.adam(0.03)
# Define and JIT-compile `train_step(p, state, x, y)` so XLA traces and fuses the operations:
@jax.jit
def train_step(p, state, x, y):
# Differentiate the objective to obtain `(value, gradients)` via automatic differentiation.
value, gradients = jax.value_and_grad(lm_loss)(p,x,y)
# Apply the computed gradient updates to update the model parameters.
updates, next_state = optimizer.update(gradients,state,p)
return optax.apply_updates(p,updates), next_state, valueCode 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.vmap(fn, in_axes=..., out_axes=...)— Vectorizes a single-example function across a batch axis without writing a Python loop.jax.jit(fn) / @jax.jit— Tracesfnwith abstract shapes and compiles a fused XLA executable cached by input shape and dtype.
train_step returns new parameters, optimizer state and the pre-update scalar loss; it does not mutate the caller.
5 Run the first twenty updates
Append this training block. You will save this state after twenty steps; keep the optimizer state alongside parameters.
# Step 5 — 5. Run the first twenty updates: Adam count should now equal twenty; initialization is the only...
# Create or split explicit PRNG key(s) (`params`) for reproducible randomness.
params = init_lm(jax.random.key(3))
state = optimizer.init(params)
initial = float(lm_loss(params,train_x,train_y))
for _ in range(20):
params, state, _ = train_step(params,state,train_x,train_y)
# Save numeric leaves only, with a schema checked against this program's template.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.
Adam count should now equal twenty; initialization is the only random operation in this deterministic fixture.
6 Write and validate the snapshot format
Append the save/load helpers. Read each rejection check. Rebuilding a tree template supplies structure, not the saved numeric state.
# Step 6 — 6. Write and validate the snapshot format: The loader checks architecture, vocabulary, numeric leaf...
def save_snapshot(folder, p, state, step):
leaves, _ = jax.tree.flatten((p,state))
# Convert `` to a host NumPy array for inspection or verification.
np.savez(folder/"state.npz", **{f"leaf_{i}":np.asarray(x) for i,x in enumerate(leaves)})
manifest = {"schema":1,"step":step,"vocabulary":VOCAB,"context":CONTEXT,
"shapes":[list(x.shape) for x in leaves],
"dtypes":[str(x.dtype) for x in leaves]}
# Write the serialized artifact payload to disk.
(folder/"manifest.json").write_text(json.dumps(manifest))
def load_snapshot(folder):
meta = json.loads((folder/"manifest.json").read_text())
# Guard input contract (`meta['schema'] != 1 or meta['vocabulary'] != VOCAB or meta['context'] != CONTEXT`) and fail fast if violated.
if meta["schema"] != 1 or meta["vocabulary"] != VOCAB or meta["context"] != CONTEXT:
raise ValueError("checkpoint architecture/vocabulary mismatch")
# Guard input contract (`type(meta['step']) is not int or meta['step'] < 0`) and fail fast if violated.
if type(meta["step"]) is not int or meta["step"] < 0:
raise ValueError("invalid checkpoint step")
# Create or split explicit PRNG key(s) (`template`) for reproducible randomness.
template = init_lm(jax.random.key(0))
template_leaves, structure = jax.tree.flatten((template,optimizer.init(template)))
arrays = []
with np.load(folder/"state.npz",allow_pickle=False) as data:
# Guard input contract (`set(data.files) != {f'leaf_{i}' for i in range(len(template_leaves))}`) and fail fast if violated.
if set(data.files) != {f"leaf_{i}" for i in range(len(template_leaves))}:
raise ValueError("checkpoint leaf count mismatch")
for i, leaf in enumerate(template_leaves):
saved = data[f"leaf_{i}"]
# Guard input contract (`saved.shape != leaf.shape or saved.dtype != np.asarray(leaf).dtype`) and fail fast if violated.
if saved.shape != leaf.shape or saved.dtype != np.asarray(leaf).dtype:
raise ValueError("checkpoint leaf contract mismatch")
# Create device-backed JAX array ``.
arrays.append(jnp.array(saved))
restored_p, restored_state = jax.tree.unflatten(structure,arrays)
# Guard input contract (`int(restored_state[0].count) != meta['step']`) and fail fast if violated.
if int(restored_state[0].count) != meta["step"]:
raise ValueError("checkpoint step and Adam count disagree")
return restored_p, restored_state, meta["step"]Code building blocks in this step
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.jax.random.PRNGKey(seed) & jax.random.split(key)— Manages explicit, stateless PRNG keys—always split a key before passing subkeys into independent random draws.
The loader checks architecture, vocabulary, numeric leaf contracts, step type and agreement with Adam count. It disables pickle.
7 Verify resume and evaluate the final fit
Append the disk round trip and next-step comparison, then the remaining eighty updates and held-out evaluation. The temporary folder is cleaned up.
# Step 7 — 7. Verify resume and evaluate the final fit: Uninterrupted and restored next transitions agree; the held-out...
# Create an isolated temporary directory to run and inspect artifacts safely:
with TemporaryDirectory() as directory:
folder = Path(directory)
save_snapshot(folder,params,state,20)
restored, restored_state, step = load_snapshot(folder)
assert step == 20
next_a, state_a, _ = train_step(params,state,train_x,train_y)
next_b, state_b, _ = train_step(restored,restored_state,train_x,train_y)
assert all(jnp.allclose(a,b,atol=1e-7) for a,b in zip(
jax.tree.leaves((next_a,state_a)),jax.tree.leaves((next_b,state_b))))
for _ in range(80):
params, state, _ = train_step(params,state,train_x,train_y)
# Vectorize across the batch dimension with `jax.vmap` (`held_logits`).
held_logits = jax.vmap(logits,in_axes=(None,0))(params,held_x)
# Reduce along axis=-1 to compute `accuracy`.
accuracy = jnp.mean(jnp.argmax(held_logits,axis=-1)==held_y)
final_loss = lm_loss(params,held_x,held_y)
print("Initial / held loss / held accuracy:",initial,float(final_loss),float(accuracy))
assert final_loss < 0.05
assert accuracy > 0.99Code 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).jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jax.vmap(fn, in_axes=..., out_axes=...)— Vectorizes a single-example function across a batch axis without writing a Python loop.
Uninterrupted and restored next transitions agree; the held-out periodic corpus tests pipeline behavior but repeats learned contexts.
8 Define greedy decoding
Append the prompt encoder and generation loop. Locate the context truncation and the deterministic argmax policy.
# Step 8 — 8. Define greedy decoding: The generator recomputes a short window each step.
def generate(p, prompt, count):
tokens = [VOCAB[c] for c in prompt]
# Guard input contract (`not tokens`) and fail fast if violated.
if not tokens:
raise ValueError("generation needs a nonempty prompt")
inverse = {i:c for c,i in VOCAB.items()}
for _ in range(count):
window = jnp.array(tokens[-CONTEXT:],dtype=jnp.int32)
new = int(jnp.argmax(logits(p,window)[-1]))
tokens.append(new)
return "".join(inverse[i] for i in tokens)Code building blocks in this step
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.
The generator recomputes a short window each step. It has no sampling, key/value cache or serving scheduler.
9 Check the expected continuation
Append the final call and run python main.py. Compare the continuation to the abc cycle you derived before training.
# Step 9 — 9. Check the expected continuation: The expected continuation is abcabcabcabc, with held-out loss...
continuation = generate(params,"abca",8)
print("Greedy continuation:",continuation)
assert continuation == "abcabcabcabc"Code building blocks in this step
generate(...)— Callgeneratewith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
The expected continuation is abcabcabcabc, with held-out loss below and accuracy above for this fixture.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — 1. Prepare packages and the vocabulary: Vocabulary size is three, model width is eight, and the context...
import jax
import jax.numpy as jnp
import numpy as np
import optax
import json
from pathlib import Path
from tempfile import TemporaryDirectory
VOCAB = {"a":0, "b":1, "c":2}
CONTEXT = 4
# Step 2 — 2. Reuse the verified causal block: The block returns one model-width vector per token; it has no...
def layer_norm(x):
# Reduce along axis=-1 to compute `mean`.
mean = jnp.mean(x, axis=-1, keepdims=True)
# Reduce along axis=-1 to compute `variance`.
variance = jnp.mean((x-mean)**2, axis=-1, keepdims=True)
return (x-mean) / jnp.sqrt(variance + 1e-5)
def init_block(key, width=8):
# Create or split explicit PRNG key(s) (`keys`) for reproducible randomness.
keys = jax.random.split(key, 6)
shapes = [(width,width)]*4 + [(width,2*width),(2*width,width)]
return {name: jax.random.normal(k,s)*0.1 for name,k,s in zip(
["q","k","v","o","up","down"], keys, shapes)}
def block_forward(p, x):
h = layer_norm(x)
q, k, v = h@p["q"], h@p["k"], h@p["v"]
scores = q@k.T / jnp.sqrt(x.shape[-1])
# Create evenly spaced index values in `allowed`.
allowed = jnp.arange(x.shape[0])[:,None] >= jnp.arange(x.shape[0])[None,:]
# Apply nonlinear activation or probability normalization to compute `weights`.
weights = jax.nn.softmax(jnp.where(allowed, scores, -jnp.inf), axis=-1)
residual = x + (weights@v)@p["o"]
return residual + jax.nn.gelu(layer_norm(residual)@p["up"])@p["down"]
# Step 3 — 3. Add embeddings and the vocabulary head: The head has three columns; make_examples shifts each window by...
def init_lm(key):
# Create or split explicit PRNG key(s) (`(embed_key, block_key, head_key)`) for reproducible randomness.
embed_key, block_key, head_key = jax.random.split(key, 3)
return {"embed":jax.random.normal(embed_key,(3,8))*0.1,
"position":jnp.arange(CONTEXT*8,dtype=jnp.float32).reshape(CONTEXT,8)*0.001,
"block":init_block(block_key),
"head":jax.random.normal(head_key,(8,3))*0.1}
def logits(p, tokens):
h = p["embed"][tokens] + p["position"][:tokens.shape[0]]
return layer_norm(block_forward(p["block"],h))@p["head"]
def make_examples(text):
ids = np.array([VOCAB[c] for c in text],dtype=np.int32)
# Combine or mask array elements to form `rows`.
rows = np.stack([ids[i:i+CONTEXT+1] for i in range(len(ids)-CONTEXT)])
return jnp.array(rows[:,:-1]), jnp.array(rows[:,1:])
# Step 4 — 4. Define the loss and compiled state transition: train_step returns new parameters, optimizer state and the...
train_x, train_y = make_examples("abc"*12)
held_x, held_y = make_examples("bca"*6)
def lm_loss(p, x, y):
# Vectorize across the batch dimension with `jax.vmap` (`predictions`).
predictions = jax.vmap(logits,in_axes=(None,0))(p,x)
return jnp.mean(optax.softmax_cross_entropy_with_integer_labels(predictions,y))
# Configure or step the Optax optimizer state (`optimizer`).
optimizer = optax.adam(0.03)
# Define and JIT-compile `train_step(p, state, x, y)` so XLA traces and fuses the operations:
@jax.jit
def train_step(p, state, x, y):
# Differentiate the objective to obtain `(value, gradients)` via automatic differentiation.
value, gradients = jax.value_and_grad(lm_loss)(p,x,y)
# Apply the computed gradient updates to update the model parameters.
updates, next_state = optimizer.update(gradients,state,p)
return optax.apply_updates(p,updates), next_state, value
# Step 5 — 5. Run the first twenty updates: Adam count should now equal twenty; initialization is the only...
# Create or split explicit PRNG key(s) (`params`) for reproducible randomness.
params = init_lm(jax.random.key(3))
state = optimizer.init(params)
initial = float(lm_loss(params,train_x,train_y))
for _ in range(20):
params, state, _ = train_step(params,state,train_x,train_y)
# Save numeric leaves only, with a schema checked against this program's template.
# Step 6 — 6. Write and validate the snapshot format: The loader checks architecture, vocabulary, numeric leaf...
def save_snapshot(folder, p, state, step):
leaves, _ = jax.tree.flatten((p,state))
# Convert `` to a host NumPy array for inspection or verification.
np.savez(folder/"state.npz", **{f"leaf_{i}":np.asarray(x) for i,x in enumerate(leaves)})
manifest = {"schema":1,"step":step,"vocabulary":VOCAB,"context":CONTEXT,
"shapes":[list(x.shape) for x in leaves],
"dtypes":[str(x.dtype) for x in leaves]}
# Write the serialized artifact payload to disk.
(folder/"manifest.json").write_text(json.dumps(manifest))
def load_snapshot(folder):
meta = json.loads((folder/"manifest.json").read_text())
# Guard input contract (`meta['schema'] != 1 or meta['vocabulary'] != VOCAB or meta['context'] != CONTEXT`) and fail fast if violated.
if meta["schema"] != 1 or meta["vocabulary"] != VOCAB or meta["context"] != CONTEXT:
raise ValueError("checkpoint architecture/vocabulary mismatch")
# Guard input contract (`type(meta['step']) is not int or meta['step'] < 0`) and fail fast if violated.
if type(meta["step"]) is not int or meta["step"] < 0:
raise ValueError("invalid checkpoint step")
# Create or split explicit PRNG key(s) (`template`) for reproducible randomness.
template = init_lm(jax.random.key(0))
template_leaves, structure = jax.tree.flatten((template,optimizer.init(template)))
arrays = []
with np.load(folder/"state.npz",allow_pickle=False) as data:
# Guard input contract (`set(data.files) != {f'leaf_{i}' for i in range(len(template_leaves))}`) and fail fast if violated.
if set(data.files) != {f"leaf_{i}" for i in range(len(template_leaves))}:
raise ValueError("checkpoint leaf count mismatch")
for i, leaf in enumerate(template_leaves):
saved = data[f"leaf_{i}"]
# Guard input contract (`saved.shape != leaf.shape or saved.dtype != np.asarray(leaf).dtype`) and fail fast if violated.
if saved.shape != leaf.shape or saved.dtype != np.asarray(leaf).dtype:
raise ValueError("checkpoint leaf contract mismatch")
# Create device-backed JAX array ``.
arrays.append(jnp.array(saved))
restored_p, restored_state = jax.tree.unflatten(structure,arrays)
# Guard input contract (`int(restored_state[0].count) != meta['step']`) and fail fast if violated.
if int(restored_state[0].count) != meta["step"]:
raise ValueError("checkpoint step and Adam count disagree")
return restored_p, restored_state, meta["step"]
# Step 7 — 7. Verify resume and evaluate the final fit: Uninterrupted and restored next transitions agree; the held-out...
# Create an isolated temporary directory to run and inspect artifacts safely:
with TemporaryDirectory() as directory:
folder = Path(directory)
save_snapshot(folder,params,state,20)
restored, restored_state, step = load_snapshot(folder)
assert step == 20
next_a, state_a, _ = train_step(params,state,train_x,train_y)
next_b, state_b, _ = train_step(restored,restored_state,train_x,train_y)
assert all(jnp.allclose(a,b,atol=1e-7) for a,b in zip(
jax.tree.leaves((next_a,state_a)),jax.tree.leaves((next_b,state_b))))
for _ in range(80):
params, state, _ = train_step(params,state,train_x,train_y)
# Vectorize across the batch dimension with `jax.vmap` (`held_logits`).
held_logits = jax.vmap(logits,in_axes=(None,0))(params,held_x)
# Reduce along axis=-1 to compute `accuracy`.
accuracy = jnp.mean(jnp.argmax(held_logits,axis=-1)==held_y)
final_loss = lm_loss(params,held_x,held_y)
print("Initial / held loss / held accuracy:",initial,float(final_loss),float(accuracy))
assert final_loss < 0.05
assert accuracy > 0.99
# Step 8 — 8. Define greedy decoding: The generator recomputes a short window each step.
def generate(p, prompt, count):
tokens = [VOCAB[c] for c in prompt]
# Guard input contract (`not tokens`) and fail fast if violated.
if not tokens:
raise ValueError("generation needs a nonempty prompt")
inverse = {i:c for c,i in VOCAB.items()}
for _ in range(count):
window = jnp.array(tokens[-CONTEXT:],dtype=jnp.int32)
new = int(jnp.argmax(logits(p,window)[-1]))
tokens.append(new)
return "".join(inverse[i] for i in tokens)
# Step 9 — 9. Check the expected continuation: The expected continuation is abcabcabcabc, with held-out loss...
continuation = generate(params,"abca",8)
print("Greedy continuation:",continuation)
assert continuation == "abcabcabcabc"Run the example
python3 phases/07-transformers/04-train-checkpoint-and-generate/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 — 1. Prepare packages and the vocabulary: Vocabulary size is three, model width is eight, and the context...
import jax
import jax.numpy as jnp
import numpy as np
import optax
import json
from pathlib import Path
from tempfile import TemporaryDirectory
VOCAB = {"a":0, "b":1, "c":2}
CONTEXT = 4
# Step 2 — 2. Reuse the verified causal block: The block returns one model-width vector per token; it has no...
def layer_norm(x):
# Reduce along axis=-1 to compute `mean`.
mean = jnp.mean(x, axis=-1, keepdims=True)
# Reduce along axis=-1 to compute `variance`.
variance = jnp.mean((x-mean)**2, axis=-1, keepdims=True)
return (x-mean) / jnp.sqrt(variance + 1e-5)
def init_block(key, width=8):
# Create or split explicit PRNG key(s) (`keys`) for reproducible randomness.
keys = jax.random.split(key, 6)
shapes = [(width,width)]*4 + [(width,2*width),(2*width,width)]
return {name: jax.random.normal(k,s)*0.1 for name,k,s in zip(
["q","k","v","o","up","down"], keys, shapes)}
def block_forward(p, x):
h = layer_norm(x)
q, k, v = h@p["q"], h@p["k"], h@p["v"]
scores = q@k.T / jnp.sqrt(x.shape[-1])
# Create evenly spaced index values in `allowed`.
allowed = jnp.arange(x.shape[0])[:,None] >= jnp.arange(x.shape[0])[None,:]
# Apply nonlinear activation or probability normalization to compute `weights`.
weights = jax.nn.softmax(jnp.where(allowed, scores, -jnp.inf), axis=-1)
residual = x + (weights@v)@p["o"]
return residual + jax.nn.gelu(layer_norm(residual)@p["up"])@p["down"]
# Step 3 — 3. Add embeddings and the vocabulary head: The head has three columns; make_examples shifts each window by...
def init_lm(key):
# Create or split explicit PRNG key(s) (`(embed_key, block_key, head_key)`) for reproducible randomness.
embed_key, block_key, head_key = jax.random.split(key, 3)
return {"embed":jax.random.normal(embed_key,(3,8))*0.1,
"position":jnp.arange(CONTEXT*8,dtype=jnp.float32).reshape(CONTEXT,8)*0.001,
"block":init_block(block_key),
"head":jax.random.normal(head_key,(8,3))*0.1}
def logits(p, tokens):
h = p["embed"][tokens] + p["position"][:tokens.shape[0]]
return layer_norm(block_forward(p["block"],h))@p["head"]
def make_examples(text):
ids = np.array([VOCAB[c] for c in text],dtype=np.int32)
# Combine or mask array elements to form `rows`.
rows = np.stack([ids[i:i+CONTEXT+1] for i in range(len(ids)-CONTEXT)])
return jnp.array(rows[:,:-1]), jnp.array(rows[:,1:])
# Step 4 — 4. Define the loss and compiled state transition: train_step returns new parameters, optimizer state and the...
train_x, train_y = make_examples("abc"*12)
held_x, held_y = make_examples("bca"*6)
def lm_loss(p, x, y):
# Vectorize across the batch dimension with `jax.vmap` (`predictions`).
predictions = jax.vmap(logits,in_axes=(None,0))(p,x)
return jnp.mean(optax.softmax_cross_entropy_with_integer_labels(predictions,y))
# Configure or step the Optax optimizer state (`optimizer`).
optimizer = optax.adam(0.03)
# Define and JIT-compile `train_step(p, state, x, y)` so XLA traces and fuses the operations:
@jax.jit
def train_step(p, state, x, y):
# Differentiate the objective to obtain `(value, gradients)` via automatic differentiation.
value, gradients = jax.value_and_grad(lm_loss)(p,x,y)
# Apply the computed gradient updates to update the model parameters.
updates, next_state = optimizer.update(gradients,state,p)
return optax.apply_updates(p,updates), next_state, value
# Step 5 — 5. Run the first twenty updates: Adam count should now equal twenty; initialization is the only...
# Create or split explicit PRNG key(s) (`params`) for reproducible randomness.
params = init_lm(jax.random.key(3))
state = optimizer.init(params)
initial = float(lm_loss(params,train_x,train_y))
for _ in range(20):
params, state, _ = train_step(params,state,train_x,train_y)
# Save numeric leaves only, with a schema checked against this program's template.
# Step 6 — 6. Write and validate the snapshot format: The loader checks architecture, vocabulary, numeric leaf...
def save_snapshot(folder, p, state, step):
leaves, _ = jax.tree.flatten((p,state))
# Convert `` to a host NumPy array for inspection or verification.
np.savez(folder/"state.npz", **{f"leaf_{i}":np.asarray(x) for i,x in enumerate(leaves)})
manifest = {"schema":1,"step":step,"vocabulary":VOCAB,"context":CONTEXT,
"shapes":[list(x.shape) for x in leaves],
"dtypes":[str(x.dtype) for x in leaves]}
# Write the serialized artifact payload to disk.
(folder/"manifest.json").write_text(json.dumps(manifest))
def load_snapshot(folder):
meta = json.loads((folder/"manifest.json").read_text())
# Guard input contract (`meta['schema'] != 1 or meta['vocabulary'] != VOCAB or meta['context'] != CONTEXT`) and fail fast if violated.
if meta["schema"] != 1 or meta["vocabulary"] != VOCAB or meta["context"] != CONTEXT:
raise ValueError("checkpoint architecture/vocabulary mismatch")
# Guard input contract (`type(meta['step']) is not int or meta['step'] < 0`) and fail fast if violated.
if type(meta["step"]) is not int or meta["step"] < 0:
raise ValueError("invalid checkpoint step")
# Create or split explicit PRNG key(s) (`template`) for reproducible randomness.
template = init_lm(jax.random.key(0))
template_leaves, structure = jax.tree.flatten((template,optimizer.init(template)))
arrays = []
with np.load(folder/"state.npz",allow_pickle=False) as data:
# Guard input contract (`set(data.files) != {f'leaf_{i}' for i in range(len(template_leaves))}`) and fail fast if violated.
if set(data.files) != {f"leaf_{i}" for i in range(len(template_leaves))}:
raise ValueError("checkpoint leaf count mismatch")
for i, leaf in enumerate(template_leaves):
saved = data[f"leaf_{i}"]
# Guard input contract (`saved.shape != leaf.shape or saved.dtype != np.asarray(leaf).dtype`) and fail fast if violated.
if saved.shape != leaf.shape or saved.dtype != np.asarray(leaf).dtype:
raise ValueError("checkpoint leaf contract mismatch")
# Create device-backed JAX array ``.
arrays.append(jnp.array(saved))
restored_p, restored_state = jax.tree.unflatten(structure,arrays)
# Guard input contract (`int(restored_state[0].count) != meta['step']`) and fail fast if violated.
if int(restored_state[0].count) != meta["step"]:
raise ValueError("checkpoint step and Adam count disagree")
return restored_p, restored_state, meta["step"]
# Step 7 — 7. Verify resume and evaluate the final fit: Uninterrupted and restored next transitions agree; the held-out...
# Create an isolated temporary directory to run and inspect artifacts safely:
with TemporaryDirectory() as directory:
folder = Path(directory)
save_snapshot(folder,params,state,20)
restored, restored_state, step = load_snapshot(folder)
assert step == 20
next_a, state_a, _ = train_step(params,state,train_x,train_y)
next_b, state_b, _ = train_step(restored,restored_state,train_x,train_y)
assert all(jnp.allclose(a,b,atol=1e-7) for a,b in zip(
jax.tree.leaves((next_a,state_a)),jax.tree.leaves((next_b,state_b))))
for _ in range(80):
params, state, _ = train_step(params,state,train_x,train_y)
# Vectorize across the batch dimension with `jax.vmap` (`held_logits`).
held_logits = jax.vmap(logits,in_axes=(None,0))(params,held_x)
# Reduce along axis=-1 to compute `accuracy`.
accuracy = jnp.mean(jnp.argmax(held_logits,axis=-1)==held_y)
final_loss = lm_loss(params,held_x,held_y)
print("Initial / held loss / held accuracy:",initial,float(final_loss),float(accuracy))
assert final_loss < 0.05
assert accuracy > 0.99
# Step 8 — 8. Define greedy decoding: The generator recomputes a short window each step.
def generate(p, prompt, count):
tokens = [VOCAB[c] for c in prompt]
# Guard input contract (`not tokens`) and fail fast if violated.
if not tokens:
raise ValueError("generation needs a nonempty prompt")
inverse = {i:c for c,i in VOCAB.items()}
for _ in range(count):
window = jnp.array(tokens[-CONTEXT:],dtype=jnp.int32)
new = int(jnp.argmax(logits(p,window)[-1]))
tokens.append(new)
return "".join(inverse[i] for i in tokens)
# Step 9 — 9. Check the expected continuation: The expected continuation is abcabcabcabc, with held-out loss...
continuation = generate(params,"abca",8)
print("Greedy continuation:",continuation)
assert continuation == "abcabcabcabc"Expected result: Held-out loss below and accuracy above for the periodic fixture. Snapshot next-step replay agrees. Greedy continuation: abcabcabcabc. Exact loss depends on the tested backend.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
Initial / held loss / held accuracy: 0.9807573556900024 0.0017881905660033226 1.0
Greedy continuation: abcabcabcabc
Initial / held loss / held accuracy: 0.9807573556900024 0.0017881905660033226 1.0
Greedy continuation: abcabcabcabc
PASS: transformers-04
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Verify uniform-logit cross entropy
# Experiment — Verify uniform-logit cross entropy: A known probability distribution independently checks target...
zero_head = {**params,"head":jnp.zeros_like(params["head"])}
assert jnp.allclose(lm_loss(zero_head,train_x,train_y),jnp.log(3.),atol=1e-6)Expected: Mean loss log(3) ≈ .
A known probability distribution independently checks target handling and reduction.
Test causality at the final model output
# Experiment — Test causality at the final model output: Causal independence should hold for the full model, not only the...
example = jnp.array([0,1,2,0])
original = logits(params,example)
modified = logits(params,example.at[-1].set(1))
assert jnp.allclose(original[:3],modified[:3],atol=1e-5)Expected: Earlier three logit rows remain the same.
Causal independence should hold for the full model, not only the standalone attention operation.
Make it yours
Where to put your code: Append your solution at the bottom of phases/07-transformers/04-train-checkpoint-and-generate/code/main.py (or inside the Make it yours cell of exercises/transformers-04.ipynb) and run python3 phases/07-transformers/04-train-checkpoint-and-generate/code/main.py to verify your prediction before opening the reference solution.
Generate twelve new tokens from prompt bcab. Derive the expected cycle first; verify the string. Explain why success on this periodic example is separate from natural-language performance.
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:
generate(...)— Callgeneratewith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
2. Step-by-step implementation plan:
- Set up the changed input or configuration parameters requested in the prompt.
- Run the target function or transformation and assert that the output matches your prediction.
# Exercise solution: Generate twelve new tokens from prompt bcab.
assert generate(params,"bcab",12) # TODO: complete assertion check
Reveal the reference solution
# Exercise solution: Generate twelve new tokens from prompt bcab.
assert generate(params,"bcab",12) == "bcabcabcabcabcab"Reject an incompatible checkpoint
Transfer / diagnosis
Save a fresh snapshot, edit only the vocabulary metadata, and prove the loader rejects it. Restore the metadata and verify recovery. Then forge step=101 while retaining the 100-step optimizer state and verify that this mismatch is also rejected.
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.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
2. Step-by-step implementation plan:
- Create an isolated temporary directory to run and inspect artifacts safely:
- Write the serialized artifact payload to disk.
- Write the serialized artifact payload to disk.
- Write the serialized artifact payload to disk.
# Reject an incompatible checkpoint (Transfer / diagnosis): The metadata is part of the model contract even when...
# Create an isolated temporary directory to run and inspect artifacts safely:
with TemporaryDirectory() as directory:
folder = Path(...) # TODO: compute folder
save_snapshot(folder,params,state,100)
metadata_path = ... # TODO: compute metadata_path
correct = metadata_path.read_text(...) # TODO: compute correct
metadata = json.loads(...) # TODO: compute metadata
metadata["vocabulary"] = ... # TODO: compute metadata["vocabulary"]
# Write the serialized artifact payload to disk.
metadata_path.write_text(json.dumps(metadata))
try:
load_snapshot(folder)
except ValueError:
pass
else:
raise AssertionError("wrong vocabulary accepted")
forged_step = json.loads(...) # TODO: compute forged_step
forged_step["step"] = ... # TODO: compute forged_step["step"]
# Write the serialized artifact payload to disk.
metadata_path.write_text(json.dumps(forged_step))
try:
load_snapshot(folder)
except ValueError:
pass
else:
raise AssertionError("wrong step accepted")
# Write the serialized artifact payload to disk.
metadata_path.write_text(correct)
recovered, _, recovered_step = load_snapshot(...) # TODO: compute recovered, _, recovered_step
assert recovered_step # TODO: complete assertion check
assert jnp.allclose(logits(recovered,jnp.array([0,1,2,0])),logits(params,jnp.array([0,1,2,0]))) # TODO: complete assertion check
Show a hint
Change one vocabulary mapping rather than numeric leaves.
Reveal reference solution and reasoning
# Reject an incompatible checkpoint (Transfer / diagnosis): The metadata is part of the model contract even when...
# Create an isolated temporary directory to run and inspect artifacts safely:
with TemporaryDirectory() as directory:
folder = Path(directory)
save_snapshot(folder,params,state,100)
metadata_path = folder/"manifest.json"
correct = metadata_path.read_text()
metadata = json.loads(correct)
metadata["vocabulary"] = {"a":1,"b":0,"c":2}
# Write the serialized artifact payload to disk.
metadata_path.write_text(json.dumps(metadata))
try:
load_snapshot(folder)
except ValueError:
pass
else:
raise AssertionError("wrong vocabulary accepted")
forged_step = json.loads(correct)
forged_step["step"] = 101
# Write the serialized artifact payload to disk.
metadata_path.write_text(json.dumps(forged_step))
try:
load_snapshot(folder)
except ValueError:
pass
else:
raise AssertionError("wrong step accepted")
# Write the serialized artifact payload to disk.
metadata_path.write_text(correct)
recovered, _, recovered_step = load_snapshot(folder)
assert recovered_step == 100
assert jnp.allclose(logits(recovered,jnp.array([0,1,2,0])),logits(params,jnp.array([0,1,2,0])))The metadata is part of the model contract even when parameter array shapes still match.
Make prompt failures explicit
Transfer / diagnosis
Verify empty prompt rejection and unknown-character rejection. Record the difference between a ValueError for missing context and a KeyError for an unknown token.
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:
explicit(...)— Callexplicitwith your updated parameters or inputs from this lesson's workspace.generate(...)— Callgeneratewith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
2. Step-by-step implementation plan:
- Run the boundary check and catch the expected exception:
- Run the boundary check and catch the expected exception:
# Make prompt failures explicit (Transfer / diagnosis): A decoding interface needs a defined input contract; neither...
# Run the boundary check and catch the expected exception:
try:
generate(params,"",1)
except ValueError:
pass
else:
raise AssertionError("empty prompt accepted")
# Run the boundary check and catch the expected exception:
try:
generate(params,"x",1)
except KeyError:
pass
else:
raise AssertionError("unknown token accepted")
assert generate(params,"abca",0) # TODO: complete assertion check
Show a hint
The vocabulary mapping intentionally accepts only a, b and c.
Reveal reference solution and reasoning
# Make prompt failures explicit (Transfer / diagnosis): A decoding interface needs a defined input contract; neither...
# Run the boundary check and catch the expected exception:
try:
generate(params,"",1)
except ValueError:
pass
else:
raise AssertionError("empty prompt accepted")
# Run the boundary check and catch the expected exception:
try:
generate(params,"x",1)
except KeyError:
pass
else:
raise AssertionError("unknown token accepted")
assert generate(params,"abca",0) == "abca"A decoding interface needs a defined input contract; neither failure should be mistaken for a training problem.
Diagnose the result
If loss does not improve, inspect the first shifted input/target window and verify the zero-head log(3) result before tuning the rate. If a resumed next step differs, compare both parameter leaves and Adam moments/count; a matching current loss is insufficient. The loader rejects vocabulary, shape, dtype and step/count mismatches. Generation failures require checking the prompt and context contract separately from training.
Carry forward
- A known probability distribution independently checks target handling and reduction.
- Causal independence should hold for the full model, not only the standalone attention operation.
Keep your evidence
Save the target alignment, uniform-loss derivation, training/held-out metrics with corpus limitations, snapshot continuation check, generated cycle and metadata/prompt rejection repairs.
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 07 Integration Project
Bring this phase together: A checkpointed causal language model
Checkpoint to prove: Verify masking, sequence handling, restoration, and generation before comparing speed.
Begin with token contracts and training. Complete the linked performance and deployment prerequisites before cache, export and profiling checks. Copy projects/text-harness/starter/model.py to projects/text-harness/my_model.py and write your code in projects/text-harness/my_model.py. Run python3 projects/text-harness/tests/check.py --implementation projects/text-harness/my_model.py --stage 1 from the top-level folder to verify stages 1, 2, 3, 4.