cs336 / lessons/03 · modern recipelesson 4 / 20

Part I — Basics

The modern recipe — what changed since GPT-2

Lesson 02 built the decoder-only Transformer and proved it can be trained by one parallel cross-entropy pass. But the block we built is the 2019 GPT-2 block, and at modern depth and scale it misbehaves: the residual stream's scale grows with depth and big runs spike during training, LayerNorm and GELU spend parameters that buy nothing, learned absolute positions cannot see past Tmax, and a full set of K/V heads bloats the cache you carry at inference. This lesson swaps five components — each forced by one of those weaknesses — to get the Llama-style block that every frontier dense model ships in 2024–2025. The thesis throughout: each swap buys efficiency or stability at ≈ zero quality cost, nearly all at ≈ zero extra parameters and little added compute — though a few (SwiGLU's gate, QK-norm) do add a small cost.

The previous step left this broken
The vanilla GPT-2 block trains, but it is a 2019 artifact and four things break as you push it. (1) Instability at depth: even with pre-norm, the residual stream's scale grows with depth and big runs spike — loss jumps, sometimes never recovers. (2) Wasted parameters: LayerNorm carries a mean-centering step and a bias β, every linear carries a bias, and the GELU MLP is parameter-hungry per unit of quality. (3) No extrapolation: learned absolute position embeddings are a fixed Tmax × d table — feed a sequence longer than training and there is literally no vector to look up. (4) A fat KV cache: at inference every one of h heads stores its own K and V, and that cache, not compute, is what caps batch size and context (lesson 18). None of these is fatal alone; together they make the vanilla block the wrong thing to scale.
Linear position
Forced by: the vanilla GPT-2 block is unstable and parameter-wasteful at modern depth/scale, can't extrapolate context, and carries a fat KV cache.
New idea: five swaps that each buy efficiency or stability at ≈ 0 cost — pre-norm + RMSNorm, RoPE, SwiGLU, GQA, and bias-free linears + QK-norm / z-loss — yielding the Llama-style block.
Forces next: a fixed architecture still has to be optimized down the loss without diverging or wasting the budget — that is the training recipe (lesson 4).
The plan
Five swaps, one section each, every one framed as the forced fix for a vanilla weakness. (1) pre-norm + RMSNorm kills the depth spikes and drops dead params. (2) RoPE replaces the position table with a rotation, so attention sees relative distance and extrapolates. (3) SwiGLU rebuilds the MLP as a gated unit that beats GELU per parameter. (4) GQA shares K/V across query-head groups and slashes the KV cache. (5) bias-free linears plus QK-norm / z-loss buy large-scale stability for free. Then a GPT-2-vs-Llama table, and a widget where you toggle each swap and watch params, KV bytes, stability, and usable context move.

1 · RMSNorm — keep pre-norm, drop the dead params

Lesson 02 already adopted pre-norm placement — normalizing the input to each sublayer, inside the residual branch — so the only norm change left versus the block we inherited is which normalizer sits there: LayerNorm → RMSNorm. Normalization exists to keep the residual stream's scale under control so gradients stay sane through L blocks. The inherited block uses LayerNorm: subtract the mean, divide by the standard deviation, then apply a learned scale γ and bias β:

LayerNorm(x) = (x − mean(x)) / √(var(x) + ε) · γ + β

RMSNorm asks: do we need the mean-centering and the bias at all? It drops both, keeping only the root-mean-square rescale and the learned gain:

RMSNorm(x) = x / √(mean(x²) + ε) · γ

The win is twofold. Cheaper: no mean to compute (one fewer reduction over d), no subtraction, no β to store or update — you save d parameters per norm and a memory pass. Since norms are memory-bound elementwise ops (lesson 6's roofline), fewer reductions and fewer bytes moved is a direct latency win — see the kernel-cost view in gpu_kernels/02 · memory hierarchy. Equal quality: mean-centering buys essentially nothing in a residual Transformer because the residual stream is already roughly centered; ablations show RMSNorm matches LayerNorm. The verdict is settled: RMSNorm won — nobody ships LayerNorm in a new LM.

This swap keeps the pre-norm placement from lesson 2 and only changes the normalizer; it is worth recalling why pre-norm is the placement we hold fixed. Pre-norm normalizes the input to each sublayer, inside the residual branch: x = x + sublayer(RMSNorm(x)). Post-norm (the original 2017 design) normalizes after the add: x = RMSNorm(x + sublayer(x)). The difference is everything at depth. In pre-norm there is a clean identity path from input to output — the gradient flows straight down the residual highway without passing through a norm, so it neither vanishes nor explodes across L layers; the stream's variance grows at most linearly in L and a final norm cleans it up. In post-norm the backward gradient threads through L norms in series, small Jacobian errors compound, and you must add an LR warmup just to survive the first hundreds of steps. Pre-norm removes that fragility for free (same params, same FLOPs), which is why every modern stack is pre-norm.

  post-norm (2017)                pre-norm (modern)
  x ─┬─► sublayer ─► (+) ─► Norm   x ─┬─► Norm ─► sublayer ─► (+) ─►
     └────────────────►            └───────────────────────►
  gradient crosses a Norm          clean identity path: gradient
  every layer → compounds          skips every Norm → stable at depth

2 · RoPE — rotate Q and K so attention sees relative position

The learned absolute position table has two flaws. It costs Tmax · d parameters, and it is a hard wall: position 8,193 in a model trained to Tmax = 8,192 has no embedding row, so the model has never seen it and cannot extrapolate. We want a positional scheme that is parameter-free and degrades gracefully past the training length. The mechanism that wins is Rotary Position Embedding (RoPE).

RoPE injects position not by adding a vector but by rotating the query and key vectors by an angle proportional to their absolute position. Split each head's dhead dimensions into pairs; pair i rotates at frequency θi = θbase−2i/dhead with base θbase (commonly 10,000). The query at position m is rotated by angle m·θi; the key at position n by n·θi.

Here is the payoff, and it falls straight out of the trigonometry. Take one 2-D coordinate pair of the query and key, and let R(·) be the 2×2 rotation. RoPE rotates the query at position m by angle i and the key at position n by i: qm = R(mθi)q and kn = R(nθi)k. The score this pair contributes is then

qmTkn = qᵀ R(mθi)ᵀ R(nθi) k = qᵀ R((n − m)θi) k,

using the fact that rotations compose as R(a)ᵀR(b) = R(b − a) (a rotation's transpose is its inverse, R(a)ᵀ = R(−a), and rotations add). So the contribution depends only on the relative offset (n − m) — never on m or n separately. The full head applies dhead/2 such independent 2-D rotations, one per pair, at frequencies θi, so the whole attention score qm · kn ends up a function of (n − m) — the relative distance — not the absolute positions m and n.

That single property is the whole reason RoPE dominates. The model learns "the token 5 back," "the token 1 back," and those relations are translation-invariant: a pattern learned at positions 100–105 transfers to 6,000–6,005 unchanged. Zero parameters — it is a fixed rotation applied inside attention, nothing to store. And it extrapolates: a position past Tmax is just a larger rotation angle, which is well-defined, so the model degrades gently rather than hitting a missing-row wall. RoPE is applied to Q and K only — never to V, because V carries content, not position.

Raw RoPE only extrapolates gently, though. Pushing the context to 10–100× the trained window — via base-θbase scaling, position interpolation, NTK-aware scaling, and YaRN, usually plus a short continued-training phase — is its own engineering step, not something you get for free here. The full treatment lives in lesson 15 · Long context; this lesson just plants the property that makes it possible.

3 · SwiGLU — a gated MLP that beats GELU per parameter

The GPT-2 MLP is d → 4d → d with GELU: proj(GELU(fc(x))), two matrices of total size 8d². SwiGLU replaces it with a gated linear unit: one branch is a Swish-activated projection, a second branch is a plain linear gate, and they are multiplied elementwise before the down-projection:

SwiGLU(x) = ( Swish(x·W₁) ⊙ (x·W₃) ) · W₂

where Swish(z) = z · σ(z) (a.k.a. SiLU), is elementwise product, and W₁, W₃ map d → dff while W₂ maps dff → d. The gate x·W₃ lets each hidden unit modulate the others multiplicatively — a richer, data-dependent nonlinearity than a single pointwise GELU — and ablations (Shazeer 2020) show it consistently improves loss per parameter.

But SwiGLU uses three weight matrices instead of two, so a naive dff = 4d would inflate the MLP from 8d² to 12d² — 50% more params. To keep the block's parameter budget fixed (so the comparison with GELU is fair), shrink the hidden width to dff ≈ (8/3)·d ≈ 2.667d. Then the three matrices total 3 · d · (8/3)d = 8d² — exactly the old MLP's count. Same parameters, same FLOPs, lower loss: a free upgrade. (In practice dff is rounded to a hardware-friendly multiple, so you'll see values like 11,008 rather than a clean 8/3 fraction.)

GPT-2 MLP (GELU)SwiGLU MLP
matrices2  (Wfc, Wproj)3  (W₁ gate-in, W₃ value-in, W₂ down)
hidden widthdff = 4ddff ≈ (8/3)d
params / block8d²≈ 8d²  (matched on purpose)
nonlinearitypointwise GELUmultiplicative Swish gate
loss per parambaselinebetter

4 · GQA — share K/V across query-head groups, slash the KV cache

This swap costs almost nothing at training time and saves enormously at inference, so its motivation lives in lesson 18 — but the mechanism belongs in the architecture. At generation time the model caches the K and V vectors of every past token so it doesn't recompute them each step. In standard multi-head attention (MHA) each of the h query heads has its own K and V head, so the cache stores 2 · L · h · dhead values per token. For a large model over a long context, that KV cache — not compute — is what caps batch size and sequence length (we forward-reference the full derivation in lesson 18 · inference).

Grouped-query attention (GQA) keeps all h query heads but shares each K/V head across a group of g query heads. You go from h K/V heads down to nkv = h / g K/V heads. The cache shrinks by exactly the group factor g. Two endpoints bracket the spectrum:

MHA  (g = 1)
nkv = h. Every query head has its own K/V. Biggest cache, highest quality, the GPT-2 default.
GQA  (1 < g < h)
nkv = h/g. Groups of query heads share a K/V head. Cache ÷ g for a tiny quality drop — the modern sweet spot (e.g. 32 query heads, 8 KV heads → g = 4).
MQA  (g = h)
nkv = 1. All query heads share one K/V head. Smallest possible cache, measurable quality loss; GQA exists to recover most of it.

Concretely: KV-cache bytes per token = 2 · L · nkv · dhead · bytes. Take a 7B-class model: L = 32, h = 32, dhead = 128, bf16 (2 bytes). MHA stores 2 · 32 · 32 · 128 · 2 = 524,288 bytes = 512 KB per token. With GQA at g = 4 (nkv = 8) it's 128 KB per token — a 4× cut. Over a 32K context that is the difference between ~17.2 GB and ~4.3 GB of cache. The query side is untouched, so the model still attends with all h heads; only the stored keys/values are shared. Quality cost: small and well-characterized — which is why Llama-2 70B, Llama-3, Mistral, and essentially every recent serving-oriented model ship GQA.

5 · Bias-free linears + QK-norm / z-loss — stability for free

Two last cleanups, both about large-scale stability with negligible cost.

Drop the biases. GPT-2 puts a bias on every linear and on LayerNorm. In a Transformer these biases contribute almost nothing to quality — the residual stream and the (now bias-free) RMSNorm gain already supply the needed offsets — yet they add parameters, a little memory traffic, and at very large scale they are a documented source of training-loss spikes. So modern blocks make every projection bias=False. The saving is small in parameters but the stability and simplicity are worth it; combined with RMSNorm's dropped β, the whole block is essentially bias-free.

QK-norm. At large width and high learning rate the attention logits q·k can grow until the softmax saturates and the loss spikes. QK-norm applies an RMSNorm (or L2 normalization) to Q and K just before the dot product, capping the logit magnitude so attention can't blow up. It costs two tiny norms per attention and removes a whole class of divergence at the 10B-plus scale.

z-loss. The output softmax has a similar failure: the log-partition (the log-sum-exp normalizer Z) can drift to large values, which interacts badly with bf16 and destabilizes training. z-loss adds a small penalty λ · (log Z)² to the objective that keeps the normalizer near 1 — a cheap regularizer that PaLM and others credit with smoother large runs. Neither QK-norm nor z-loss changes what the model can represent; they are guardrails that keep the optimizer in the stable regime, paying off at the scale where a single spike can waste days of budget.

6 · The whole swap, GPT-2 vs Llama-style

componentGPT-2 (2019)Llama-style (2024–25)why it changed
normalizationLayerNorm (mean-center + γ, β)RMSNorm (γ only)cheaper, equal quality, fewer params
norm placementpre-norm (GPT-2's own shift)pre-normstable gradient at depth, no warmup needed
positionslearned absolute (Tmax·d params)RoPE (0 params)relative, extrapolates past Ttrain
MLPd→4d→d, GELU (8d²)SwiGLU, dff≈(8/3)d (≈8d²)better loss per parameter
attentionMHA (h K/V heads)GQA (h/g K/V heads)KV cache ÷ g for tiny quality cost
biaseson all linears + LNbias-freefewer params, fewer spikes
stability extrasnoneQK-norm, z-loss (optional)cap logits / partition at large scale

None of these is a research gamble anymore; together they are the boring, settled default. The skill is not picking them — it is knowing which weakness each one repairs, so when a run spikes or a context overflows you reach for the right knob. The widget makes that mapping tangible.

7 · Toggle the recipe

Each checkbox turns one swap on or off, starting from a 7B-class shape (≈6.2B with the modern swaps on, ~7.0B vanilla) (L = 32, d = 4096, h = 32, dhead = 128, V = 128,000, Ttrain = 8,192). Watch the KPIs move: param count (GQA shrinks the K/V projections — the biggest mover here; RoPE removes the position table; SwiGLU is param-matched; bias-free trims a sliver), KV-cache bytes / token (GQA's group factor g divides it directly), stability at depth (pre-norm/RMSNorm dominates, with bias-free + QK-norm/z-loss adding a smaller push toward green), and max usable context (learned absolute caps you at Ttrain; RoPE unlocks far beyond it). The configuration that "breaks": turn everything off — you get the vanilla GPT-2 block, with the biggest cache, a hard context wall at Ttrain, and a stability marker in the red.

Recipe toggles — five swaps, four KPIs
All off = the 2019 vanilla block (red stability, fat cache, context capped at Ttrain). Flip GQA on and drag g to watch the KV cache divide by g. Flip RoPE on to unlock context past Ttrain. The stability bar is qualitative — it sums the two stability swaps (pre-norm/RMSNorm, and bias-free + QK-norm/z-loss), with pre-norm/RMSNorm the dominant contributor.
group factor g: 4  →  8 K/V heads
Total params
KV cache / token
Stability at depth
Max usable context

Failure modes & checklist

Failure modes

  • Applying RoPE to V. Rotating values mixes position into content and quietly hurts quality. Signal: loss is fine but the model is oddly position-sensitive; you rotated Q, K, and V instead of only Q and K.
  • Keeping dff=4d with SwiGLU. Three matrices at 4d is 12d² — a stealth 50% MLP-param bloat. Signal: your "same size" model is suddenly ~1.5× the MLP params of the GELU baseline; you forgot the (8/3)d shrink.
  • Serving past Ttrain without RoPE scaling. RoPE extrapolates gently, but raw (no base/NTK/YaRN scaling) it still degrades far out. Signal: quality falls off a cliff well beyond the training length until you raise θbase or interpolate.
  • Confusing GQA with fewer query heads. GQA shares K/V but keeps all h query heads; cutting query heads instead loses representational capacity. Signal: bigger quality drop than the ~tiny GQA cost — you reduced n_q, not n_kv.
  • Post-norm at depth without warmup. Spikes/divergence in deep stacks. Signal: loss is jagged early and needs a long warmup to survive — move the norm inside the residual branch (pre-norm).

Checklist

  • Pre-norm, always. RMSNorm inside each residual branch; drop mean-centering and β.
  • RoPE on Q,K only. Pick the θbase for your target context; plan NTK/YaRN scaling if you'll serve past Ttrain.
  • SwiGLU at (8/3)d. Round dff to a hardware-friendly multiple, then verify params match the GELU baseline.
  • Choose g from your serving budget. KV cache = 2·L·(h/g)·dhead·bytes/token; pick g so the cache fits the context × batch you must serve.
  • Bias-free everywhere; add QK-norm / z-loss at scale. Cheap guardrails that stop logit/partition blow-ups on 10B-plus runs.

Checkpoint

Try it
Open the widget with the default 7B-class config. First, read the KV cache with GQA on at g = 4 (128 KB/token), then turn GQA off (MHA) and confirm it roughly quadruples to 512 KB/token — that factor is exactly g. Now turn off RoPE and watch "max usable context" snap back to Ttrain = 8,192; turn it back on and watch it unlock far beyond. Finally, turn off the two stability swaps (pre-norm/RMSNorm, and bias-free + QK-norm/z-loss) one at a time and watch the stability marker slide toward red — that red bar is the vanilla GPT-2 block you started lesson 2 with. In one sentence each: which single toggle most changes inference memory, and which most changes training stability?

Where this points next

You now have a defined architecture — the Llama-style block, every component justified by the vanilla weakness it repairs. But a defined model is inert: nothing yet says how to move its parameters down the loss without the run diverging or burning the budget on a schedule that spikes. RMSNorm, pre-norm, QK-norm, and z-loss only buy stability headroom; they don't choose the optimizer, the learning-rate schedule, the warmup, the gradient clipping, or the precision that actually keep a billion-parameter run on the rails. That is the training recipe — AdamW, warmup→cosine, clipping, and bf16 mixed precision — in lesson 4 · Training.

Takeaway
The modern LM block is the GPT-2 block with five forced swaps, each repairing one vanilla weakness at ≈ zero cost. RMSNorm (x/√(mean(x²)+ε)·γ) drops mean-centering and β for a cheaper, equal-quality norm, and pre-norm placement gives a clean identity gradient path that is stable at depth without warmup. RoPE rotates Q and K by a position-dependent angle so attention scores depend only on relative distance (n−m) — zero parameters, and it extrapolates past Ttrain (scale θbase / NTK / YaRN for long context — lesson 15). SwiGLU ((Swish(xW₁)⊙xW₃)W₂) is a gated MLP that beats GELU per parameter, with dff≈(8/3)d chosen to hold the 8d² param budget fixed. GQA shares K/V across groups of g query heads, dividing the KV cache — the real inference bottleneck (lesson 18) — by g at tiny quality cost. And bias-free linears + QK-norm / z-loss remove dead parameters and cap logits/partition to kill large-scale spikes. The architecture is now settled; what remains is to optimize it (lesson 4).

Interview prompts