Transformer Training Explained
← /training

Chapter 7 of 16

Initialisation, normalisation and µP

Pre-LN vs post-LN, RMSNorm, scale through depth, hyperparameter transfer.

Slides for this chapter: Transformer Training 06, Initialisation, Normalisation and µP

Before the first step, every weight is a random number, and how large those numbers are decides whether the signal and the gradient survive the trip through many layers. Where the LayerNorms sit decides it too. Below, an 8-block version of the Decoder's model at initialisation, with the LayerNorm before each sublayer (pre-LN, the Decoder's design) and after each residual addition (post-LN, the original Transformer's).

Loading the animation…

Concept

The scale of the initial weights. Each weight matrix multiplies a vector of dd numbers; if its entries have standard deviation σ\sigma, the output's size is about σd\sigma\sqrt d times the input's. Choosing σ≈1/d\sigma \approx 1/\sqrt{d} (Xavier initialisation; He initialisation adds a factor 2\sqrt 2 for ReLU) keeps it the same, layer after layer. The Decoder's model uses σ=0.02\sigma = 0.02, GPT-2's choice (Radford et al. 2019), which for d=16d = 16 is far smaller than 1/16=0.251/\sqrt{16} = 0.25: every sublayer starts out adding almost nothing, the residual stream is just the embedding plus the positional encoding (Decoder, chapter 5), and the two designs look alike (pick std 0.02 in the animation). GPT-2 also scales the residual layers' weights by 1/N1/\sqrt N for NN residual layers, because every block adds to the same stream and their sum grows with depth.

Pre-LN vs post-LN. With std 0.25 the difference shows. In the pre-LN model each block adds its output to the stream, so the stream grows with depth: its RMS is 0.72 entering block 1 and 4.30 leaving block 8. Post-LN normalises after every addition, so the stream stays at RMS 1, but the gradient pays for it: the gradient reaching a block has passed through every LayerNorm above it, and at initialisation the blocks nearest the output get the largest gradients (post-LN: 1.29 at block 1, 2.29 at block 8). Xiong et al. (2020) proved this for post-LN at initialisation, and showed it is why post-LN transformers need a learning-rate warmup: a full-size step on those large gradients makes training unstable. In pre-LN the residual path carries the gradient back unchanged (chapter 4's "highway"), so the gradients are of similar size at every depth.

The chart shows what that means for training: a 6-block model at a peak learning rate of 0.01, three seeds each. Pre-LN trains with or without warmup (2.378 ± 0.128 without, 2.310 ± 0.074 with 200 warmup steps). Post-LN without warmup ends at 2.949 ± 0.002: every seed is still on chapter 1's plateau after 1,500 steps. With warmup it ends at 2.877 ± 0.124 (seeds: 2.735, 2.950, 2.947): warmup lets some runs start learning, not all. This is why GPT-2 and nearly every large model since put the norm first.

validation loss (mean of 3 seeds)22.533.54050010001500step
pre-LN, warmup 0 · pre-LN, warmup 200 · post-LN, warmup 0 · post-LN, warmup 200.Peak learning rate 0.01, cosine decay. results.md §7b.

RMSNorm. LayerNorm subtracts the mean and divides by the standard deviation, then scales and shifts. RMSNorm (Zhang & Sennrich 2019) skips the mean and the shift: y=γ⊙x/RMS⁡(x)y = \gamma \odot x / \operatorname{RMS}(x). It is cheaper and, in practice, as good; LLaMA uses it, as pre-normalisation (Touvron et al. 2023, §2.2). On the Decoder's model (the default run, three seeds), RMSNorm even ends lower: 2.140 ± 0.008 against LayerNorm's 2.277 ± 0.090, with 5,296 parameters instead of 5,376; LayerNorm's spread over the seeds is wide, so read that as "at least as good".

µP: one learning rate for every width. Tuning the learning rate of a large model directly costs several large runs. The maximal update parameterisation (µP; Yang et al. 2022) rescales the initialisation, the learning rate and a few multipliers with the width so that each step changes every layer's activations by about the same amount whatever the width. Then the best learning rate found on a narrow model carries over to a wide one ("µTransfer"). Under the standard parameterisation it does not: with Adam, each weight moves by about the learning rate per step, and a wider matrix sums more of those moves, so the same learning rate is a bigger effective step for a wider model.

The coordinate check below measures exactly that: ten AdamW steps at the same learning rate for widths 16 to 256, and how far the logits move. Under the standard parameterisation they move 1.43 at width 16 and 4.48 at width 256; under µP 1.43 and 1.42.

RMS change of the logits after 10 steps (log scale)0.3131030163264128256width d_model (log scale)
standard parameterisation · muP (base width 16).Learning rate 0.01, the same batches for every width. results.md §7d.

What µP changes, relative to a base width d0d_0 (here 16, the Decoder's, so at width 16 the two are identical) with m=d/d0m = d/d_0: hidden matrices start at std 0.02/m0.02/\sqrt m and Adam's learning rate for them is divided by mm; the attention scores are scaled by 1/dk1/d_k instead of 1/dk1/\sqrt{d_k}; the logits are divided by mm. The second chart trains widths 16 and 64 at three learning rates, three seeds each, for 1,500 steps. With the standard parameterisation the wider model is no better than the narrow one at any rate tried (width 64: 2.440, 2.531, 2.811 at 0.01, 0.03, 0.1; width 16's best 2.420). With µP the same width-64 model reaches 2.314, 2.310 and 2.682, best at the same rates as width 16. The grid is coarse and the spreads are wide (± 0.03 to 0.2: the plateau of chapter 1 ends at a different step in every run), so the toy shows the direction rather than an optimum. A GPU notebook runs the real version, widths 128 to 1,024 (below).

validation loss after 1,500 steps22.22.42.62.830.010.030.1peak learning rate (log scale)
width 16 (both) · width 64, standard · width 64, muP.Mean ± sd over three seeds. results.md §7e.

Run it yourself on a GPU. Notebook 07 (open in Colab) sweeps widths 128–1,024 and seven learning rates under both parameterisations on a GPU (about an hour on an A100). The notebook is one of the planned GPU replications listed under Runs to come; every number on this page comes from the in-browser model.

Maths

Pre-LN block: h=x+Attn⁡(LN⁡1(x))h = x + \operatorname{Attn}(\operatorname{LN}_1(x)), y=h+FFN⁡(LN⁡2(h))y = h + \operatorname{FFN}(\operatorname{LN}_2(h)). Post-LN block: h=LN⁡1(x+Attn⁡(x))h = \operatorname{LN}_1(x + \operatorname{Attn}(x)), y=LN⁡2(h+FFN⁡(h))y = \operatorname{LN}_2(h + \operatorname{FFN}(h)). Backward through a residual addition copies the gradient into both terms; backward through LN⁡\operatorname{LN} multiplies it by the Jacobian 1σ(I−1d11⊤−1dx^x^⊤)diag⁡(γ)\tfrac{1}{\sigma}\big(I - \tfrac1d \mathbf{1}\mathbf{1}^\top - \tfrac1d \hat x \hat x^\top\big)\operatorname{diag}(\gamma), which in post-LN lies on the only path from the loss to every earlier layer.

RMSNorm: y=γ⊙x / 1d∑ixi2+ϵy = \gamma \odot x \,/\, \sqrt{\tfrac1d \sum_i x_i^2 + \epsilon}, gradient δx=1r(δx^−x^ δx^x^‾)\delta_x = \tfrac{1}{r}\big(\delta_{\hat x} - \hat x\, \overline{\delta_{\hat x} \hat x}\big) with δx^=γ⊙δy\delta_{\hat x} = \gamma \odot \delta_y.

µP for Adam, width multiplier m=d/d0m = d/d_0 (Yang et al. 2022, Table 3): hidden weights σ=σ0/m\sigma = \sigma_0/\sqrt m, learning rate η/m\eta/m; embedding unchanged; attention q⊤k/dkq^\top k/d_k (here written 1dkdk,0/dk\tfrac{1}{\sqrt{d_k}}\sqrt{d_{k,0}/d_k}, exactly the Decoder's at m=1m = 1); output logits ×1/m\times 1/m (the head is tied to the embedding). Each variant's gradients match PyTorch autograd to 1e-16 (scripts/training/checks.py).

Code

The post-LN block's backward pass, src/lib/training/model-grad.ts (matched to the Python reference to 1e-12):

const n2 = layernormBackward(b.z!, v(params, `${p}.ln2.gamma`), dy);
addVecTo(v(grads, `${p}.ln2.gamma`), n2.dGamma);
addVecTo(v(grads, `${p}.ln2.beta`), n2.dBeta);
const dz = n2.dX;
const l2 = linearBackward(b.ffn.act, m(params, `${p}.ffn.W2`), dz);
addTo(m(grads, `${p}.ffn.W2`), l2.dW);
addVecTo(v(grads, `${p}.ffn.b2`), colSums(dz));
record?.push({ name: `${p}.dact`, value: l2.dX });
const dpre = geluBackward(b.ffn.pre, l2.dX);
record?.push({ name: `${p}.dpre`, value: dpre });
const l1 = linearBackward(b.h, m(params, `${p}.ffn.W1`), dpre);
addTo(m(grads, `${p}.ffn.W1`), l1.dW);
addVecTo(v(grads, `${p}.ffn.b1`), colSums(dpre));
// z = h + FFN(h): the gradient reaches h directly and through the FFN
const dh = addInto(dz, l1.dX);
record?.push({ name: `${p}.dh`, value: dh });
const n1 = layernormBackward(b.u!, v(params, `${p}.ln1.gamma`), dh);
addVecTo(v(grads, `${p}.ln1.gamma`), n1.dGamma);
addVecTo(v(grads, `${p}.ln1.beta`), n1.dBeta);
const du = n1.dX;
// u = x + Attn(x)
const dx = addInto(du, attentionBackward(params, config, p, b, du, grads));

Comments

Be the first to leave a comment on this section.

Sign in (top-right) to leave a comment.