← All posts

Foundations

Post 11 of 15

The Transformer: attention, residuals and positions

Posts in this series · 15

In the attention post we built a layer that lets every token look at every other token and take a weighted blend of what it finds. At the end of that post we hit a problem: attention has no sense of order. Shuffle the tokens and each one gets exactly the same blend.

In 2017 Vaswani and colleagues published “Attention Is All You Need” and showed that a model built mostly from attention, with no recurrence at all, could translate better and train faster than the recurrent models of the day. They called it the Transformer. Almost every model in Richard’s speech posts is one: Pocket TTS has a 6-layer Transformer backbone, Mimi hides a small one inside its encoder, Whisper is a pair of them, and Moshi sits on a 7-billion-parameter one called Helium.

This post puts the pieces together. We already have most of them:

The new pieces are the residual connection, the feed-forward network, and positions. Then we run a whole block by hand and count the parameters of a real model.

The block diagram

In one sentence: turn tokens into vectors, add where each one sits, pass them through a stack of identical blocks that each mix and then think, and turn the final vectors into scores for the next token.

Here is the whole machine, top to bottom:

  1. Embedding. Each token number picks a row of a learned table: one vector of dd numbers per token (dd is the model width).

  2. Positions. Add a position vector to each token vector, so “dog bites man” and “man bites dog” look different. (Some models, like those using RoPE, inject position inside attention instead; see below.)

  3. N blocks, all the same shape, each with two sub-layers:

    • an attention sub-layer, where tokens exchange information;
    • a feed-forward sub-layer, where each token is processed on its own.

    Each sub-layer is wrapped in a residual connection (its output is added to its input) and a normalisation.

  4. Output head. A final normalisation, then one linear layer that turns each dd-number vector into one score per word in the vocabulary. Softmax turns the scores into probabilities (post 2).

Everyday picture: a meeting with NN rounds. In each round everyone first listens to everyone else (attention), then thinks quietly on their own notes (feed-forward). Nobody throws their notes away: each round only adds comments to the margin (the residual connection). The normaliser makes sure nobody is shouting.

One fact to hold onto: every block takes in TT vectors of width dd and gives back TT vectors of width dd (TT is the number of tokens). Same shape in, same shape out. That is why blocks can be stacked like plates.

We use the pre-norm arrangement throughout, which most modern models use (post 5 explains why). For one block, with input xx:

h=x+Attention(Norm(x))h = x + \text{Attention}(\text{Norm}(x)) y=h+FFN(Norm(h))y = h + \text{FFN}(\text{Norm}(h))

The original 2017 model used post-norm instead: Norm(x+Attention(x))\text{Norm}(x + \text{Attention}(x)). Same parts, different order.

A Transformer block diagram with a token vector moving through it
Arrangement
Figure 1. One token’s trip through a block. Press Step to follow one vector from the embedding table to the output scores. The shape stays 3 tokens by width d inside every block; only the head changes it.

Residual connections

In one sentence: instead of asking a layer to produce the whole new vector, ask it for a correction, and add that correction to what you already had.

y=x+F(x)y = x + F(x)

The path that carries xx straight to the addition is called the skip connection or shortcut. Picture editing a document with tracked changes: each editor adds notes, but the original text is always still there. A bad editor can at worst add nothing.

Where it came from

He, Zhang, Ren and Sun introduced residual networks (ResNets) for image recognition in 2016 (CVPR; arXiv 1512.03385). They had noticed something strange: a plain 56-layer convolutional network did worse than a 20-layer one, even on its own training data. More layers should never hurt on training data, because the extra layers could just learn to pass things through unchanged. But learning “do nothing” turned out to be hard for a stack of weights and activations. With a shortcut, “do nothing” is easy: F(x)=0F(x) = 0. Their residual networks trained well at 152 layers and won the ImageNet competition of 2015. Vaswani and colleagues borrowed the idea for the Transformer.

Why it helps the gradient

Recall from post 1 that backpropagation multiplies one local slope per layer. Take the derivative of y=x+F(x)y = x + F(x):

dydx=1+F′(x)\frac{dy}{dx} = 1 + F'(x)

Without the shortcut, each layer contributes F′(x)F'(x) to the chain. If F′F' is around 0.50.5, ten layers give 0.5100.5^{10}: that is the vanishing gradient we met in recurrent networks. With the shortcut, each layer contributes 1+F′(x)1 + F'(x). Even if F′F' is tiny, the factor is close to 1, not close to 0. The gradient always has a clean road back.

Worked by hand on a scalar stack

Take a stack of one-number layers. Each layer computes F(x)=tanh⁡(0.5 x)F(x) = \tanh(0.5\,x), whose slope is F′(x)=0.5 (1−tanh⁡2(0.5 x))F'(x) = 0.5\,(1 - \tanh^2(0.5\,x)). Start at x0=1x_0 = 1.

Plain stack (xk+1=F(xk)x_{k+1} = F(x_k)). Each link multiplies the gradient by F′(xk)F'(x_k):

layerinput xkx_kslope F′(xk)F'(x_k)running gradient
11.00000.39320.3932
20.46210.47420.1865
30.22700.49360.0920
40.11300.49840.0459
50.05650.49960.0229

For layer 1: tanh⁡(0.5)=0.4621\tanh(0.5) = 0.4621, so the slope is 0.5×(1−0.46212)=0.5×0.7864=0.39320.5 \times (1 - 0.4621^2) = 0.5 \times 0.7864 = 0.3932. After five layers only about 2 percent of the gradient reaches the input. After 10 layers it is 0.00072; after 20, 0.0000007.

Residual stack (xk+1=xk+F(xk)x_{k+1} = x_k + F(x_k)). Each link multiplies by 1+F′(xk)1 + F'(x_k):

layerinput xkx_kslope F′(xk)F'(x_k)factor 1+F′1 + F'running gradient
11.00000.39321.39321.3932
21.46210.30551.30551.8188
32.08580.19661.19662.1763
42.86490.10201.10202.3984
53.75700.04461.04462.5053

Layer 1: x1=1+0.4621=1.4621x_1 = 1 + 0.4621 = 1.4621, and the factor is 1+0.3932=1.39321 + 0.3932 = 1.3932. The gradient never shrinks. After 10 layers it is 2.576, after 20 layers 2.577: once xx grows, tanh⁡\tanh flattens, F′F' goes to 0 and each factor settles at exactly 1.

What would change: if the layers were plain linear with slope 0.50.5 (F(x)=0.5xF(x) = 0.5x), the residual factor would be 1.51.5 every time, and ten layers would give 1.510=57.71.5^{10} = 57.7: an exploding gradient. Real Transformers avoid that by keeping branches small at the start (normalisation before each branch, small initial weights, and in Mimi a learned scale starting at 0.01). The shortcut gives the gradient a floor of about 1; the normalisation keeps it from growing.

The gradient walking back through a plain stack and a residual stack
Layer
Figure 2. The gradient with and without shortcuts. Each step walks the gradient back one layer and multiplies by F′ (plain) or 1 + F′ (residual). At depth 5 the defaults end at 0.0229 and 2.5053; at depth 10, 0.00072 and 2.576.

The feed-forward network

In one sentence: after the tokens have talked to each other, each token goes through the same small two-layer network on its own, to process what it just heard.

FFN(x)=ϕ(xW1+b1) W2+b2\text{FFN}(x) = \phi(x W_1 + b_1)\, W_2 + b_2

This is exactly the two-layer network from post 1, applied to every token separately with the same weights. Attention mixes information between tokens; the feed-forward network works within each token. Everyday picture: attention is the group discussion, the feed-forward network is each person going home and thinking it over.

Why widen then narrow? The wide middle gives room for many feature detectors at once (each of the dffd_{\text{ff}} hidden units can fire for a different pattern), and narrowing keeps the block’s output the same shape as its input so it can be added back.

The activation: ReLU or GELU

The 2017 Transformer used ReLU (rectified linear unit): ReLU(z)=max⁡(0,z)\text{ReLU}(z) = \max(0, z), keep positives, zero out negatives. BERT and GPT switched to GELU, the Gaussian error linear unit of Hendrycks and Gimpel (2016, arXiv 1606.08415):

GELU(z)=z⋅Φ(z)\text{GELU}(z) = z \cdot \Phi(z)

So GELU multiplies each input by a soft “how positive is it” switch. It is a smoothed ReLU: GELU(1)=0.841\text{GELU}(1) = 0.841, GELU(−1)=−0.159\text{GELU}(-1) = -0.159, GELU(2)=1.955\text{GELU}(2) = 1.955. Big positives pass almost unchanged, big negatives go to 0, and near zero the curve bends smoothly instead of having a corner. (Helium, Moshi’s backbone, uses a gated variant with SiLU, a close cousin; see the Moshi review.)

Worked on a tiny vector

Take d=2d = 2 and dff=4d_{\text{ff}} = 4 (that is 2d2d rather than 4d4d, to keep the table small). Input x=(1,−1)x = (1, -1).

W1=(101−10111),b1=(0,0,0,0.5),W2=(10010.50.5−11),b2=(0,0)W_1 = \begin{pmatrix} 1 & 0 & 1 & -1 \\ 0 & 1 & 1 & 1 \end{pmatrix}, \quad b_1 = (0, 0, 0, 0.5), \quad W_2 = \begin{pmatrix} 1 & 0 \\ 0 & 1 \\ 0.5 & 0.5 \\ -1 & 1 \end{pmatrix}, \quad b_2 = (0, 0)

Widen. Each hidden unit is a column of W1W_1 dotted with xx, plus its bias:

Activate (ReLU). (1,−1,0,−1.5)(1, -1, 0, -1.5) becomes (1,0,0,0)(1, 0, 0, 0). Only unit 1 fires.

Narrow. Output =1×(1,0)+0+0+0== 1 \times (1, 0) + 0 + 0 + 0 = (1, 0).

With GELU instead, the hidden values are (0.841,−0.159,0,−0.100)(0.841, -0.159, 0, -0.100) and the output is (0.942, −0.259): the negative units leak a little.

What would change: raise b1b_1‘s last entry from 0.50.5 to 2.52.5 and unit 4 becomes 0.50.5, which ReLU passes, adding 0.5×(−1,1)0.5 \times (-1, 1) to give (0.5,0.5)(0.5, 0.5).

Parameter count

W1W_1 has d×4d=4d2d \times 4d = 4d^2 numbers, W2W_2 another 4d24d^2, plus biases 4d+d4d + d:

FFN parameters=8d2+5d\text{FFN parameters} = 8d^2 + 5d

For d=768d = 768: 8×589,824+3,840=8 \times 589{,}824 + 3{,}840 = 4,722,432. Compare attention’s 4d2+4d=4d^2 + 4d = 2,362,368 (post 10). The feed-forward network holds about two thirds of every block’s weights.

Two numbers widened to four hidden units, activated, and narrowed back to two
Activation
Figure 3. Widen, activate, narrow. With x = (1, −1) the hidden sums are 1, −1, 0, −1.5; ReLU keeps only the first, and the output is (1, 0). Switch to GELU to get (0.942, −0.259).

Giving attention a sense of order

Attention compares every token with every other token, but nothing in the comparison says where a token sits. Two fixes are common: add a position vector to each token before the first block, or rotate queries and keys inside attention.

Sinusoidal positions

Vaswani and colleagues added a fixed pattern of waves. For position pp and dimension pair ii:

PE(p,2i)=sin⁡ ⁣(p100002i/d),PE(p,2i+1)=cos⁡ ⁣(p100002i/d)PE(p, 2i) = \sin\!\left(\frac{p}{10000^{2i/d}}\right), \qquad PE(p, 2i+1) = \cos\!\left(\frac{p}{10000^{2i/d}}\right)

Everyday picture: a clock. The second hand moves fast and tells apart moments close together; the hour hand moves slowly and tells apart moments far apart. Reading all hands together gives a unique time. Each dimension pair is one hand.

Worked for d=4d = 4. There are two pairs. Pair 0 has stretch 100000=110000^{0} = 1. Pair 1 has stretch 100002/4=100000.5=10010000^{2/4} = 10000^{0.5} = 100. So the four numbers at position pp are sin⁡p\sin p, cos⁡p\cos p, sin⁡(p/100)\sin(p/100), cos⁡(p/100)\cos(p/100) (angles in radians):

positionsin⁡p\sin pcos⁡p\cos psin⁡(p/100)\sin(p/100)cos⁡(p/100)\cos(p/100)
00101
10.84150.54030.01001.0000
20.9093−0.41610.02000.9998
30.1411−0.99000.03000.9996

The first pair swings a lot from one position to the next; the second pair barely moves over four positions but would separate positions 0 and 300. These four numbers are added to the token’s embedding. Whisper’s encoder uses these fixed waves.

Why sine and cosine together? A shift by kk positions turns each pair by a fixed angle, which is a rotation. That makes “three positions later” the same simple change everywhere, a hint of what RoPE does directly.

Learned positions

The simplest alternative: a second embedding table with one learned row per position, up to a maximum length. BERT and GPT-2 do this, and so does Whisper’s decoder (Whisper review). Vaswani and colleagues tried both and found nearly identical results. The cost: a learned table knows nothing beyond its last row, so a model trained with 1,024 positions has no vector for position 1,025.

A heatmap of sinusoidal position values, fast waves on the left and slow waves on the right
Width d
Figure 4. Waves at many speeds. Each column is one dimension; fast waves on the left, slow ones on the right. In the d = 4 mode, position 1 reads 0.8415, 0.5403, 0.0100, 1.0000.

Rotary position embeddings

Su and colleagues introduced rotary position embeddings (RoPE) in RoFormer (arXiv 2104.09864, 2021; journal version in Neurocomputing, 2024). Instead of adding anything to the tokens, RoPE rotates each query and key by an angle proportional to its position, just before the dot product.

In one sentence: turn the query by its position times a fixed angle, turn the key by its position times the same angle, and the dot product only feels the difference.

For one pair of numbers (a,b)(a, b) at position mm, rotation by angle mθm\theta is:

R(mθ)(ab)=(acos⁡mθ−bsin⁡mθasin⁡mθ+bcos⁡mθ)R(m\theta)\begin{pmatrix} a \\ b \end{pmatrix} = \begin{pmatrix} a\cos m\theta - b\sin m\theta \\ a\sin m\theta + b\cos m\theta \end{pmatrix}

Why the score only depends on the gap: rotating both arrows by the same extra amount does not change the angle between them, and the dot product depends only on the two lengths and that angle. A query at position mm and a key at position nn end up separated by their original angle plus (m−n)θ(m - n)\theta. Picture two people on a merry-go-round: wherever it has turned, the distance between them is the same.

Worked in 2-D. Take θ=30°\theta = 30° (a toy angle chosen for easy cosines), query q=(1,0)q = (1, 0), key k=(2,1)k = (2, 1). Unrotated, q⋅k=2q \cdot k = 2.

Same gap of 1, same score, wherever the pair sits. A gap of 2 (positions 2 and 0, or 4 and 2) gives 1.866 every time; a gap of −1 (query at 0, key at 1) gives 1.232. The score depends on the order too, not just the distance.

Real models split each query and key into d/2d/2 pairs, each with its own angle θi=10000−2i/d\theta_i = 10000^{-2i/d}, the same ladder of speeds as the sinusoidal clock. RoPE adds no parameters. Helium (Moshi’s backbone) and Mimi’s encoder Transformer use it (Moshi review, Mimi post).

A query and a key rotated by their positions; their dot product depends only on the gap
Figure 5. Rotation keeps only the gap. Drag both positions together and the arrows spin, but the dot product stays 2.232 for a gap of 1 (θ = 30°, q = (1, 0), k = (2, 1)).

A full forward pass by hand

Now one whole pre-norm block on three tokens, “the cat sat”, with d=2d = 2. Toy simplifications, to keep every number on the page: one attention head; the query, key and value matrices and the output projection are all the identity (each vector is its own query, key and value); no mask (every token sees all three, as in an encoder); RMSNorm without its learned gain and with ϵ=0\epsilon = 0; a feed-forward width of 4; no biases. Numbers are rounded to three or four places.

Step one: embedding plus position. Token vectors (1,0)(1, 0), (0,1)(0, 1), (1,1)(1, 1); learned position vectors (0,0)(0, 0), (0.5,0)(0.5, 0), (0,0.5)(0, 0.5). Add:

x=(100.5111.5)x = \begin{pmatrix} 1 & 0 \\ 0.5 & 1 \\ 1 & 1.5 \end{pmatrix}

Step two: normalise (RMSNorm). For each row, divide by the root mean square (a2+b2)/2\sqrt{(a^2 + b^2)/2} (post 5):

Step three: attention. Scores are dot products divided by 2=1.4142\sqrt{2} = 1.4142. Row “the”: 1.4142×1.4142/1.4142=1.41421.4142 \times 1.4142 / 1.4142 = 1.4142; with cat 1.4142×0.6325/1.4142=0.63251.4142 \times 0.6325 / 1.4142 = 0.6325; with sat 0.78450.7845. All scores:

(1.41420.63250.78450.63251.41421.40330.78451.40331.4142)\begin{pmatrix} 1.4142 & 0.6325 & 0.7845 \\ 0.6325 & 1.4142 & 1.4033 \\ 0.7845 & 1.4033 & 1.4142 \end{pmatrix}

Softmax each row (post 10). Row “the”: e1.4142=4.113e^{1.4142} = 4.113, e0.6325=1.882e^{0.6325} = 1.882, e0.7845=2.191e^{0.7845} = 2.191, total 8.1868.186, weights 0.502, 0.230, 0.268. Rows “cat” and “sat”: 0.187, 0.409, 0.404 and 0.211, 0.392, 0.397.

Blend the normalised vectors. For “the”: 0.502×(1.4142,0)+0.230×(0.6325,1.2649)+0.268×(0.7845,1.1767)=(1.066,0.606)0.502 \times (1.4142, 0) + 0.230 \times (0.6325, 1.2649) + 0.268 \times (0.7845, 1.1767) = (1.066, 0.606). All three:

Attn=(1.0660.6060.8400.9930.8580.963)\text{Attn} = \begin{pmatrix} 1.066 & 0.606 \\ 0.840 & 0.993 \\ 0.858 & 0.963 \end{pmatrix}

Step four: first residual add. h=x+Attnh = x + \text{Attn}. For “the”: (1+1.066,0+0.606)=(2.066,0.606)(1 + 1.066, 0 + 0.606) = (2.066, 0.606).

h=(2.0660.6061.3401.9931.8582.463)h = \begin{pmatrix} 2.066 & 0.606 \\ 1.340 & 1.993 \\ 1.858 & 2.463 \end{pmatrix}

“the” started with nothing in its second number; it now holds 0.6060.606 borrowed from “cat” and “sat”.

Step five: normalise again. Root mean squares 1.5221.522, 1.6981.698, 2.1812.181, giving

Norm(h)=(1.3570.3980.7891.1740.8521.129)\text{Norm}(h) = \begin{pmatrix} 1.357 & 0.398 \\ 0.789 & 1.174 \\ 0.852 & 1.129 \end{pmatrix}

Step six: feed-forward network. Weights

W1=(1−10101−11),W2=(0.5000.5−0.500−0.5)W_1 = \begin{pmatrix} 1 & -1 & 0 & 1 \\ 0 & 1 & -1 & 1 \end{pmatrix}, \qquad W_2 = \begin{pmatrix} 0.5 & 0 \\ 0 & 0.5 \\ -0.5 & 0 \\ 0 & -0.5 \end{pmatrix}

For “the”, (1.357,0.398)(1.357, 0.398): hidden sums 1.3571.357, −1.357+0.398=−0.959-1.357 + 0.398 = -0.959, −0.398-0.398, 1.357+0.398=1.7551.357 + 0.398 = 1.755. ReLU gives (1.357,0,0,1.755)(1.357, 0, 0, 1.755). Narrow: (0.5×1.357, −0.5×1.755)=(0.679,−0.877)(0.5 \times 1.357,\ -0.5 \times 1.755) = (0.679, -0.877). All three:

FFN=(0.679−0.8770.395−0.7890.426−0.852)\text{FFN} = \begin{pmatrix} 0.679 & -0.877 \\ 0.395 & -0.789 \\ 0.426 & -0.852 \end{pmatrix}

Step seven: second residual add. y=h+FFNy = h + \text{FFN}:

y=(2.744−0.2721.7351.2032.2841.611)y = \begin{pmatrix} 2.744 & -0.272 \\ 1.735 & 1.203 \\ 2.284 & 1.611 \end{pmatrix}

Three tokens of width 2 in, three of width 2 out, ready for the next block. In a trained model the weights are learned, not chosen; the steps are exactly these.

Three token vectors passing through every operation of one Transformer block
Show the arithmetic for
Mask
Figure 6. A whole block, three tokens, every number. Step through the seven operations. With the defaults, “the” goes (1, 0) to (2.066, 0.606) after attention and to (2.744, −0.272) after the feed-forward network.

Three shapes of Transformer

The same block comes in three arrangements.

Training and generating

A decoder-only model is trained by next-token prediction: at every position, predict the token that comes next, scored with cross-entropy (post 2). The input during training is the real text, not the model’s own guesses; that is called teacher forcing. The full story of tokens and language models is in post 15.

Why training is fast: with the causal mask, one forward pass over a 1,000-token sentence produces 1,000 predictions at once, each seeing only its own past. All positions are computed together as big matrix products, which graphics processors love. A recurrent network has to walk the 1,000 steps one after another.

Why generating is slow: at generation time the next token does not exist until the model has produced it. So the model predicts token 1, appends it, predicts token 2, appends it, one at a time. The KV cache saves recomputing the past, but the loop is still sequential. Pocket TTS lives with exactly this loop, one 80-millisecond audio frame per step.

Counting the parameters of a real model

Take 12 layers, d=768d = 768, 12 heads, a vocabulary of 50,257 tokens and 1,024 learned positions. That is GPT-2’s smallest configuration, and pre-norm with layer norm.

Per block:

Twelve blocks: 12×7,087,872=12 \times 7{,}087{,}872 = 85,054,464.

Outside the blocks:

Grand total: 124,439,808, which is the familiar “124M” of GPT-2 small. Notice that almost a third of it is the vocabulary table. Double dd and the block weights grow about four times, because they scale with d2d^2.

The same count in PyTorch. nn.TransformerEncoderLayer builds one block (attention, feed-forward, two layer norms), and the rest is the embeddings and the final norm:

import torch.nn as nn

count = lambda m: sum(p.numel() for p in m.parameters())
d, layers, vocab, context = 768, 12, 50257, 1024
block = nn.TransformerEncoderLayer(d, nhead=12, dim_feedforward=4 * d,
                                   norm_first=True)
print("one block", count(block))
# blocks + token table + position table + final layer norm
total = layers * count(block) + vocab * d + context * d + 2 * d
print("GPT-2 small", total)

It prints:

one block 7087872
GPT-2 small 124439808

GPT-2’s own blocks use a causal mask and GELU, but neither has parameters, so the count is the same.

Where a Transformer's parameters live: embeddings, positions, attention, feed-forward and norms
Figure 7. Where the parameters live. At 12 layers, width 768, 12 heads, vocabulary 50,257 and 1,024 positions, the total is 124,439,808. Move the heads slider and nothing changes.

What to remember

Try it yourself

Sources