The model
In §5.6 you assembled a Transformer from blocks and counted its parameters as about , where is the number of layers and the width. Plug in GPT-2 small, with and , and you get 85 million. But the checkpoint OpenAI released has 124,439,808 parameters. Where do the other 39 million come from, and why would a model spend almost a third of its budget on them?
This section answers that by building GPT-2 exactly, the real architecture with every detail that matters, first in NumPy so you can see each operation, then in PyTorch. The two will agree to within , and the PyTorch version will count to 124,439,808 on the nose. Along the way we'll meet the three details that make GPT-2 more than "a stack of Transformer blocks", namely where the LayerNorms go, why the input and output embeddings are the same matrix, and why some weights start times smaller than the rest.
The whole model in one picture
A GPT maps a batch of token ids to a batch of next-token scores. Read the diagram top to bottom, with shapes for a batch of sequences of tokens, vocabulary size , width , and attention heads of size .
idx (B, T) integers in [0, V) │ token embedding E[idx] + position embedding P[0:T] x⁰ (B, T, d) │ L times: x ← x + Attn(LN₁(x)) causal self-attention, h heads │ x ← x + MLP(LN₂(x)) d → 4d → d with GELU xᴸ (B, T, d) │ final LayerNorm, then multiply by Eᵀ (the same E as at the bottom) logits (B, T, V) softmax(logits[b, t]) predicts token t + 1
Here is the token embedding table, is the learned position table, and here is the maximum context, 1,024 for GPT-2. Every arrow is something you've built before. The new things are the exact choices.
| GPT-2 small | our tiny model | |
|---|---|---|
| vocabulary | 50,257 (byte-level BPE) | 56 (characters plus EOT) |
| context | 1,024 | 64 |
| layers | 12 | 2 |
| width | 768 | 64 |
| heads , head size | 12, 64 | 4, 16 |
| MLP width | 3,072 () | 256 () |
| parameters | 124,439,808 | 107,776 |
Embeddings, tokens plus positions
The first layer looks up a row of for each token, exactly the lookup table of §1.2, and adds a row of for each position,
In plain words, the vector at position starts as "which token is here" plus "where here is". Attention without a mask can't tell positions apart (it treats its input as a set, §4.6). The causal mask leaks a little order, since position averages over exactly inputs, and causal models trained with no position embedding at all do learn some positional information (Haviv et al., 2022). But that signal is weak and indirect, and an explicit hands every position its address from the first layer. GPT-2 learns from scratch, the learned absolute positions of §5.4. Adding rather than concatenating keeps the width at , and training is free to give positions and tokens different directions in that space.
The learned table has a hard edge. There is no row for position , so a GPT-2 model cannot read token 1,025 at all. That's one reason later models switched to rotary embeddings (§6.7).
The block, with LayerNorm in front
Each of the blocks does two residual updates,
In plain words, each sublayer reads a normalized copy of the residual stream and adds its answer back to the unnormalized stream (§5.1). This is the pre-LN arrangement. GPT-1, like the original Transformer, normalized after each addition. GPT-2 moved the LayerNorm to the input of each sublayer and added one more LayerNorm after the last block (Radford et al., 2019). Pre-LN keeps a clean identity path from the embeddings to the output, which is why deep pre-LN stacks are far less sensitive to learning-rate warmup than post-LN ones (Xiong et al., 2020). GPT-2 reproductions still warm up, but a post-LN stack of the same depth often fails to train at all without it (§5.2 works through why).
LayerNorm normalizes each vector over its features with the population variance and a small , then applies a learned gain and bias ,
Causal self-attention
The attention sublayer is the multi-head causal attention of §4.5. For one sequence, stack the normalized vectors into . GPT-2 computes queries, keys, and values with one fused matrix and splits the result,
Each of the three is cut into heads of width (columns to belong to head ), and each head runs scaled dot-product attention with a causal mask , whose entries are 0 where and where ,
In plain words, every position mixes the values of itself and the positions before it, weighted by how well its query matches their keys, in independent ways, and the output projection blends the heads back together. The scale is , the head size, not , for the variance reason of §4.3.
The MLP
The second sublayer works on each position separately, widening to , applying GELU, and projecting back (§5.3),
GPT-2 uses the tanh approximation of GELU (Hendrycks & Gimpel, 2016),
which differs from the exact by less than 0.001. That's small, but it matters when you load someone else's weights and expect identical outputs. PyTorch's F.gelu(x, approximate="tanh") is this version.
One ingredient is missing from the diagram, dropout. GPT-1 applied dropout with rate 0.1 to the embeddings, the attention weights, and each residual branch (Radford et al., 2018). Single-epoch pretraining on a large corpus rarely needs it, because the model sees each token about once and has little chance to memorize, so nanoGPT defaults to a rate of 0 for pretraining and suggests 0.1 or more for fine-tuning on small data. Our models leave it out to keep the code short. The widget model of later sections trains for many epochs on a small corpus, so it uses dropout. Whatever the rate, it must be switched off at evaluation time.
The head, and why it reuses the embedding
After the last block and the final LayerNorm, each position holds a vector . The language-model head turns it into scores. GPT-2 does that with the same matrix it used at the bottom,
In plain words, the score of token is the dot product between the model's final vector and token 's own embedding. The model predicts a token by pointing its final vector toward that token's row of .
This is weight tying (Press & Wolf, 2017; Inan et al., 2017), and §1.5 previewed it. There are two reasons to do it. The first is parameters. An untied head would be another matrix, 38.6 million parameters for GPT-2 small, 31 percent of the model. The second is learning signal. With tying, a token's embedding gets gradient every time the token is predicted, and every time it is not predicted (through the softmax denominator), not only when it appears in the input. Press and Wolf found that tied models reach lower perplexity than untied ones of the same size.
The softmax-denominator gradient has a side effect worth knowing for interviews. A token that never occurs in the training data still gets pushed, on every step, toward "less likely than whatever came next". Its row drifts to an odd place in the embedding space, and if it ever shows up as input, the model sees a vector it never learned to read. That's one root of the under-trained "glitch tokens" of §2.6.
Tying is a trade-off, not a law. Many larger models untie the head, giving the input and output sides separate capacity, because at 7 billion parameters a second matrix costs only a few percent (§6.7).
The gradient of a tied embedding
A tied embedding gets gradient from both ends of the model. Take this miniature model, with , , input ids idx of shape (n,), and targets of shape (n,),
Implement tied_lm_loss_and_grad(E, A, idx, targets) returning (loss, dE) with dE of shape (V, d). Both paths must be included, and idx may contain repeated ids.
Initialization, and the 1/√(2L) rule
GPT-2 initializes every weight matrix and both embedding tables from , every bias to zero, and every LayerNorm to , . The 0.02 is a fixed constant inherited from GPT-1, close to for widths of a couple of thousand. (OpenAI's code used 0.01 for the position table, and reproductions commonly use 0.02 everywhere.) Then comes the one special rule. The two matrices that write into the residual stream, and , are scaled down by , so their standard deviation is . The GPT-2 paper states it as scaling "the weights of residual layers at initialization by a factor of where is the number of residual layers". The paper does not spell out , and reproductions such as nanoGPT read it as , since each block has two residual branches.
The reason is the variance argument that §5.7 derives and measures. Each of the residual branches adds a roughly independent random vector to the stream at initialization, variances add, and so the stream's variance grows linearly with depth unless each write is made times smaller. For GPT-2 small () that puts the two projections at , and for GPT-2 XL () at . In OpenAI's code both matrices are named c_proj, and in ours they are w_o and w_proj. Let's set up the model with exactly these values.
Our matrices are stored as (input, output), so a layer is x @ W, matching the math . Now the forward pass, one function per box of the diagram.
Look at the head split, reshape(B, T, h, d_k).transpose(0, 2, 1, 3). Reshaping straight to (B, h, T, d_k) without the transpose also runs, and also produces the right shape, but it scrambles positions and features across heads. It's the most common silent bug in hand-written attention, and the exercise tests below catch it.
Now we can test the model the way you should test every new model, before training it.
Two checks passed. First, the loss at initialization is almost exactly . Small random weights make every logit nearly equal, so the prediction is close to uniform, and a uniform guess over tokens costs nats (§0.5). For GPT-2 that number is . An initial loss far above means the initialization is too large and the model is confidently wrong before it has learned anything. Second, changing the token at position 20 leaves every earlier logit untouched to the last bit, which proves the mask is causal. A model that peeks at the future trains to a suspiciously low loss and then generates garbage.
The GPT forward pass, matched against a reference
Implement gpt_logits(P, idx, n_head) for a parameter dict shaped like init_gpt's, with idx of shape (B, T), returning logits of shape (B, T, V). It must be GPT-2 exactly. That means learned positions added to token embeddings, pre-LN blocks (LayerNorm with population variance and ), fused QKV split into n_head heads, attention scaled by with a causal mask, tanh GELU, a final LayerNorm, and a head tied to wte. No loops over positions or heads. The tests compare against a reference to and tell you which classic mistake you made if they disagree.
Counting every parameter
Now the puzzle from the top. Go through the diagram box by box. The embeddings hold token parameters and position parameters. Each block holds
the final LayerNorm adds , and the tied head adds nothing. So the total is
In plain words, the familiar counts only the blocks' matrices. A GPT also pays for its vocabulary, which for a small model is enormous.
For GPT-2 small, , , each block holds , the twelve blocks hold 85,054,464, and the final LayerNorm holds 1,536. The sum is 124,439,808. The 39 million "missing" parameters are the token embedding (38.6 million) and the position embedding (0.8 million), and without tying there would be another 38.6 million. The GPT-2 paper's table lists the four sizes as 117M, 345M, 762M, and 1542M, but the released checkpoints have the counts below, which is why everyone now says "124M".
Two conventions trip people up when comparing counts. Scaling-law papers usually report non-embedding parameters (Kaplan et al., 2020), everything except the token and position tables, which for GPT-2 small is , so the same model is "85M" in one paper and "124M" in another. And training repositories pad the vocabulary to 50,304, a multiple of 128 that makes the head's matmul faster (§6.7), which adds rows that no token ever uses and gives 124,475,904. In memory the weights alone take 4 bytes each in float32, about 498 MB for GPT-2 small, or half that in bfloat16.
The share of parameters in the embedding shrinks from 31 percent to 5 percent as the model grows, because blocks grow as and the vocabulary only as . That asymmetry is behind several decisions in §6.7, including untying the head in big models. Explore it below, including the GPT-3 configuration (Brown et al., 2020) and our tiny model.
Count a GPT exactly
Write count_gpt2_params(V, T, d, L, tied=True, bias=True) returning the exact parameter count of the GPT-2 architecture. With tied=False the head is a separate matrix (still without a bias). With bias=False every linear layer and every LayerNorm has no bias (LayerNorms keep their gains), as in models that drop biases.
In PyTorch
Here is the same model as a PyTorch module, written the way nanoGPT and the "Let's reproduce GPT-2" video do it. F.scaled_dot_product_attention with is_causal=True replaces our masked softmax and dispatches to a fused kernel (FlashAttention, §12.3) on a GPU. The meta device builds the full GPT-2 small with shapes but no memory, so we can count its parameters instantly.
model.parameters() yields each tensor once, so the tied matrix is counted once, which is what we want. (Summing over state_dict() instead would count it twice, since the state dict lists both names.) Now the real test. Copy our NumPy parameters into the PyTorch model and compare logits in float64.
They agree to about , which is float64 rounding. Notice the transposes. nn.Linear stores its weight as (output, input) and computes , while our NumPy matrices, like the Conv1D layers in OpenAI's GPT-2 checkpoint, are stored as (input, output). Forgetting that transpose is the classic bug when loading GPT-2 weights into your own module. With square matrices like it doesn't even raise a shape error.
| design choice | GPT-1 (2018) | GPT-2 (2019) | LLaMA-style (§6.7) |
|---|---|---|---|
| normalization | post-LN, LayerNorm | pre-LN plus a final LayerNorm | pre-norm RMSNorm |
| positions | learned absolute, 512 | learned absolute, 1,024 | rotary (RoPE), none in the input |
| MLP | GELU, | GELU (tanh), | SwiGLU, about |
| biases | yes | yes | none |
| head | tied | tied | usually untied |
| residual init | scaled by | varies by codebase |
Read more
- Radford et al. (2018). Improving Language Understanding by Generative Pre-Training. GPT-1, the 12-layer decoder, 0.02 initialization, and the pretrain-then-fine-tune recipe.
- Radford et al. (2019). Language Models are Unsupervised Multitask Learners. GPT-2's changes in §2.3, including pre-LN, the final LayerNorm, and the residual initialization.
- Press & Wolf (2017). Using the Output Embedding to Improve Language Models. Why tying the input and output embeddings helps.
- Hendrycks & Gimpel (2016). Gaussian Error Linear Units (GELUs). The activation and its tanh approximation.
- Karpathy. nanoGPT model.py. A compact reference implementation of exactly this model, including loading OpenAI's weights.
- Xiong et al. (2020). On Layer Normalization in the Transformer Architecture. Why pre-LN trains with far less dependence on warmup than post-LN.
- Haviv et al. (2022). Transformer Language Models without Positional Encodings Still Learn Positional Information. How much order a causal mask leaks on its own.
- Kaplan et al. (2020). Scaling Laws for Neural Language Models. Its scaling laws count non-embedding parameters, the convention behind "85M" for GPT-2 small.