Attention

By the end of this chapter you will be able to compute attention by hand for a four-word sentence, read $\text{softmax}(QK^\top/\sqrt{d_k})V$ as a plain sentence rather than a spell, and explain why every modern language model is built around it.

Read this sentence: "The animal didn't cross the street because it was too tired." What does "it" refer to? You knew instantly: the animal. Now swap one word: "…because it was too wide." Suddenly "it" is the street. Nothing about the word "it" changed. Its meaning came entirely from the words around it.

That is the problem attention solves. After the embedding layer, every token is a fixed vector: "it" is the same vector in both sentences, and "bank" is the same vector whether you are fishing or depositing a cheque. A language model cannot predict the next word well with vectors like that. It needs a way for each token's vector to be updated using the other tokens in the sentence, and it needs to decide, per token, per sentence, which other tokens matter.

Attention is that mechanism. It is not complicated. It is three matrix multiplications and a softmax. But it is the single idea that made the transformer work, and it is worth understanding so deeply that you could rebuild it from memory with a pencil. That is the goal here.

The problem: a token's meaning depends on context

Let's be precise about what we need. We have a sequence of $L$ token vectors $x_1, x_2, \dots, x_L$, each of width $d_{model}$. We want to produce a new sequence $y_1, \dots, y_L$ where each $y_i$ is a version of $x_i$ that has "absorbed" the relevant context. The mechanism must satisfy four requirements:

  • Content-based. Which tokens matter to "it" depends on what the other tokens say, not on fixed positions. Sometimes the referent is two words back, sometimes twenty.
  • Variable-length. The same weights must work for a 5-token prompt and a 5,000-token document.
  • Parallel. We want to process every token at once on a GPU, not one after another.
  • Differentiable. Every step must have a gradient so the whole thing can be learned from data.

Before 2017 the dominant answer was the recurrent network: read tokens one by one, squeezing everything seen so far into a single hidden state. That state is a bottleneck. By the time "it" arrives, "animal" has been compressed, overwritten, and half-forgotten. Bahdanau et al. (2014) patched this for translation by letting the decoder "look back" over all encoder states with learned weights, and called it attention. Vaswani et al. (2017) then asked the radical question: what if we drop the recurrence entirely and use only attention?

Theanimaldidn'tcrossthestreetbecauseitwastootired. "it" needs to read from "animal" …or from "street", if the sentence ends in "wide"
Figure 1. The same token "it" must pull information from different earlier tokens depending on the rest of the sentence. Whatever mechanism does this has to look at content, not fixed positions.
Where it came from

Bahdanau, Cho and Bengio (2014), "Neural Machine Translation by Jointly Learning to Align and Translate", introduced soft attention so a translator could look back at the source sentence. Vaswani et al. (2017), "Attention Is All You Need", made it the whole architecture: the transformer.

The library analogy: query, key, value

Here is the mental picture that makes attention obvious. Imagine walking into a library with a question in your head. Every book has a label on its spine. You compare your question against every label, pull out the books whose labels match best, and read them. Three things are in play:

  • Your query $q$: what you are looking for.
  • Each book's key $k_j$: the label on the spine, what the book advertises.
  • Each book's value $v_j$: the content inside, what you actually take away when you read it.

Attention is exactly this, with two twists. First, it is soft: instead of picking one book, you read all of them, weighted by how well each label matched, so a book that matched 70% contributes 70% of what you take away. Second, every token is simultaneously a reader and a book. Token $i$ produces a query (to go looking), and also a key and a value (so that others can find it and read from it).

Why three different vectors?

Because what a token looks for, what it advertises, and what it hands over are three different things. The verb "sat" looks for a subject (query: "who is doing this?"). The noun "cat" advertises "I am a noun, I could be a subject" (key). And what "cat" hands over when found is its meaning: furry, small, animal (value). If we used a single vector for all three roles, a token could only find tokens that look like itself.

Where do $q$, $k$, $v$ come from? Each is a learned linear projection of the token's embedding. Three weight matrices, $W_Q$, $W_K$, $W_V$, are shared by every token and learned during training. That is all the "knowledge" attention has: it learns what kinds of questions tokens should ask and what kinds of labels they should wear.

Anatomy of attention: one token's point of view

Let's build attention for a single token, "sat", in the four-token sentence "The cat sat down". We will use vectors of width $d = 3$ so every number fits on a napkin. There are five steps.

Step 1 — Project: make a query, keys and values

Suppose the embedding of "sat" is $x_{\text{sat}} = [0,\ 1,\ 1]$, and the learned query matrix is

$$W_Q = \begin{bmatrix} 1 & 0 & 1 \\ 1 & 1 & 0 \\ 0 & 1 & 0 \end{bmatrix}.$$

The query is the row vector times the matrix. Multiply and add:

$$q_{\text{sat}} = x_{\text{sat}} W_Q = [\,0\!\cdot\!1 + 1\!\cdot\!1 + 1\!\cdot\!0,\ \ 0\!\cdot\!0 + 1\!\cdot\!1 + 1\!\cdot\!1,\ \ 0\!\cdot\!1 + 1\!\cdot\!0 + 1\!\cdot\!0\,] = [1,\ 2,\ 0].$$

So "sat" is now asking a question, encoded as $[1, 2, 0]$. Every token does the same thing with $W_K$ and $W_V$ to get its key and its value. To keep the arithmetic short, here are the results (you can invent any $W_K$, $W_V$ and embeddings that produce them; the mechanism does not care):

tokenkey $k_j$value $v_j$
The$[0, 0, 1]$$[1, 0, 0]$
cat$[1, 2, 0]$$[0, 1, 0]$
sat$[1, 0, 1]$$[0, 0, 1]$
down$[0, 1, 1]$$[0, 1, 1]$

Step 2 — Score every key

How well does the question match each label? We use the dot product, because it is cheap and it is large when two vectors point the same way. The score of token $j$ for our query is $s_j = q \cdot k_j$:

$$s_{\text{The}} = 1\!\cdot\!0 + 2\!\cdot\!0 + 0\!\cdot\!1 = 0,\quad s_{\text{cat}} = 1 + 4 + 0 = 5,\quad s_{\text{sat}} = 1 + 0 + 0 = 1,\quad s_{\text{down}} = 0 + 2 + 0 = 2.$$

"cat" scores highest. Its key $[1,2,0]$ happens to be exactly what the query was looking for. That is the whole idea: the query and key spaces are learned so that "verb looking for a subject" lands near "noun that can be a subject".

Step 3 — Scale

We divide every score by $\sqrt{d}$, here $\sqrt{3} \approx 1.732$. We will see exactly why in a moment; for now, think of it as keeping the numbers in a sensible range no matter how wide the vectors are.

$$\tilde{s} = \frac{[0,\ 5,\ 1,\ 2]}{1.732} = [0,\ 2.89,\ 0.58,\ 1.15].$$

Step 4 — Softmax: turn scores into weights

Scores are arbitrary real numbers. We want weights: non-negative, summing to one, so they can be used as "how much to read from each token". Softmax does this by exponentiating each score and normalising:

$$a_j = \frac{e^{\tilde{s}_j}}{\sum_{m} e^{\tilde{s}_m}}.$$

Plugging in: $e^{0}=1$, $e^{2.89}\approx 17.9$, $e^{0.58}\approx 1.78$, $e^{1.15}\approx 3.17$; their sum is $\approx 23.9$. So

$$a = [0.04,\ 0.75,\ 0.07,\ 0.13].$$

"sat" will take 75% of what it reads from "cat", 13% from "down", and a little from itself and "The". Softmax also has a useful property: because of the exponential, gaps between scores turn into ratios between weights. A score lead of 2 becomes a weight ratio of $e^2 \approx 7.4$.

Step 5 — Weighted sum of values

Finally we read. The output for "sat" is the values, blended by the weights:

$$y_{\text{sat}} = \sum_j a_j v_j = 0.04\,[1,0,0] + 0.75\,[0,1,0] + 0.07\,[0,0,1] + 0.13\,[0,1,1] = [0.04,\ 0.88,\ 0.20].$$

Look at what happened. The output is mostly the value of "cat" (the second coordinate is 0.88), with a bit of "down" mixed in. The token "sat" now carries, inside its vector, the information "my subject is a cat". Downstream layers can use that. And notice that nothing in the five steps depended on there being exactly four tokens: with forty tokens we would do the same thing, just with forty scores.

Check it yourself

Redo the example with the query for "down" instead: suppose $q_{\text{down}} = [1, 0, 1]$. Scores against the four keys are $[1, 1, 2, 1]$; scaled by $1.732$ they are $[0.58, 0.58, 1.15, 0.58]$; softmax gives roughly $[0.21, 0.21, 0.37, 0.21]$. "down" mostly reads from "sat", the verb it modifies. Its output is $0.21[1,0,0] + 0.21[0,1,0] + 0.37[0,0,1] + 0.21[0,1,1] \approx [0.21, 0.42, 0.58]$.

InteractiveAnatomy of attentionclick a token, then step through

Pick which token is asking the question, then walk through the five steps. Vectors are $d=4$ and hand-picked so the roles are readable: dimension 0 ≈ "determiner", 1 ≈ "noun", 2 ≈ "verb", 3 ≈ "particle".

The matrix form

We just did attention for one query. But every token is asking a question at the same time, and a GPU is happiest doing everything at once. So we stack. Put the $L$ token embeddings as rows of a matrix $X$ of shape $(L, d_{model})$. Then all the queries, keys and values fall out of three matrix multiplications:

$$Q = XW_Q,\qquad K = XW_K,\qquad V = XW_V,$$

where $W_Q, W_K$ have shape $(d_{model}, d_k)$ and $W_V$ has shape $(d_{model}, d_v)$. Now $Q$, $K$ and $V$ each have one row per token. Every dot product between every query and every key is a single product, $QK^\top$, which has shape $(L, L)$: row $i$ holds the scores of query $i$ against all $L$ keys. Scale, softmax each row, multiply by $V$, and you have the famous formula:

$$\text{Attention}(Q,K,V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right)V.$$

Read it as a sentence: "compare every query with every key, turn each row of comparisons into weights, and use those weights to blend the values." The five steps from before are all in there; the matrix form just does them for every token in one shot.

Symbols
$L$ = sequence length
$d_{model}$ = width of a token vector
$d_k$ = width of queries and keys
$d_v$ = width of values
$X$ = $(L, d_{model})$ token matrix
$W_Q, W_K, W_V$ = learned projections
$A$ = $(L, L)$ attention weights
STEP 1
Project
$Q = XW_Q$, $K = XW_K$, $V = XW_V$. Three cheap matmuls give every token a query, a key and a value. Shapes: $(L, d_k)$, $(L, d_k)$, $(L, d_v)$.
STEP 2
Score
$S = QK^\top$, shape $(L, L)$. Entry $S_{ij}$ is how much query $i$ likes key $j$. This is the only step whose cost grows with $L^2$.
STEP 3
Scale
$S \leftarrow S / \sqrt{d_k}$. Keeps the scores' standard deviation near 1 regardless of $d_k$, so softmax does not saturate.
STEP 4
Mask
(Language models only.) Set $S_{ij} = -\infty$ wherever $j > i$, so no token can see the future.
STEP 5
Softmax
$A = \text{softmax}(S)$ row by row. Each row becomes non-negative weights that sum to 1. Masked entries become exactly 0.
STEP 6
Mix
$Y = AV$, shape $(L, d_v)$. Row $i$ of $Y$ is the weighted blend of values that token $i$ chose to read.
QL × d_k × Kᵀd_k × L = scoressoftmax rowsL × L × VL × d_v = YL × d_v one row per token, all at once ÷ √d_k first
Figure 2. The shape of the computation. The only square thing is the $(L,L)$ score matrix in the middle; everything else is "long and thin". Every row of it is softmaxed independently, and then used to mix the rows of $V$.
Worked example, in matrix form

Take the keys and values from the table above and let all four queries be the keys themselves ($Q = K$, a common quick test). Then $QK^\top$ is the $4\times4$ matrix of all pairwise dot products. Its second row (query = "cat" $=[1,2,0]$) is $[0,\ 5,\ 1,\ 2]$, exactly the scores we computed for "sat" earlier, because we happened to give "sat" the query $[1,2,0]$ too. The diagonal is $[1, 5, 2, 2]$: every token scores itself by its squared length. Scale, softmax each row, multiply by the $4\times3$ value matrix and you get a $4\times3$ output: one context-aware vector per token.

Why divide by √dk?

The scaling looks like a fussy detail. It is not; without it attention barely trains at wide $d_k$. Here is the argument. Suppose the entries of $q$ and $k$ are independent with mean 0 and variance 1 (roughly true at initialisation). Their dot product is a sum of $d_k$ terms, each a product of two such numbers:

$$q\cdot k = \sum_{t=1}^{d_k} q_t k_t.$$

Each term has mean 0 and variance $1 \times 1 = 1$, and the variance of a sum of independent terms is the sum of the variances. So the dot product has variance $d_k$, and standard deviation $\sqrt{d_k}$. With $d_k = 64$ (GPT-2's per-head width), typical raw scores are around $\pm 8$; with $d_k = 512$ they are $\pm 22$.

Now recall that softmax turns score gaps into weight ratios. A gap of 16 between the best and second-best key (easily produced at $d_k=64$) gives a ratio of $e^{16} \approx 9{,}000{,}000$. The softmax is effectively a hard argmax: one weight is 1.0, the rest are 0.0. That has two bad consequences. The model can no longer blend, and the gradient through softmax (which is proportional to $a_j(1-a_j)$) is essentially zero everywhere, so nothing learns.

Dividing by $\sqrt{d_k}$ brings the standard deviation back to 1 for every width. The formula is

$$\text{Var}\!\left(\frac{q\cdot k}{\sqrt{d_k}}\right) = \frac{d_k}{d_k} = 1.$$

and now a typical gap is around 1–2, giving weight ratios of $e^1$ to $e^2$, a gentle preference rather than a verdict. The model can still learn to be sharp when it needs to (by growing $W_Q$ and $W_K$), but it starts soft.

Numeric demonstration

Let $d_k = 64$ and let every entry of $q$ and of two keys be $\pm 1$ at random. A typical dot product has magnitude around $\sqrt{64} = 8$. Say $q\cdot k_1 = +8$ and $q\cdot k_2 = -8$. Unscaled softmax: $e^{8}/(e^{8}+e^{-8}) = 0.99999989$. The second key is dead. Scaled by $8$: scores become $+1$ and $-1$, so the weights are $e^{1}/(e^{1}+e^{-1}) = 0.88$ and $0.12$. Both keys still participate, and both still receive gradient.

InteractiveWhy √dk: dot products grow with widthdrag d from 2 to 512

We sample 400 random unit-variance query/key pairs and histogram their dot products, raw (amber) and scaled by √d (teal). Below: the softmax over 8 of those scores, raw versus scaled.

Why $\sqrt{d_k}$ and not $d_k$?

Because standard deviation, not variance, is what sets the scale of a number. Variance is $d_k$, so standard deviation is $\sqrt{d_k}$; dividing by $\sqrt{d_k}$ makes the scaled scores have standard deviation 1. Dividing by $d_k$ would over-correct, shrinking scores to std $1/\sqrt{d_k}$ and making every attention row nearly uniform at large widths.

Causal masking: no peeking at the future

A language model is trained to predict the next token. Position 3 should predict token 4 using only tokens 1–3. But plain attention lets every token read every other token, including the ones to its right. If "sat" could read "down", the model would learn to simply copy the answer from the future, and at generation time, when the future does not exist yet, it would fall apart.

The fix is a causal mask (also called a look-ahead mask). Before the softmax, we set every score $S_{ij}$ with $j > i$ to $-\infty$. Since $e^{-\infty} = 0$, those entries get exactly zero weight, and the remaining weights in each row still sum to 1. Token $i$ can only read tokens $1, \dots, i$.

$$S_{ij} \leftarrow \begin{cases} S_{ij} & j \le i \\ -\infty & j > i \end{cases}, \qquad A = \text{softmax}(S).$$
keys → queries ↓ Thecatsatdown Thecatsatdown 1.00 −∞ −∞ −∞ 0.67 0.33 −∞ −∞ 0.16 0.63 0.21 −∞ 0.09 0.19 0.56 0.16 Lower triangle: allowed. Row i may read columns 1…i, including itself. Upper triangle: −∞ before softmax. Becomes exactly 0 after softmax; each row still sums to 1. The diagonal is always open. So no row is ever entirely masked.
Figure 3. Causal attention for "The cat sat down" (the weights are those from the interactive above, causal mode). The first token can only attend to itself, so its weight is exactly 1. Notice the triangle: the further down the row, the more tokens it may read.

Two practical notes. First, the mask is a fixed pattern that depends only on $L$, so it costs nothing to learn; you build it once with torch.tril. Second, the diagonal is always allowed, which guarantees every row has at least one finite score. A row that is entirely $-\infty$ would make softmax divide zero by zero and produce not-a-number values, and that is a classic bug when people add padding masks carelessly.

Common confusion: mask before softmax, not after

Zeroing out attention weights after the softmax is wrong: the remaining weights no longer sum to 1, and the future tokens still influenced the normalising sum. Setting scores to $-\infty$ before softmax is the only way to make the future contribute nothing at all. The same goes for padding masks.

Encoder vs decoder attention

BERT-style encoders skip the causal mask: every token sees the whole sentence, which is great for classification but useless for generation. GPT-style decoders always use it. Encoder–decoder models (the original transformer, T5) use both, plus a third kind, cross-attention, where the queries come from the decoder and the keys and values from the encoder. The formula is identical; only where $Q$ and $K,V$ come from changes.

Multi-head attention

Here is a limitation of what we have built. One attention layer produces one weight vector per token. "sat" can attend 75% to "cat", but it cannot at the same time attend mostly to "cat" for one purpose (who is the subject?) and mostly to "down" for another (what kind of sitting?). A single softmax has to compromise.

The fix is almost embarrassingly simple: run several attention operations in parallel, each with its own $W_Q, W_K, W_V$, and concatenate the results. Each parallel copy is a head. Head 1 can learn to look for subjects; head 2 can learn to look at the previous word; head 3 can learn to track punctuation. Since each head has its own projections, each can ask a different question of the same context.

Splitting the model width into heads

If we simply ran $h$ full-width heads, the cost would multiply by $h$. Instead the transformer keeps the total cost the same by making each head narrower: with $d_{model} = 768$ and $h = 12$ heads, each head works in $d_{head} = 768/12 = 64$ dimensions. Concretely:

$$\text{head}_i = \text{Attention}(XW_Q^{(i)},\ XW_K^{(i)},\ XW_V^{(i)}),\qquad W^{(i)}_{Q,K,V} \in \mathbb{R}^{d_{model}\times d_{head}}$$ $$\text{MultiHead}(X) = \text{Concat}(\text{head}_1, \dots, \text{head}_h)\,W_O,\qquad W_O \in \mathbb{R}^{h\,d_{head} \times d_{model}}.$$

Each head outputs $(L, d_{head})$. Concatenating $h$ of them side by side gives $(L, h\cdot d_{head}) = (L, d_{model})$ again. The final output projection $W_O$ mixes the heads' findings together, so the answer from head 3 can be combined with the answer from head 7 before being written back into the token's vector.

Worked shapes: GPT-2 small

$d_{model} = 768$, $h = 12$, $d_{head} = 64$, $L = 1024$. Input $X$: $(1024, 768)$. In practice all twelve $W_Q^{(i)}$ are stored as one matrix $W_Q$ of shape $(768, 768)$, so $Q = XW_Q$ is $(1024, 768)$ and we simply view it as $(12, 1024, 64)$: twelve slices of 64 columns. Same for $K$ and $V$. Each head's score matrix is $(1024, 1024)$; twelve of them is $12 \times 1024^2 \approx 12.6$ million numbers, 25 MB in fp16, per layer, per sequence. Each head's output is $(1024, 64)$; concatenated, $(1024, 768)$; through $W_O \in \mathbb{R}^{768\times768}$, still $(1024, 768)$. Parameter count: four $768\times768$ matrices $= 4 \times 589{,}824 \approx 2.36$M per layer (plus biases).

XL × d_model h₁ h₂ h₃ each: own W_Q W_K W_V width d_head = d_model / h attentionper head concat → L × d_model × W_Od_model × d_model multi-head attention (h = 3 shown)
Figure 4. Multi-head attention. The token matrix is projected into $h$ narrow query/key/value sets, each head runs attention independently, the outputs are laid side by side and mixed by $W_O$. Total cost is roughly the same as one full-width head.
InteractiveMulti-head shapes and memorymove the sliders

See how $(L, d_{model})$ is carved into $h$ heads of width $d_{head}$, and how much memory the $h$ score matrices take at a given context length (fp16, one layer, one sequence).

What heads learn

Nothing in the architecture tells head 3 to do anything in particular. Yet when you train a transformer on text and then look at its attention weights, you find heads with recognisable, stable jobs. This is one of the most satisfying findings in interpretability research, and it is worth knowing the main characters.

Previous-token heads and positional heads

Many heads in early layers attend almost entirely to the token immediately before the current one, or to a fixed offset like two back. Their attention matrix is a bright sub-diagonal. They are not "understanding" anything; they are giving each token a copy of its neighbour's information, which turns out to be a building block that later heads need (see induction heads below). Some heads attend mostly to the very first token, which usually means the head has nothing useful to do for that query and parks its weight on a harmless "sink" (Xiao et al., 2023, "Efficient Streaming Language Models with Attention Sinks").

Induction heads

Suppose the context contains "… Mr Dursley … Mr" and the model must predict what follows the second "Mr". A smart strategy: find the earlier "Mr", look at what came after it, and copy that. Olsson et al. (2022), "In-context Learning and Induction Heads", found that transformers reliably learn exactly this circuit, using two heads in sequence. A previous-token head in an earlier layer first stamps every token with "the token before me was X". Then an induction head in a later layer, sitting on the second "Mr", asks "who has a previous token equal to me?", attends to "Dursley", and copies its value forward. Their attention pattern looks like a shifted copy of the token-match pattern. Olsson et al. showed that these heads appear abruptly during training, at the same moment that the model's ability to do in-context learning jumps.

Attention patterns as pictures

The easiest way to build intuition is to look at attention matrices as images: rows are queries, columns are keys, brightness is weight. Once you have seen a few, you can recognise a previous-token head or an induction head at a glance.

previous-token headaabbccddaabbinduction headaabbccddaabb
Figure 5. Two idealised heads on the sequence a b c d a b (rows = queries, columns = keys, upper triangle masked). Left: a previous-token head lights the sub-diagonal. Right: an induction head. At the second "a" (row 5) it attends to "b" (column 2), the token that followed the first "a"; at the second "b" it attends to "c". It is predicting "what came after this last time".
InteractiveAttention pattern explorerhover a cell; switch head type

Three idealised head types on an 8-token sentence. Lower the temperature to see softmax sharpen; toggle the causal mask to see the upper triangle vanish.

What "temperature" means here

Real attention has no temperature knob; the slider divides the scores by $T$ before softmax, which is exactly what changing the scale of $W_Q$ and $W_K$ (or the $\sqrt{d_k}$ factor) would do. Trained models learn their own sharpness per head. Some heads are almost one-hot; others spread evenly over dozens of tokens.

The cost: attention is quadratic in sequence length

Now for the bad news. Look again at Step 2: $S = QK^\top$ is an $(L, L)$ matrix. Computing it takes $L^2 d_k$ multiply-adds, or about $2L^2 d_k$ FLOPs. Multiplying $A$ by $V$ costs the same again. So the core of attention costs roughly $4L^2 d$ FLOPs per layer, and it needs $L^2$ numbers of memory per head to store the scores (before FlashAttention-style tricks). Double the context and both quadruple.

Compare that to everything else in the layer. The four projections cost $8Ld^2$ and the feed-forward block (next chapter) costs about $16Ld^2$: linear in $L$. So attention's quadratic term is small for short sequences and dominant for long ones. The crossover, where $4L^2d = 24Ld^2$, is at $L = 6d$: about $4{,}600$ tokens for GPT-2 small ($d = 768$), about $25{,}000$ tokens for a $d = 4096$ model. Modern models with 128k-token contexts are far past the crossover.

InteractiveQuadratic cost of attentionchange d_model, read the crossover

FLOPs per layer as a function of sequence length $L$, log–log. The attention core grows as $L^2$; projections and the MLP grow as $L$.

The KV cache: a preview

At generation time the situation is a little different. We produce one token at a time, so there is only ever one new query, but it must attend to every key and value so far. Recomputing all previous keys and values at every step would be wasteful, since they never change (each depends only on its own token's vector, and the causal mask guarantees earlier tokens never see later ones). So we cache them: the KV cache. Generation then costs $O(L)$ per new token in compute, but memory grows with $L$: per token, per layer, we store $2 \times h \times d_{head}$ numbers. That memory is what forces the trick in the next section. The inference chapter goes deep on this.

Escaping the quadratic

A large research programme exists to make attention cheaper than $L^2$: sparse patterns, low-rank approximations, and linear attention, which rewrites the softmax so that keys and values can be summarised in a fixed-size state. The Linear Attention chapter builds that family from scratch. FlashAttention (Dao et al., 2022, arXiv) is different: it does the exact computation, but tiles it so the $(L,L)$ matrix never touches slow memory. It reduces memory from $O(L^2)$ to $O(L)$ and is much faster in wall-clock time, but the FLOP count is unchanged.

Grouped-query and multi-query attention

Take Llama-2 7B: 32 layers, 32 heads, $d_{head} = 128$. The KV cache costs $2 \times 32 \times 128 = 8{,}192$ numbers per token per layer, times 32 layers, times 2 bytes in fp16: about 512 KB per token. A single 4,096-token conversation needs 2 GB of cache; serve 32 users at once and the cache alone is 64 GB, more than the weights. Worse, at generation time every step has to read the entire cache from GPU memory, and memory bandwidth, not compute, is what limits decoding speed.

The observation that saves the day: the queries are what make heads different, but the keys and values are just "labels" and "content" of the context. Do we really need 32 separate labellings of the same context? Multi-query attention (MQA; Shazeer, 2019, "Fast Transformer Decoding: One Write-Head is All You Need") says no: keep $h$ query heads but share a single key head and value head among all of them. The cache shrinks by a factor of $h$.

MQA is a little too aggressive: quality drops slightly and training can be less stable. Grouped-query attention (GQA; Ainslie et al., 2023, arXiv) is the compromise. Use $h_{kv}$ key/value heads, with $h_{kv}$ between 1 and $h$, and let each KV head serve a group of $h / h_{kv}$ query heads. Llama-2 70B and all Llama-3 models use $h_{kv} = 8$: with 64 query heads, each KV head serves 8 query heads, and the cache is 8× smaller than full multi-head attention at nearly identical quality.

Multi-head (MHA) 8 Q heads, 8 KV heads KV cache: 8 units Grouped-query (GQA) 8 Q heads, 2 KV heads KV cache: 2 units (4× smaller) Multi-query (MQA) 8 Q heads, 1 KV head KV cache: 1 unit (8× smaller) blue = query heads, amber = key/value heads; arrows show which KV head each query head reads
Figure 6. MHA, GQA and MQA differ only in how many key/value heads exist. Query heads stay at full count so the model keeps its expressiveness; the KV cache and the memory traffic at decode time shrink by the grouping factor.

The maths of a GQA layer is the same as before with one extra step: after projecting $K$ and $V$ with $h_{kv}$ heads, repeat each KV head $h/h_{kv}$ times so it lines up with its group of query heads (repeat_interleave in PyTorch), then run ordinary per-head attention. The parameter count of $W_K$ and $W_V$ drops from $d_{model}\times d_{model}$ to $d_{model} \times h_{kv} d_{head}$ each.

Implementation walkthrough in PyTorch

Let's build it, matching code/lumen/attention.py. First a single head, as a function.

import math
import torch
import torch.nn as nn

def scaled_dot_product_attention(q, k, v, mask=None):
    """q: (..., L_q, d_k)   k: (..., L_k, d_k)   v: (..., L_k, d_v)
    mask: bool tensor broadcastable to (..., L_q, L_k); True = may attend.
    Returns (output (..., L_q, d_v), weights (..., L_q, L_k))."""
    d_k = q.size(-1)
    scores = q @ k.transpose(-2, -1) / math.sqrt(d_k)          # step 2 + 3: (..., L_q, L_k)
    if mask is not None:
        scores = scores.masked_fill(~mask, float("-inf"))       # step 4: -inf where NOT allowed
    weights = torch.softmax(scores, dim=-1)                     # step 5: rows sum to 1
    return weights @ v, weights                                 # step 6: blend the values

def causal_mask(L, device=None):
    """(L, L) lower-triangular bool matrix: row i may see columns 0..i."""
    return torch.tril(torch.ones(L, L, dtype=torch.bool, device=device))

Line by line. k.transpose(-2, -1) swaps the last two axes so that the product is $QK^\top$; using negative axis indices means the same code works with or without batch and head dimensions in front. Dividing by math.sqrt(d_k) is Step 3. masked_fill(~mask, -inf) writes $-\infty$ where the mask is False (note the ~: our mask says where attention is allowed). softmax(dim=-1) normalises each row, i.e. over keys. Finally weights @ v is $AV$. We return the weights too, which is handy for plotting.

Now multi-head. The whole trick is in two view/transpose calls, so let's not use einsum and instead watch the shapes.

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads, bias=True):
        super().__init__()
        assert d_model % n_heads == 0, "d_model must divide evenly into heads"
        self.n_heads = n_heads
        self.d_head = d_model // n_heads
        self.qkv = nn.Linear(d_model, 3 * d_model, bias=bias)   # W_Q, W_K, W_V fused side by side
        self.out = nn.Linear(d_model, d_model, bias=bias)       # W_O

    def forward(self, x, mask=None):
        B, L, D = x.shape                                       # batch, seq len, d_model
        q, k, v = self.qkv(x).split(D, dim=-1)                  # three (B, L, D) tensors
        # (B, L, D) -> (B, L, H, d_head) -> (B, H, L, d_head): heads become a batch dim
        q = q.view(B, L, self.n_heads, self.d_head).transpose(1, 2)
        k = k.view(B, L, self.n_heads, self.d_head).transpose(1, 2)
        v = v.view(B, L, self.n_heads, self.d_head).transpose(1, 2)
        if mask is None:
            mask = causal_mask(L, x.device)                     # broadcasts over (B, H)
        y, _ = scaled_dot_product_attention(q, k, v, mask)     # (B, H, L, d_head)
        y = y.transpose(1, 2).contiguous().view(B, L, D)        # concat heads: (B, L, H*d_head)
        return self.out(y)                                      # mix heads with W_O

Walk through the shapes with GPT-2 small and a batch of 2 sequences of 1024 tokens. x is $(2, 1024, 768)$. The fused qkv linear produces $(2, 1024, 2304)$; split(768) cuts it into three $(2, 1024, 768)$ tensors. This fused layer is mathematically identical to three separate linears; it is just one bigger, faster matmul. view(B, L, 12, 64) reinterprets each 768-vector as 12 chunks of 64 with no data movement. transpose(1, 2) gives $(2, 12, 1024, 64)$, so that the attention function sees "batch × heads" as leading dimensions and does 24 independent attentions in one call. The mask $(1024, 1024)$ broadcasts against the scores $(2, 12, 1024, 1024)$. After attention, transpose(1,2) puts the heads back next to their $d_{head}$ columns, contiguous() makes the memory layout match (a transpose is only a view, and view needs contiguous memory), and view(B, L, D) is the concatenation: head 0's 64 numbers, then head 1's, and so on. self.out is $W_O$.

Fast paths

Since PyTorch 2.0, torch.nn.functional.scaled_dot_product_attention(q, k, v, is_causal=True) does Steps 2–6 with a fused FlashAttention-style kernel when available. Use it in real code; use ours to understand what it does. The two agree to floating-point precision, which is exactly what Exercise 1 asks you to verify.

Grouped-query attention needs only a small change: separate projections with fewer KV heads, and a repeat_interleave before attention.

class GroupedQueryAttention(nn.Module):
    def __init__(self, d_model, n_heads, n_kv_heads):
        super().__init__()
        assert n_heads % n_kv_heads == 0
        self.n_heads, self.n_kv_heads = n_heads, n_kv_heads
        self.d_head = d_model // n_heads
        self.q_proj = nn.Linear(d_model, n_heads * self.d_head, bias=False)
        self.k_proj = nn.Linear(d_model, n_kv_heads * self.d_head, bias=False)   # smaller!
        self.v_proj = nn.Linear(d_model, n_kv_heads * self.d_head, bias=False)   # smaller!
        self.o_proj = nn.Linear(n_heads * self.d_head, d_model, bias=False)

    def forward(self, x, mask=None):
        B, L, _ = x.shape
        q = self.q_proj(x).view(B, L, self.n_heads, self.d_head).transpose(1, 2)      # (B, H, L, dh)
        k = self.k_proj(x).view(B, L, self.n_kv_heads, self.d_head).transpose(1, 2)   # (B, Hkv, L, dh)
        v = self.v_proj(x).view(B, L, self.n_kv_heads, self.d_head).transpose(1, 2)
        rep = self.n_heads // self.n_kv_heads
        k = k.repeat_interleave(rep, dim=1)   # (B, H, L, dh): each KV head serves `rep` query heads
        v = v.repeat_interleave(rep, dim=1)
        if mask is None:
            mask = causal_mask(L, x.device)
        y, _ = scaled_dot_product_attention(q, k, v, mask)
        y = y.transpose(1, 2).contiguous().view(B, L, -1)
        return self.o_proj(y)

With n_kv_heads == n_heads this is ordinary multi-head attention; with n_kv_heads == 1 it is multi-query. The repeat_interleave is only there for clarity; production kernels read the shared KV head directly without copying it.

Common confusions

Attention weights are not explanations

It is tempting to read "token 7 attends 80% to token 2" as "the model used token 2 to make its decision". But the weights only say which values were mixed; what those values contain depends on all previous layers, and the residual stream carries information that never passes through attention at all. Jain and Wallace (2019), "Attention is not Explanation", showed that very different attention patterns can give the same predictions. Treat attention maps as a useful diagnostic, not a verdict.

Keys are not values

Keys are only ever used for scoring; they never appear in the output. Values are only ever used in the output; they never affect which tokens are chosen. A token can advertise one thing (key) and deliver something else entirely (value), and that separation is exactly what lets, say, an induction head match on "previous token equals me" while copying "the token that came next".

The mask goes before the softmax

Repeated here because it is the most common bug in from-scratch implementations. If you multiply the weights by a 0/1 mask after softmax, the rows no longer sum to 1 and future tokens have already influenced the normaliser. Use $-\infty$ before softmax.

Heads are not specialised by design

Nothing in the code says "head 3 tracks syntax". All heads start as random projections; the roles we described emerge from training, and many heads never acquire a clean story. Pruning studies (Voita et al., 2019, arXiv; Michel et al., 2019, "Are Sixteen Heads Really Better than One?") find that a large fraction of heads can be removed after training with little loss.

Attention alone does not know about order

Look at the formula again: nothing in $\text{softmax}(QK^\top/\sqrt{d_k})V$ depends on the positions of the tokens. Shuffle the input rows and the output rows shuffle identically. The only reasons a transformer knows that "cat" came before "sat" are the causal mask (which is asymmetric) and the positional encoding mixed into the embeddings or the queries and keys.

Practice

Exercise 1 — causal attention from scratch, checked against torch

Implement scaled_dot_product_attention and causal_mask as above in code/lumen/attention.py. Create random q, k, v of shape (2, 4, 16, 32) and compare your output with torch.nn.functional.scaled_dot_product_attention(q, k, v, is_causal=True) using torch.allclose(..., atol=1e-5). Then check two properties by hand: every row of your weights sums to 1, and every entry above the diagonal is exactly 0.

Solution sketch

The function is nine lines; the checks are weights.sum(-1) compared with ones, and torch.triu(weights, diagonal=1).abs().max() == 0. If allclose fails, the usual culprits are forgetting the $\sqrt{d_k}$, masking with a large negative number instead of -inf (fine numerically but not exactly zero), or softmaxing over the wrong axis. If you are on torch older than 2.0, compare against nn.MultiheadAttention with batch_first=True and a causal attn_mask instead.

Exercise 2 — multi-head, and the concat check

Implement MultiHeadAttention. Verify the reshape logic without trusting it: build the same layer with n_heads=1 and with n_heads=4 using the same qkv and out weights, and confirm the outputs differ (they should; the heads change the maths). Then confirm that with n_heads=4, zeroing the output-projection columns for heads 1–3 makes the result depend only on head 0. Finally, load GPT-2's weights for one layer (see code/lumen/gpt2.py) and plot the twelve attention matrices for a sentence of your choice. Find a previous-token head.

Solution sketch

For the second check: layer.out.weight[:, 64:] = 0 zeros the columns that read heads 1–3 (with $d_{head}=64$). For plotting, return the weights from scaled_dot_product_attention and imshow each of the 12 $(L,L)$ slices. In GPT-2 small, several layer-1 to layer-4 heads are almost purely previous-token heads; a few heads in layers 5–7 behave like induction heads on repeated text.

Exercise 3 — count attention FLOPs for GPT-2 small at L = 1024

Using $d_{model}=768$, $h=12$, 12 layers, and counting a multiply-add as 2 FLOPs: (a) how many FLOPs do the four projection matmuls cost per layer for a 1024-token sequence? (b) How many does the attention core ($QK^\top$ and $AV$) cost per layer? (c) What fraction of the attention layer's FLOPs is the quadratic part? (d) At what $L$ would the two be equal?

Solution sketch

(a) Four $(768\times768)$ matmuls applied to 1024 tokens: $4 \times 2 \times 1024 \times 768^2 \approx 4.83$ GFLOPs. (b) $QK^\top$ per head: $2 \times 1024^2 \times 64$; $AV$ the same; times 12 heads: $4 \times 1024^2 \times 768 \approx 3.22$ GFLOPs. (c) $3.22 / (4.83 + 3.22) \approx 40\%$. (d) Equal when $4L^2 d = 8Ld^2$, i.e. $L = 2d = 1536$ (counting only the projections; including the MLP moves it to $L = 6d = 4608$).

Check yourself
In the library analogy, which vector decides how much a token is attended to, and which decides what is read from it?
Scores come from query·key, so keys (with the query) decide the weights. The output is a weighted sum of values, so values decide the content. Keys never appear in the output; values never affect the weights.
Why divide the scores by $\sqrt{d_k}$?
Variance of a sum of $d_k$ unit-variance products is $d_k$, so the standard deviation is $\sqrt{d_k}$. Dividing keeps scores at std ≈ 1 for any width, so softmax stays soft and gradients flow. Softmax weights are always in [0,1] regardless.
Where must the causal mask be applied?
Setting scores to −∞ makes $e^{-\infty} = 0$ so masked tokens get exactly zero weight and the remaining weights still sum to 1. Zeroing after softmax breaks normalisation and lets the future affect the normaliser.
GPT-2 small has $d_{model}=768$ and 12 heads. What is the shape of one head's attention-weight matrix for a 1024-token input, and how many such matrices are there per layer?
Weights compare every query with every key: $(L, L) = (1024, 1024)$, and there is one per head. Each head's output is $(1024, 64)$; concatenated they give $(1024, 768)$.
What does grouped-query attention change, and why?
GQA keeps all query heads but lets several of them share one key/value head. The cache stores only KV, so it shrinks by the grouping factor; the FLOP count of the attention core is unchanged and the quadratic cost remains.

Key takeaways

  • Attention lets each token build a context-aware vector by asking a question (query), scoring every other token's label (key), and blending their content (values) with softmax weights.
  • $\text{softmax}(QK^\top/\sqrt{d_k})V$ is the five hand steps done for all tokens at once; the $(L,L)$ score matrix is the only quadratic object.
  • Dividing by $\sqrt{d_k}$ keeps scores at unit variance so softmax does not collapse to argmax; the causal mask is applied as $-\infty$ before softmax.
  • Multi-head attention runs $h$ narrow attentions in parallel and mixes them with $W_O$, at the same cost as one wide head; heads learn jobs like previous-token and induction, but only by training.
  • Attention costs $O(L^2)$ compute and (naively) memory; the KV cache makes decoding $O(L)$ per token, and GQA/MQA shrink that cache by sharing key/value heads.
  • Attention weights show what was mixed, not why; and attention by itself is blind to order.

Further reading