Transformers and Attention
Compute attention, mask it, split heads, add positions and stack the block. PyTorch and NumPy from the lessons.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
One pre-LN decoder block#
Attention forward#
Shapes#
| Tensor | Shape |
|---|---|
| Token ids | (B, T), int64 |
| Token embedding matrix | (V, |
| Input to attention | (B, |
| QKV projection output | (B, |
| Q, K, V per head | (B, H, T, d_head), d_head = D // H |
| Scores and weights | (B, |
| Logits | (B, T, 50257) for GPT-2 small |
| Cross attention weights | (batch, |
Key formulas#
- Attention
- \text{Attention}(Q, K, V) = \mathop{\mathrm{softmax}}\left( \frac{Q K^T}{\sqrt{d_k}} \right) V
- Head width
- d_{\text{head}} = d_{\text{model}} / n_{\text{heads}}
- Causal mask
- M_{ij} = \begin{cases} 0 & \text{if } j \le i \\ -\infty & \text{if } j > i \end{cases}
- Sinusoidal even
- PE_{(pos, 2i)} = \sin\left(pos / 10000^{2i / d_{\text{model}}}\right)
- Sinusoidal odd
- PE_{(pos, 2i+1)} = \cos\left(pos / 10000^{2i / d_{\text{model}}}\right)
- RoPE angle
- \theta_i = \text{base}^{-2i / d_{\text{head}}}. Score depends on (m - n).
- ALiBi
- \text{attn\_score}[i, j] = \frac{q_i \cdot k_j}{\sqrt{d}} - m_h \cdot |i - j|
- Stable softmax
- \mathop{\mathrm{softmax}}(x - c) = \mathop{\mathrm{softmax}}(x)
From Lesson 07.02, Lesson 07.03, Lesson 07.04, Lesson 07.07, Lesson 10.04
Tasks in code#
| Task | Code |
|---|---|
| Apply causal mask | scores.masked_fill(mask_slice = |
| Softmax over keys | F.softmax(scores, |
| Merge heads | x.transpose(1, |
| Add positions | x = |
| Fill sinusoidal table | pe[:, |
| Rotate Q and K | apply_rope(x, |
| Fused kernel with GQA | scaled_dot_product_attention(q, |
| Tie LM head | self.lm_head.weight = |
From 19.33 code/main.py, 19.32 code/main.py, Lesson 07.03, Lesson 07.04, 19.35 code/main.py
MultiHeadSelfAttention forward#
self.qkv_proj = nn.Linear(d_model, 3 * d_model, bias=True)Q, K, V in one matmulcausal_mask = torch.tril(torch.ones(max_context_length, max_context_length))self.register_buffer("causal_mask", causal_mask, persistent=False)no gradqkv = self.qkv_proj(x)(B, T, 3D)q, k, v = qkv.chunk(3, dim=-1)three (B, T, D) viewsq = self._split_heads(q)(B, H, T, d_head)k = self._split_heads(k)v = self._split_heads(v)scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.d_head)mask_slice = self.causal_mask[:t, :t]top left (T, T)scores = scores.masked_fill(mask_slice == 0, float("-inf"))weights = F.softmax(scores, dim=-1)rows sum to 1context = self._merge_heads(torch.matmul(weights, v))(B, T, D)out = self.out_proj(context)mixes the headsFrom 19.33 code/main.py
Defaults and numbers#
| Setting | Default | Note |
|---|---|---|
| Sinusoidal and RoPE base | 10000 | raise RoPE base for longer context |
| mlp_expansion | 4 | MLP is D to 4D to D |
| d_head | 64 | 64 or 128 in production |
| Init std | 0.02 | Linear and Embedding weights |
| Residual projection scale | 1 / sqrt(2 * num_layers) | out_proj and fc2 |
| GPT-2 small d_model | 768 | 12 heads, 12 layers, about 124M |
| KV bytes per token per layer | 2 * d_head * dtype_size | per head, K and V |
| Compute optimal ratio | 20 | tokens per parameter |
From Lesson 07.04, 19.34 code/main.py, Lesson 07.03, 19.35 code/main.py, Lesson 19.35, Lesson 07.12, Lesson 07.13
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
ValueError on d_model and n_heads | D is not divisible by H | pick H that divides D |
seq_len exceeds max_context_length | T is larger than the mask | crop to the last context_length tokens |
| Model cheats on next token | no causal mask | mask the upper triangle with -inf |
Softmax gives inf | exp of a large number | subtract the row max first |
| Learning stalls | scores not scaled | divide by sqrt(d_head) |
| Deep stack diverges | post-LN on the residual path | use pre-LN |
| Weight tying saves nothing | weights copied, not shared | assign the same parameter |
| Bad output past trained length | positions do not extrapolate | use RoPE with base scaling |
From 19.33 code/main.py, Lesson 19.33, Lesson 19.34, Lesson 19.35, Lesson 10.04, Lesson 05.10, Lesson 07.04
Abbreviations#
- MHA
- Multi-head attention. N query heads, N K/V heads.
- MQA
- Multi-Query Attention. One shared K/V projection.
- GQA
- Grouped-Query Attention. Fewer K/V heads, repeated to match Q.
- MLA
- Multi-head Latent Attention. K and V in a low rank latent.
- RoPE
- Rotary Position Embeddings. Rotates Q and K by position.
- ALiBi
- Attention with Linear Biases. Distance penalty on scores.
- RMSNorm
- LayerNorm without the mean.
- KV cache
- stored K and V of every prefix token.
Questions#
- Why divide by sqrt(d_k)?
- Large dot products saturate softmax and gradients vanish.
- Why -inf in the mask?
- exp(-inf) = 0, so future positions get zero weight.
- Why add positions?
- Attention alone is permutation invariant.
- Why GQA?
- It cuts KV cache memory by N/G with nearly full quality.
- Is Flash Attention approximate?
- No. A running (max, sum) keeps it exact.
From Lesson 07.02, Lesson 07.07, Lesson 07.04, Lesson 07.03, Lesson 07.12
Tips#
- Test that weight rows sum to 1.
- Test that future tokens do not change past outputs.
- Expect a loss near log(V) at random init.
- Count parameters against the reference model.
- Put dropout after the softmax and after the second MLP linear.
- Do not use attention weights as proof of reasoning.
From Lesson 19.33, 19.33 code/tests/test_attention.py, Lesson 19.34, Lesson 19.35, Lesson 05.10