Lesson 18 of 23 · Transformer block
Hide the future before probabilities exist.
Your win: build one scaled query-key score row, apply a causal mask, and explain why the mask must precede softmax.
Scores have one row per query
For one head, \(Q'\in\mathbb R^{T\times d_h}\) and \(K'^{\mathsf T}\in\mathbb R^{d_h\times T}\), so \(S\in\mathbb R^{T\times T}\). Row \(i\) asks: “How strongly does query token \(i\) match every key token \(j\)?”
Numbers substituted: one scaled dot product
Let \(d_h=4\), \(\mathbf q'_3=(1,2,0,-1)\), and \(\mathbf k'_2=(2,0,1,1)\). Then
The factor \(1/\sqrt{d_h}\) keeps dot-product magnitudes from growing merely because a head has more coordinates.
The causal mask is triangular
At query position \(i=3\), raw scores \((0.7,1.1,0.2,2.0)\) become \((0.7,1.1,0.2,-\infty)\). The tempting future score \(2.0\) is removed before normalization.
Code checkpoint · mask scores before softmax
The upper triangle represents future key positions. Replacing those scores with \(-\infty\) guarantees zero probability after softmax.
Show score scaling and the causal mask
scores = q @ k.transpose(-2, -1) / math.sqrt(head_dim)
# scores: [B, H, query_token, key_token]
tokens = scores.size(-1)
future = torch.ones(tokens, tokens, dtype=torch.bool, device=x.device)
future = future.triu(diagonal=1)
scores = scores.masked_fill(future, float("-inf"))Trace it: In row 2 of a \(4\times4\) mask, which column receives \(-\infty\)?
Engineering bridge · fast path and explicit fallback
MiniMind uses PyTorch's scaled dot-product attention when the inputs are a plain training prefix. It keeps an explicit score path for cached decoding and padded or otherwise masked batches, where the key length can exceed the current query length.
Show the runtime decision
if self.flash and seq_len > 1 and past_key_value is None \
and (attention_mask is None or torch.all(attention_mask == 1)):
output = F.scaled_dot_product_attention(
q, k, v, dropout_p=self.dropout if self.training else 0.0,
is_causal=True,
)
else:
scores = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
# Only the newest query rows need a new causal triangle.
scores[:, :, :, -seq_len:] += torch.full(
(seq_len, seq_len), float("-inf"), device=scores.device
).triu(1)
if attention_mask is not None:
scores += (1.0 - attention_mask[:, None, None, :]) * -1e9
output = F.softmax(scores.float(), dim=-1).to(q.dtype) @ vShape check: with a cached prefix, \(T_q=\) the new token count but \(T_k=T_{\mathrm{past}}+T_q\). The output still returns only \(T_q\) rows.
Retrieval check
Answer once before reopening the code; use the feedback to correct your explanation.
Which keys are visible to query position \(i=2\)?
STAGE 4 · PROVE IT
Practice studio · recall → build → verify
- For \(T=4\), write all four rows of the causal mask using \(0\) and \(-\infty\).
- Mask the row \((3,-1,2,5)\) for query position \(i=2\).
- Compute \(S_{1,2}\) when \(d_h=2\), \(\mathbf q'_1=(2,1)\), and \(\mathbf k'_2=(1,-1)\).
- If \(Q'\) has shape \(6\times8\), give the shapes of \(K'^{\mathsf T}\) and \(S\).
- Why is the mask added before softmax?
- Does causal masking remove a key vector from memory, or only prevent particular query-key connections?
Check solutions
- \((0,-\infty,-\infty,-\infty)\), \((0,0,-\infty,-\infty)\), \((0,0,0,-\infty)\), and \((0,0,0,0)\).
- \((3,-1,-\infty,-\infty)\).
- \(((2)(1)+(1)(-1))/\sqrt2=1/\sqrt2\approx0.707\).
- \(K'^{\mathsf T}:8\times6\); \(S:6\times6\).
- So forbidden entries exponentiate to zero and receive no probability mass.
- Only particular connections are blocked; the key still exists for queries at its own or later positions.
Primary source: Vaswani et al., Sections 3.2.1 and 3.2.3.