Phase 07 / Transformers & language models
Masking, tokenization, and sequence packing
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 90 minutes · CPU practice
Construct a causal mask and prevent packed sequences from attending across boundaries.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: Attention from small pieces
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/02-masking-tokenization-and-sequence-packing/code/main.py.\.venv\Scripts\python.exe phases/07-transformers/02-masking-tokenization-and-sequence-packing/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-02.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: Causal means ; no weight above the diagonal.
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
- Build a causal mask from positions
- Keep token IDs and targets aligned
- Combine causal and segment boundaries
- Handle padding without an all-masked softmax
The problem
An attention head should not always see every token. For example, a next-token model must not peek at the future. We’ll build a causal mask one row at a time, then add padding and boundaries between packed examples. Before training, you’ll be able to explain exactly which positions may read each other.
The idea
An attention mask controls which positions supply context. A loss mask controls which targets contribute to the objective. Tokenization, target shifting and sequence packing decide the positions to which those different rules apply.
Inspect an attention mask
Read across a query row to see which keys contribute to its output. Allowed cells show their weights; blocked cells are zero. Every row sums to one, apart from display rounding. Equal weights come from equal allowed scores in this example, not from masking alone.
In causal mode, the nonzero triangle includes the current position and its past. Packed mode splits that triangle into two separate examples, so the second cannot read the first. Full context removes both restrictions. Move the last value and watch which outputs change; the weights stay fixed because this control changes values, not scores. This is an analytic browser calculation; the Python experiments verify the same weighted averages in JAX.
Keep context eligibility separate from scored targets
At input position , causal next-token training predicts target . A response-only role flag must follow that shifted target. Using unshifted role flags can score the wrong token at the prompt/response boundary.
An unscored prompt token can still influence a scored response through attention. Removing its direct loss does not remove it from the computation. A blocked attention edge instead prevents a context contribution.
Packed documents need segment boundaries in addition to a causal triangle. A later segment must not read an unrelated earlier segment merely because its positions occur earlier in the tensor. Trace one query and target across these rules before trusting a batch average.
Build a causal mask from positions
For query position i, allow keys j ≤ i. Including the diagonal lets a token use its own representation. The model predicts the next token from that prefix; the target array is shifted one position ahead. Excluding the diagonal is a different convention that requires changing how inputs and labels are aligned.
With values , , , and uniform permitted scores, the outputs are prefix means: , , and . Changing the last value must not change the first three outputs. This counterfactual tests causal isolation directly.
allowed key columns
query 0: 1 0 0 0
query 1: 1 1 0 0
query 2: 1 1 1 0
query 3: 1 1 1 1Keep token IDs and targets aligned
For vocabulary {a:, b:, c:}, abcab becomes . Inputs pair with next-token targets . Do not train the last token to predict the first token of an unrelated packed example. A separator or an ignored loss position must express that boundary.
Production tokenizers add Unicode normalization, special control IDs, and subword merges on top of this exact index-alignment contract. Validate unknown characters explicitly rather than silently assigning an existing ID.
Combine causal and segment boundaries
Packing segments means positions and belong to a new example. Require equal segment IDs as well as causal order. With our values, packed means become , , and . Position must not read earlier positions from segment even though they are causally prior.
Segment IDs must identify separate examples within a packed row. Reusing an ID for two unrelated spans could reconnect them. The mask is only one part of packing: target eligibility must also stop at each segment boundary.
Handle padding without an all-masked softmax
An all-masked row represented by all −infinity scores has no normalized distribution and can produce NaN. Padding queries create precisely that row if you forbid every key. Provide a harmless fallback key for those queries, then replace their outputs with zero and ignore their loss. Every real query still uses its original permitted keys.
The fallback is a numerical convention, not permission for padding to carry semantic information. Test that perturbing padded values does not change real outputs. Also reject any real query with no valid key when that violates your model contract.
Turn the allowed positions into weights
For causal attention, query position may use key position only when . We leave an allowed score unchanged and replace a blocked score with negative infinity before softmax. This gives blocked positions zero weight when the row has at least one allowed key. If a row is entirely blocked, an ordinary softmax has no valid distribution; handle that case explicitly, as the code does.
A causal mask blocks future information
Predict before looking: Which cells must remain zero?
Read the figure
Rows select query positions and columns select key positions. The diagonal and cells to its left are allowed; the white cells to the right of the diagonal have exactly zero attention weight because they refer to future tokens.
Query puts all its weight on key . Query splits weight equally across keys and . The last row assigns to every key because, at the last position, all four positions are available.
Connect it to the computation
The allowed scores are equal in this constructed example, so each row averages its visible prefix. The causal mask determines which entries must be zero; it does not generally make the remaining entries uniform. Learned scores can distribute weight unevenly within the allowed region.
For values , query returns , not the full-sequence average . The zero in its last column is what prevents the future value from influencing that result. Verify normalization across rows, not down columns.
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/02-masking-tokenization-and-sequence-packing/code/main.py (or downloaded exercises/transformers-02.py / exercises/transformers-02.ipynb) and execute from the repository root with python3 phases/07-transformers/02-masking-tokenization-and-sequence-packing/code/main.py.
1 Prepare the experiment
In the CPU environment from setup, create main.py and add this block. Write down the dimensions and the known result before continuing.
# Step 1 — 1. Prepare the experiment: These imports provide JAX array transformations and NumPy...
import jax
import jax.numpy as jnp
import numpy as npThese imports provide JAX array transformations and NumPy reference calculations. Define the numerical functions next; the final build block supplies the concrete fixture and checks.
2 Implement the mechanism
Append this block in the same file. Follow the shape diagram above and identify each reduction or state transition.
# Step 2 — 2. Implement the mechanism: allowed key columns query 0: 1 0 0 0 query 1: 1 1 0 0 query 2: 1 1...
def masked_mean(values, allowed):
# Combine or mask array elements to form `scores`.
scores = jnp.where(allowed, 0., -jnp.inf)
# Apply nonlinear activation or probability normalization to compute `weights`.
weights = jax.nn.softmax(scores, axis=-1)
return weights @ values, weights
def make_mask(segment_ids, valid):
positions = jnp.arange(segment_ids.shape[0])
causal = positions[:, None] >= positions[None, :]
same_segment = segment_ids[:, None] == segment_ids[None, :]
return causal & same_segment & valid[:, None] & valid[None, :]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.
allowed key columns query : query : query : query :
3 Run and check
Append this block and run python main.py. Compare its output with your prediction. An assertion failure means a stated contract needs investigation.
# Step 3 — 3. Run and check: Causal means [2, 3, 4.666667, 7.5]; no weight above the diagonal.
values = jnp.array([[2.], [4.], [8.], [16.]])
causal = jnp.arange(4)[:, None] >= jnp.arange(4)[None, :]
out, weights = masked_mean(values, causal)
assert jnp.allclose(out[:, 0], jnp.array([2., 3., 14/3, 7.5]))
assert jnp.allclose(jnp.triu(weights, 1), 0.)
print("Causal means:", out[:, 0])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.jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
Causal means ; no weight above the diagonal.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — 1. Prepare the experiment: These imports provide JAX array transformations and NumPy...
import jax
import jax.numpy as jnp
import numpy as np
# Step 2 — 2. Implement the mechanism: allowed key columns query 0: 1 0 0 0 query 1: 1 1 0 0 query 2: 1 1...
def masked_mean(values, allowed):
# Combine or mask array elements to form `scores`.
scores = jnp.where(allowed, 0., -jnp.inf)
# Apply nonlinear activation or probability normalization to compute `weights`.
weights = jax.nn.softmax(scores, axis=-1)
return weights @ values, weights
def make_mask(segment_ids, valid):
positions = jnp.arange(segment_ids.shape[0])
causal = positions[:, None] >= positions[None, :]
same_segment = segment_ids[:, None] == segment_ids[None, :]
return causal & same_segment & valid[:, None] & valid[None, :]
# Step 3 — 3. Run and check: Causal means [2, 3, 4.666667, 7.5]; no weight above the diagonal.
values = jnp.array([[2.], [4.], [8.], [16.]])
causal = jnp.arange(4)[:, None] >= jnp.arange(4)[None, :]
out, weights = masked_mean(values, causal)
assert jnp.allclose(out[:, 0], jnp.array([2., 3., 14/3, 7.5]))
assert jnp.allclose(jnp.triu(weights, 1), 0.)
print("Causal means:", out[:, 0])Run the example
python3 phases/07-transformers/02-masking-tokenization-and-sequence-packing/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 the experiment: These imports provide JAX array transformations and NumPy...
import jax
import jax.numpy as jnp
import numpy as np
# Step 2 — 2. Implement the mechanism: allowed key columns query 0: 1 0 0 0 query 1: 1 1 0 0 query 2: 1 1...
def masked_mean(values, allowed):
# Combine or mask array elements to form `scores`.
scores = jnp.where(allowed, 0., -jnp.inf)
# Apply nonlinear activation or probability normalization to compute `weights`.
weights = jax.nn.softmax(scores, axis=-1)
return weights @ values, weights
def make_mask(segment_ids, valid):
positions = jnp.arange(segment_ids.shape[0])
causal = positions[:, None] >= positions[None, :]
same_segment = segment_ids[:, None] == segment_ids[None, :]
return causal & same_segment & valid[:, None] & valid[None, :]
# Step 3 — 3. Run and check: Causal means [2, 3, 4.666667, 7.5]; no weight above the diagonal.
values = jnp.array([[2.], [4.], [8.], [16.]])
causal = jnp.arange(4)[:, None] >= jnp.arange(4)[None, :]
out, weights = masked_mean(values, causal)
assert jnp.allclose(out[:, 0], jnp.array([2., 3., 14/3, 7.5]))
assert jnp.allclose(jnp.triu(weights, 1), 0.)
print("Causal means:", out[:, 0])Expected result: Causal means ; no weight above the diagonal.
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
Causal means: [2. 3. 4.666667 7.5 ]
Causal means: [2. 3. 4.666667 7.5 ]
PASS: transformers-02
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Perturb the future
# Experiment — Perturb the future: Perturbation tests whether earlier representations depend on...
changed = values.at[-1, 0].set(1000.)
changed_out, _ = masked_mean(changed, causal)
assert jnp.allclose(changed_out[:3], out[:3])
assert not jnp.allclose(changed_out[-1], out[-1])Expected: Only the last output changes.
Perturbation tests whether earlier representations depend on future information.
Pack two independent examples
# Experiment — Pack two independent examples: Causal order alone does not isolate independent packed examples.
segments = jnp.array([0, 0, 1, 1])
valid = jnp.ones(4, dtype=bool)
packed = make_mask(segments, valid)
packed_out, _ = masked_mean(values, packed)
assert jnp.allclose(packed_out[:, 0], jnp.array([2., 3., 8., 12.]))Expected: Packed means .
Causal order alone does not isolate independent packed examples.
Make it yours
Where to put your code: Append your solution at the bottom of phases/07-transformers/02-masking-tokenization-and-sequence-packing/code/main.py (or inside the Make it yours cell of exercises/transformers-02.ipynb) and run python3 phases/07-transformers/02-masking-tokenization-and-sequence-packing/code/main.py to verify your prediction before opening the reference solution.
Encode abcab with a three-character vocabulary. Create shifted inputs/targets, and a loss-eligibility vector for packed segments that excludes a cross-segment next-token target.
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.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.
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: Encode abcab with a three-character vocabulary.
vocab = ... # TODO: compute vocab
ids = jnp.array(...) # TODO: compute ids
assert jnp.array_equal(ids[:-1], jnp.array([0,1,2,0])) # TODO: complete assertion check
assert jnp.array_equal(ids[1:], jnp.array([1,2,0,1])) # TODO: complete assertion check
seg = jnp.array(...) # TODO: compute seg
loss_valid = ... # TODO: compute loss_valid
assert jnp.array_equal(loss_valid, jnp.array([True,True,False,True])) # TODO: complete assertion check
Reveal the reference solution
# Exercise solution: Encode abcab with a three-character vocabulary.
vocab = {"a":0, "b":1, "c":2}
ids = jnp.array([vocab[c] for c in "abcab"])
assert jnp.array_equal(ids[:-1], jnp.array([0,1,2,0]))
assert jnp.array_equal(ids[1:], jnp.array([1,2,0,1]))
seg = jnp.array([0,0,0,1,1])
loss_valid = seg[:-1] == seg[1:]
assert jnp.array_equal(loss_valid, jnp.array([True,True,False,True]))Repair padding-query NaNs
Transfer / diagnosis
Use valid=[True,True,False,False]. Reproduce the undefined all-masked rows, then give padded queries a fallback and zero their outputs.
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.zeros / jnp.ones(shape, dtype=...)— Allocates a tensor of the givenshapeinitialized with constants.jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jnp.isfinite(x)— Returns a boolean mask verifying that no element isNaNorInf.
2. Step-by-step implementation plan:
- Combine or mask array elements to form
(bad_pad, _). - Confirm that all computed values remain finite (no NaN or Inf).
- Combine or mask array elements to form
(pad_out, _). - Combine or mask array elements to form
pad_out. - Confirm that all computed values remain finite (no NaN or Inf).
# Repair padding-query NaNs (Transfer / diagnosis): A fallback makes normalization defined; zeroing and loss...
pad_valid = jnp.array(...) # TODO: compute pad_valid
pad_mask = make_mask(...) # TODO: compute pad_mask
# Combine or mask array elements to form `(bad_pad, _)`.
bad_pad, _ = masked_mean(...) # TODO: compute bad_pad, _
# Confirm that all computed values remain finite (no NaN or Inf).
assert not jnp.all(jnp.isfinite(bad_pad)) # TODO: complete assertion check
fallback = ... # TODO: compute fallback
# Combine or mask array elements to form `(pad_out, _)`.
pad_out, _ = masked_mean(...) # TODO: compute pad_out, _
# Combine or mask array elements to form `pad_out`.
pad_out = jnp.where(...) # TODO: compute pad_out
# Confirm that all computed values remain finite (no NaN or Inf).
assert jnp.all(jnp.isfinite(pad_out)) # TODO: complete assertion check
assert jnp.allclose(pad_out[:,0], jnp.array([2.,3.,0.,0.])) # TODO: complete assertion check
Show a hint
Insert a diagonal fallback only for invalid queries.
Reveal reference solution and reasoning
# Repair padding-query NaNs (Transfer / diagnosis): A fallback makes normalization defined; zeroing and loss...
pad_valid = jnp.array([True,True,False,False])
pad_mask = make_mask(jnp.zeros(4, dtype=int), pad_valid)
# Combine or mask array elements to form `(bad_pad, _)`.
bad_pad, _ = masked_mean(values, pad_mask)
# Confirm that all computed values remain finite (no NaN or Inf).
assert not jnp.all(jnp.isfinite(bad_pad))
fallback = (~pad_valid[:, None]) & jnp.eye(4, dtype=bool)
# Combine or mask array elements to form `(pad_out, _)`.
pad_out, _ = masked_mean(values, pad_mask | fallback)
# Combine or mask array elements to form `pad_out`.
pad_out = jnp.where(pad_valid[:, None], pad_out, 0.)
# Confirm that all computed values remain finite (no NaN or Inf).
assert jnp.all(jnp.isfinite(pad_out))
assert jnp.allclose(pad_out[:,0], jnp.array([2.,3.,0.,0.]))A fallback makes normalization defined; zeroing and loss masking retain the semantic padding contract.
Detect packed-example leakage
Transfer / diagnosis
Compare causal-only and segment-aware outputs at query . Explain which prior values caused the difference.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
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.
# Detect packed-example leakage (Transfer / diagnosis): The earlier values 2 and 4 belong to another example; they...
assert jnp.allclose(out[2,0], 14/3) # TODO: complete assertion check
assert jnp.allclose(packed_out[2,0], 8.) # TODO: complete assertion check
assert not jnp.allclose(out[2,0], packed_out[2,0]) # TODO: complete assertion check
Show a hint
Query should see only value from its own segment.
Reveal reference solution and reasoning
# Detect packed-example leakage (Transfer / diagnosis): The earlier values 2 and 4 belong to another example; they...
assert jnp.allclose(out[2,0], 14/3)
assert jnp.allclose(packed_out[2,0], 8.)
assert not jnp.allclose(out[2,0], packed_out[2,0])The earlier values and belong to another example; they must be excluded despite being past positions.
Diagnose the result
NaN in a padded output can mean that its mask row permits no keys; count allowed keys before blaming the optimizer. Add a fallback only for invalid queries, zero their outputs, and ignore their loss. If a packed output includes another example’s values, inspect equal-segment and causal conditions separately. Recheck the hand-derived result.
Carry forward
- Perturbation tests whether earlier representations depend on future information.
- Causal order alone does not isolate independent packed examples.
Keep your evidence
Keep token/target alignment, the two masks, future perturbation, segment-leakage comparison and all-masked-row repair. Verify that masks and ignored loss positions enforce the same boundaries.
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).