Module 14: Transformers 45 min12 min quick readTransformers
Self-attention
Compute scaled dot-product attention by hand, track the shape of Q, K, V and every intermediate matrix, and explain scaling, masking, and multiple heads.
Before this: Vectors and matrices, The neural-network forward pass, Word embeddings
Visual explainer: How self-attention mixes words, a 7-minute scroll-through where the lab moves as you read. Or press Watch on the lab below.
Quick read: the intuition, interactive visualization, summary, and interview questions. The math, implementation, engineering, and knowledge-check sections are hidden.
Mental model. Attention is a soft, learned lookup. Each token asks a question (its query), every token advertises what it contains (its key), and the asking token receives a blend of everyone's content (their values), weighted by how well each key matches the question.
You will learn to
- Explain queries, keys, and values first without equations, then with them.
- Compute the full attention output for a small example by hand.
- Write the shape of every matrix in .
- Explain why scores are divided by and why decoders use a causal mask.
- Describe what multi-head attention adds and what attention costs as sequences grow.
Why it matters. Self-attention is the core operation of the transformer, and therefore of every modern LLM, embedding model, and most vision and speech models. It replaced recurrence because it lets every position look at every other position directly, and because the whole sequence can be processed in parallel during training.
1. Intuition
Read the sentence "The animal didn't cross the street because it was too tired." To understand "it," you look back and decide that "animal" is the relevant word, not "street." Attention gives a model a mechanism for exactly that: when building the new representation of "it," draw heavily on "animal."
Each token's embedding is turned into three different vectors by three learned linear maps:
- Query (Q): what this token is looking for. ("I'm a pronoun; I need my referent.")
- Key (K): what this token offers to others who are searching. ("I'm a noun, singular, animate.")
- Value (V): the information this token actually passes on if chosen.
For each token, compare its query with every key (a dot product: large when they point the same way). Turn those similarity scores into weights that sum to 1 with a softmax. The token's output is the weighted average of all the values.
Compare with a recurrent network, which reads left to right and squeezes everything so far into one hidden state. In attention, "it" can reach "animal" in a single step, however far apart they are, and all positions are computed at once.
NoteAttention is older than the transformer. It was added to RNN translation models in 2014 (Bahdanau et al.): at each output step the decoder scored every encoder hidden state, took a softmax, and read a weighted average of them, instead of relying on one fixed summary vector. That version scored with a small neural network ("additive" attention); Luong et al. (2015) showed a plain dot product works as well and is cheaper. The 2017 transformer kept the dot product, added the scale, and removed the recurrence entirely, so a sequence attends to itself.
2. Visualization
Self-attention lab
Toy embeddings (d_model = 4) and random projections (d_k = 2), fixed seeds. Real models learn these.
| d1 | d2 | d3 | d4 | |
|---|---|---|---|---|
| 1:the | -0.89 | 0.72 | -0.78 | 1.54 |
| 2:cat | 1.13 | 0.20 | 0.19 | 1.73 |
| 3:sat | -1.53 | -1.35 | -0.09 | 0.71 |
| 4:on | -0.76 | -1.10 | 1.51 | 1.46 |
| 5:the | -1.65 | -0.93 | -0.74 | 1.54 |
| 6:mat | -1.02 | 2.53 | -0.90 | 1.71 |
| k1 | k2 | |
|---|---|---|
| d1 | 0.04 | -0.54 |
| d2 | -0.24 | 0.91 |
| d3 | 0.45 | 0.19 |
| d4 | 0.49 | 0.61 |
Try it (predict first, then check):
- In the course example, predict which value the output mostly copies before stepping to the end.
- Turn off the causal mask. Which cells of the heatmap change, and why must a decoder keep them masked?
What to notice:
- Every row of an attention matrix sums to 1: each query distributes one unit of attention.
- With the causal mask, row has zeros after column . The first token can only attend to itself, so its weight is exactly 1.
- Without scaling, large dot products make softmax nearly one-hot. Gradients through a saturated softmax are tiny, which stalls training.
3. The math
Symbols
| Symbol | Meaning | Shape |
|---|---|---|
| sequence length (number of tokens) | scalar | |
| embedding width | scalar | |
| token embeddings (plus positional encodings), one row per token | ||
| learned projection matrices | , , | |
| queries | ||
| keys | ||
| values | ||
| raw scores: | ||
| attention weights, each row a probability distribution | ||
| mask: 0 where allowed, where forbidden |
Scaled dot-product attention
with the softmax applied to each row separately:
Why divide by the square root of d_k
If the entries of and are independent with mean 0 and variance 1, then is a sum of terms each with variance 1, so its variance is and its standard deviation is . The script measures 2.00, 7.98, and 22.42 for , against . Dividing by brings scores back to unit scale, so softmax stays in its responsive range.
Causal masking
A decoder that predicts the next token must not see future tokens during training. Setting for makes , so token attends only to positions . Encoders (BERT-style) omit the mask and see the whole sequence in both directions.
Multi-head attention
Run attention operations in parallel, each with its own smaller projections (), concatenate the outputs, and mix them with an output matrix :
Each head can specialize (one tracks the previous token, another a syntactic relation, another a coreference) at about the same total cost as one full-width head. With and , each head has . , , , and together hold parameters per layer (ignoring biases).
Worked example
One query and two keys, :
Step 1: dot-product scores. and .
Step 2: scale. , so the scaled scores are .
Step 3: exponentiate. and . Their sum is .
Step 4: normalize. Weights are . They sum to 1.
Step 5: weighted sum of values.
The query matched better, so the output is mostly . Notice it is still a blend: contributes a third, because a scaled score of 0.71 versus 0 is not a decisive gap. Learned projections produce sharper or softer patterns as needed.
Shapes, for a full sequence. With tokens, and : is ; is ; , , are ; is ; is ; is .
4. Implementation
The full script is code/14-transformers/attention_scratch.py (NumPy only). It builds attention in four pieces, then checks them against the worked example above.
Build it from scratch
Step 1: a softmax that cannot overflow. Attention turns scores into weights with a softmax. np.exp(1000) is infinity, so subtract each row's maximum first. That changes nothing mathematically, because softmax is unchanged when you add the same constant to every input.
def softmax(z, axis=-1):
z = z - z.max(axis=axis, keepdims=True) # shift so the largest entry is 0
e = np.exp(z) # every value now in (0, 1]
return e / e.sum(axis=axis, keepdims=True)Predict: what does softmax([1000, 1001, 1002]) return? (Hint: the same as softmax([0, 1, 2]).)
Step 2: scaled dot-product attention. This is the formula from section 3, line for line. np.swapaxes(K, -1, -2) transposes only the last two axes, so the same function works for one head [n, d] or many heads [h, n, d]. The causal mask writes above the diagonal before the softmax, never after.
def attention(Q, K, V, causal=False):
d_k = Q.shape[-1]
scores = Q @ np.swapaxes(K, -1, -2) / np.sqrt(d_k) # [..., n, n]
if causal:
n = scores.shape[-1]
future = np.triu(np.ones((n, n), dtype=bool), k=1)
scores = np.where(future, -np.inf, scores) # exp(-inf) = 0
A = softmax(scores) # each row sums to 1
return A @ V, A # [..., n, d_v], [..., n, n]Predict: with causal=True, what is row 0 of A, whatever the inputs are?
Step 3: multi-head attention, the readable way. Each head uses its own slice of the projection columns, so with and each head works in 4 dimensions. Run attention per head, concatenate, and mix with .
def mha_loop(X, Wq, Wk, Wv, Wo, h, causal=True):
d_head = X.shape[-1] // h
outs = []
for i in range(h):
cols = slice(i * d_head, (i + 1) * d_head) # this head's slice of the projections
out, _ = attention(X @ Wq[:, cols], X @ Wk[:, cols], X @ Wv[:, cols], causal)
outs.append(out) # [n, d_head]
return np.concatenate(outs, axis=-1) @ Wo # [n, d_model]Predict: if you set h = 8 with , what is each head's , and what happens to the scale?
Step 4: the same thing without the loop. Real implementations never loop over heads. They project once, reshape [n, d_model] into [h, n, d_head], and let the head axis act like a batch axis. Merging reverses the reshape. The script checks that this gives exactly the loop's answer.
def split_heads(M, h): # [n, d] -> [h, n, d/h]
n, d = M.shape
return M.reshape(n, h, d // h).transpose(1, 0, 2)
def mha_batched(X, Wq, Wk, Wv, Wo, h, causal=True):
Q, K, V = (split_heads(X @ W, h) for W in (Wq, Wk, Wv))
out, A = attention(Q, K, V, causal) # all heads in one call
merged = out.transpose(1, 0, 2).reshape(X.shape[0], -1) # [n, d_model]
return merged @ Wo, APredict: if you forgot the transpose(1, 0, 2) before the final reshape, would the shapes still work? Would the answer still match the loop?
Use the library
PyTorch does all four steps in fused kernels. F.scaled_dot_product_attention is step 2 (and picks a FlashAttention-style kernel when it can, so the matrix is never stored in full). nn.MultiheadAttention is steps 3 and 4 plus the four projection matrices and their biases.
import torch
import torch.nn.functional as F
# q, k, v: [batch, heads, n, d_head]
out = F.scaled_dot_product_attention(q, k, v, is_causal=True)
# or a full multi-head layer with projections:
mha = torch.nn.MultiheadAttention(embed_dim=512, num_heads=8, batch_first=True)
out, weights = mha(x, x, x, need_weights=True) # self-attention: Q, K, V all come from xRun it
uv run code/14-transformers/attention_scratch.pyKey lines of the output (the multi-head weights come from synthetic random matrices):
== Course worked example (one query, two keys, d_k = 2) ==
weights: [0.6698 0.3302] output: [6.6976 1.9814]
softmax([1000, 1001, 1002]) = [0.09 0.2447 0.6652]
row 0 (first token can only see itself): [1. 0. 0. 0. 0. 0.]
upper triangle all zero: True
output shape: (6, 8) per-head weights shape: (2, 6, 6)
loop and batched agree: TrueThe first two lines reproduce the worked example in section 3. The figure script, with the masked sentence and the measurement, is code/14-transformers/self_attention.py.
5. Engineering
Cost. Computing and takes time, and storing takes memory per head per layer. Doubling the context length quadruples attention cost. That is why long context is expensive, and why techniques like FlashAttention (which never materializes the full matrix in slow memory), sliding-window attention, and grouped-query attention exist.
KV caching at inference. When generating token by token, the keys and values of earlier tokens don't change (thanks to the causal mask). Caching them means each new token computes only its own query against the cached keys: per step instead of recomputing the whole prefix. The cache grows with context length and batch size and is often the main GPU-memory cost of serving LLMs.
Position information. Attention by itself is permutation-invariant: shuffle the tokens and each output is the same weighted sum, just reordered. Order has to be injected through positional encodings (sinusoidal, learned, or rotary embeddings, RoPE, applied to Q and K).
Interpreting attention. Heatmaps are useful for debugging and intuition, but attention weights are not a faithful explanation of a model's decision: there are many heads and layers, and values and the feed-forward layers transform information too.
Watch outFailure modes. Forgetting the causal mask during training lets the model "cheat" by seeing the answer, giving excellent training loss and useless generation. Padding tokens must be masked too, or real tokens will attend to padding. Softmax over very large unscaled scores saturates. Memory blows up quadratically with long inputs.
Common mistakes
- Dividing by instead of , or forgetting the scaling entirely.
- Applying softmax over the wrong axis (columns instead of rows).
- Masking with a large negative number that overflows in half precision; use the framework's mask support.
- Assuming the attention matrix size depends on . It is regardless.
- Reading a single head's heatmap as "what the model thinks."
6. Knowledge check
Quiz: Self-attention
Scaled scores for one query are . What attention weight goes to the first key? (Hint: .)
Practice exercise. Change the query in the worked example to , keeping the keys and values. Compute the weights and the output.
Solution
Scores: , . Scaled: . Exponentials: and ; sum . Weights . Output . Now the second value dominates.
Implementation challenge. Implement multi-head self-attention from scratch: project to of width , reshape to , run attention per head, concatenate, and apply . Compare your output with torch.nn.MultiheadAttention after copying in the same weights.
Summary
- Each token produces a query, key, and value through learned projections.
- Scores are query-key dot products, scaled by , masked if causal, and softmaxed row by row into weights.
- Each output is the attention-weighted average of the values; every row of weights sums to 1.
- Multi-head attention runs several smaller attentions in parallel so heads can specialize.
- Cost grows as in sequence length; KV caching makes generation per new token.
Next: The transformer architecture
Related: Tokenization and pretraining · Recurrent networks · Model card: transformer decoder
Parts of this lesson are adapted from AI Engineering from Scratch by Rohit Ghumare (MIT license, notice): Attention Mechanism — The Breakthrough, Why Transformers — The Problems with RNNs, Self-Attention from Scratch, Multi-Head Attention, Multi-Head Self-Attention.
Interview angle
Why do we scale attention scores by the square root of d_k?
To keep the scores at unit scale so the softmax doesn't saturate. If the entries of and are independent with mean 0 and variance 1, then is a sum of terms each with variance 1, so its variance is and its standard deviation . With , raw scores have a standard deviation around 22.6. Softmax of values that far apart is nearly one-hot: one key gets almost all the weight, and the gradient of a saturated softmax is close to zero, so the query and key projections barely learn. Dividing by restores standard deviation 1 regardless of head size. Dividing by instead would over-shrink scores, making attention nearly uniform for large heads. Scaling doesn't change which key scores highest, only how peaked the distribution is.
Walk me through what happens to memory when context length doubles during decoding.
KV-cache memory doubles, per-token attention compute doubles, and prefill attention compute roughly quadruples. During decoding each new token attends to the keys and values of all earlier tokens, which are cached. The cache holds per token. For 32 layers, 32 KV heads, , in fp16: bytes, 0.5 MiB per token. At 4,096 tokens that is 2 GiB per sequence; at 8,192 it is 4 GiB, multiplied by batch size. That is why the cache, not the weights, limits batch size at long contexts. Prefill builds scores, so it scales as , though FlashAttention avoids storing that matrix. Grouped-query attention with 8 KV heads cuts the cache 4x, and paged attention reduces fragmentation waste.
Your decoder-only model reaches a suspiciously low training loss but generates garbage. What do you check?
First suspect is the causal mask: if it is missing or wrong, position can attend to position , which is the token it is supposed to predict. The model learns to copy the answer, training loss collapses, and at generation time, when the future doesn't exist, it has learned nothing useful. Test it directly: change a future token and check that logits at earlier positions don't move, or inspect an attention matrix and confirm the upper triangle is exactly zero. Second, check the target shift: logits at position must be scored against token , not token . Third, padding: padding tokens must be masked as keys and excluded from the loss. Fourth, inference parity: the generation loop must use the same tokenizer, position handling, and mask as training, and the KV-cache path should match a full recompute on a short prompt.
What does multi-head attention buy you, and why do modern LLMs use grouped-query attention instead of full multi-head attention?
Multi-head attention runs smaller attentions in parallel, each with , so different heads can track different relations (previous token, syntax, coreference) at about the cost of one full-width head. With and , each head has . The cost appears at inference: every head has its own keys and values in the KV cache, and decoding is usually limited by memory bandwidth for reading that cache. Grouped-query attention keeps all query heads but shares each key/value head across a group of query heads. With 32 query heads and 8 KV heads, the cache shrinks 4x, which allows bigger batches and longer contexts with a small quality cost. Multi-query attention is the extreme case, a single KV head, with the largest savings and a larger quality risk. GQA is the common middle ground.