Training Systems · Local CPULaptop CPULesson 2 of 4 in Phase 07

Phase 07 / Transformers & language models

Masking, tokenization, and sequence packing

Revision v0.7.0 ·

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.

macOS / Linux terminal · repository folder
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-cpu.txt

PowerShell · repository folder
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-cpu.txt

After activation, run the complete worked example from that same folder:

macOS / Linux terminal · activated environment
python phases/07-transformers/02-masking-tokenization-and-sequence-packing/code/main.py

PowerShell command · repository folder
.\.venv\Scripts\python.exe phases/07-transformers/02-masking-tokenization-and-sequence-packing/code/main.py

For 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.

Check: Causal means [2,3,4.666667,7.5][2, 3, 4.666667, 7.5]; 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.

Phase 07.02 Stage 1 of 5 · Understand
Stage 1 of 5 · Understand the MechanismCore mental model, equations & visual walkthrough

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 tt, causal next-token training predicts target t+1t+1. 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 22, 44, 88, 1616 and uniform permitted scores, the outputs are prefix means: 22, 33, 14/314/3 and 7.57.5. 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 1

Keep token IDs and targets aligned

For vocabulary {a:00, b:11, c:22}, abcab becomes [0,1,2,0,1][0,1,2,0,1]. Inputs [0,1,2,0][0,1,2,0] pair with next-token targets [1,2,0,1][1,2,0,1]. 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 [0,0,1,1][0,0,1,1] means positions 22 and 33 belong to a new example. Require equal segment IDs as well as causal order. With our values, packed means become 22, 33, 88 and 1212. Position 22 must not read earlier positions from segment 00 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 ii may use key position jj only when j≤ij\le i. 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.

S~ij={Sijif key j is allowed for query i−∞otherwise\widetilde S_{ij}=\begin{cases}S_{ij}&\text{if key }j\text{ is allowed for query }i\\-\infty&\text{otherwise}\end{cases}

A causal mask blocks future information

Predict before looking: Which cells must remain zero?

A causal mask blocks future information. 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 \(0\) puts all its weight on key \(0\). Query \(1\) splits weight equally across keys \(0\) and \(1\). The last row assigns \(1/4\) to every key because, at the last position, all four positions are available.
Plot from a recorded CPU computation · JAX 0.9.2 · 1 CPU device

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 00 puts all its weight on key 00. Query 11 splits weight equally across keys 00 and 11. The last row assigns 1/41/4 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 (2,4,8,16)(2,4,8,16), query 22 returns (2+4+8)/3=14/3(2+4+8)/3=14/3, not the full-sequence average 7.57.5. The zero in its last column is what prevents the future value 1616 from influencing that result. Verify normalization across rows, not down columns.

The notebook contains the data experiment, plotting code, and recorded figure output. Rerun it to test your changes.

Stage 2 of 5 · Build & Run the ReferenceStep-by-step code construction & verified execution output

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.

main.py · append build step 1
# 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

These 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.

main.py · append build step 2
# 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 00: 11 00 00 00 query 11: 11 11 00 00 query 22: 11 11 11 00 query 33: 11 11 11 11

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.

main.py · append build step 3
# 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 [2,3,4.666667,7.5][2, 3, 4.666667, 7.5]; no weight above the diagonal.

View the cumulative build in main.py

These blocks append in order to the same file.

main.py · cumulative Python build
# 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.py
About 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
main.py · complete Python 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 [2,3,4.666667,7.5][2, 3, 4.666667, 7.5]; 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.

2026-10-20T10:47:08.194358+00:00 · Python 3.14.3 · JAX 0.9.2 · 1 CPU device

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.

Stage 3 of 5 · Controlled ExperimentsPredict before running & test boundary conditions

Perturb the future

Perturb the future · Python experiment
# 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

Pack two independent examples · Python experiment
# 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 [2,3,8,12][2, 3, 8, 12].

Causal order alone does not isolate independent packed examples.

Stage 4 of 5 · Hands-On Exercise & CheckpointModify the code yourself, solve transfer tasks & test your understanding
Hands-On Exercise · Modify & Verify Run in the declared lesson environment

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 [0,0,0,1,1][0,0,0,1,1] 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:

  1. Set up the changed input or configuration parameters requested in the prompt.
  2. Run the target function or transformation and assert that the output matches your prediction.
Starter code scaffold · copy into your file & fill in the TODOs
# 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
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 given shape initialized 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 is NaN or Inf.

2. Step-by-step implementation plan:

  1. Combine or mask array elements to form (bad_pad, _).
  2. Confirm that all computed values remain finite (no NaN or Inf).
  3. Combine or mask array elements to form (pad_out, _).
  4. Combine or mask array elements to form pad_out.
  5. Confirm that all computed values remain finite (no NaN or Inf).
Starter code scaffold · copy into your file & fill in the TODOs
# 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 · Python solution
# 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 22. 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:

  1. Set up the changed input or configuration parameters requested in the prompt.
  2. Run the target function or transformation and assert that the output matches your prediction.
Starter code scaffold · copy into your file & fill in the TODOs
# 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 22 should see only value 88 from its own segment.

Reveal reference solution and reasoning
Detect packed-example leakage · Python solution
# 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 22 and 44 belong to another example; they must be excluded despite being past positions.

Check your understanding

What must a packed causal mask enforce?

Choose one answer

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 [2,3,8,12][2,3,8,12] result.

Stage 5 of 5 · Keep Your EvidenceRecord your verification receipt & carry takeaways forward

Carry forward

  • Perturbation tests whether earlier representations depend on future information.
  • Causal order alone does not isolate independent packed examples.

Keep your evidence

Download evidence template

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.

Save work for this lesson

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