AI / ML Theory
Chapter 7 · 24 min

7.3 — The forward pass at scale: shapes for GPT-2-small

Goal: Walk the forward pass of GPT-2-small (12 layers, 12 heads, d=768, n_ctx=1024, V=50257) shape-by-shape; recover the 124M parameter count; count the FLOPs and the per-step memory footprint.

1. Why this session exists

Chapter 6 derived the architecture in symbolic form: dd this, LL that, #θ≈12Ld2\#\theta \approx 12 L d^2. This session pins it to one concrete model — GPT-2-small — and walks every tensor shape and every parameter from input ids to output logits. After this session you can:

  • Draw the GPT-2-small computation graph from memory.
  • Reproduce the 124M parameter headline from first principles.
  • State the per-step FLOPs and memory cost for training, in numbers.
  • Adapt the same recipe to any other model size (GPT-2-medium/large/xl, LLaMA-7B, GPT-3) — the shapes scale, the recipe doesn't change.

GPT-2-small is the right reference for two reasons: it is small enough to fit on a single GPU (so the shapes are concrete, not hypothetical), and it is what the nanoGPT walkthrough of 6.16 implements. The numbers here should match what you saw running model.eval() in that session.


2. The GPT-2-small configuration

From Radford et al. (2019), Language Models are Unsupervised Multitask Learners (GPT-2 paper), Table 2 — the smallest of four GPT-2 variants:

| Hyperparameter | Symbol | GPT-2-small value | |---|---|---:| | Vocabulary size | VV | 50,257 | | Maximum context length | TmaxT_\text{max} | 1,024 | | Residual stream width | dd | 768 | | Number of layers | LL | 12 | | Number of attention heads | hh | 12 | | Head dimension | dh=d/hd_h = d / h | 64 | | FFN inner width | dff=4dd_\text{ff} = 4 d | 3,072 | | Total parameters | | ~124M |

GPT-2-small is the reference model for "small but not tiny" Transformer experiments. nanoGPT trains it on Shakespeare; CS336 uses it; every research codebase has a gpt2-small config.

A few notes:

  • V=50,257V = 50{,}257 is GPT-2's byte-level BPE vocabulary (7.2 §6). The ∼50K\sim 50K choice was made before training the model.
  • Tmax=1024T_\text{max} = 1024 is the training context length. Inference can use up to 10241024 tokens of context; longer prompts must be truncated.
  • L=12,h=12,d=768L = 12, h = 12, d = 768 are the three free architectural numbers; everything else derives from them and the choice dff=4dd_\text{ff} = 4d.

3. Shape-by-shape forward pass

Let BB = batch size, TT = sequence length (up to Tmax=1024T_\text{max} = 1024). Pick B=8,T=1024B = 8, T = 1024 for concreteness.

3.1 Input ids

input_ids ∈ ℤ^{B × T}     # shape: (8, 1024); each entry is an integer in [0, V)

A token ID array. After 7.2, each entry is the BPE id of one token. Shape (B, T) = (8, 1024).

3.2 Embedding

GPT-2 uses learned absolute positional embeddings (Appendix 6.B), so the input embedding is the sum of a token embedding and a position embedding:

tok_emb = E[input_ids]               # shape: (B, T, d) = (8, 1024, 768)
pos_emb = W_pos[:T]                   # shape: (T, d)    = (1024, 768)
X^(0)   = tok_emb + pos_emb           # broadcast along batch: (8, 1024, 768)

Two parameter tensors:

  • E∈RV×dE \in \mathbb{R}^{V \times d} — the token embedding matrix. 50,257×768=38,597,37650{,}257 \times 768 = 38{,}597{,}376 parameters.
  • Wpos∈RTmax×dW_\text{pos} \in \mathbb{R}^{T_\text{max} \times d} — the position embedding matrix. 1024×768=786,4321024 \times 768 = 786{,}432 parameters.

Together: ∼39.4\sim 39.4M parameters. About 32%32\% of the total — a big chunk for such a small model.

3.3 One Transformer block

Each block is the pre-LN block of 6.10:

# Input to block ell:
X ∈ (B, T, d) = (8, 1024, 768)

# --- Attention sub-layer ---
X1  = LayerNorm_1(X)                 # (B, T, d), uses γ_1, β_1 ∈ (d,)
QKV = X1 @ W_attn                    # (B, T, 3d) = (8, 1024, 2304); W_attn shape (d, 3d) = (768, 2304)
Q, K, V = split(QKV, axis=-1)        # each (B, T, d) = (8, 1024, 768)
Q = Q.view(B, T, h, d_h).transpose(1, 2)   # (B, h, T, d_h) = (8, 12, 1024, 64)
K, V = same reshape                       # (B, h, T, d_h) = (8, 12, 1024, 64)

S = Q @ K.transpose(-2, -1) / sqrt(d_h)    # (B, h, T, T) = (8, 12, 1024, 1024)
S = S + causal_mask                        # causal mask is upper triangle = -inf
A = softmax(S, axis=-1)                    # (B, h, T, T)
Z = A @ V                                  # (B, h, T, d_h) = (8, 12, 1024, 64)
Z = Z.transpose(1, 2).view(B, T, d)        # (B, T, d) = (8, 1024, 768)
attn_out = Z @ W_O                         # W_O shape (d, d) = (768, 768)

X = X + attn_out                           # residual: (B, T, d)

# --- FFN sub-layer ---
X2  = LayerNorm_2(X)                       # (B, T, d), uses γ_2, β_2 ∈ (d,)
H   = X2 @ W_1 + b_1                       # (B, T, 4d) = (8, 1024, 3072); W_1 (d, 4d) = (768, 3072)
H   = GELU(H)
ffn_out = H @ W_2 + b_2                    # (B, T, d) = (8, 1024, 768); W_2 (4d, d) = (3072, 768)

X = X + ffn_out                            # residual: (B, T, d)

Twelve such blocks. After block 12, X(12)∈RB×T×dX^{(12)} \in \mathbb{R}^{B \times T \times d}.

3.4 Final LN and LM head

Z = LayerNorm_final(X^(12))          # (B, T, d) = (8, 1024, 768)
logits = Z @ E^T                      # (B, T, V) = (8, 1024, 50257), weight-tied to E

The LM head is tied to the embedding matrix EE — same parameters, used at both ends. The output logits are a probability distribution over the vocabulary at every position.

3.5 Loss

For a training step, the target is target_ids ∈ ℤ^{B × T} — usually the same as input_ids shifted by one position (next-token prediction). The loss is row-wise cross-entropy (1.8):

L=−1BT∑b=1B∑t=1Tlog⁡softmax(logitsb,t)targetb,t.\mathcal{L} = -\frac{1}{B T} \sum_{b = 1}^{B} \sum_{t = 1}^{T} \log\text{softmax}(\text{logits}_{b, t})_{\text{target}_{b, t}}.

A single scalar.


4. Parameter count, from first principles

The 124M figure breaks down as follows.

4.1 Per-block parameters

| Component | Shape | Count | |---|---|---:| | LayerNorm_1: γ1,β1\gamma_1, \beta_1 | (d,)×2(d,) \times 2 | 2d=1,5362d = 1{,}536 | | W_attn (combined Q, K, V) | (d,3d)(d, 3d) | 3d2=1,769,4723 d^2 = 1{,}769{,}472 | | b_attn (QKV bias) | (3d,)(3d,) | 3d=2,3043d = 2{,}304 | | W_O (attention output) | (d,d)(d, d) | d2=589,824d^2 = 589{,}824 | | b_O (attention output bias) | (d,)(d,) | d=768d = 768 | | LayerNorm_2: γ2,β2\gamma_2, \beta_2 | (d,)×2(d,) \times 2 | 2d=1,5362d = 1{,}536 | | W_1 (FFN expand) | (d,4d)(d, 4d) | 4d2=2,359,2964 d^2 = 2{,}359{,}296 | | b_1 (FFN expand bias) | (4d,)(4d,) | 4d=3,0724d = 3{,}072 | | W_2 (FFN contract) | (4d,d)(4d, d) | 4d2=2,359,2964 d^2 = 2{,}359{,}296 | | b_2 (FFN contract bias) | (d,)(d,) | d=768d = 768 | | Per block total | | 7,087,872\mathbf{7{,}087{,}872} |

So one block is ≈7.09\approx 7.09M parameters. Times L=12L = 12 blocks: 85,054,464≈8585{,}054{,}464 \approx 85M.

4.2 Embeddings and final LN

| Component | Shape | Count | |---|---|---:| | Token embedding EE | (V,d)=(50257,768)(V, d) = (50257, 768) | 38,597,37638{,}597{,}376 | | Position embedding WposW_\text{pos} | (Tmax,d)=(1024,768)(T_\text{max}, d) = (1024, 768) | 786,432786{,}432 | | Final LayerNorm: γf,βf\gamma_f, \beta_f | (d,)×2(d,) \times 2 | 1,5361{,}536 | | LM head (tied to EE) | — | 00 | | Embeddings + LN total | | 39,385,344\mathbf{39{,}385{,}344} |

4.3 Grand total

#θGPT-2-small=85,054,464+39,385,344=124,439,808≈124.4 M.\#\theta_\text{GPT-2-small} = 85{,}054{,}464 + 39{,}385{,}344 = 124{,}439{,}808 \approx 124.4\,\text{M}.

This matches OpenAI's headline number. The often-quoted 124M is the round-down; the precise count is ≈124.4\approx 124.4M.

4.4 Cross-check against the 12Ld212 L d^2 rule

The Cap 6 first-order rule was #θ≈Vd+12Ld2+2d\#\theta \approx V d + 12 L d^2 + 2d. Plug in:

50,257⋅768+12⋅12⋅7682+2⋅768=38,597,376+84,934,656+1,536=123,533,568.50{,}257 \cdot 768 + 12 \cdot 12 \cdot 768^2 + 2 \cdot 768 = 38{,}597{,}376 + 84{,}934{,}656 + 1{,}536 = 123{,}533{,}568.

That's ≈123.5\approx 123.5M — within 1%1\% of the exact 124.4124.4M. The discrepancy is the biases (which the rule dropped) and the per-block LNs. The 12Ld212 L d^2 formula is accurate to a few percent at this scale; you can use it for quick parameter estimates and worry about the corrections only when designing precisely.


5. FLOPs and memory at training time

From 6.12:

#FLOPs/block forward=24Td2+4T2d.\#\text{FLOPs/block forward} = 24 T d^2 + 4 T^2 d.

5.1 GPT-2-small forward FLOPs

At T=1024T = 1024:

#FLOPs/block=24⋅1024⋅7682+4⋅10242⋅768=1.449×1010+3.221×109=1.771×1010.\begin{aligned} \#\text{FLOPs/block} &= 24 \cdot 1024 \cdot 768^2 + 4 \cdot 1024^2 \cdot 768 \\ &= 1.449 \times 10^{10} + 3.221 \times 10^{9} \\ &= 1.771 \times 10^{10}. \end{aligned}

Times L=12L = 12 blocks: ≈2.13×1011\approx 2.13 \times 10^{11} FLOPs per forward pass per batch slot. Per batch of B=8B = 8: ≈1.70×1012\approx 1.70 \times 10^{12} FLOPs. Plus the LM head, 2BTVd≈6.3×10112 B T V d \approx 6.3 \times 10^{11}. Total forward ≈2.3×1012\approx 2.3 \times 10^{12} FLOPs per training batch.

5.2 Backward FLOPs

The backward pass is roughly twice the forward — every matrix multiply in the forward pass turns into two matrix multiplies in the backward (one for the input gradient, one for the weight gradient). The standard rule of thumb:

#FLOPs/training step≈3⋅#FLOPs/forward.\#\text{FLOPs/training step} \approx 3 \cdot \#\text{FLOPs/forward}.

So GPT-2-small at B=8,T=1024B = 8, T = 1024: ≈7×1012\approx 7 \times 10^{12} FLOPs per training step.

The Chinchilla heuristic of 6ND6 N D (6.12 Aside) gives: 6⋅124×106⋅8192=6.1×10126 \cdot 124 \times 10^6 \cdot 8192 = 6.1 \times 10^{12} — close enough.

5.3 Memory: parameters

124×106124 \times 10^6 params, fp32: 4⋅124×106=4964 \cdot 124 \times 10^6 = 496 MB just for the weights. In fp16 (or bf16): ∼250\sim 250 MB.

5.4 Memory: optimiser state

Adam (3.5) tracks first moment mm and second moment vv, each the same shape as the parameters. Plus the master copy of weights in fp32 (for mixed-precision training, 7.4):

Adam memory=#θ⋅4⏟master weights, fp32+#θ⋅4⏟m,fp32+#θ⋅4⏟v,fp32=12⋅#θ bytes.\text{Adam memory} = \underbrace{\#\theta \cdot 4}_{\text{master weights, fp32}} + \underbrace{\#\theta \cdot 4}_{m, fp32} + \underbrace{\#\theta \cdot 4}_{v, fp32} = 12 \cdot \#\theta\ \text{bytes}.

For GPT-2-small: 12⋅124×106=1.512 \cdot 124 \times 10^6 = 1.5 GB.

5.5 Memory: activations

The dominant memory cost at training time. Per training step, every intermediate activation is stored for the backward pass:

  • Input embedding output: BTd=6.3×106B T d = 6.3 \times 10^6 entries per layer.
  • Attention scores: BhT2=8⋅12⋅10242=1.0×108B h T^2 = 8 \cdot 12 \cdot 1024^2 = 1.0 \times 10^8 entries per block ← the dominant term.
  • FFN hidden state: BT⋅4d=2.5×107B T \cdot 4 d = 2.5 \times 10^7 entries per block.

In fp16 (2 bytes per entry), per block:

∼(1.0×108+6⋅107)⋅2=320 MB per block.\sim (1.0 \times 10^8 + 6 \cdot 10^7) \cdot 2 = 320\ \text{MB per block}.

Times L=12L = 12 blocks: ∼3.8\sim 3.8 GB of activations. Memory is dominated by activations, not by parameters or optimiser state.

This is the structural reason for activation checkpointing (7.7) — recompute activations on demand instead of storing them.

5.6 Full training memory budget for one GPT-2-small step

| Component | Size (fp16/bf16 with fp32 Adam master) | |---|---:| | Parameters (fp16) | ∼250\sim 250 MB | | Gradients (fp16) | ∼250\sim 250 MB | | Optimizer state (fp32) | ∼1.5\sim 1.5 GB | | Activations (fp16) | ∼3.8\sim 3.8 GB | | Total | ∼5.8\sim 5.8 GB |

Fits comfortably on a 24 GB consumer GPU (RTX 3090, 4090) with batch size 8. The headroom is what lets you push to larger contexts or batch sizes. For larger models (GPT-2-medium and up), the activation memory dominates and forces activation checkpointing.


6. Scaling the recipe

The exact same calculation works for any decoder-only Transformer; only the constants change. Quick table for the GPT-2 family and beyond:

| Model | LL | hh | dd | dffd_\text{ff} | TmaxT_\text{max} | #θ\#\theta | |---|---:|---:|---:|---:|---:|---:| | GPT-2-small | 12 | 12 | 768 | 3,072 | 1,024 | 124M | | GPT-2-medium | 24 | 16 | 1,024 | 4,096 | 1,024 | 355M | | GPT-2-large | 36 | 20 | 1,280 | 5,120 | 1,024 | 774M | | GPT-2-xl | 48 | 25 | 1,600 | 6,400 | 1,024 | 1.5B | | GPT-3-125M | 12 | 12 | 768 | 3,072 | 2,048 | 125M | | GPT-3-350M | 24 | 16 | 1,024 | 4,096 | 2,048 | 350M | | GPT-3-1.3B | 24 | 24 | 2,048 | 8,192 | 2,048 | 1.3B | | GPT-3-6.7B (curie) | 32 | 32 | 4,096 | 16,384 | 2,048 | 6.7B | | GPT-3-13B | 40 | 40 | 5,120 | 20,480 | 2,048 | 13B | | GPT-3-175B (davinci) | 96 | 96 | 12,288 | 49,152 | 2,048 | 175B | | LLaMA-7B | 32 | 32 | 4,096 | 11,008 (≈ 83d\frac{8}{3} d) | 4,096 | 6.7B | | LLaMA-70B | 80 | 64 | 8,192 | 28,672 | 4,096 | 70B |

A few patterns to notice:

  • GPT-2 → GPT-3 architectural change is minimal. Same shape, larger LL and dd, longer TmaxT_\text{max}. The "scale is what matters" thesis is this.
  • LLaMA's dffd_\text{ff} is slightly less than 4d4 d. Because it uses SwiGLU (Cap 8.4), which uses three matrices in the FFN. Adjusting dffd_\text{ff} down keeps the parameter count similar to standard-FFN models.
  • The ratio dff/d≈4d_\text{ff} / d \approx 4 (or 8/38/3 for SwiGLU) is constant across the family — this is the only thing tied to a "shape rule"; everything else scales freely.

7. What we keep going forward

Three things from this session:

  1. Every Transformer is a small set of hyperparameters (L,h,d,dff,V,Tmax)(L, h, d, d_\text{ff}, V, T_\text{max}) that determine everything else. Memorise GPT-2-small as (12,12,768,3072,50257,1024)(12, 12, 768, 3072, 50257, 1024).
  2. Parameter count is approximately Vd+12Ld2V d + 12 L d^2. Accurate to a few percent. For GPT-2-small: 39M embed + 85M blocks ≈ 124M total.
  3. Memory cost is dominated by activations, not parameters or optimiser state, at training time. This will be the central concern of 7.6 and 7.7.

Next session opens up the precision axis: training a 124M model in pure fp32 is wasteful; modern training uses fp16 or bf16 for forward/backward and fp32 for the optimiser. Each choice has a numerical-stability story we have to know.

Self-test

(a) Reproduce the GPT-2-small parameter count from scratch using the per-block table in §4.1. Identify the single largest term.

(b) A larger model has d=4096,L=32,V=50000,Tmax=2048d = 4096, L = 32, V = 50000, T_\text{max} = 2048. Estimate the parameter count using the Vd+12Ld2V d + 12 L d^2 rule. Now compute the exact count using §4.1 (including biases and LNs). Compare.

(c) For GPT-2-small at B=8,T=1024B = 8, T = 1024, the FLOPs per training step are roughly 7×10127 \times 10^{12}. On an A100 GPU at ~300 TFLOPs/s fp16, what is the wall-clock per step? Compare with the same calculation for GPT-3-175B at B=1024,T=2048B = 1024, T = 2048.

(d) The activation memory dominates the training memory budget (§5.5). State which activation contributes the largest term and why it scales as hT2h T^2.

(e) A GPT-2-small model has 50,257 vocabulary entries × 768 dim = 38.6M embedding parameters. What fraction of the total is this? Now repeat for GPT-3-175B (V same, d=12288). What does this say about the "embedding-dominated" regime?

[1]

References

  1. [1]

    Chen, Z., Deng, Y., Yuan, H., et al. (2024). "Self-play fine-tuning converts weak language models to strong language models" (SPIN). arXiv:2401.01335 — Iterative DPO with SFT data as ycy_c (9.11 §7.3).

    ↩