Module 07 · Part II · The Transformer

The Block and the Full Model

Positional Encodings, the Residual Stream, LayerNorm, the Feed-Forward Network, the Output Layer, and One Token Traced Through the Whole Model

By the end you can trace one token through a full transformer and name every tensor's shape along the way.

Builds onModule 6·
Check yourself ▸

Before you start, can you already…

  • ☐compute attention by hand, prove why scores are divided by √d_k, and explain what multiple heads add. (Module 6)

If any is shaky, the linked module teaches it.

Open in Colab
5 Hours·Core·Intermediate·0/6

Attention is one part of a transformer. This module supplies the rest: position information so word order counts, residual connections and normalization so deep stacks train, the feed-forward network, and the output layer. Section 7.6 assembles them and follows one input through the whole model.

7.1 Positional EncodingsRun this section

The 2017 paper gives each position its own vector and adds it to the token's embedding before the first layer:

xinput,pos=xtoken+ppos\mathbf{x}_{\text{input}, pos} = \mathbf{x}_{\text{token}} + \mathbf{p}_{pos}

Both have width . The paper builds ppos\mathbf{p}_{pos} from waves, in pairs of features: pair ii uses feature 2i2i (sine) and 2i+12i+1 (cosine) at the same frequency:

PE(pos,2i)=sin⁡(pos100002i/dmodel)PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i / d_{\text{model}}}}\right)
PE(pos,2i+1)=cos⁡(pos100002i/dmodel)PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i / d_{\text{model}}}}\right)

pospos runs from 00 to N−1N-1 and ii from 00 to dmodel/2−1d_{\text{model}}/2 - 1. Pair ii has frequency and so repeats every 2π/ωi2\pi / \omega_i positions, its wavelength: about 6.286.28 for pair 0, growing to almost 2π⋅100002\pi \cdot 10000 for the last.

Architecture Flowchart
Diagram: Every position (d_model = 512); Pair i = 0: wavelength 2π ≈ 6.28 tokens (tells neighbors apart); Pair i = 64: wavelength 2π · 10 ≈ 63 tokens (tells apart positions within a passage); Pair i = 255 (last): wavelength ≈ 60,600 tokens (tells apart distant parts of a document)

In words. Short waves separate nearby positions; long waves separate distant ones. Read together, like the hands of a clock, they give every position a distinct vector, with nearby positions similar.

The Linear Shift Property

Sines and cosines make moving kk positions forward the same operation at every starting position: one 2×22 \times 2 matrix per pair, from the angle-addition formulas:

sin⁡(ω(pos+k))=sin⁡(ωpos)cos⁡(ωk)+cos⁡(ωpos)sin⁡(ωk)\sin(\omega (pos + k)) = \sin(\omega pos)\cos(\omega k) + \cos(\omega pos)\sin(\omega k)
cos⁡(ω(pos+k))=cos⁡(ωpos)cos⁡(ωk)−sin⁡(ωpos)sin⁡(ωk)\cos(\omega (pos + k)) = \cos(\omega pos)\cos(\omega k) - \sin(\omega pos)\sin(\omega k)

For fixed kk, cos⁡(ωk)\cos(\omega k) and sin⁡(ωk)\sin(\omega k) are constants, so each shifted value is a fixed mix of the unshifted pair:

[PE(pos+k,2i)PE(pos+k,2i+1)]=[cos⁡(ωik)sin⁡(ωik)−sin⁡(ωik)cos⁡(ωik)][PE(pos,2i)PE(pos,2i+1)]\begin{bmatrix} PE_{(pos+k, 2i)} \\ PE_{(pos+k, 2i+1)} \end{bmatrix} = \begin{bmatrix} \cos(\omega_i k) & \sin(\omega_i k) \\ -\sin(\omega_i k) & \cos(\omega_i k) \end{bmatrix} \begin{bmatrix} PE_{(pos, 2i)} \\ PE_{(pos, 2i+1)} \end{bmatrix}

The matrix rotates each pair by the angle ωik\omega_i k. It contains kk but not pospos, so the same matrix works at every position. (Its minus sign sits bottom-left, unlike section 15.1's rotation, only because each pair is written sine first.)

Figure 7.1
Positions as clock hands

Each pair of features is a hand turning at its own speed. Move the position and every hand turns; the gold arc from pos to pos + k stays the same size wherever you start.

dmodel = 512, as in the original transformer. The heatmap shows the sine half of each pair; the cosine half is the same wave shifted by a quarter turn.
Predict, then try

Pick two positions far apart, say 3 and 40, and then 103 and 140. Before looking at the hands, predict whether the turn from the first position to the second is the same in both pairs.

In words. The encoding as a heatmap and as six clock hands, one per frequency pair. Moving k positions forward turns every hand by an angle depending only on k and the hand's speed, wherever you start.

The hope was that attention could then learn relative patterns such as "the word 3 places back" once, for every position. Nothing forces the model to use the structure. Section 15.1 covers RoPE (rotary position embedding), which builds the rotation into the query–key product itself.

Measured (lab 2a, experiments/ab_positional.py). The property holds. Fitting the best linear map from PEposPE_{pos} to PEpos+kPE_{pos+k} over all positions (by least squares) leaves an error of 2.9×10−62.9 \times 10^{-6} for k=1,7k = 1, 7 and 2525. That is the rounding floor of float32 arithmetic, not a flaw in the encoding. The fitted 2×22 \times 2 matrix matches the rotation matrix in this section to within 6×10−86 \times 10^{-8}.

Which Encoding the Reference Implementation Uses

The reference implementation uses learned absolute positional embeddings, as GPT-1 and GPT-2 did: a table with one trainable vector per position, up to the context length LL. This section derives the sinusoids because RoPE builds on their maths.

python
# src/transformer/positional_embedding.py
self.embedding = nn.Embedding(
    num_embeddings=context_length,
    embedding_dim=d_model,
)

The choice shows up in three places:

learned absolutesinusoidal
parameters at dmodel=64d_{\text{model}} = 64, L=64L = 644,0964{,}096 (a 64×6464 \times 64 table)00
position beyond LL when the trained model is usedno vector existscomputed by the formula
with a KV cache (section 12.2: stored keys and values reused while generating)the code must pass the true position, and it cannot exceed LLthe code must pass the true position, and any position works

Measured (lab 2a). Over 600 training steps with the same seed and the same batches, the sinusoidal version reaches 1.94421.9442 validation loss (the error on text held out from training; lower is better) and the learned table reaches 2.09782.0978. The sinusoidal version is better by 0.15360.1536 and has 4,096 fewer parameters. Read this with care: it is two layers, 600 steps and one seed. It does not show that sinusoids beat learned tables in general, and both are used in production. It shows that at this scale the choice does not decide the result.

7.2 Residual Connections and the Residual StreamRun this section

A block has two sub-layers, attention then the feed-forward network (section 7.4), and each one's output is added to its input: x+Sublayer⁡(x)\mathbf{x} + \operatorname{Sublayer}(\mathbf{x}), which the 2017 paper then normalizes (section 7.3). The x+\mathbf{x} + is a (He et al., 2016). It does two jobs:

  • Each sub-layer only learns a change. With nothing useful to add, it outputs near zero and the vector passes through untouched, so a deep stack starts out harmless rather than scrambling its input.
  • The gradient gets a path with no multiplications. The derivative of x+f(x)\mathbf{x} + f(\mathbf{x}) is ; the identity passes the gradient straight through, curing the long-chain problem of section 2.6. Section 8.5 makes this exact for a whole stack.

Picture the running vector as a shared channel up through the model, which every sub-layer reads from and writes into: the residual stream. Section 14.1 shows how much this picture explains.

7.3 LayerNormRun this section

Adding to the stream again and again lets the numbers drift: growing, or lopsided with a few features dominating. LayerNorm (Ba et al., 2016) rescales each token's vector to mean 0 and variance 1, then applies a learned scale γ\boldsymbol{\gamma} and shift , keeping values in a steady range so softmax stays out of saturation and gradients stay healthy. Normalizing after the add, as in 2017, is Post-LN; section 8.5 compares it with Pre-LN.

LayerNorm⁡(x)=x−μσ2+ϵ⊙γ+β,μ=1d∑i=1dxi,σ2=1d∑i=1d(xi−μ)2\operatorname{LayerNorm}(\mathbf{x}) = \frac{\mathbf{x} - \mu}{\sqrt{\sigma^2 + \epsilon}} \odot \boldsymbol{\gamma} + \boldsymbol{\beta}, \quad \mu = \frac{1}{d}\sum_{i=1}^d x_i, \quad \sigma^2 = \frac{1}{d}\sum_{i=1}^d (x_i - \mu)^2

ϵ\epsilon is a tiny constant that keeps the denominator away from zero. The mean and variance are over the dd features of one token, not across tokens or the batch.

Worked example. Take x=[2,4,6,8]\mathbf{x} = [2, 4, 6, 8], with γ=1\boldsymbol{\gamma} = 1 and β=0\boldsymbol{\beta} = 0. The mean is 55 and the variance is 9+1+1+94=5\frac{9 + 1 + 1 + 9}{4} = 5, so the standard deviation is . The output is [−1.342,−0.447,0.447,1.342][-1.342, -0.447, 0.447, 1.342]: mean 0, variance 1. Doubling x\mathbf{x} to [4,8,12,16][4, 8, 12, 16] gives exactly the same output, because LayerNorm discards the overall scale.

Figure 7.3
LayerNorm keeps the pattern and discards the overall size

One token’s eight features, before and after each stage of LayerNorm. Scale or shift the whole input and the normalized bars do not move; change one feature and they do.

The learned scale γ and shift β are fixed example values here; in a model they are trained, one per feature.
Predict, then try

Multiply the input vector by 10 using the scale slider. Before you look, predict what happens to LayerNorm's output, and what that implies about information carried in a vector's overall size.

In words. An eight-feature vector as bars: as input, after subtracting the mean, after dividing by the standard deviation, and after the learned scale and shift. Scaling or shifting the whole input leaves the normalized bars still; changing one feature moves them.

7.4 The Feed-Forward NetworkRun this section

Every attention sub-layer is followed by a feed-forward network (FFN): a one-hidden-layer MLP (section 4.2) applied to each token separately, with the same weights at every position.

Why? Attention moves information between tokens but barely changes it. Token ii's output yi=∑jAijvj\mathbf{y}_i = \sum_j A_{ij} \mathbf{v}_j is a weighted average of values: the weights depend nonlinearly on the input through softmax, but the features themselves pass only through linear maps ( and WOW^O). Attention cannot compute something like "switch this feature on only if these two others are both large". The FFN supplies that per-token nonlinearity:

FFN⁡(x)=max⁡(0,xW1+b1)W2+b2\operatorname{FFN}(\mathbf{x}) = \max(0, \mathbf{x} W_1 + \mathbf{b}_1) W_2 + \mathbf{b}_2

widens the vector fourfold (the paper uses dff=2048d_{ff} = 2048 for dmodel=512d_{\text{model}} = 512), ReLU zeroes the negative entries, and W2∈R4dmodel×dmodelW_2 \in \mathbb{R}^{4d_{\text{model}} \times d_{\text{model}}} brings it back to model width. GPT-2 and the reference implementation use GELU, a smooth ReLU that bends gently near zero instead of at a corner:

GELU⁡(z)≈0.5 z(1+tanh⁡(2/π(z+0.044715z3)))\operatorname{GELU}(z) \approx 0.5\,z\left(1 + \tanh\left(\sqrt{2/\pi}\left(z + 0.044715 z^3\right)\right)\right)
python
# src/transformer/feed_forward.py — the shipped activation
self.net = nn.Sequential(
    nn.Linear(d_model, d_ff),
    nn.GELU(),
    nn.Linear(d_ff, d_model),
)

Section 14.2 reads the FFN another way, as a store of memorized associations.

7.5 The Output LayerRun this section

After the last block, each token's dmodeld_{\text{model}} numbers must become one score per vocabulary entry. The output layer (unembedding, or language-model head) does this with one more matrix:

z=hfinalWunembed,Wunembed∈Rdmodel×∣V∣\mathbf{z} = \mathbf{h}_{\text{final}} W_{\text{unembed}}, \quad W_{\text{unembed}} \in \mathbb{R}^{d_{\text{model}} \times |V|}

Logit jj is the final vector dotted with column jj, large when they point the same way. Softmax turns the logits into probabilities, and training applies cross-entropy to them.

In a small model this is usually the largest matrix: 38.6 million numbers at ∣V∣=50,257|V| = 50{,}257 and dmodel=768d_{\text{model}} = 768. In training it runs at every position, since each predicts its own next token (section 8.4); in generation only the last position's scores are used.

Tied vs. Untied Embeddings

The input embedding and the output layer have transposed shapes. The original Transformer and GPT-2 tied them (), saving a whole ∣V∣×dmodel|V| \times d_{\text{model}} block of parameters.

LLaMA and the larger Qwen models untie them, because the jobs differ: the embedding should place similar tokens close together, while the output layer is a classifier that must separate near-identical tokens such as "run", "runs" and "running". The extra parameters matter less in a large model, where the embedding is a small share. Small models often still tie; Gemma ties at every size because its 256,000-token vocabulary makes the table unusually large.

7.6 The Complete ModelRun this section

The whole 2017 model (Post-LN) for NN tokens, with each step's output shape. Module 15 modernizes it piece by piece.

  1. Token ids: NN integers.
  2. Embedding lookup (section 1.5): N×dmodelN \times d_{\text{model}}.
  3. Add position vectors (section 7.1): N×dmodelN \times d_{\text{model}}.
  4. Repeat for each of LL blocks:
    1. Multi-head attention (Module 6), whose hh heads each produce an table of weights; their joined outputs go through WOW^O back to N×dmodelN \times d_{\text{model}}.
    2. Add to the input and normalize (sections 7.2 and 7.3): N×dmodelN \times d_{\text{model}}.
    3. Feed-forward network (section 7.4), widening each token to dffd_{ff} and back: N×dmodelN \times d_{\text{model}}.
    4. Add and normalize again: N×dmodelN \times d_{\text{model}}.
  5. Output layer (section 7.5): N×∣V∣N \times |V| logits, then softmax.

From step 2 through step 4 every tensor is N×dmodelN \times d_{\text{model}}: the token vectors are refined in place. Only the first and last steps change the shape.

Counting the Parameters

With d=dmodeld = d_{\text{model}} and dff=4dd_{ff} = 4d:

  • Attention: four d×dd \times d matrices (WQW^Q, WKW^K, WVW^V, WOW^O, with the heads together), so 4d24d^2.
  • Feed-forward: d×4dd \times 4d and 4d×d4d \times d, so 8d28d^2.
  • Norms and biases: a few times dd, negligible.

So each block holds about 12d212d^2, and the model about

P≈12Ld2+∣V∣dP \approx 12 L d^2 + |V| d

where the last term is the embedding table (shared with the output layer if they are tied). For GPT-2 Small, L=12L = 12 and d=768d = 768: 12⋅12⋅7682≈8512 \cdot 12 \cdot 768^2 \approx 85 million in the blocks plus 50,257⋅768≈3950{,}257 \cdot 768 \approx 39 million in the embedding, about 124 million in total, which is its published size.

Follow One Input

The model from Module 0's loop, opened up stage by stage, with every stage explained.

Figure 7.6
Follow one input through a whole transformer

Pick a stage to see the tensor it produces. Every number is computed in your browser by a two-block model trained on Shakespeare, so the final stage makes real guesses.

d_model = 16, 4 heads, 2 blocks, a 28-character vocabulary, about 5,800 parameters, trained on CPU for 8,000 steps (validation loss 1.887 nats per character; guessing uniformly scores 3.33). Hover a cell for its value. Red is positive, green negative.
Predict, then try

Open Block 1 · attention weights and look at the first row of each head. Before you do, predict what it must contain, whatever the weights of the model are.

In words. The two-block model trained on Shakespeare from Module 0 lists every stage: token ids, embedding, added positions, each block's attention weights and outputs, and the final next-character probabilities. Choosing a stage shows its tensor as a heatmap with its shape, the operation that produced it and the section that teaches it.

Exercises

Check

Which steps of the full model change the shape of the tensor, and which keep it at N×dmodelN \times d_{\text{model}}?

Apply

Apply LayerNorm (with γ=1\boldsymbol{\gamma} = 1, β=0\boldsymbol{\beta} = 0 and ϵ\epsilon negligible) to [1,1,1,5][1, 1, 1, 5]. What is the last entry of the output?

Apply

Using P≈12Ld2P \approx 12Ld^2, how many parameters are in the blocks of a model with L=24L = 24 and d=1024d = 1024, in millions?

Apply

Show that the shift matrix for the sinusoidal encoding does not depend on pospos, by writing sin⁡(ω(pos+k))\sin(\omega(pos + k)) with the angle-addition formula.

Extend

Without the residual connection, what would the derivative of a sub-layer's output with respect to its input be, and why does the II in I+∂f/∂xI + \partial f/\partial \mathbf{x} matter for a 48-layer stack?

Core Academic References

  • Vaswani et al. (2017): Attention Is All You Need. NeurIPS 2017. arXiv:1706.03762
  • He et al. (2016): Deep Residual Learning for Image Recognition. CVPR 2016: 770–778.
  • Ba, Kiros & Hinton (2016): Layer Normalization. arXiv:1607.06450
  • Hendrycks & Gimpel (2016): Gaussian Error Linear Units (GELUs). arXiv:1606.08415
  • Radford et al., OpenAI (2019): Language Models are Unsupervised Multitask Learners (GPT-2). Technical Report.

Measured labs

These labs test what this module says by running real code. Each card states the idea, what the experiment found, and what it does not prove. The numbers come from experiments/results.json, the output of running experiments/run_all.sh on the tiny-transformers reference code at version 137f7ef. The project checks fail if a measured number in the module text stops matching that file.

Lab 2a — sine-wave positions against a learned position table

What the module says

Section 7.1 builds position encodings from sine and cosine waves, PE(pos, 2i)=sin⁡ ⁣(pos/100002i/d)PE_{(pos,\,2i)} = \sin\!\left(pos / 10000^{2i/d}\right). It shows that moving forward by a fixed number of positions kk is a rotation: for each sine/cosine pair, one 2×2 matrix turns PEposPE_{pos} into PEpos+kPE_{pos+k}, whatever pospos is.

What the experiment found

The rotation property holds. Predicting PEpos+kPE_{pos+k} from PEposPE_{pos} with the best-fit matrix leaves an error of at most 2.9e-06, which is the rounding limit of 32-bit floats. The fitted matrix matches the formula from the module to within 6e-08. However, the repository's positional_embedding.py does not use sine waves: it is a learned table (nn.Embedding). Both approaches are valid, and both are used in real models. The module is right to teach the sine waves, but you should not assume the code uses them.

rotation-fit error for k=1,7,25k = 1, 7, 25
2.9e-06, 2.8e-06, 2.0e-06

float32 rounding, not a flaw in the encoding

formula vs fitted matrix, k=7k = 7
0.00000006

largest difference between any two entries

learned table parameters
4,096
sine-wave parameters
0

nothing to learn: the waves are fixed

validation loss, learned vs sine waves
2.0978 vs 1.9442

lower is better; 600 steps, same seed and same batches

What this does not prove

The sine-wave model ends 0.154 lower in validation loss, with 4,096 fewer parameters. That is one short run (600 steps, two layers), so it does not show that sine waves beat learned tables in general. The point of this lab is the derivation, and which of the two the code actually uses.

Run it yourself

bash
cd tiny-transformers && .venv/bin/python experiments/ab_positional.py

The code being tested