Roadmap / Theory

Attention & Transformers

Scaled dot-product attention, causal masking, multi-head attention, rotary embeddings, grouped-query attention and a full LLaMA-style block.

The MLP (multilayer perceptron) language model of section 5 reads a fixed window of tokens and treats each position with its own weights. The transformer (Vaswani et al., 2017) replaces that with attention: every token looks at every earlier token and decides, from their content, how much of each to take.

This page builds, in order: attention; its masked and multi-head forms; the rotary position encoding and grouped-query attention used by current open models; and finally the full transformer block. Each part has a small example with numbers.

Attention: a soft lookup

Think of a dictionary lookup: you have a question, you compare it with the labels of the entries, and you read the entry whose label matches. Attention does the same thing, but softly: it compares the question with every label, and reads a weighted mix of all the entries, with more weight where the match is better.

Each token produces three vectors from its embedding, with three learned linear layers:

  • a query qq: what it is looking for (the question);
  • a key kk: what it offers (the label);
  • a value vv: what it passes on (the content of the entry).

The output for a token is a weighted average of all the values, where each weight says how well the token's query matches that key:

S=QK⊤/dAttention(Q,K,V)=softmax(S) V\begin{aligned} S &= QK^\top / \sqrt{d} \\ \mathrm{Attention}(Q, K, V) &= \mathrm{softmax}(S)\, V \end{aligned}

where:

  • QQ stacks the queries as rows, shape (T,d)(T, d) for TT tokens; KK stacks the keys, shape (S,d)(S, d); VV stacks the values, shape (S,dv)(S, d_v);
  • QK⊤QK^\top is the table of all dot products between queries and keys (K⊤K^\top is KK transposed): entry (t,s)(t, s) measures how well query tt matches key ss;
  • SS holds these scores, divided by d\sqrt{d} (explained below);
  • the softmax is taken along each row, turning the scores of one query into weights that add up to 1;
  • multiplying by VV takes, for each query, the weighted average of the values.

Example. Three tokens, d=2d = 2, with queries and keys equal for simplicity: token 0 has (1,0)(1, 0), token 1 has (0,1)(0, 1), token 2 has (1,1)(1, 1). Values: v0=(1,0)v_0 = (1, 0), v1=(0,10)v_1 = (0, 10), v2=(5,5)v_2 = (5, 5).

  1. Scores for token 0: its query (1,0)(1, 0) against the three keys gives dot products 1,0,11, 0, 1; divided by 2\sqrt{2}: (0.71,0,0.71)(0.71, 0, 0.71).
  2. Weights: softmax of the scores, (0.40,0.20,0.40)(0.40, 0.20, 0.40). Token 0 matches tokens 0 and 2 best.
  3. Output: 0.40⋅(1,0)+0.20⋅(0,10)+0.40⋅(5,5)≈(2.41,3.98)0.40 \cdot (1, 0) + 0.20 \cdot (0, 10) + 0.40 \cdot (5, 5) \approx (2.41, 3.98).

For token 1 the weights are (0.20,0.40,0.40)(0.20, 0.40, 0.40) and the output (2.20,6.02)(2.20, 6.02): it takes more of v1v_1, whose key matches its query.

Bahdanau, Cho and Bengio (2015) introduced attention for translation; the transformer made it the only way tokens interact.

Why divide by d\sqrt{d}. If the entries of qq and kk are independent, with mean 0 and variance 1, their dot product is a sum of dd products, and its variance is dd. With d=128d = 128 the scores have a standard deviation of about 11. Softmax of numbers that far apart puts almost all the weight on one token, and its gradients become tiny, so the model stops learning which tokens to attend to. Dividing by d\sqrt{d} brings the variance back to 1 (Vaswani et al., section 3.2.1).

Example. In one random draw of 8 scores with standard deviation 11, the largest softmax weight is 0.999990.99999: all the attention goes to one token. The same scores divided by 128≈11.3\sqrt{128} \approx 11.3 give a largest weight of 0.410.41: the attention is spread over several tokens.

Causal masking

A language model must not see the future: the prediction at position tt may use tokens up to tt only. Otherwise, during training, it could simply read the answer. A causal mask sets every score above the diagonal to −∞-\infty before the softmax, so those weights become exactly 0:

scorests=−∞for s>t\mathrm{scores}_{ts} = -\infty \quad \text{for } s > t

where tt is the position of the query and ss the position of the key. Since e−∞=0e^{-\infty} = 0, a token gets no weight from any later token.

Example. The same three tokens. The scores, with the mask applied:

query key 0 key 1 key 2
token 0 0.71 −∞-\infty −∞-\infty
token 1 0 0.71 −∞-\infty
token 2 0.71 0.71 1.41

After the softmax, the weights of the three rows are (1,0,0)(1, 0, 0), (0.33,0.67,0)(0.33, 0.67, 0) and (0.25,0.25,0.50)(0.25, 0.25, 0.50). Token 0 can only look at itself, so its output is exactly v0v_0. Token 2, the last one, sees everything, as before. The allowed cells form a lower triangle.

This is what makes training efficient: one forward pass over a sequence of length TT computes all TT next-token predictions at once, each one seeing only its own past.

Multi-head attention

One attention pattern per layer is limiting: a token may need to look at the previous word for grammar, and at a name ten tokens back for meaning. Multi-head attention (MHA) runs several attentions side by side, each with its own queries, keys and values. It splits the model dimension DD into HH heads of size D/HD/H, runs attention in each head independently, places the results side by side and mixes them with an output projection WOW_O:

MHA(X)=[head1,…,headH] WO\mathrm{MHA}(X) = [\mathrm{head}_1, \dots, \mathrm{head}_H]\, W_O

where XX holds the input vectors, headh\mathrm{head}_h is the output of attention in head hh, and the square brackets mean "place side by side".

Example. D=8D = 8 and H=2H = 2 heads of size 4. Each token's query vector of 8 numbers is read as two queries of 4 numbers: the first 4 for head 0, the last 4 for head 1. Each head computes its own scores and weights, and returns 4 numbers per token. Side by side they make 8 numbers again, and WOW_O, an 8×88 \times 8 matrix, mixes them.

In code there is no loop over heads: project once to shape (B,T,D)(B, T, D), reshape to (B,T,H,D/H)(B, T, H, D/H), transpose to (B,H,T,D/H)(B, H, T, D/H), run batched attention, then undo the transpose and the reshape. The einsum exercise of the NumPy section is exactly this shape work.

Position: rotary embeddings

Attention by itself ignores order: shuffle the tokens, and each output is shuffled the same way. "dog bites man" and "man bites dog" would look the same. The original transformer added fixed sine and cosine waves to the embeddings. RoPE (rotary position embedding; Su et al., 2024) instead rotates the queries and keys, by an angle that grows with the position.

The dd numbers of a vector are grouped into d/2d/2 pairs, and each pair is treated as a point in the plane. Pair ii of the vector at position mm is rotated by the angle mθim\theta_i:

x2i′=x2icos⁡mθi−x2i+1sin⁡mθix2i+1′=x2isin⁡mθi+x2i+1cos⁡mθi\begin{aligned} x'_{2i} &= x_{2i} \cos m\theta_i - x_{2i+1} \sin m\theta_i \\ x'_{2i+1} &= x_{2i} \sin m\theta_i + x_{2i+1} \cos m\theta_i \end{aligned}

where:

  • x2ix_{2i} and x2i+1x_{2i+1} are the two numbers of pair ii, and x′x' the rotated ones;
  • mm is the token's position: 0,1,2,…0, 1, 2, \dots;
  • θi=10000−2i/d\theta_i = 10000^{-2i/d} is the rotation speed of pair ii: the first pairs turn fast, the last ones slowly.

Example. The pair (1,0)(1, 0) rotated by 90°90°: cos⁡90°=0\cos 90° = 0 and sin⁡90°=1\sin 90° = 1, so it becomes (1⋅0−0⋅1, 1⋅1+0⋅0)=(0,1)(1 \cdot 0 - 0 \cdot 1,\ 1 \cdot 1 + 0 \cdot 0) = (0, 1), the same point turned a quarter circle. Its length does not change, only its direction.

The key property: rotating qq by mθm\theta and kk by nθn\theta changes their dot product only through the difference m−nm - n. So attention scores depend on the relative position of two tokens, and nothing is added to the values.

Example. One pair, θ=0.5\theta = 0.5, query q=(1,2)q = (1, 2) and key k=(3,1)k = (3, 1); without rotation, q⋅k=5q \cdot k = 5.

  • Query at position 3, key at position 1 (distance 2): the score becomes −1.506-1.506.
  • Query at position 7, key at position 5 (distance 2 again): −1.506-1.506, exactly the same.
  • Query at position 5, key at position 7 (distance −2-2): 6.9096.909, different.

The score depends on how far apart the two tokens are, not on where they are.

LLaMA (Large Language Model Meta AI) uses RoPE. Implementations differ in how they pair dimensions: the RoFormer paper pairs neighbours (2i,2i+1)(2i, 2i+1), as here, while some libraries pair dimension ii with i+d/2i + d/2. The two are equivalent up to a fixed reordering of the weights.

Multi-query and grouped-query attention

When a model generates text, it keeps the keys and values of every previous token in memory, in a cache (section 11), so that it does not recompute them for every new token. That cache, not the computation, limits how many texts can be generated at once and how long they can be. Fewer key/value heads mean a smaller cache:

  • Multi-query attention (Shazeer, 2019) keeps HH query heads but a single key head and a single value head, shared by all of them.
  • GQA (grouped-query attention; Ainslie et al., 2023) is the middle ground: GG key/value heads, each shared by a group of H/GH/G query heads. Query head hh uses key/value head ⌊h/(H/G)⌋\lfloor h / (H/G) \rfloor, where ⌊⋅⌋\lfloor \cdot \rfloor rounds down.

Example. H=32H = 32 query heads and G=8G = 8 key/value heads: each group of 32/8=432 / 8 = 4 query heads shares one key/value head. Query heads 0 to 3 use head 0, query heads 4 to 7 use head 1, and so on: query head 13 uses ⌊13/4⌋=3\lfloor 13 / 4 \rfloor = 3. The cache stores 8 heads instead of 32: it is 4 times smaller than with full multi-head attention.

The transformer block

A model is a stack of identical blocks. Each block has two sub-layers:

  • attention, where tokens exchange information;
  • an MLP, where each token is processed on its own.

Each sub-layer is wrapped in a residual connection (He et al., 2016): its output is added to its input, so the block only adds a correction to what it receives:

x←x+Attention(Norm(x))x \leftarrow x + \mathrm{Attention}(\mathrm{Norm}(x))x←x+MLP(Norm(x))x \leftarrow x + \mathrm{MLP}(\mathrm{Norm}(x))

where xx holds the token vectors and Norm\mathrm{Norm} is LayerNorm (layer normalization) or RMSNorm (root mean square layer normalization) from section 4.

Why add instead of replace? With the addition, the gradient has a direct path from the loss back to the first layers (the derivative of x+f(x)x + f(x) always contains the 1 from xx), so even a stack of dozens of blocks still trains. The vector xx that flows through the stack, collecting all these corrections, is called the residual stream.

Normalizing the input of each sub-layer (pre-norm) rather than its output is what GPT-2 (Generative Pre-trained Transformer 2) did, and Xiong et al. (2020) showed it makes training stable without a long warmup.

The MLP widens each vector and narrows it back. GPT-2 uses GELU(xW1)W2\mathrm{GELU}(xW_1)W_2, with a hidden size of 4D4D; GELU (Gaussian error linear unit) is a smooth version of ReLU (rectified linear unit). LLaMA uses SwiGLU (Swish-gated linear unit; Shazeer, 2020), a gated variant with three matrices:

MLP(x)=(SiLU(xW1)⊙xW3) W2\mathrm{MLP}(x) = \big(\mathrm{SiLU}(x W_1) \odot x W_3\big)\, W_2

where:

  • W1W_1 and W3W_3 widen the vector, and W2W_2 narrows it back;
  • SiLU(z)=z σ(z)\mathrm{SiLU}(z) = z\,\sigma(z) is the SiLU (sigmoid linear unit), zz times its sigmoid;
  • ⊙\odot is the elementwise product: the second branch, xW3x W_3, works as a gate that scales each entry of the first.

Example. For one entry, if xW1xW_1 gives 22 and xW3xW_3 gives 0.50.5: SiLU(2)=2⋅σ(2)≈2⋅0.881=1.76\mathrm{SiLU}(2) = 2 \cdot \sigma(2) \approx 2 \cdot 0.881 = 1.76, times the gate 0.50.5, is 0.880.88. A gate of 0 would switch that entry off entirely.

A full model is: token embedding, LL blocks, a final norm, and a linear layer to VV logits. Every piece of that line is now an exercise you have written.

References

Practice

0/5