Training dynamics: warmup, clipping, loss spikes, large batches, mixed precision
Before this lesson: lesson 02 (gradient descent on and the limit ), lesson 05 (pre-LN and post-LN, and why post-LN needs warmup), lesson 08 (gradient clipping, first look), lesson 09 (attention logits and softmax saturation) and lesson 11 (the transformer block). Floating-point numbers are in linear-algebra lesson 13. Adam, AdamW and schedules are covered in the optimization track; mixed precision in more depth in the ML-systems track. Time: about 80 minutes, plus the notebook.
Why this matters #
A large training run can be healthy for 80,000 steps and then diverge in 20 steps. The cause is rarely a bug in the model code. It is usually a problem of training dynamics: a step too large for the current curvature, a saturated softmax, an out-of-date optimizer statistic, or a number that does not fit in 16 bits. Every large language model (GPT-3, PaLM, LLaMA, OPT) reports these problems and the fixes in its paper, and the default recipe of nanoGPT contains all of them: warmup, cosine decay, clipping at 1.0, and bf16. Interviews ask "your loss spiked at step 50k: what do you do?". In ML for science the same problems appear when a weather or Earth-system foundation model is trained in bf16 on many GPUs.
1. Predict first #
2. The picture #
One parameter. Take gradient descent on with curvature and learning rate (lesson 02). One step is . It shrinks when , that is, when . Above this limit every step makes larger and the loss explodes.
In a network, the role of is played by the sharpness: the largest eigenvalue of the Hessian of the loss. It is not constant. It is often large at initialization and smaller later. So a learning rate with at step 10,000 can have at step 10.
Compare the two runs. Both use the same peak learning rate. In this toy model the curvature falls from 20 to 2 during the first steps. The magenta run uses the peak learning rate from step 0: starts far above the dashed edge 2 and explodes. The blue run warms up linearly and stays below 2 at every step. Shorten the warmup until the blue run fails too, and find the largest constant that is safe without warmup.
The rest of the lesson follows from this picture and from finite number formats:
| Problem | What grows | Fix | Section |
|---|---|---|---|
| early steps too large | at high early curvature | warmup | 3.1 |
| one huge gradient | global-norm clipping | 3.2 | |
| attention saturates | attention logits, slowly | qk-layernorm, soft-capping | 3.3 |
| output logits drift | of the output softmax | z-loss, weight decay | 3.3 |
| Adam's step explodes | after a quiet period | lower | 3.3 |
| larger batch, fewer steps | learning rate needed per step | linear scaling + warmup | 3.4 |
| 16-bit numbers | rounding of small updates, underflow | fp32 master weights, loss scaling | 3.5 |
The sharpness of a network is about 40 in the first steps and about 4 later. You use plain gradient descent with from step 0. What happens?
The limit is with the current curvature. Early, , so steps grow . Later, . A warmup that starts below and reaches once the curvature has fallen avoids the early instability.
3. Definitions and main results #
3.1 Learning-rate warmup and decay #
Let be the step, the warmup length, the total number of steps and the peak learning rate. A linear warmup is
(nanoGPT uses , so the first step does not have .) After the warmup comes a decay to a small :
- cosine decay: with ;
- linear decay: ;
- warmup–stable–decay (WSD): until step , then a short decay (linear, for example) over the last steps, often 10–20% of the run.
WSD has a practical advantage: you can start the decay from any saved checkpoint, so you do not need to fix before the run starts (Hägele et al. 2024). nanoGPT's defaults are a typical cosine recipe: , , cosine decay to at step 600,000, AdamW with , weight decay 0.1 and clipping at 1.0.
Switch between the schedules and move the sliders. The violet curve is the selected schedule; the grey dashed curves are the other two. The yellow dot reads the learning rate at step . Notice that WSD stays at the peak for most of the run, so its mean learning rate is the largest of the three.
Why warmup helps. Three mechanisms are supported by the literature:
- High curvature at the start. The sharpness is often largest at initialization, as in the picture of section 2. A small learning rate first lets training move to flatter regions, where the later large learning rate is below (Kalra and Barkeshli 2024). In post-LN transformers the gradients near the output are large at initialization (lesson 05, Xiong et al. 2020), which makes this worse.
- Adam's statistics are noisy at the start. In the first step Adam's bias-corrected moments are and , so the update has size in every coordinate, whatever the gradient. In the next few steps still averages very few squared gradients, so the per-coordinate step size is noisy and can be very large (Liu et al. 2020, RAdam).
- Large batches. The learning rate of a large batch (section 3.4) is too large while the network changes fast, which happens at the start (Goyal et al. 2017).
A longer warmup also makes the final loss less sensitive to the choice of , more so for larger models (Wortsman et al. 2023).
The edge of stability
With full-batch gradient descent, the sharpness often does not stay below by itself. Cohen et al. (2021) observed that it rises during training ("progressive sharpening") until it reaches about , and then hovers there while the loss keeps falling, but not monotonically. So training often runs at the edge of the stability limit, and the learning rate partly decides the sharpness of the region where training ends. This is one more reason why the safe learning rate changes during training.
3.2 Global-norm gradient clipping #
Let be the gradients of all parameter tensors, seen as one long vector, with the global norm . For a threshold , global-norm clipping is
Result 1. has the same direction as , and .
Proof.
- with . A positive multiple keeps the direction.
- If , then and the norm is .
- If , then and the norm is .
Lesson 08 has a figure that compares clipping by norm with clipping each entry. The details that matter in practice:
- PyTorch (
torch.nn.utils.clip_grad_norm_) uses the factor , clamped at 1, and returns the norm before clipping. Log it: a rising gradient norm is often the first sign of a coming spike. - Global, not per tensor or per entry. Clipping each tensor separately, or each entry
(
clip_grad_value_), changes the direction of the full gradient. - With SGD the step length is at most . With Adam clipping does not cap the step, because Adam divides by . It limits how much one bad gradient enters and .
- With fp16 loss scaling (section 3.5) the gradients are multiplied by the scale . Unscale first
(
scaler.unscale_(optimizer)), then clip.
The gradients of two tensors are and , and the clipping threshold is . What does global-norm clipping return?
The global norm is , so every tensor is multiplied by the same factor . The direction of the full gradient is kept and the new norm is . Clipping only the tensor changes the direction. Dividing by the norm gives length 1, not . The global norm adds squares: , not .
3.3 Loss spikes and divergence #
A loss spike is a sudden jump of the training loss, after which the loss either comes back or diverges. Each cause below shows up first in one quantity. So log: the gradient norm before clipping, the update size per layer, the largest attention logit, the mean of the output softmax, and in fp16 the loss scale. The table at the end of this section connects the signals to the causes.
(a) Attention-logit growth
The logit of query and key is (lesson 09). It is a product of two weight matrices, so it grows with the square of the weight scale: if and both grow by a factor , every logit grows by . When the weights grow during training, the logits become very large, the attention weights become one-hot (entropy near 0), the softmax saturates and its gradient vanishes, and the loss diverges. This was seen in ViT-22B (Dehghani et al. 2023) and reproduced in small models at high learning rates (Wortsman et al. 2023).
Fix: qk-layernorm. Apply a LayerNorm to each query and each key (per head) before the dot product. With it, small models train stably at learning rates where the plain models diverge (Wortsman et al. 2023). Logit soft-capping is an alternative: , so (Gemma 2 uses for the attention logits and for the final logits).
Result 2 (qk-layernorm bounds the logits). Let and in , without gain and bias. Then , whatever the size of and .
Proof.
- , where and are the mean and the variance of the entries of . So , that is, . The same holds for .
- Cauchy–Schwarz: .
- Divide by .
With learned gains (no biases), the bound is multiplied by . For the bound is . The bound is reached only when and are parallel; typical logits are much smaller.
Move the weight scale . Every plain logit grows like : on the log–log chart the magenta curve has slope 2, and the blue attention weights of query 1 become one-hot (look at the entropy). Switch on qk-layernorm: the aqua curve is flat and stays below the dashed bound , whatever is.
(b) Output-logit drift
Without weight decay, the output logits of a language model can drift to very large negative values late in training, and the loss diverges (Wortsman et al. 2023). The reason is a flat direction of the cross-entropy.
Result 3 (cross-entropy has a flat direction). For logits , a label , and , the loss is . Then (i) for every , and (ii) , whose entries sum to 0.
Proof.
- , and also grows by . The difference is unchanged.
- and , so . Its entries sum to .
So nothing in the loss pulls back when all logits drift together: the gradient has no component along . Fix: the z-loss (PaLM, Chowdhery et al. 2022), an extra term with , which keeps near 0. Its gradient is , because . The entries of this gradient sum to : it pushes all logits down when and up when . Weight decay also prevents this drift in Wortsman et al.'s experiments.
The z-loss term is with . The logits are , so and . What is the sum of the entries of the z-loss gradient with respect to ?
The gradient is and , so the entries sum to . A descent step subtracts it: all logits move down, toward . The cross-entropy gradient sums to 0; the z-loss is exactly the term that does not. The value is the z-loss itself, not its gradient.
(c) Adam's second moment is out of date
Adam divides by , an exponential average of with a memory of about steps: 1,000 steps for . Suppose some parameters had tiny gradients for a long time, so is tiny. If they then receive a large gradient, is huge and the update is far too large. In CLIP training, 28 of 30 loss spikes came 1–8 steps after the RMS of in the patch-embedding layer jumped far above 1 (Wortsman, Dettmers et al. 2023, "stuck in the past").
Fix: a lower . With (common for LLMs: GPT-3, nanoGPT), follows the gradients faster. A that is too low makes the steps noisy and slows training. PaLM uses at step . Update clipping, as in AdaFactor, is another fix.
(d) Adam's is too large. When the gradient RMS of a layer falls to the order of , the update becomes small and the layer stops learning. This is not a spike but a silent stall. Wortsman et al. (2023) found that a much smaller (for example instead of ) helps large models.
(e) The practical procedure. PaLM 540B had about 20 loss spikes despite gradient clipping. Each time, the authors restarted from a checkpoint about 100 steps before the spike and skipped about 200–500 data batches. The same batches did not cause a spike from a different, earlier checkpoint. So the cause was the combination of specific batches with a specific parameter state, not "bad data" alone. OPT-175B restarted from earlier checkpoints with a lower learning rate and lowered the clipping threshold from 1.0 to 0.3; its divergences came with the fp16 loss scale falling toward 0.
| Signal in the logs | Likely cause | Fix |
|---|---|---|
| largest attention logit grows, attention entropy falls | attention-logit growth | qk-layernorm, soft-capping, lower learning rate |
| drifts far from 0, logits very negative | output-logit drift | z-loss, weight decay |
| jumps in one layer, spike a few steps later | out-of-date second moment | lower , update clipping |
| gradient RMS of a layer near , updates vanish | too large | smaller |
| fp16 loss scale falls step after step | overflow (inf) at every step: a divergence | restart earlier, lower learning rate, bf16 |
| one isolated spike | batch + parameter-state interaction | restart about 100 steps earlier, skip batches |
3.4 Large-batch training #
Linear scaling rule (Goyal et al. 2017): when the mini-batch size is multiplied by , multiply the learning rate by and keep the other hyperparameters.
Result 4 (why the rule works). Let be the loss of example . Compare SGD steps on mini-batches of size with learning rate , against one step on their union (size ) with learning rate .
- small steps: .
- One large step: .
- If for , the two results are equal exactly when , that is, .
The assumption in step 3 fails in two cases. Early in training the network changes fast, so the gradients at and differ. The fix is gradual warmup: increase the learning rate linearly from to over the first epochs (5 epochs in Goyal et al.). With it, ResNet-50 on ImageNet at batch 8,192 reaches 23.74% top-1 validation error, against 23.60% for the batch-256 baseline. Beyond some batch size the accuracy degrades quickly (about 8k for ResNet-50 on ImageNet).
Where the rule stops. Beyond the critical batch size (McCandlish et al. 2018), a larger batch gives little gain per step: the gradient is already accurate, and only the number of steps matters. For Adam a square-root rule is a common start (Malladi et al. 2022 derive it).
Other pitfalls (Goyal et al.): normalize the loss by the global batch , not by the per-worker batch (an all-reduce sums the gradients of the workers); compute BatchNorm statistics per worker; and weight decay is part of the gradient, so scaling the loss is not the same as scaling the learning rate.
SGD with batch 256 and learning rate 0.1 works well. You move to batch 1,024. What does the linear scaling rule suggest?
, so . Result 4 needs the gradients to change little over 4 small steps, which fails early in training, so the rule is used with a gradual warmup. With Adam, a factor of about is a more common start.
3.5 Mixed precision #
| Format | Exponent bits | Fraction bits | Largest | Smallest normal | |
|---|---|---|---|---|---|
| fp32 | 8 | 23 | |||
| fp16 | 5 | 10 | 65,504 | ||
| bf16 | 8 | 7 |
Here machine epsilon is the gap between 1 and the next larger number of the format. The number of exponent bits sets the range; the number of fraction bits sets the precision. fp16 also has subnormal numbers down to . bf16 ("brain float") has the range of fp32 but only 8 significant bits; fp16 has more precision but a small range.
Move . The top chart is the gap between neighboring numbers near for bf16, fp16 and fp32: a staircase that doubles at every power of 2. bf16's gap is 8 times fp16's at every where both are normal. Try 1e−8: fp16 underflows to 0, bf16 does not. Try 70000: fp16 overflows.
Write ("unit in the last place") for the gap between the format's numbers near .
Result 5 (when an update is lost). Let a format have fraction bits, and let be a number of the format with . The spacing of the numbers there is . With round-to-nearest, is rounded back to for every positive update .
Why. The next number of the format above is . A value between them that is closer to , that is, less than half a spacing above , is rounded to .
For and , the update is lost when : when the ratio is above 2,048 in fp16 and above 256 in bf16. (Just below a power of two the spacing is half as large, so for a negative update from the threshold is half as large too.)
So a weight stored in 16 bits ignores small updates, and many small updates in a row are all lost. Micikevicius et al. (2018) gave the fixes that define mixed-precision training:
- fp32 master weights. Keep an fp32 copy of the weights and of the optimizer states. Apply the update in fp32, then cast to 16 bits for the forward and backward passes. With Adam this costs 16 bytes per parameter: 2 (16-bit weights) + 2 (16-bit gradients) + 4 + 4 + 4 (fp32 master weights, , ).
- fp32 accumulation. Dot products multiply in 16 bits but add the products into an fp32 accumulator (GPU tensor cores do this). Large reductions (normalization statistics, softmax sums) run in fp32. A 16-bit accumulator stops growing once it is large compared with the terms, because each new term is below half its spacing (Result 5 again).
- Loss scaling (fp16 only). Many activation gradients are smaller than fp16's range and become 0. Multiply the loss by a scale before the backward pass. By the chain rule every gradient is multiplied by , so small values move into the range. Then unscale (, in fp32) before clipping and the update, so no hyperparameter changes. A fixed must keep .
Dynamic loss scaling (torch.amp.GradScaler): start at . If any gradient is inf or NaN,
skip the optimizer step and halve . After 2,000 steps in a row without overflow, double .
bf16 usually needs no loss scaling. It has the 8 exponent bits of fp32, so gradients practically never underflow. Its precision is lower than fp16's, so fp32 master weights and fp32 accumulation matter even more.
Lost updates. A weight gets the same update for 1,000 steps. The aqua line is an fp32 master weight; the 16-bit weight is magenta when every update is lost and blue otherwise. Try u = 0.6 ulp: each update is rounded up to a full ulp and the 16-bit weight moves too fast. Then switch to loss scaling: the orange histogram shows gradient sizes; move until no gradient becomes 0 or inf (magenta), and switch to bf16 to see why it needs no scaling.
Why does bf16 training usually work without loss scaling, while fp16 training needs it?
Loss scaling fixes underflow: fp16's smallest positive number is about , so many gradients become 0. bf16 has the exponent range of fp32. Its weakness is the opposite one: only 7 fraction bits (, eight times fp16's ), so fp32 master weights and fp32 accumulation are still needed.
Answers to section 1. (1) It stays at . Above 1 the fp16 spacing is , and is less than half of it, so every addition rounds back to 1.0. An fp32 master weight reaches . (2) , so try . Not from step 0: ramp up from 0.05 to 1.6 over the first few epochs (gradual warmup), because the assumption of Result 4 fails early in training. (3) No. Result 3: the cross-entropy is unchanged by . Only grows by 50; the z-loss sees it.
4. Worked example #
(a) Clipping two tensors. The gradients are for and for , and .
- Global norm: .
- Factor: .
- Clipped gradients: and . The new global norm is .
Per-tensor clipping would give : and : . That is a different direction, with total norm .
(b) Warmup + cosine with nanoGPT's settings. , , , , warmup .
- At (warmup): .
- At : and , so .
(c) Large batch. Base: batch 128, . Target: batch 4,096.
- , so .
- ImageNet has 1,281,167 training images, so one epoch is steps, and 5 epochs of warmup are about 1,564 steps.
- The learning rate grows by per step.
(d) A lost update. fp16, , .
- , so ; fp16 has . The threshold is .
- (equivalently, ), so rounds to 1.0 at every step.
- The fp32 master weight reaches ; cast to fp16 it is 1.2998.
(e) Loss scaling. fp16, .
- A gradient of is below half of fp16's smallest subnormal (), so it becomes 0.
- Scaled, it is , a normal fp16 number. Unscaled in fp32 it is : a relative error of .
- A gradient of 1.5 becomes : inf. The scaler skips this step and sets .
(f) Logit shift and z-loss. Logits , label 0.
- , and .
- Loss: .
- Shift by : gives the same and the same loss, but .
- z-loss (): before the shift and after it. Only the z-loss sees the shift.
5. In code #
PyTorch's own functions check the numbers of the worked example:
import torch
import torch.nn.functional as F
torch.manual_seed(0)
# (a) global-norm clipping over two tensors
w, b = torch.nn.Parameter(torch.zeros(2)), torch.nn.Parameter(torch.zeros(1))
w.grad, b.grad = torch.tensor([2.0, -4.0]), torch.tensor([4.0])
total = torch.nn.utils.clip_grad_norm_([w, b], max_norm=1.5)
print(total.item(), [round(v, 4) for v in w.grad.tolist() + b.grad.tolist()])
# 6.0 [0.5, -1.0, 1.0]
# the three formats: machine epsilon, largest value, smallest normal value
for dt in (torch.float16, torch.bfloat16, torch.float32):
f = torch.finfo(dt)
print(dt, f"{f.eps:.3g} {f.max:.3g} {f.smallest_normal:.3g}")
# torch.float16 0.000977 6.55e+04 6.1e-05
# torch.bfloat16 0.00781 3.39e+38 1.18e-38
# torch.float32 1.19e-07 3.4e+38 1.18e-38
# (d) lost updates: fp16 weight against an fp32 master weight
w16, w32 = torch.tensor(1.0, dtype=torch.float16), torch.tensor(1.0)
u16 = torch.tensor(3e-4, dtype=torch.float16)
for _ in range(1000):
w16, w32 = w16 + u16, w32 + 3e-4
print(w16.item(), round(w32.item(), 4))
# 1.0 1.3
# (e) underflow, loss scaling and overflow in fp16
S = 2.0**16
print(torch.tensor(2e-8, dtype=torch.float16).item(),
f"{torch.tensor(2e-8 * S, dtype=torch.float16).float().item() / S:.4e}",
torch.tensor(1.5 * S, dtype=torch.float16).item())
# 0.0 1.9994e-08 inf
# GradScaler: an inf gradient skips the step and halves the scale
model = torch.nn.Linear(2, 1)
opt = torch.optim.SGD(model.parameters(), lr=0.1)
scaler = torch.amp.GradScaler("cpu")
before = model.weight.detach().clone()
scaler.scale(model(torch.ones(1, 2)).sum()).backward()
model.weight.grad[0, 0] = float("inf") # pretend the backward pass overflowed
scaler.step(opt)
scaler.update()
print(scaler.get_scale(), torch.equal(before, model.weight.detach()))
# 32768.0 True
# (f) shift invariance of cross-entropy, and the z-loss
s = torch.tensor([[10.0, 9.0, 8.0]], requires_grad=True)
y = torch.tensor([0])
ce = F.cross_entropy(s, y)
ce.backward()
log_z = torch.logsumexp(s, dim=-1)
print(round(ce.item(), 4), round(F.cross_entropy(s - 10, y).item(), 4),
round(s.grad.sum().item(), 6), round(log_z.item(), 4), round(1e-4 * log_z.item() ** 2, 4))
# 0.4076 0.4076 0.0 10.4076 0.0108
# attention logits grow with the square of the weight scale; qk-layernorm bounds them
d = 64
x = torch.randn(32, d)
W_q, W_k = torch.randn(d, d) / d**0.5, torch.randn(d, d) / d**0.5
for c in (1.0, 4.0, 16.0):
q, k = x @ (c * W_q), x @ (c * W_k)
raw = (q @ k.T / d**0.5).abs().max().item()
qn, kn = F.layer_norm(q, (d,)), F.layer_norm(k, (d,))
normed = (qn @ kn.T / d**0.5).abs().max().item()
print(c, f"{raw:.1f}", f"{normed:.2f}")
# 1.0 4.2 3.51
# 4.0 67.0 3.51
# 16.0 1072.7 3.51
The largest plain logit grows by a factor of 16 each time grows by 4; the normalized logit stays at 3.51, below the bound . The notebook has these cells and the exercises.
6. Where it shows up #
- LLM pretraining. AdamW with , warmup then cosine or WSD decay, global clipping at 1.0, and bf16 with fp32 master weights is the default recipe (nanoGPT, PaLM, LLaMA). The z-loss is used in PaLM; qk-layernorm in ViT-22B and many later models; soft-capping in Gemma 2.
- Data-parallel training on many GPUs. The global batch is (batch per GPU) × (number of GPUs) ×
(gradient-accumulation steps). When you add GPUs, the linear rule (SGD) or the square-root rule
(Adam) gives the start of the learning-rate search. With sharded training (FSDP), the gradient
norm for clipping must be reduced over all shards; FSDP has its own
clip_grad_norm_for this. - Mixture-of-experts models add one more softmax, the router, with its own z-loss (lesson 15).
- ML for science. Weather and Earth-system foundation models are large transformers or graph networks trained in bf16. Physical fields have very different scales (pressure in Pa, humidity near ), so normalize inputs and targets, or small values lose precision. Fine-tuning on long autoregressive rollouts gives large, rare gradients, where clipping and a lower learning rate help.
7. Common mistakes #
- Clipping before unscaling in fp16. The gradients are still multiplied by , so almost every step is clipped by a huge factor (check yourself 2).
- Per-entry or per-tensor clipping when global-norm clipping is meant. It changes the direction.
- Logging the gradient norm after clipping. It is always at most and hides the problem.
- Scaling the learning rate with the batch but not warming up, or keeping the warmup length in steps fixed when the number of steps per epoch changes.
- Summing the gradients over workers without dividing by the global batch. This multiplies the effective learning rate by the number of workers.
- 16-bit weights with no fp32 master copy. Small updates vanish silently, and the loss stalls instead of diverging.
- No loss scaling with fp16. Small gradients are lost (bf16 does not need it).
8. Check yourself #
- The global gradient norm is 3 and
max_normis 6. What doesclip_grad_norm_do? What would value clipping at 1 do to the gradient ?Answer
Nothing: the factor is clamped to 1. Value clipping gives , which points in a different direction. - Why must you call
scaler.unscale_(optimizer)before clipping in fp16 training?Answer
The gradients are still multiplied by (for example ). The clip would compare with , clip almost every step by a factor of about , and shrink the steps by orders of magnitude. - An fp16 weight equals 64. Which positive updates survive the addition?
Answer
In the spacing is . Only updates larger than half the spacing, , change the stored value. An update of exactly is a tie: round-to-nearest-even keeps 64 (its last significand bit is 0), so it is lost too. - is near 0, and then all output logits drift up by 50. What happens to the cross-entropy,
to the z-loss and to the precision of the logits in bf16?
Answer
The cross-entropy does not change (Result 3). grows by 50, so the z-loss grows to about , and its gradient pushes the logits back down. In bf16, numbers in have spacing , so the logit differences, which decide , lose precision. - Why does Goyal et al.'s gradual warmup work better than a constant low learning rate for the first
5 epochs?
Answer
With the constant warmup, the learning rate jumps from to after 5 epochs, and the training error spikes. At batch 8k the top-1 validation error is 25.88% with constant warmup against 23.74% with gradual warmup. A gradual ramp never makes a sudden jump. - An fp16 run shows the loss scale halving again and again, down to values below 1, and then NaN
losses. Is this underflow? What do you do?
Answer
No. The scale halves only on overflow, so some gradients are inf at every step: the values are truly huge, which is a divergence. OPT-175B saw the same. Restart from a checkpoint where the scale was healthy, lower the learning rate (and possibly the clipping threshold), and look for logit growth. bf16 removes the overflow, not the divergence.
When is gradient descent on stable, and what plays the role of in a network?
When . In a network, is the sharpness: the largest eigenvalue of the Hessian, which changes during training.
Why does a learning-rate warmup help? Give one reason.
The sharpness is often largest at the start, so the peak learning rate would have there. (Also: Adam's second moment is noisy in the first steps; large batches need it.)
Write global-norm gradient clipping with threshold .
, with the norm of all gradients together. It keeps the direction; the new norm is .
Why does qk-layernorm bound the attention logits, and by how much?
After LayerNorm (no gain), and , so by Cauchy–Schwarz .
What is the z-loss, and why is it needed?
An extra term (PaLM: ). Cross-entropy is flat along , so only the z-loss keeps near 0.
Why does a lower in Adam prevent some loss spikes?
then forgets old gradients faster. A parameter that was quiet for a long time has a tiny , so a sudden large gradient gives a huge ; with the memory is only about 20 steps.
State the linear scaling rule and its condition.
Batch → learning rate (SGD). It assumes the gradient changes little over small steps, which fails early in training, so use a gradual warmup.
A weight is stored in fp16. Which updates are lost?
Those with : half the spacing near 1.
Why does mixed-precision training keep fp32 master weights?
Updates much smaller than the weight (ratio above 2,048 in fp16, 256 in bf16) are rounded away in 16 bits. The fp32 copy keeps them.
What does dynamic loss scaling do, and why does bf16 not need it?
Multiply the loss by so small fp16 gradients do not underflow; on inf, skip the step and halve ; after 2,000 good steps, double . bf16 has fp32's exponent range, so gradients do not underflow.
9. Interview angle #
- "Your loss spiked. What do you do?" — Read the logs of section 3.3 first. Restart from an earlier checkpoint; skip the batches around the spike or lower the learning rate; add qk-layernorm, a z-loss or a lower if a signal points to them; change one thing at a time.
- "Write gradient clipping." — One global norm over all tensors; factor ; keeps the direction; with fp16, unscale first.
- "Batch size ×8. What changes?" — Learning rate ×8 for SGD (about × for Adam) with a gradual warmup; loss normalized by the global batch; returns diminish beyond the critical batch size.
- "fp16 or bf16? Why master weights?" — bf16 has fp32's range, so no loss scaling, but fewer significant bits; master weights keep small updates (ratio above 2,048 in fp16, 256 in bf16) from being rounded away.
10. Practice #
The notebook for this lesson runs in Colab. It has five exercises, each with a check cell and a folded solution:
- Write
warmup_cosine(nanoGPT's convention) andwsd, and check them against the values of the worked example. - Write global-norm clipping over a list of tensors and check it against
clip_grad_norm_. - Write
half_ulp(w, p)and test it against real fp16 and bf16 additions. - Write a dynamic loss scaler and check it step by step against
torch.amp.GradScaler. - Compute attention logits with and without qk-layernorm and check the quadratic growth and the bound .
11. Watch and read #
- Wortsman et al., Small-scale proxies for large-scale Transformer training instabilities (2023): attention-logit growth and qk-layernorm, output-logit divergence and the z-loss, warmup and the AdamW .
- Goyal et al., Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour (2017): the linear scaling rule, gradual warmup and the pitfalls of distributed SGD.
- Micikevicius et al., Mixed Precision Training (2018): fp32 master weights, loss scaling and fp32 accumulation.
- Chowdhery et al., PaLM: Scaling Language Modeling with Pathways (2022), section 5: the z-loss, the schedule and how loss spikes were handled.
- Wortsman, Dettmers et al., Stable and low-precision training for large-scale vision-language models (2023): loss spikes from an out-of-date second moment.
- Kalra and Barkeshli, Why Warmup the Learning Rate? Underlying Mechanisms and Improvements (2024); Cohen et al., Gradient Descent on Neural Networks Typically Occurs at the Edge of Stability (2021).
- Hägele et al., Scaling Laws and Compute-Optimal Training Beyond Fixed Training Durations (2024): warmup–stable–decay schedules.
- McCandlish et al., An Empirical Model of Large-Batch Training (2018): the gradient noise scale and the critical batch size.
- Karpathy, nanoGPT
train.py:get_lr(warmup + cosine) and the training loop (scaler.unscale_, thenclip_grad_norm_, thenscaler.step).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.