Backpropagation and the optimiser

To train, the model needs to know how each of its weights affected the loss. Backpropagation gets all of those gradients in one backward sweep using the chain rule. The residual stream gives the gradient a straight road down through the stack, and an optimiser such as Adam, with a warm-up and decay schedule, turns gradients into updates.

Key idea
Chain rule backwards through every layer; residuals keep gradients alive; Adam scales each step; learning rate warms up then decays
Real world
A relay team loses a race. To fix it, the coach works backwards from the finish: how much time did the last runner lose, and how much of that came from a bad hand-off by the runner before? Each runner gets their share of the blame.
In an LLM
After one forward pass over a batch, one backward pass gives a gradient for every one of GPT-2 small’s 124 million weights, at about twice the cost of the forward pass. Adam then moves each weight by a tiny amount, on the order of the learning rate (a typical peak for a model this size is about 0.0006).

What training needs

The training objective gives one number, the loss, for a batch of text. Gradient descent lowers it by moving every weight a little against its gradient: how much the loss would change if that weight changed. A model has millions or billions of weights, so we need millions or billions of gradients, every step.

Wiggling each weight separately and re-running the model would take one forward pass per weight. gets all of them from a single backward pass.

The chain rule at a glance

A network is a chain of simple steps. If aa affects bb, and bb affects the loss LL, then

∂L∂a=∂L∂b⋅∂b∂a\frac{\partial L}{\partial a} = \frac{\partial L}{\partial b} \cdot \frac{\partial b}{\partial a}

how much LL changes per unit of aa is how much it changes per unit of bb, times how much bb changes per unit of aa. Backprop applies this from the loss backwards: start with ∂L/∂L=1\partial L / \partial L = 1, and at every step multiply by one local slope, the derivative of that step on its own. Each step only needs its own inputs from the forward pass, which is why training stores the forward pass’s intermediate values (the activations).

forward 1
x1.000
× w₁⟶× 1.0
a?∂?
tanh⟶× 0.56
b?∂?
× w₂⟶× 1.5
c?∂?
½(c − y)²⟶×
L?∂?
value (forward)gradient ∂L/∂node (backward)
local slopes
∂a/∂w₁ = x = 1.0∂b/∂a = 1 − b² = 0.559∂c/∂b = w₂ = 1.5∂L/∂c = c − y = 0.796∂L/∂w₁ = 0.796 × 1.5 × 0.559 × 1.0 = 0.668
Forward pass: compute each value from the one before. a = w₁ × x = 0.800.
A four-step network with one weight to learn (w₁). Play the forward pass, then the backward pass. Each gradient is the gradient after it times one local slope. Move w₁ and replay.

A transformer is the same, just bigger: the “steps” are matrix multiplications, softmaxes, norms and activations, and the local slopes are matrices instead of single numbers. Frameworks like PyTorch record every operation during the forward pass and run this backward sweep automatically.

example · a relay race

A relay team finishes 2 seconds behind the winner. The coach works backwards: the anchor ran her leg 0.5 seconds slow, but she also started 1.5 seconds late because the hand-off fumbled. That 1.5 seconds is then split between the third runner’s pace and the baton change. Each runner’s share of the blame is the blame passed back to them times how much their part mattered. That is the chain rule, and the coach never has to re-run the race to work it out.

Gradients flowing down the stack

In a transformer, the gradient starts at the loss, goes back through the unembedding, and then down through every block from the top to the bottom. Here is where the residual stream earns its keep.

In a plain stack, xl+1=fl(xl)x_{l+1} = f_l(x_l), the gradient reaching layer 1 is a product of one local slope per layer. If those slopes are typically a bit below 1, the product shrinks exponentially with depth: the vanishing gradient. A bit above 1 and it explodes. With a residual, xl+1=xl+fl(xl)x_{l+1} = x_l + f_l(x_l), the local slope is 1+fl′1 + f'_l: there is always a path where the gradient is passed straight through, multiplied by exactly 1. Watch it in a simulated network:

reached layer 0
plain: x ← tanh(Wx)residual: x ← x + tanh(Wx)
plain, at layer 03.9e-8
residual, at layer 03.0e+0
ratio8e+7×
In the plain stack the gradient reaching layer 0 is 3.9e-8: each layer multiplied it by something below 1, 32 times. The first layers would barely learn. With residuals the identity path adds the gradient straight through, and it arrives at 3.02.
A 16-wide toy network with tanh layers and random weights, simulated in your browser. The gradient starts at size 1 at the top and is passed down one layer at a time. Press play to watch it travel; compare the plain stack with the residual one.

With the default settings, the plain 32-layer stack delivers a gradient of a few times 10−810^{-8} to its first layer: those weights would effectively never change. The residual stack delivers about 3. This is why every deep network since ResNet, and every transformer, has residual connections, and why pre-norm (which keeps the residual path free of norms) trains more reliably than post-norm.

example · the residual highway in GPT-2

GPT-2 small has 12 blocks, so a gradient from the loss passes 24 sub-layers (12 attention, 12 MLP) on its way to the embedding table. Thanks to the residual path, the embedding of “ cat” gets a direct share of the gradient from every position where “ cat” helped or hurt the prediction, not a version that has been squeezed through 24 multiplications. Llama 3 70B has 80 blocks, 160 sub-layers; without the highway it could not be trained at all.

Adam: one step size per weight

Plain gradient descent uses one learning rate for every weight. That works badly for transformers, whose gradients differ in size by orders of magnitude between, say, the embedding of a rare token and a busy attention matrix. (Kingma and Ba, 2014) keeps two running averages for every weight:

  • mm, the average gradient (momentum: keep going in a consistent direction),
  • vv, the average squared gradient (how large and noisy this weight’s gradients usually are),

and moves each weight by η⋅m/(v+ϵ)\eta \cdot m / (\sqrt{v} + \epsilon). Dividing by v\sqrt{v} means every weight takes steps of roughly the size η\eta, whatever the scale of its gradients. AdamW (Loshchilov and Hutter, 2017) adds weight decay, a gentle pull of every weight towards zero, and is the standard optimiser for LLMs. Its cost: two extra numbers stored per weight, which is why training needs far more memory than inference (see low-precision training).

The learning-rate schedule

The learning rate η\eta is not fixed. Almost every LLM uses the same shape:

  1. : start near zero and rise linearly over the first few thousand steps. At the start the weights are random and Adam’s averages are empty, so large steps would be based on bad statistics and can make the loss spike.
  2. Decay: after the peak, lower the learning rate along a cosine curve to a small floor. Late in training the model is fine-tuning many small details, and small steps stop it from bouncing around the minimum.
For the first 2,000 steps the learning rate climbs from 0 to 6e-4, giving Adam time to collect gradient statistics before it takes big steps. Then it follows half a cosine down to 10% of the peak, so late training takes small, careful steps.
A learning-rate schedule over 100,000 training steps: a linear warm-up to the peak, then a cosine curve down to a floor. Move the sliders.

GPT-3 warmed up over its first 375 million tokens and decayed to 10% of the peak. Llama 3 405B warmed up over 8,000 steps to a peak of 8×10−58 \times 10^{-5} and decayed by a cosine to 8×10−78 \times 10^{-7}. Some recent runs use “warm-up, stable, decay” instead: a long flat stretch at the peak and a short decay at the end, which makes it easy to keep training from a checkpoint.

You can see all of this at work, gradients, Adam and a schedule, in train a tiny transformer live.

going deeper

Adam in full

For each weight, with gradient gtg_t at step tt:

mt=β1mt−1+(1−β1)gt,vt=β2vt−1+(1−β2)gt2m^t=mt/(1−β1t),v^t=vt/(1−β2t)wt=wt−1−η (m^t/(v^t+ϵ)+λwt−1)\begin{aligned} m_t &= \beta_1 m_{t-1} + (1 - \beta_1) g_t, & v_t &= \beta_2 v_{t-1} + (1 - \beta_2) g_t^2\\ \hat m_t &= m_t / (1 - \beta_1^t), & \hat v_t &= v_t / (1 - \beta_2^t)\\ w_t &= w_{t-1} - \eta\,\big(\hat m_t / (\sqrt{\hat v_t} + \epsilon) + \lambda w_{t-1}\big) \end{aligned}

Typical LLM settings: β1=0.9\beta_1 = 0.9, β2=0.95\beta_2 = 0.95, ϵ=10−8\epsilon = 10^{-8}, weight decay λ=0.1\lambda = 0.1.

Warm-up and cosine

η(t)={ηmax⁡ t/twt<twηmin⁡+12(ηmax⁡−ηmin⁡)(1+cos⁡(πt−twT−tw))t≥tw\eta(t) = \begin{cases} \eta_{\max}\, t / t_w & t < t_w \\ \eta_{\min} + \tfrac12(\eta_{\max} - \eta_{\min})\big(1 + \cos(\pi \tfrac{t - t_w}{T - t_w})\big) & t \ge t_w \end{cases}

In code

opt = torch.optim.AdamW(model.parameters(), lr=6e-4, betas=(0.9, 0.95), weight_decay=0.1)
for step, batch in enumerate(data):
    for g in opt.param_groups:
        g["lr"] = lr_at(step)                  # warm-up + cosine
    loss = model(batch).loss
    loss.backward()                            # backprop: fills p.grad for every weight
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    opt.step(); opt.zero_grad()

In papers

Related

  • training-objective - A language model is trained on one task: predict the next token. Shift the text one place to get the answers, and score every position at once by the negative log of the probability the model gave the right token. Average, and that is the loss.
  • train-live - A real transformer, one layer and one attention head, learns to reverse and to sort five digits while you watch. Within a few hundred steps its attention goes from noise to a clean pattern you can read, and that pattern is exactly the algorithm that solves the task.
  • residual-stream - Every token carries one vector up through the model. Each attention layer and each MLP reads that vector, computes something, and adds the result back. Nothing is overwritten. This running sum is the residual stream, and it is why deep transformers can be trained at all.
  • normalization - Before each attention layer and each MLP, the model rescales the token’s vector to a standard size. LayerNorm subtracts the mean and divides by the standard deviation; RMSNorm just divides by the root mean square. Where the norm sits, before the layer or after it, decides whether a deep model trains.