Contents The Transformer Era Code Course home

Chapter 17A Transformer You Can Check By Hand

Why do this arithmetic

Chapter 16 took the transformer apart and explained each piece, and this chapter puts one back together and runs it with numbers small enough to check on paper.

The model here has four dimensions, two heads, one block and a vocabulary of eight, which comes to 172 parameters against GPT-3’s 175 billion, and every equation is the same. That last sentence is the reason for the chapter, because scale changes what a model can do without changing what a model is. If you can follow four numbers through sixteen steps then you have followed the architecture, and the only thing left to learn about the large ones is what emerges when the same arithmetic is run at a size no one can trace.

The prompt is four tokens in and one prediction out.

a woodchuck would chuck →\;\rightarrow\; ?

The vocabulary is a, chuck, how, much, wood, woodchuck, would and the full stop, which is eight tokens indexed alphabetically from zero.

One warning about the weights before we start. They are chosen rather than trained, because every matrix in this chapter was written by hand so that its job is visible, and the chapter says what each one is for. A trained model reaches its weights by the process of Chapter 8 and they are never this tidy. What is genuinely the same is the arithmetic they go through, which is the subject here.

From words to vectors

Step 1: the prompt becomes integers

Nothing in a model consumes text, so the tokenizer of Chapter 5 converts the string to integers, and here every word already sits in the vocabulary, which makes the mapping a lookup.

position token id
0 a 0
1 woodchuck 5
2 would 6
3 chuck 1

Step 2: each integer indexes one row

The embedding table EE is 8×48 \times 4, with one row per token and four numbers per row, and the lookup is not a multiplication because the row is the answer.

To make the arithmetic readable the four dimensions have been given meanings, namely thing, action, quantifier and wood. A trained table has no such labelled columns, which is exactly the difficulty of Chapter 9.

token thing action quantifier wood
a 0.0 0.0 1.0 0.0
woodchuck 1.0 0.0 0.0 0.5
would 0.0 1.0 0.0 0.0
chuck 0.0 1.0 0.0 0.5

Woodchuck is a thing that is half about wood while chuck is an action that is half about wood, and that overlap is what makes the example worth running.

Step 3: position, because attention cannot see order

Self-attention takes a set of vectors, so permuting the input permutes the output with it and changes nothing else. A woodchuck and woodchuck a would therefore be identical, which is a fatal flaw.

The fix is to make position part of the vector, and Chapter 16 gave the sinusoids:

PE(pos,2i)=sin⁡(pos100002i/d),PE(pos,2i+1)=cos⁡(pos100002i/d).(17.1)\begin{equation} PE_{(pos,\,2i)} = \sin\!\left(\frac{pos}{10000^{2i/d}}\right), \qquad PE_{(pos,\,2i+1)} = \cos\!\left(\frac{pos}{10000^{2i/d}}\right). \label{eq:tt-pe} \quad\text{(17.1)} \end{equation}

With d=4d = 4 there are two frequencies, one that turns once per token and one a hundred times slower.

position d0d_0 sin d1d_1 cos d2d_2 sin d3d_3 cos
0 0.0000 1.0000 0.0000 1.0000
1 0.8415 0.5403 0.0100 1.0000
2 0.9093 −0.4161-0.4161 0.0200 0.9998
3 0.1411 −0.9900-0.9900 0.0300 0.9996

The fast pair separates neighbours, while the slow pair barely moves across four tokens and would be doing the work if the context were a thousand long.

Step 4: the two are added

xp=E[idp]+PEp.(17.2)\begin{equation} x_p = E[\mathrm{id}_p] + PE_p. \label{eq:tt-input} \quad\text{(17.2)} \end{equation}

Added rather than concatenated, which is worth a pause. The model gets four dimensions in total, for meaning and position together, and it has to share them.

token x0x_0 x1x_1 x2x_2 x3x_3
a 0.0000 1.0000 1.0000 1.0000
woodchuck 1.8415 0.5403 0.0100 1.5000
would 0.9093 0.5839 0.0200 0.9998
chuck 0.1411 0.0100 0.0300 1.4996

This is the block’s input, and everything that follows is a function of these sixteen numbers.

The attention sublayer

Step 5: normalise first

LN(x)=x−μσ2+ϵ,μ=1d∑jxj,σ2=1d∑j(xj−μ)2.(17.3)\begin{equation} \mathrm{LN}(x) = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}}, \qquad \mu = \frac{1}{d}\sum_j x_j, \qquad \sigma^2 = \frac{1}{d}\sum_j (x_j - \mu)^2. \label{eq:tt-ln} \quad\text{(17.3)} \end{equation}

Each row is normalised on its own across its four dimensions, with no reference to the other tokens or to the batch. Row a has mean 0.750.75 and variance 0.18750.1875, so its first entry becomes (0−0.75)/0.1875+ϵ=−1.7320(0 - 0.75)/\sqrt{0.1875 + \epsilon} = -1.7320 with ϵ=10−5\epsilon = 10^{-5}. That ϵ\epsilon stops a row of identical values, whose variance is zero, from dividing by nothing.

token n0n_0 n1n_1 n2n_2 n3n_3
a −1.7320-1.7320 0.5773 0.5773 0.5773
woodchuck 1.1856 −0.5906-0.5906 −1.3145-1.3145 0.7194
would 0.7324 −0.1157-0.1157 −1.5850-1.5850 0.9683
chuck −0.4463-0.4463 −0.6561-0.6561 −0.6241-0.6241 1.7265

Normalising before the sublayer rather than after it is the pre-norm arrangement, and it is the reason a stack of ninety-six blocks can be trained at all, since the residual path from the input to the loss passes through no normalisation and gradients therefore reach the early layers undiminished. This is the gradient highway of Chapter 13, built again in a different architecture.

Steps 6 and 7: three projections, then one comparison

Each head sees the same normalised rows through three different matrices, one per projection.

Q=XnWQ,K=XnWK,V=XnWV.(17.4)\begin{equation} Q = X_n W^Q, \qquad K = X_n W^K, \qquad V = X_n W^V. \label{eq:tt-qkv} \quad\text{(17.4)} \end{equation}

Head 1 was written to ask what thing am I acting on, so its query reads the action dimension while its key reads the thing dimension, and head 2 asks what else here is about wood. The comparison is then a scaled dot product, and the scores below are head 1’s.

S=QK⊤dk,dk=2,dk=1.4142.(17.5)\begin{equation} S = \frac{QK^{\top}}{\sqrt{d_k}}, \qquad d_k = 2, \qquad \sqrt{d_k} = 1.4142. \label{eq:tt-scores} \quad\text{(17.5)} \end{equation}

query ∖\backslash key a woodchuck would chuck
a −0.4714-0.4714 0.7777 0.6943 0.5226
woodchuck 1.0170 −0.1291-0.1291 0.1867 1.0647
would 0.5369 0.3956 0.6030 1.2186
chuck 1.5083 0.3283 0.8423 2.3148

The division by dk\sqrt{d_k} looks cosmetic at dk=2d_k = 2 and is nothing of the kind at dk=64d_k = 64, for the reason Chapter 16 measured: the dot product of two vectors of unit variance has standard deviation dk\sqrt{d_k}, and a softmax fed large numbers saturates onto one key before training has begun.

Step 8: the mask, then the softmax

The upper triangle is set to −∞-\infty before the softmax, so that e−∞=0e^{-\infty} = 0 and no weight survives there.

A=softmax(S+M),Mij={0j≤i−∞j>i(17.6)\begin{equation} A = \mathrm{softmax}\!\left(S + M\right), \qquad M_{ij} = \begin{cases} 0 & j \le i \\ -\infty & j > i \end{cases} \label{eq:tt-mask} \quad\text{(17.6)} \end{equation}

query ∖\backslash key a woodchuck would chuck
head 1
a 1.0000 . . .
woodchuck 0.7588 0.2412 . .
would 0.3405 0.2957 0.3638 .
chuck 0.2462 0.0757 0.1265 0.5516
head 2
a 1.0000 . . .
woodchuck 0.1382 0.8618 . .
would 0.0401 0.3684 0.5915 .
chuck 0.0674 0.1847 0.2821 0.4658

Three things are worth reading here. The first row of each head is 1.00001.0000 and it has no choice, because the first token can attend to nothing but itself, so the softmax is over a single value and whatever the model knows about a at this point it knew before attention ran.

Every row sums to one, which is the softmax doing its job and the reason attention is called a weighted average rather than a sum.

The two heads disagree, which is the argument for having more than one. Looking from woodchuck, head 1 sends three quarters of its weight back to a, the determiner that introduced it. Head 2 keeps 0.86180.8618 on woodchuck itself, because it is asking about wood and woodchuck is the wood-bearing token so far. One mechanism, two questions, in parallel.

The honest reading of the last row is that both heads keep most of their weight on the current token, which is what untrained attention does. Specialisation is something training produces, and the heads that get published as clean pictures of syntax are heads that were trained into that shape.

Steps 9 and 10: combine, project, and add back

Each head returns a weighted average of its values, and the heads are then concatenated back to width four and mixed by WOW^O.

MHA(X)=[head1;head2]WO,headh=AhVh.(17.7)\begin{equation} \mathrm{MHA}(X) = \left[\mathrm{head}_1 ; \mathrm{head}_2\right] W^O, \qquad \mathrm{head}_h = A_h V_h. \label{eq:tt-mha} \quad\text{(17.7)} \end{equation}

Then comes the residual connection, which is one addition and the most important addition in the architecture.

x′=x+MHA(LN(x)).(17.8)\begin{equation} x' = x + \mathrm{MHA}(\mathrm{LN}(x)). \label{eq:tt-residual} \quad\text{(17.8)} \end{equation}

token after the residual
a −1.7320-1.7320 1.0000 1.0000 −0.7320-0.7320
woodchuck 0.8132 0.5403 0.0100 2.7299
would 0.9365 0.5839 0.0200 3.0060
chuck −0.3492-0.3492 0.0100 0.0300 3.2513

Read the last column, which was near 1.51.5 for chuck before attention and is 3.253.25 after, because the wood dimension has been amplified by looking at woodchuck and its wood-ish neighbours. Information has moved between positions, which is the one thing attention is for.

The feed-forward sublayer

Step 11: widen, rectify, narrow

FFN(z)=max⁡(0,zW1+b1)W2+b2.(17.9)\begin{equation} \mathrm{FFN}(z) = \max(0,\, z W_1 + b_1)\, W_2 + b_2. \label{eq:tt-ffn} \quad\text{(17.9)} \end{equation}

Four dimensions in, eight hidden and four out, with every position going through this independently, using the same weights and with no reference to any other position.

token u0u_0 u1u_1 u2u_2 u3u_3 u4u_4 u5u_5 u6u_6 u7u_7
a 0.000 0.953 0.953 0.000 0.000 0.453 0.453 0.000
woodchuck 0.000 0.000 0.000 1.662 0.000 0.000 0.000 1.162
would 0.000 0.000 0.000 1.658 0.000 0.000 0.000 1.158
chuck 0.000 0.000 0.000 1.723 0.000 0.000 0.000 1.223

Count the zeros. Of the 32 hidden values, 22 are zero, which is 6969 per cent, and they are zero because the ReLU cut them off. Note that the pattern is uneven: row a keeps four of its eight units alive while the other three rows keep only two each. This sparsity is not an accident of a toy, because the feed-forward layers of real models are sparsely active in exactly this way, and that observation is what makes mixture-of-experts routing possible, a thread Chapter 24 picks up.

Note also that the division of labour is now complete. Attention is the only part of the block that moves information sideways, while the feed-forward layer is the only part that transforms a position on its own, and two thirds of the parameters in a real transformer sit in this second job.

Step 12: the second residual

y=x′+FFN(LN(x′)).(17.10)\begin{equation} y = x' + \mathrm{FFN}(\mathrm{LN}(x')). \label{eq:tt-block} \quad\text{(17.10)} \end{equation}

Equations (17.8) and (17.10) together are the block, and a large model is this pair ninety-six times over with wider matrices.

From vectors back to words

Step 13: only the last position matters

The block produced four vectors and only the last one is used, because only the last one has seen the whole prompt, while the other three were needed to build it. After a final layer norm the last position is

z=(−0.6805,−0.5283,−0.5198,1.7285).z = (-0.6805,\; -0.5283,\; -0.5198,\; 1.7285).

Step 14: the tied unembedding

To turn four numbers into a distribution over eight tokens, multiply by a 4×84 \times 8 matrix, and the transformer uses one it already has in E⊤E^{\top}, the embedding table read the other way.

ℓ=zE⊤,p=softmax(ℓ).(17.11)\begin{equation} \ell = z E^{\top}, \qquad p = \mathrm{softmax}(\ell). \label{eq:tt-logits} \quad\text{(17.11)} \end{equation}

This is weight tying, and the argument for it is exactly the distributional hypothesis, because if a row of EE says what a token means on the way in then the same row can score how well the output matches that token on the way out. It also saves V×dV \times d parameters, which is 32 here and 38 million in GPT-2.

token logit probability
wood 1.0480 0.3107 31.1%
chuck 0.3360 0.1524 15.2%
woodchuck 0.1838 0.1309 13.1%
. 0.0000 0.1089 10.9%
how −0.2599-0.2599 0.0840 8.4%
much −0.2599-0.2599 0.0840 8.4%
a −0.5198-0.5198 0.0648 6.5%
would −0.5283-0.5283 0.0642 6.4%

The model’s answer is wood, at 31 per cent, and it is worth being clear about why, since it is not because the model knows the tongue twister. The last position accumulated a large value in the wood dimension by attending to woodchuck and chuck, the row of EE with the largest wood component is wood, and the dot product in Equation (17.11) is doing nothing more than measuring that agreement.

Note that how and much tie exactly at −0.2599-0.2599, because they have identical embedding rows and nothing anywhere in the model can ever separate them. Two tokens with the same vector are the same token, whatever the spelling.

Learning, in one step

Step 15: the loss and its gradient

The target is wood, and cross-entropy asks one question, which is what probability you gave the right answer.

L=−log⁡pt=−log⁡0.3107=1.1690 nats.(17.12)\begin{equation} L = -\log p_{t} = -\log 0.3107 = 1.1690 \text{ nats}. \label{eq:tt-loss} \quad\text{(17.12)} \end{equation}

Now the derivative, which is the reason softmax and cross-entropy are always paired, because for a softmax followed by cross-entropy the gradient with respect to the logits collapses to something you can write down without any chain rule at all.

∂L∂ℓ=p−y,(17.13)\begin{equation} \frac{\partial L}{\partial \ell} = p - y, \label{eq:tt-grad} \quad\text{(17.13)} \end{equation}

where yy is the one-hot target, so it is the predicted probability minus what it should have been, and that is the whole derivative.

token pp target ∂L/∂ℓ\partial L / \partial \ell pp after one step
a 0.0648 0 0.0648 0.0575
chuck 0.1524 0 0.1524 0.1294
how 0.0840 0 0.0840 0.0738
much 0.0840 0 0.0840 0.0738
wood 0.3107 1 −0.6893\mathbf{-0.6893} 0.4017
woodchuck 0.1309 0 0.1309 0.1123
would 0.0642 0 0.0642 0.0570
. 0.1089 0 0.1089 0.0945

One gradient is negative and seven are positive, so the negative one raises the right answer while the positive ones lower every wrong one, and their magnitudes are exactly the probabilities that were wrongly assigned. A step of size 0.50.5 takes the loss from 1.16901.1690 to 0.91190.9119, an improvement of 0.25700.2570.

That is one step, on one example, on the logits alone. Training pushes this same vector backwards through Equation (17.11), then (17.10), (17.9), (17.8), (17.7), (17.6), (17.5), (17.4), (17.3) and into EE itself, by the chain rule of Chapter 8, and repeats it for every token of every document in the corpus.

What scale changes

Step 16: counting this model

part shape parameters
embedding EE, tied 8×48 \times 4 32
attention, per head 3×4×23 \times 4 \times 2 24
attention, two heads 48
output projection WOW^O 4×44 \times 4 16
feed-forward W1,b1W_1, b_1 4×8+84 \times 8 + 8 40
feed-forward W2,b2W_2, b_2 8×4+48 \times 4 + 4 36
total 172

The same equations, four sizes

model dmodeld_{\text{model}} layers heads parameters
this chapter 4 1 2 172
GPT-2 small 768 12 12 123,578,112
GPT-2 medium 1024 24 16 353,575,936
GPT-3 12288 96 96 174,569,631,744

Only three numbers changed the parameter count: the width, the number of blocks and the vocabulary. The head count moved too, and it is free, for the reason Chapter 16 gave. No equation in this chapter was added to, removed or altered, so a billion-parameter model is Equations (17.2) through (17.11) run wider and more often.

What does change is what the weights end up containing, and that is the subject of the rest of the book. Chapter 18 asks what happens when this block is trained on a corpus instead of written by hand, and what changes when the causal mask is removed.

Try it yourself. code/worked_examples/tiny_transformer.py produces every table in this chapter. Run it with no arguments for all sixteen steps in order, or take one stage at a time: --embed for the lookup and the sinusoids, --attention for both heads with their masks, --ffn for the sparsity of the hidden layer, --logits for the tied unembedding, --loss for the gradient step, and --params for the count.

Change one weight and run it again. The fastest way to believe the architecture is to break it: set the mask aside and watch the model see its own answer, or zero the positional encoding and watch a woodchuck and woodchuck a become the same prompt.

Then build the same block as torch.nn.TransformerEncoderLayer(d_model=4, nhead=2, dim_feedforward=8) and sum its parameters. It comes to 172172, the same total as Step 16, and not one of its parts matches. The library spends nothing on an embedding and spends the 3232 that buys it here on attention biases and two layer norms instead. Itemise both columns until you can say where every parameter went, because a total that agrees for the wrong reasons is the easiest kind of number to trust by mistake.

Run it in ColabNotebookSource

Code for this chapter

Each one reproduces the tables above. Open it in Colab, change an input and watch which numbers move.

From Natural Language Processing: From Corpus Statistics to Grounded LLMs, by Ramaseshan Ramachandran.