Layers of Understanding
By the end of this chapter you will know every piece of a transformer block — the feed-forward network, residual connections, LayerNorm — why each one is there, how they are stacked into a deep model, and how to count its parameters and FLOPs on the back of an envelope.
In the attention chapter, "sat" learned to pull information from "cat". Good. But attention only moves information between tokens. It is a weighted average of value vectors. It never computes anything new about a token on its own: no "this verb is past tense so the subject probably did something already", no "this looks like the start of a Python function". Mixing is not thinking.
And one layer of anything is shallow. Suppose "it" has now absorbed "animal". To predict the next word the model might need to notice that "the animal … was too tired" is an explanation, which is why "because" appeared, which suggests the sentence is nearly over. Each of those steps depends on the previous one. You cannot do them in a single pass of attention, however many heads it has.
So a transformer needs two more things: a per-token computation that can transform what attention gathered, and a way to stack many rounds of gather-then-transform without the network becoming untrainable. This chapter builds both, then shows what the stack does with them.
The problem: mixing is not thinking
Let's make the limitation of attention precise. The output of an attention head for token $i$ is $\sum_j a_{ij} v_j$, a convex combination of value vectors (the weights are non-negative and sum to one). Whatever the head produces lies inside the "cloud" spanned by the values that already exist in the sequence. If none of the tokens carries the feature "this is a question", attention cannot conjure it. It is a linear operation once the weights are fixed, and it has no nonlinearity of its own except the softmax that chooses the weights.
What we want is a function applied to each token's vector separately, $f(x_i)$, that can compute arbitrary new features from the ones present. Neural-network 101 says: a matrix, a nonlinearity, another matrix. That is exactly what the transformer uses, and it calls it the feed-forward block, or the MLP.
The feed-forward block (MLP)
Expand, squash, project back
The block takes a $d_{model}$-vector, expands it to a wider hidden vector (by a factor of 4 in almost every transformer), applies an element-wise nonlinearity, and projects back down to $d_{model}$. In symbols, with $W_1 \in \mathbb{R}^{d \times 4d}$ and $W_2 \in \mathbb{R}^{4d \times d}$:
$$\text{MLP}(x) = \text{GELU}(xW_1 + b_1)\,W_2 + b_2.$$The first matrix asks $4d$ different linear questions of the token ("how much does it look like a verb?", "how much like the start of a date?"). GELU keeps the positive answers and quietly suppresses the negative ones. The second matrix turns the surviving answers into an update to the token's vector. The same $W_1, W_2$ are applied to every position, independently, which is why this is sometimes called a position-wise feed-forward network.
Why expand to $4d$? Because the nonlinearity is what gives the block its power, and it acts element-wise: more hidden units means more independent yes/no features the block can detect. Four is not sacred (it was in the original paper and nobody found a strong reason to change it), but the expansion has to be substantial for the block to be worth its cost. It is also where most of the parameters live: $2 \times d \times 4d = 8d^2$ per block versus $4d^2$ for attention.
A worked tiny example
To keep the arithmetic short we use $d = 2$ and a hidden width of 4 (a $2\times$ expansion instead of $4\times$; nothing else changes). Let the input be $x = [1, -1]$ and
$$W_1 = \begin{bmatrix} 1 & 0 & -1 & 1 \\ 0 & 1 & 1 & 1 \end{bmatrix},\qquad W_2 = \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ 1 & 1 \\ -1 & 1 \end{bmatrix},\qquad b_1 = b_2 = 0.$$First the expansion. Each hidden unit is a dot product of $x$ with a column of $W_1$:
$$xW_1 = [\,1\!\cdot\!1 + (-1)\!\cdot\!0,\ \ 1\!\cdot\!0 + (-1)\!\cdot\!1,\ \ 1\!\cdot\!(-1) + (-1)\!\cdot\!1,\ \ 1\!\cdot\!1 + (-1)\!\cdot\!1\,] = [1,\ -1,\ -2,\ 0].$$Now GELU, element by element: $\text{GELU}(1) \approx 0.84$, $\text{GELU}(-1) \approx -0.16$, $\text{GELU}(-2) \approx -0.05$, $\text{GELU}(0) = 0$. So the hidden activation is $h = [0.84, -0.16, -0.05, 0]$. Unit 1 fired; units 2 and 3 are nearly switched off; unit 4 is exactly off.
Finally project back:
$$hW_2 = [\,0.84 - 0.05,\ \ -0.16 - 0.05\,] = [0.79,\ -0.21].$$That is the MLP's write: a small vector that will be added to $x$ by the residual connection, giving $[1.79, -1.21]$. Notice the block did something attention cannot: it detected a feature (unit 1 says "first coordinate positive and second not") and produced a new direction from it.
What the MLP is thought to do
A useful way to read the two matrices: each row of $W_1^\top$ (a column of $W_1$) is a key pattern that the input is matched against, and each row of $W_2$ is the value that gets written when that key fires. Geva et al. (2021), "Transformer Feed-Forward Layers Are Key-Value Memories", showed this is not just a metaphor: in trained models, individual hidden units respond to human-recognisable input patterns (a particular topic, a syntactic construction, the end of a sentence) and their corresponding value rows push the prediction towards specific next tokens. The MLP is a big associative memory: $4d$ key–value pairs per block, looked up in parallel, with soft matching.
Attention moves information between tokens; the MLP transforms information within a token. Attention is where the model looks things up in the context; the MLP is where it looks things up in its weights, i.e. in what it memorised during training. Both are needed on every layer.
GELU, and why not ReLU
The original transformer used ReLU, $\max(0, x)$. GPT-2 and BERT switched to GELU (Hendrycks and Gimpel, 2016, arXiv), and Llama-family models use SiLU inside a gated block. Why fuss over the squashing function? Because ReLU has a hard corner at zero: units that are slightly negative get exactly zero output and exactly zero gradient, so they can get stuck. GELU is a smoothed ReLU:
$$\text{GELU}(x) = x\,\Phi(x),$$where $\Phi$ is the standard normal cumulative distribution function. Read it as "$x$, times the probability that a standard normal is less than $x$". For large positive $x$ it is $\approx x$; for large negative $x$ it is $\approx 0$; in between it curves smoothly and even dips slightly below zero (minimum about $-0.17$ at $x \approx -0.75$). SiLU (also called Swish) is the same idea with a sigmoid instead of $\Phi$: $\text{SiLU}(x) = x\,\sigma(x)$. In practice the three give similar results; GELU and SiLU train a little more smoothly.
Three activation functions and their values at your chosen input. Look at the region just below zero, where ReLU is dead and the others are not.
Shazeer (2020), "GLU Variants Improve Transformer", found that a gated feed-forward block trains better: $\text{SwiGLU}(x) = \big(\text{SiLU}(xW_{gate}) \odot xW_{up}\big)W_{down}$. One branch decides how open each hidden unit is, the other carries the content, and they are multiplied element-wise. It has three matrices instead of two, so to keep the parameter count the same the hidden width is usually set to about $\tfrac{8}{3}d$ rather than $4d$ (Llama-2 7B uses 11,008 for $d = 4096$). code/lumen/block.py has both variants.
Residual connections: the stream
The problem: deep networks were untrainable
Now we want to stack: attention, MLP, attention, MLP, dozens of times. Historically this failed. Stack twenty plain layers, $x \leftarrow f_\ell(x)$, and the network trains worse than a ten-layer one, not because it overfits but because the optimiser cannot even fit the training data. The reason is the chain rule: the gradient reaching layer 1 is the product of twenty Jacobians. If each one shrinks the signal a little, the product vanishes; if each one grows it, the product explodes. Either way the early layers stop learning.
He et al. (2016), "Deep Residual Learning for Image Recognition", fixed this with a change so small it fits on a napkin. Instead of replacing $x$, each layer adds to it:
$$x \leftarrow x + f_\ell(x).$$The layer now learns a correction, not a whole new representation. And look at the gradient: $\partial(x + f(x))/\partial x = I + \partial f/\partial x$. That identity matrix is a highway. Even if $\partial f/\partial x$ is tiny, the gradient passes straight through the "$+$" untouched, so the bottom layers receive a clean signal no matter how deep the stack is. At initialisation, when $f$ is small and random, the whole network is close to the identity function, which is an excellent place to start optimising from.
The residual stream as working memory
The residual picture leads to the most useful mental model of a transformer. Think of the token's vector $x$ as a stream that flows upward through the layers. Each attention head and each MLP reads from the stream (through its input projection), computes something, and writes a vector back into it (through its output projection) by addition. Nothing ever erases the stream; later blocks can only add. This framing comes from Elhage et al. (2021), "A Mathematical Framework for Transformer Circuits".
This picture explains several facts at once. Why can you delete a middle layer from a trained transformer and get only a mild degradation? Because the stream survives without that layer's write. Why do attention heads in layer 8 seem to "know" what a layer-2 MLP computed? Because the layer-2 write is still sitting in the stream. And why do models need the width $d_{model}$ to be large? Because the stream is shared: hundreds of heads and MLPs are all writing into the same $d_{model}$ dimensions, and they need room to avoid stepping on each other.
LayerNorm: keeping the stream well-behaved
The problem: scale drift
If every block only adds to the stream, the stream grows. After 24 blocks the vector might have entries in the hundreds, and the next attention layer's dot products would be enormous (undoing the careful $\sqrt{d_k}$ scaling). Some dimensions might be huge and others tiny, so the same learning rate would be far too big for some weights and far too small for others. We need each block to see inputs of a predictable size, regardless of what earlier blocks did.
What LayerNorm does
Layer normalisation (Ba, Kiros and Hinton, 2016, arXiv) standardises each token's vector on its own: subtract its mean, divide by its standard deviation, then apply a learned per-dimension gain $\gamma$ and bias $\beta$:
$$\text{LN}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta,\qquad \mu = \frac{1}{d}\sum_{t} x_t,\quad \sigma^2 = \frac{1}{d}\sum_t (x_t - \mu)^2.$$The mean and variance are taken across the $d$ dimensions of one token, not across the batch and not across positions; that is what makes it work for sequences of any length and batches of size 1. The $\epsilon$ (typically $10^{-5}$) prevents division by zero when a vector is constant. $\gamma$ and $\beta$ let the network undo the normalisation in any dimension where it prefers to; they add $2d$ parameters, which is negligible.
Take $x = [2, 4, 6, 8]$. Mean $\mu = 5$. Deviations $[-3, -1, 1, 3]$, squared $[9, 1, 1, 9]$, so $\sigma^2 = 20/4 = 5$ and $\sigma \approx 2.236$. Normalised: $[-1.34, -0.45, 0.45, 1.34]$. With $\gamma = 1, \beta = 0$ that is the output; it has mean 0 and variance 1 by construction.
Now the case that matters: $x = [1, 2, 3, 100]$, one dimension that has blown up. $\mu = 26.5$, $\sigma^2 = (25.5^2 + 24.5^2 + 23.5^2 + 73.5^2)/4 \approx 1801$, $\sigma \approx 42.4$. Normalised: $[-0.60, -0.58, -0.55, 1.73]$. The 100 has been tamed to $1.73$. The next layer sees a vector of sensible size, whatever happened upstream.
Set the six entries of a token vector and watch the normalised output. Try pushing one value to 10 and see it tamed. The gain and bias are single shared scalars here for simplicity (real models learn one per dimension).
Pre-LN vs post-LN
Where does the normalisation go? The original transformer put it after the residual add: $x \leftarrow \text{LN}(x + f(x))$. This is post-LN. It has a subtle flaw: the LayerNorm sits on the residual highway, so the identity path is no longer clean; every block rescales the stream, and the gradient at the bottom layers ends up much larger than at the top. Training post-LN transformers needs a careful learning-rate warm-up and still becomes unstable when they get deep.
Pre-LN moves the normalisation inside the branch: $x \leftarrow x + f(\text{LN}(x))$. Now the highway is untouched (pure addition), each sub-block sees a normalised input, and gradients are well-behaved at any depth. Xiong et al. (2020), "On Layer Normalization in the Transformer Architecture", analysed this formally and showed pre-LN trains without warm-up. GPT-2 was already pre-LN, and virtually every large model since (GPT-3, Llama, PaLM, Mistral) uses it, with one extra LayerNorm at the very end of the stack before the output projection, because the stream itself is never normalised otherwise.
RMSNorm: the modern simplification
Zhang and Sennrich (2019), "Root Mean Square Layer Normalization", asked which part of LayerNorm actually matters and found that the re-scaling does most of the work while the mean-subtraction adds little. RMSNorm drops the mean and the bias:
$$\text{RMSNorm}(x) = \gamma \odot \frac{x}{\sqrt{\frac{1}{d}\sum_t x_t^2 + \epsilon}}.$$It is cheaper (one pass over the vector instead of two) and trains just as well, so Llama, Mistral, Gemma and most recent open models use it. On our $[2,4,6,8]$ example: the root-mean-square is $\sqrt{(4+16+36+64)/4} = \sqrt{30} \approx 5.48$, giving $[0.37, 0.73, 1.10, 1.46]$. Same idea, no centring.
The full transformer block
We now have all the parts. A pre-LN transformer block is two residual updates in a row:
$$x \leftarrow x + \text{Attn}(\text{LN}_1(x)),\qquad x \leftarrow x + \text{MLP}(\text{LN}_2(x)).$$Read the two lines as: "look at the context and write what you found into the stream; then think about what is now in the stream and write your conclusion". That pair, repeated $N$ times, is the entire body of GPT-2, Llama, and every decoder-only language model in use today. The differences between models are in the details (which norm, which activation, GQA or not, how positions are encoded), not in this skeleton.
Symbols
$x$ = residual stream, $(L, d)$$\text{LN}_1, \text{LN}_2$ = two separate LayerNorms
$\text{Attn}$ = multi-head causal attention
$\text{MLP}$ = $W_2\,\text{GELU}(W_1\cdot)$
$N$ = number of blocks
Normalise
$u = \text{LN}_1(x)$. Each token standardised to unit scale, with learned gain/bias.Attend
$a = \text{Attn}(u)$. Tokens read from earlier tokens; output projected by $W_O$.Write
$x \leftarrow x + a$. The attention write is added to the stream; nothing is overwritten.Normalise
$u = \text{LN}_2(x)$. Fresh normalised copy for the MLP.Think
$m = \text{MLP}(u)$. Per-token key–value lookup in the weights.Write
$x \leftarrow x + m$. Pass the stream to the next block, or to the final LN and the output projection.Follow a single token's 6-dimensional vector through LN → attention → add → LN → MLP → add, with a fixed random block (seeded) and three earlier context tokens. Watch the residual stream change only by addition.
Stacking N blocks: what the layers do
A single block gives every token one round of "gather, then think". GPT-2 small stacks 12; Llama-3 70B stacks 80. What does the stack actually do with all that depth? We do not have a complete answer, but three findings are robust enough to be worth knowing.
Early, middle and late layers
Early layers deal with surface form. Their attention heads are mostly positional (previous-token, first-token), and their MLPs resolve things like "these three sub-word tokens form the word photosynthesis" and part-of-speech-like features. Tenney et al. (2019), "BERT Rediscovers the Classical NLP Pipeline", found that syntactic information is decodable earliest and semantic roles later. Middle layers are where the richest, most abstract representations live: the entity "it" refers to, the topic, the language, whether the text is code. Induction heads, the "copy what came after this last time" circuit from the attention chapter, tend to sit here. Late layers turn all of that into a concrete next-token prediction: their MLPs act as the key–value memories Geva et al. described, promoting specific vocabulary items, and their attention heads often copy likely continuations from the context.
The logit lens
There is a beautiful trick for watching this happen. The final step of a language model is: take the residual stream, apply the final LayerNorm, multiply by the unembedding matrix, and read off logits over the vocabulary. Nothing stops you from applying that same final step to the stream after layer 3, or layer 7. This is the logit lens, described by nostalgebraist (2020) in "interpreting GPT: the logit lens". In GPT-2, the early-layer predictions are near-nonsense, the middle layers start guessing plausible word classes, and the last few layers converge on the actual output. The stream is gradually "becoming" the prediction. Belrose et al. (2023), "Eliciting Latent Predictions from Transformers with the Tuned Lens", sharpened the tool by training a small affine correction per layer, which makes the intermediate predictions far more legible and shows that a surprising amount of the final answer is already present halfway up.
The stream as the model's working memory
Put the residual picture and the logit lens together and you get the modern understanding of a transformer: the residual stream at each position is a working memory of width $d_{model}$. Early layers write basic facts about the token into it. Attention heads copy facts from other positions' memories into this one. MLPs read the memory, recognise patterns, and write conclusions. Late layers write "and therefore the next token is probably X" into the same memory, and the unembedding reads that out. There is no separate "state"; the stream is the state, and depth is how many times the model gets to update it.
Because the two operations are sequential in nature. To decide what "it" refers to, the model must first know what the candidate nouns are (a layer-1 job), then compare them with "it" (a layer-5 attention job), then use the answer (a layer-8 job). A single wide block can only do one round of attention and one round of MLP. Depth is what buys multi-step computation; width is what buys capacity per step. Scaling laws (see the scaling chapter) show that the ratio matters surprisingly little within a wide band, but you need enough of both.
Parameter accounting per block
You should be able to count a transformer's parameters in your head; it is the fastest sanity check on any architecture description. Let $d = d_{model}$ and ignore biases and norms (they are $O(d)$, negligible next to $O(d^2)$).
- Attention: $W_Q, W_K, W_V, W_O$, each $d \times d$: $4d^2$.
- MLP: $W_1$ is $d \times 4d$ and $W_2$ is $4d \times d$: $8d^2$.
- Per block: $12d^2$. Two thirds of every block is the MLP.
$d = 768$, $N = 12$ blocks, vocabulary $50{,}257$, context $1{,}024$. Attention: $4 \times 768^2 = 2{,}359{,}296$. MLP: $2 \times 768 \times 3072 = 4{,}718{,}592$. Biases and two LayerNorms add about $10{,}000$. Per block $\approx 7.09$M; times 12 $\approx 85.1$M. Token embeddings $50{,}257 \times 768 = 38.6$M (shared with the output projection, so counted once), position embeddings $1{,}024 \times 768 = 0.79$M, final LayerNorm $1{,}536$. Total $\approx 124.4$M, which is the "124M" you see quoted. Note that almost a third of GPT-2 small is the embedding table; for big models that fraction shrinks to a few percent.
Two refinements for modern models. With SwiGLU the MLP has three matrices of size $d \times d_{ff}$; with the usual $d_{ff} \approx \tfrac{8}{3}d$ that is again $\approx 8d^2$, by design. With grouped-query attention, $W_K$ and $W_V$ shrink to $d \times h_{kv} d_{head}$ each, so attention drops below $4d^2$; in Llama-3 70B it is about $2.25d^2$.
Dropout
The original transformer and GPT-2 applied dropout (Srivastava et al., 2014) in several places: on the attention weights, on each sub-block's output before the residual add, and on the embeddings, with rate 0.1. Dropout zeros a random 10% of activations during training and scales the rest up by $1/0.9$, so no single unit can be relied on; at inference it is switched off. It is a regulariser: it fights overfitting.
Modern pre-training usually drops dropout entirely. The reason is that overfitting is not the problem when you train for a single epoch on trillions of tokens; the model never sees the same example twice, so there is nothing to memorise. Dropout then only slows learning and adds noise. Llama, PaLM, Mistral and most models trained since about 2022 use dropout 0.0 in pre-training. It reappears in fine-tuning, where datasets are small and multiple epochs are common. code/lumen/block.py keeps a dropout argument that defaults to 0.
Initialisation and the 1/√(2N) residual trick
Random initial weights matter more in deep networks than in shallow ones, for the same reason depth is hard: effects compound. GPT-2 initialises every weight matrix from $\mathcal{N}(0, 0.02^2)$ and every bias at 0, and then applies one extra rule that is easy to miss but important.
Here is the problem it solves. Each block adds two write vectors to the stream. If each write has variance $\sigma^2$ per dimension and the writes are roughly independent, after $N$ blocks the stream's variance is about $2N\sigma^2$ plus whatever it started with. With $N = 48$ (GPT-2 XL) that is a 96-fold growth: the last layers would see inputs a factor of $\sqrt{96} \approx 10$ larger than the first. Pre-LN protects the sub-blocks' inputs, but the stream itself, and therefore the relative size of each new write, still drifts.
The fix, from Radford et al. (2019), is to scale the initialisation of the two output projections in each block, $W_O$ and $W_2$ (the matrices that write into the stream), by $1/\sqrt{2N}$:
$$W_O, W_2 \sim \mathcal{N}\!\left(0,\ \frac{0.02^2}{2N}\right).$$Now each of the $2N$ writes has variance $\sigma^2/(2N)$, their sum has variance about $\sigma^2$, and the stream stays the same size from the first block to the last. It costs nothing and it is in every GPT-style codebase, usually as a special case in the init loop that checks whether a parameter's name ends in c_proj.weight.
Compute: FLOPs per token ≈ 2 × parameters
Here is the estimate you will use constantly. Almost all the compute in a transformer is matrix multiplication of activations by weight matrices. When a $d_{in}$-vector is multiplied by a $d_{in} \times d_{out}$ weight matrix, every one of the $d_{in} d_{out}$ weights is used exactly once, and each use is one multiply and one add: 2 FLOPs. So a forward pass through a weight matrix costs $2 \times (\text{number of weights})$ FLOPs per token. Sum over all matrices in the model and you get:
$$\text{FLOPs per token (forward)} \approx 2N_{params}.$$where $N_{params}$ counts the weight matrices actually multiplied (so it excludes the input embedding table, which is a lookup, but includes the output projection). The attention core, $QK^\top$ and $AV$, adds about $4Ld$ per layer per token; this is not proportional to parameters, and for GPT-2 small at $L=1024$ it is about 20% extra. At long contexts it dominates, as the previous chapter showed.
Training costs about three times the forward pass, because backpropagation needs two more matrix multiplications of the same size for each one in the forward pass (one for the gradient with respect to the input, one for the gradient with respect to the weights). Hence the famous rule from Kaplan et al. (2020), "Scaling Laws for Neural Language Models":
$$C \approx 6\,N_{params}\,D$$FLOPs to train a model of $N$ parameters on $D$ tokens. For GPT-2 small (85M non-embedding parameters) on 10 billion tokens: $6 \times 85\text{M} \times 10^{10} \approx 5 \times 10^{18}$ FLOPs, a few hours on a modern GPU cluster. For a 70B model on 15 trillion tokens: $6 \times 7\times 10^{10} \times 1.5 \times 10^{13} \approx 6 \times 10^{24}$ FLOPs. The scaling chapter is built on this formula.
Count parameters and forward FLOPs per token for a decoder-only transformer, and see where the parameters live. Presets are approximate reconstructions of published configurations.
The presets reproduce the published parameter counts to within a percent: GPT-2 small ≈ 124M, Llama-2 7B ≈ 6.7B, Llama-3 70B ≈ 70.6B. Pressing a preset sets every control; touch any slider to go back to a custom configuration. Configurations are from the respective model cards and papers and are labelled approximate because we ignore biases, norms and rounding conventions.
Implementation
Here is the block in PyTorch, matching code/lumen/block.py. LayerNorm and RMSNorm are written out rather than imported so you can see there is nothing hidden.
import torch
import torch.nn as nn
import torch.nn.functional as F
from .attention import MultiHeadAttention
class LayerNorm(nn.Module):
def __init__(self, d, eps=1e-5):
super().__init__()
self.g = nn.Parameter(torch.ones(d)) # gamma: per-dimension gain
self.b = nn.Parameter(torch.zeros(d)) # beta: per-dimension bias
self.eps = eps
def forward(self, x): # x: (..., d); statistics over the last axis only
mu = x.mean(-1, keepdim=True)
var = x.var(-1, keepdim=True, unbiased=False)
return (x - mu) / torch.sqrt(var + self.eps) * self.g + self.b
class RMSNorm(nn.Module):
def __init__(self, d, eps=1e-6):
super().__init__()
self.g = nn.Parameter(torch.ones(d))
self.eps = eps
def forward(self, x):
rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
return x / rms * self.g # no mean subtraction, no bias
keepdim=True keeps the reduced axis so the statistics broadcast back against x. unbiased=False divides by $d$ rather than $d-1$, matching the formula (and nn.LayerNorm). Everything is per token: a tensor of shape (B, L, d) gets B × L independent normalisations.
class MLP(nn.Module):
def __init__(self, d_model, d_ff=None, dropout=0.0):
super().__init__()
d_ff = d_ff or 4 * d_model
self.fc = nn.Linear(d_model, d_ff) # W1: expand
self.proj = nn.Linear(d_ff, d_model) # W2: project back
self.drop = nn.Dropout(dropout)
def forward(self, x):
return self.drop(self.proj(F.gelu(self.fc(x))))
class SwiGLU(nn.Module):
def __init__(self, d_model, d_ff):
super().__init__()
self.w_gate = nn.Linear(d_model, d_ff, bias=False)
self.w_up = nn.Linear(d_model, d_ff, bias=False)
self.w_down = nn.Linear(d_ff, d_model, bias=False)
def forward(self, x):
return self.w_down(F.silu(self.w_gate(x)) * self.w_up(x))
class TransformerBlock(nn.Module):
"""Pre-LN block: x = x + Attn(LN1(x)); x = x + MLP(LN2(x))"""
def __init__(self, d_model, n_heads, d_ff=None, dropout=0.0):
super().__init__()
self.ln1 = LayerNorm(d_model)
self.attn = MultiHeadAttention(d_model, n_heads)
self.ln2 = LayerNorm(d_model)
self.mlp = MLP(d_model, d_ff, dropout)
self.drop = nn.Dropout(dropout)
def forward(self, x, mask=None):
x = x + self.drop(self.attn(self.ln1(x), mask)) # gather from context, write to stream
x = x + self.mlp(self.ln2(x)) # think per token, write to stream
return x
The forward is two lines, and they are exactly the two equations of the symbol strip. Note what is not there: no normalisation of x itself, and nothing that replaces x. The GPT-2 init rule lives in the full model, where the number of blocks is known:
def _init_weights(self, module):
if isinstance(module, nn.Linear):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
# after self.apply(self._init_weights): shrink the two residual-writing matrices per block
for name, p in self.named_parameters():
if name.endswith("attn.out.weight") or name.endswith("mlp.proj.weight"):
nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * self.n_layers))
Common confusions
BatchNorm normalises each feature across the examples in a batch; LayerNorm normalises each example across its features. LayerNorm's statistics depend only on the one token vector being normalised, which is why it works with batch size 1, with variable-length sequences, and identically at training and inference time. There are no running averages to maintain.
The feed-forward block is applied to each position independently; position 7's MLP output depends only on position 7's stream. All communication between positions happens in attention. If your MLP implementation ever needs the sequence length, something is wrong.
The residual connection is a plain addition of two vectors of the same width. It is also always on: a block cannot be bypassed at inference; it can only learn to write a small vector. And "$x + f(x)$" is not the same as re-using $x$ as the block's output: the block's input is the normalised copy $\text{LN}(x)$, but what gets added is $f(\text{LN}(x))$, added to the raw $x$.
The rule counts one forward pass through the weight matrices for one token. Multiply by the number of tokens in the batch for a step, by 3 for forward + backward, and add the attention core, which grows with context. And the input embedding is a lookup, not a matmul, so it is free in FLOPs even though it can be a large fraction of the parameters in small models.
Practice
Implement LayerNorm, RMSNorm, MLP and TransformerBlock in code/lumen/block.py. Check your LayerNorm against torch.nn.LayerNorm(d) with torch.allclose on a random (2, 5, 16) tensor. Then run a random (2, 5, 16) input through a TransformerBlock(16, 4) and verify that the output minus the input has a much smaller norm than the input at initialisation (the block starts near the identity).
Solution sketch
Default nn.LayerNorm uses eps=1e-5 and biased variance, matching ours. For the second check, compare (y - x).norm() with x.norm(); with std-0.02 weights the ratio should be well under 0.1. If it is not, you are probably initialising with PyTorch's default (Kaiming uniform), which is much larger.
Stack 24 TransformerBlock(64, 4) modules and 24 "plain" blocks (same code but with x = f(LN(x)), no addition). Feed both a random input, compute a scalar loss (say y.pow(2).mean()), call backward(), and compare the gradient norm at the first block's mlp.fc.weight in each stack. Then repeat with the plain stack at depth 4. What do you see?
Solution sketch
In the residual stack the first-layer gradient is of the same order as the last-layer gradient. In the plain stack it shrinks roughly geometrically with depth; at 24 layers it is often orders of magnitude smaller. This is the vanishing gradient the identity path prevents. Bonus: apply the $1/\sqrt{2N}$ output scaling to the residual stack and measure how the norm of the stream at the output changes.
Using $d = 4096$, $N = 32$, SwiGLU with $d_{ff} = 11{,}008$, full multi-head attention, vocabulary $32{,}000$ with untied output embedding: compute the parameter count and compare with the interactive above. Then compute the forward FLOPs per token and the training cost for 2 trillion tokens.
Solution sketch
Attention $4 \times 4096^2 = 67.1$M; MLP $3 \times 4096 \times 11008 = 135.3$M; per block $202.4$M; times 32 $= 6.48$B; two embedding tables $2 \times 131$M $= 0.26$B; total $\approx 6.74$B. Forward $\approx 2 \times 6.6\text{B} \approx 13$ GFLOPs per token; training $\approx 6 \times 6.6\times10^9 \times 2\times10^{12} \approx 8 \times 10^{22}$ FLOPs, which matches the order of magnitude Meta reported.
Key takeaways
- Attention moves information between tokens; the MLP (expand 4×, GELU, project back) computes new features within a token and behaves like a key–value memory.
- Residual connections turn each block into an update, $x \leftarrow x + f(x)$; the identity path keeps gradients alive at any depth and makes the residual stream a shared working memory that blocks read from and write to.
- LayerNorm standardises each token independently (RMSNorm skips the mean); pre-LN keeps the highway clean and is what modern models use, plus a final norm before the unembedding.
- The block is $x \leftarrow x + \text{Attn}(\text{LN}_1 x)$, $x \leftarrow x + \text{MLP}(\text{LN}_2 x)$; stacking $N$ of them buys multi-step computation, with early layers on form, middle layers on meaning, late layers on the prediction (the logit lens makes this visible).
- Per block: $4d^2$ attention + $8d^2$ MLP; GPT-2 small is $\approx 85$M in blocks + $\approx 39$M embeddings. Forward FLOPs per token $\approx 2 \times$ parameters; training $\approx 6ND$.
- Modern pre-training drops dropout; GPT-2's $1/\sqrt{2N}$ scaling of the residual-writing matrices keeps the stream's size stable across depth.
Further reading
- Vaswani et al. (2017). Attention Is All You Need. Section 3.3 is the position-wise feed-forward network; the figure shows the (post-LN) block.
- He et al. (2016). Deep Residual Learning for Image Recognition. Where residual connections come from, and the experiments showing plain deep nets fail to fit.
- Ba, Kiros, Hinton (2016). Layer Normalization.
- Zhang and Sennrich (2019). Root Mean Square Layer Normalization.
- Xiong et al. (2020). On Layer Normalization in the Transformer Architecture. Why pre-LN trains without warm-up.
- Hendrycks and Gimpel (2016). Gaussian Error Linear Units (GELUs).
- Shazeer (2020). GLU Variants Improve Transformer. SwiGLU.
- Geva et al. (2021). Transformer Feed-Forward Layers Are Key-Value Memories.
- Elhage et al. (2021). A Mathematical Framework for Transformer Circuits. The residual-stream view.
- nostalgebraist (2020). interpreting GPT: the logit lens. Belrose et al. (2023). Eliciting Latent Predictions from Transformers with the Tuned Lens.
- Tenney, Das, Pavlick (2019). BERT Rediscovers the Classical NLP Pipeline.
- Kaplan et al. (2020). Scaling Laws for Neural Language Models. The $C \approx 6ND$ rule and its derivation in the appendix.
- Radford et al. (2019). Language Models are Unsupervised Multitask Learners. GPT-2: pre-LN and the residual init scaling.