Roadmap / Theory

Optimizers & Schedules

SGD, momentum, Adam/AdamW, warmup + cosine decay, gradient clipping.

Backpropagation gives, for every weight, the direction in which the loss grows. The optimizer decides how far to move each weight the other way. Every LLM (large language model) in this roadmap is trained with the same recipe:

  • the AdamW optimizer (Adam with decoupled weight decay);
  • a learning rate that first grows (warmup), then shrinks along a cosine curve;
  • gradients clipped to a maximum size.

This page builds each piece, starting from plain gradient descent, with one small example per idea.

Gradient descent

The gradient points uphill, towards a larger loss. Gradient descent takes a small step the other way:

θ←θ−η ∇θL(θ)\theta \leftarrow \theta - \eta \, \nabla_\theta \mathcal{L}(\theta)

where:

  • θ\theta (theta) holds all the model's weights;
  • L(θ)\mathcal{L}(\theta) is the loss, a single number that measures how wrong the model is;
  • ∇θL(θ)\nabla_\theta \mathcal{L}(\theta) is the gradient: for each weight, how fast the loss grows when that weight grows. From now on we write it gg;
  • η\eta (eta) is the learning rate, a small positive number that sets the size of the step;
  • the arrow ←\leftarrow means "replace the old value with the new one".

Example. One weight, loss L=θ2\mathcal{L} = \theta^2 (smallest at θ=0\theta = 0), gradient g=2θg = 2\theta, starting at θ=3\theta = 3.

  • With η=0.1\eta = 0.1: 3→3−0.1⋅6=2.4→1.92→1.5363 \to 3 - 0.1 \cdot 6 = 2.4 \to 1.92 \to 1.536. Each step gets closer to 0.
  • With η=1.1\eta = 1.1: 3→3−1.1⋅6=−3.6→4.32→−5.1843 \to 3 - 1.1 \cdot 6 = -3.6 \to 4.32 \to -5.184. Each step overshoots the minimum and lands further away: training diverges.

The learning rate is the most important number in training: too small and progress is slow, too large and the loss explodes.

SGD: one mini-batch at a time

Computing the gradient on the whole dataset at every step would be far too slow. Instead, each step uses a mini-batch, a small random sample of examples. Its gradient is noisy, but right on average. This is SGD (stochastic gradient descent).

Robbins and Monro (1951) showed when such noisy steps still reach the minimum: the step sizes ηt\eta_t must shrink over time, but not too fast. In symbols, ∑tηt=∞\sum_t \eta_t = \infty (the steps add up to an unlimited distance, so we can get anywhere) and ∑tηt2<∞\sum_t \eta_t^2 < \infty (they shrink fast enough for the noise to die out). This is the first reason why learning rates decrease during training.

Momentum

Plain SGD follows every bump of the noisy gradient. Momentum (Polyak, 1964) smooths the path: it keeps a running direction, the velocity, and steps along it. In the form PyTorch uses:

v←μv+g,θ←θ−η vv \leftarrow \mu v + g, \qquad \theta \leftarrow \theta - \eta \, v

where:

  • vv is the velocity, one number per weight, starting at 0;
  • gg is the gradient of the current mini-batch;
  • μ\mu (mu) is the momentum coefficient, usually 0.90.9: how much of the old velocity is kept.

In words: the new velocity is 90% of the old one plus the new gradient, and the weights move along the velocity.

Example. μ=0.9\mu = 0.9, two different gradient histories.

  • Steady direction, g=1g = 1 at every step: the velocity goes 1, 1.9, 2.71, 3.44, 4.10,…1,\ 1.9,\ 2.71,\ 3.44,\ 4.10, \dots and approaches g/(1−μ)=10g / (1 - \mu) = 10. The steps become 10 times larger than plain SGD's.
  • Noise that flips sign, g=1,−1,1,−1,…g = 1, -1, 1, -1, \dots: the velocity goes 1, −0.1, 0.91, −0.18, 0.84, −0.25,…1,\ -0.1,\ 0.91,\ -0.18,\ 0.84,\ -0.25, \dots and settles around ±0.53\pm 0.53. The back-and-forth mostly cancels out.

So momentum speeds up along directions the gradient agrees on, and damps the directions where it keeps changing its mind.

Sutskever et al. (2013) write the same idea with the learning rate inside the velocity: v←μv+ηgv \leftarrow \mu v + \eta g, then θ←θ−v\theta \leftarrow \theta - v. The two forms give the same steps while η\eta is constant. They differ once a schedule changes η\eta, which is why the PyTorch documentation points out the difference.

Adam

Different weights need very different step sizes. The row of the embedding table for a rare token gets a large gradient once in a while; the gain of a LayerNorm (layer normalization) gets small, steady ones. A single learning rate cannot suit both.

Adam (Kingma and Ba, 2015; the name comes from adaptive moment estimation) gives each weight its own step size. It keeps two running averages per weight: the average gradient mm (the direction), and the average squared gradient vv (the typical size). Then it divides one by the square root of the other, so that every weight takes a step of about the same size. There are four lines.

1. Average of the gradient (the direction):

m←β1m+(1−β1) gm \leftarrow \beta_1 m + (1 - \beta_1)\, g

2. Average of the squared gradient (the typical size):

v←β2v+(1−β2) g2v \leftarrow \beta_2 v + (1 - \beta_2)\, g^2

3. Bias correction, explained below:

m^=m1−β1t,v^=v1−β2t\hat m = \frac{m}{1 - \beta_1^t}, \qquad \hat v = \frac{v}{1 - \beta_2^t}

4. The step:

θ←θ−η m^v^+ϵ\theta \leftarrow \theta - \eta \, \frac{\hat m}{\sqrt{\hat v} + \epsilon}

where:

  • gg is the gradient and g2g^2 its square. All operations here are elementwise: each weight is handled on its own, with its own mm and vv;
  • mm and vv start at 0. Here vv is Adam's second average, not the momentum velocity of the previous section: the two papers use the same letter;
  • β1\beta_1 (beta one), usually 0.90.9, and β2\beta_2, usually 0.9990.999, set how long the averages remember: mm averages over roughly the last 10 steps, vv over the last 1,000;
  • tt is the step number, starting from 1, and β1t\beta_1^t is β1\beta_1 to the power tt;
  • m^\hat m and v^\hat v ("m hat", "v hat") are the corrected averages;
  • ϵ\epsilon (epsilon), typically 10−810^{-8}, only avoids a division by zero.

Why bias correction is needed. mm and vv start at 0, so after the first step m=(1−β1) g=0.1 gm = (1 - \beta_1)\, g = 0.1\, g: ten times too small. For vv it is a thousand times too small (1−β2=0.0011 - \beta_2 = 0.001). Dividing by 1−βt1 - \beta^t fixes exactly that: at t=1t = 1 it divides mm by 0.10.1 and vv by 0.0010.001. As tt grows, βt\beta^t goes to 0 and the correction disappears.

Example. Two weights, two steps each. Weight A gets gradients 0.50.5 then 0.40.4; weight B gets 5050 then 4040, a hundred times larger.

weight A weight B
step 1: mm, vv 0.050.05, 0.000250.00025 55, 2.52.5
step 1: m^\hat m, v^\hat v 0.50.5, 0.250.25 5050, 25002500
step 1: m^/v^\hat m / \sqrt{\hat v} 11 11
step 2: m^/v^\hat m / \sqrt{\hat v} 0.9880.988 0.9880.988

Both weights move by the same amount, about η\eta, although B's gradients are 100 times larger: multiplying gg by 100 multiplies m^\hat m by 100 and v^\sqrt{\hat v} by 100 too, and the ratio does not change.

If instead the gradient flips, 0.50.5 then −0.5-0.5, the second ratio is about −0.05-0.05: the average direction m^\hat m is almost 0, so Adam barely moves. Like momentum, it hesitates when the gradient disagrees with itself.

Weight decay, done right: AdamW

Weight decay pulls every weight a little towards zero at each step, to keep the weights small. With plain SGD, this is the same as adding a penalty to the loss, λ2∥θ∥2\tfrac{\lambda}{2}\lVert\theta\rVert^2 (L2 regularization), where λ\lambda (lambda) is the decay strength and ∥θ∥2\lVert\theta\rVert^2 is the sum of the squares of all the weights. The gradient of that penalty is λθ\lambda \theta.

With Adam the two are not the same. If λθ\lambda\theta is added to the gradient, it also gets divided by v^\sqrt{\hat v}, so weights with large gradients are barely decayed. Loshchilov and Hutter (2019) decouple the two, and call the result AdamW: the decay is applied directly to the weights, outside Adam's adaptive step. PyTorch's AdamW first shrinks the weights,

θ←θ−ηλθ\theta \leftarrow \theta - \eta \lambda \theta

then takes the Adam step above. In words, each step multiplies every weight by 1−ηλ1 - \eta\lambda.

Example. With η=0.001\eta = 0.001 and λ=0.01\lambda = 0.01, each step multiplies every weight by 0.999990.99999: a tiny pull. Over 10,000 steps it adds up to 0.9999910000≈0.9050.99999^{10000} \approx 0.905, so a weight that the gradient does not defend loses about 10% of its size.

AdamW is the usual optimizer for transformers. Weight decay is often applied only to matrices. nanoGPT, for example, decays every 2-D parameter (linear weights and embeddings) and leaves biases and LayerNorm gains alone.

Learning-rate schedules: warmup, then cosine decay

Warmup. At the start of training the weights are random, and Adam's v^\hat v is estimated from just a handful of gradients. A full-size step at this point can push the model somewhere it never recovers from. So the learning rate starts near 0 and grows in a straight line up to its maximum over the first steps. Goyal et al. (2017) used such a gradual warmup to make early training with very large batches stable.

Cosine decay. After warmup, the learning rate follows half a cosine wave, from its maximum down to a minimum. The schedule comes from SGDR (stochastic gradient descent with warm restarts; Loshchilov and Hutter, 2017):

η(s)=ηmin⁡+12(ηmax⁡−ηmin⁡)(1+cos⁡πr)\begin{aligned} \eta(s) = {} & \eta_{\min} \\ & + \tfrac{1}{2}\left(\eta_{\max} - \eta_{\min}\right)\left(1 + \cos \pi r\right) \end{aligned}

where:

  • ss is the current step and η(s)\eta(s) the learning rate used at that step;
  • ηmax⁡\eta_{\max} and ηmin⁡\eta_{\min} are the largest and smallest learning rates;
  • rr is the progress through the decay, from 0 at the end of warmup to 1 at the last step.

How to read it: cos⁡πr\cos \pi r goes from 11 (at r=0r = 0) to −1-1 (at r=1r = 1), so the bracket 1+cos⁡πr1 + \cos \pi r goes from 2 to 0, and η\eta slides from ηmax⁡\eta_{\max} down to ηmin⁡\eta_{\min}.

Example. With ηmax⁡=0.001\eta_{\max} = 0.001 and ηmin⁡=0.0001\eta_{\min} = 0.0001:

progress rr learning rate η\eta
0 0.001
0.25 0.00087
0.5 0.00055
0.75 0.00023
1 0.0001

From r=0r = 0 to 0.250.25 the rate drops by 0.000130.00013, from 0.250.25 to 0.50.5 by 0.000320.00032: it decays slowly at first, fastest in the middle, and gently again at the end.

nanoGPT's training script uses exactly this shape, with ηmin⁡=ηmax⁡/10\eta_{\min} = \eta_{\max} / 10.

Gradient clipping

A single bad batch can produce a gradient hundreds of times larger than usual, and one step with it can undo hours of training. Pascanu et al. (2013) proposed a simple guard: whenever the gradient is too long, shrink it. Its length, the norm, is measured over all parameters together, as if every gradient were laid end to end in one long vector:

∥g∥=∑tensors∑igi2\lVert g \rVert = \sqrt{\textstyle\sum_{\text{tensors}} \sum_i g_i^2}g←g⋅min⁡ ⁣(1,c∥g∥)g \leftarrow g \cdot \min\!\left(1, \frac{c}{\lVert g \rVert}\right)

where:

  • ∥g∥\lVert g \rVert is the norm: square every gradient entry gig_i of every tensor, add them all up, take the square root;
  • cc is the threshold, the largest norm allowed;
  • min⁡(1,⋅)\min(1, \cdot) means: if the norm is already below cc, multiply by 1, so nothing changes.

Example. Two gradient entries, 33 and 44, have norm 9+16=5\sqrt{9 + 16} = 5. With c=1c = 1 both are multiplied by 1/51/5 and become 0.60.6 and 0.80.8: the new norm is 1, and the direction is the same (the second entry is still 4/34/3 of the first). With c=10c = 10 the factor is min⁡(1,10/5)=1\min(1, 10/5) = 1, and nothing changes.

Scaling everything by one factor keeps the gradient's direction and only shortens it. Clipping each tensor on its own would change the direction. PyTorch's clip_grad_norm_ adds 10−610^{-6} to the norm in the denominator, and returns the norm before clipping. Training loops log that number: a norm that keeps hitting the threshold is an early sign of trouble. nanoGPT clips at c=1.0c = 1.0 by default.

References

Practice

0/4