Type at least 2 letters. Enter opens the first result, Esc closes.

    Deep Learning · Lesson 12 · Attention and transformers

    Training dynamics: warmup, clipping, loss spikes, large batches, mixed precision

    Advanced 80 min 5 interactive figures 3 videos Colab notebook

    Before this lesson: lesson 02 (gradient descent on λ2w2\frac{\lambda}{2}w^2 and the limit 2/λ2/\lambda), 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, β2=0.95\beta_2 = 0.95 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 L(w)=λ2w2L(w) = \tfrac{\lambda}{2}w^2 with curvature λ>0\lambda > 0 and learning rate η\eta (lesson 02). One step is w←w−ηλw=(1−ηλ) ww \leftarrow w - \eta\lambda w = (1 - \eta\lambda)\,w. It shrinks ∣w∣|w| when ∣1−ηλ∣<1|1 - \eta\lambda| < 1, that is, when ηλ<2\eta\lambda < 2. Above this limit every step makes ∣w∣|w| larger and the loss explodes.

    In a network, the role of λ\lambda 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 ηλ<2\eta\lambda < 2 at step 10,000 can have ηλ>2\eta\lambda > 2 at step 10.

    Interactive figure: loading…

    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: ηtλt\eta_t\lambda_t starts far above the dashed edge 2 and ∣wt∣|w_t| 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 η\eta 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 ηλ\eta\lambda at high early curvature warmup 3.1
    one huge gradient ∥g∥\lVert g\rVert global-norm clipping 3.2
    attention saturates attention logits, slowly qk-layernorm, soft-capping 3.3
    output logits drift log⁡Z\log Z of the output softmax z-loss, weight decay 3.3
    Adam's step explodes g/vg/\sqrt{v} after a quiet period lower β2\beta_2 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
    Quick check

    The sharpness of a network is about 40 in the first steps and about 4 later. You use plain gradient descent with η=0.1\eta = 0.1 from step 0. What happens?

    3. Definitions and main results #

    3.1 Learning-rate warmup and decay #

    Let tt be the step, WW the warmup length, TT the total number of steps and ηmax⁡\eta_{\max} the peak learning rate. A linear warmup is

    η(t)=ηmax⁡ tW(t<W).\eta(t) = \eta_{\max}\,\frac{t}{W} \qquad (t < W).

    (nanoGPT uses (t+1)/(W+1)(t + 1)/(W + 1), so the first step does not have η=0\eta = 0.) After the warmup comes a decay to a small ηmin⁡\eta_{\min}:

    • cosine decay: η(t)=ηmin⁡+12(ηmax⁡−ηmin⁡)(1+cos⁡(πr))\eta(t) = \eta_{\min} + \tfrac12(\eta_{\max} - \eta_{\min})\big(1 + \cos(\pi r)\big) with r=t−WT−Wr = \frac{t - W}{T - W};
    • linear decay: η(t)=ηmax⁡−(ηmax⁡−ηmin⁡) r\eta(t) = \eta_{\max} - (\eta_{\max} - \eta_{\min})\, r;
    • warmup–stable–decay (WSD): η=ηmax⁡\eta = \eta_{\max} until step T−DT - D, then a short decay (linear, for example) over the last DD 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 TT before the run starts (Hägele et al. 2024). nanoGPT's defaults are a typical cosine recipe: ηmax⁡=6⋅10−4\eta_{\max} = 6 \cdot 10^{-4}, W=2,000W = 2{,}000, cosine decay to ηmin⁡=6⋅10−5\eta_{\min} = 6 \cdot 10^{-5} at step 600,000, AdamW with β2=0.95\beta_2 = 0.95, weight decay 0.1 and clipping at 1.0.

    Interactive figure: loading…

    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 tt. 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:

    1. 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 2/λ2/\lambda (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.
    2. Adam's statistics are noisy at the start. In the first step Adam's bias-corrected moments are m^1=g1\hat m_1 = g_1 and v^1=g12\hat v_1 = g_1^2, so the update η m^1/v^1=η g1/∣g1∣\eta\,\hat m_1/\sqrt{\hat v_1} = \eta\,g_1/|g_1| has size η\eta in every coordinate, whatever the gradient. In the next few steps v^\hat v still averages very few squared gradients, so the per-coordinate step size η/v^\eta/\sqrt{\hat v} is noisy and can be very large (Liu et al. 2020, RAdam).
    3. 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 ηmax⁡\eta_{\max}, 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 2/η2/\eta by itself. Cohen et al. (2021) observed that it rises during training ("progressive sharpening") until it reaches about 2/η2/\eta, 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 g=(g(1),…,g(m))g = (g^{(1)}, \dots, g^{(m)}) be the gradients of all mm parameter tensors, seen as one long vector, with the global norm ∥g∥=(∑i∥g(i)∥22)1/2\|g\| = \big(\sum_i \|g^{(i)}\|_2^2\big)^{1/2}. For a threshold τ>0\tau > 0, global-norm clipping is

    clip⁡τ(g)=min⁡(1,τ∥g∥) g.\operatorname{clip}_\tau(g) = \min\Big(1, \frac{\tau}{\|g\|}\Big)\, g .

    Result 1. clip⁡τ(g)\operatorname{clip}_\tau(g) has the same direction as gg, and ∥clip⁡τ(g)∥=min⁡(∥g∥,τ)\|\operatorname{clip}_\tau(g)\| = \min(\|g\|, \tau).

    Proof.

    1. clip⁡τ(g)=αg\operatorname{clip}_\tau(g) = \alpha g with α=min⁡(1,τ/∥g∥)>0\alpha = \min(1, \tau/\|g\|) > 0. A positive multiple keeps the direction.
    2. If ∥g∥≤τ\|g\| \le \tau, then α=1\alpha = 1 and the norm is ∥g∥\|g\|.
    3. If ∥g∥>τ\|g\| > \tau, then α=τ/∥g∥\alpha = \tau/\|g\| and the norm is α∥g∥=τ\alpha\|g\| = \tau. ■\blacksquare

    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 τ/(∥g∥+10−6)\tau/(\|g\| + 10^{-6}), 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 ητ\eta\tau. With Adam clipping does not cap the step, because Adam divides by v^\sqrt{\hat v}. It limits how much one bad gradient enters mm and vv.
    • With fp16 loss scaling (section 3.5) the gradients are multiplied by the scale SS. Unscale first (scaler.unscale_(optimizer)), then clip.
    Quick check

    The gradients of two tensors are (3,4)(3, 4) and (12)(12), and the clipping threshold is τ=6.5\tau = 6.5. What does global-norm clipping return?

    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 log⁡Z\log Z 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 ii and key jj is xi⊤WQWK⊤xj/dkx_i^\top W_Q W_K^\top x_j/\sqrt{d_k} (lesson 09). It is a product of two weight matrices, so it grows with the square of the weight scale: if WQW_Q and WKW_K both grow by a factor cc, every logit grows by c2c^2. 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: s←κtanh⁡(s/κ)s \leftarrow \kappa \tanh(s/\kappa), so ∣s∣<κ|s| < \kappa (Gemma 2 uses κ=50\kappa = 50 for the attention logits and κ=30\kappa = 30 for the final logits).

    Result 2 (qk-layernorm bounds the logits). Let q^=LN⁡(q)\hat q = \operatorname{LN}(q) and k^=LN⁡(k)\hat k = \operatorname{LN}(k) in Rdk\mathbb{R}^{d_k}, without gain and bias. Then ∣q^⊤k^∣/dk≤dk|\hat q^\top \hat k|/\sqrt{d_k} \le \sqrt{d_k}, whatever the size of WQW_Q and WKW_K.

    Proof.

    1. q^i=(qi−μ)/σ2+ε\hat q_i = (q_i - \mu)/\sqrt{\sigma^2 + \varepsilon}, where μ\mu and σ2\sigma^2 are the mean and the variance of the entries of qq. So 1dk∑iq^i2=σ2/(σ2+ε)≤1\frac{1}{d_k}\sum_i \hat q_i^2 = \sigma^2/(\sigma^2 + \varepsilon) \le 1, that is, ∥q^∥2≤dk\|\hat q\|^2 \le d_k. The same holds for k^\hat k.
    2. Cauchy–Schwarz: ∣q^⊤k^∣≤∥q^∥ ∥k^∥≤dkdk=dk|\hat q^\top \hat k| \le \|\hat q\|\,\|\hat k\| \le \sqrt{d_k}\sqrt{d_k} = d_k.
    3. Divide by dk\sqrt{d_k}. ■\blacksquare

    With learned gains γq,γk\gamma_q, \gamma_k (no biases), the bound is multiplied by max⁡i∣γq,i∣⋅max⁡i∣γk,i∣\max_i|\gamma_{q,i}| \cdot \max_i|\gamma_{k,i}|. For dk=128d_k = 128 the bound is 128≈11.3\sqrt{128} \approx 11.3. The bound is reached only when q^\hat q and k^\hat k are parallel; typical logits are much smaller.

    Interactive figure: loading…

    Move the weight scale cc. Every plain logit grows like c2c^2: 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 dk\sqrt{d_k}, whatever cc 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 s∈RVs \in \mathbb{R}^V, a label yy, Z=∑jesjZ = \sum_j e^{s_j} and p=softmax⁡(s)p = \operatorname{softmax}(s), the loss is ℓ(s)=log⁡Z−sy\ell(s) = \log Z - s_y. Then (i) ℓ(s+a1)=ℓ(s)\ell(s + a\mathbf{1}) = \ell(s) for every a∈Ra \in \mathbb{R}, and (ii) ∇sℓ=p−ey\nabla_s \ell = p - e_y, whose entries sum to 0.

    Proof.

    1. log⁡∑jesj+a=a+log⁡Z\log \sum_j e^{s_j + a} = a + \log Z, and sy+as_y + a also grows by aa. The difference is unchanged.
    2. ∂log⁡Z/∂sj=esj/Z=pj\partial \log Z/\partial s_j = e^{s_j}/Z = p_j and ∂sy/∂sj=δjy\partial s_y/\partial s_j = \delta_{jy}, so ∇sℓ=p−ey\nabla_s \ell = p - e_y. Its entries sum to ∑jpj−1=0\sum_j p_j - 1 = 0. ■\blacksquare

    So nothing in the loss pulls log⁡Z\log Z back when all logits drift together: the gradient has no component along 1\mathbf{1}. Fix: the z-loss (PaLM, Chowdhery et al. 2022), an extra term cz(log⁡Z)2c_z(\log Z)^2 with cz=10−4c_z = 10^{-4}, which keeps log⁡Z\log Z near 0. Its gradient is 2czlog⁡Z⋅p2c_z \log Z \cdot p, because ∂log⁡Z/∂sj=pj\partial \log Z/\partial s_j = p_j. The entries of this gradient sum to 2czlog⁡Z2c_z\log Z: it pushes all logits down when log⁡Z>0\log Z > 0 and up when log⁡Z<0\log Z < 0. Weight decay also prevents this drift in Wortsman et al.'s experiments.

    Quick check

    The z-loss term is cz(log⁡Z)2c_z(\log Z)^2 with cz=10−4c_z = 10^{-4}. The logits are s=(10,9,8)s = (10, 9, 8), so log⁡Z≈10.41\log Z \approx 10.41 and p≈(0.665,0.245,0.090)p \approx (0.665, 0.245, 0.090). What is the sum of the entries of the z-loss gradient with respect to ss?

    (c) Adam's second moment is out of date

    Adam divides by v\sqrt{v}, an exponential average of g2g^2 with a memory of about 1/(1−β2)1/(1 - \beta_2) steps: 1,000 steps for β2=0.999\beta_2 = 0.999. Suppose some parameters had tiny gradients for a long time, so vv is tiny. If they then receive a large gradient, g/vg/\sqrt{v} 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 g/vg/\sqrt{v} in the patch-embedding layer jumped far above 1 (Wortsman, Dettmers et al. 2023, "stuck in the past").

    Fix: a lower β2\beta_2. With β2=0.95\beta_2 = 0.95 (common for LLMs: GPT-3, nanoGPT), vv follows the gradients faster. A β2\beta_2 that is too low makes the steps noisy and slows training. PaLM uses β2=1−k−0.8\beta_2 = 1 - k^{-0.8} at step kk. Update clipping, as in AdaFactor, is another fix.

    (d) Adam's ε\varepsilon is too large. When the gradient RMS of a layer falls to the order of ε\varepsilon, the update m/(v+ε)m/(\sqrt v + \varepsilon) 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 ε\varepsilon (for example 10−1510^{-15} instead of 10−810^{-8}) 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
    log⁡Z\log Z drifts far from 0, logits very negative output-logit drift z-loss, weight decay
    g/vg/\sqrt v jumps in one layer, spike a few steps later out-of-date second moment lower β2\beta_2, update clipping
    gradient RMS of a layer near ε\varepsilon, updates vanish ε\varepsilon too large smaller ε\varepsilon
    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 kk, multiply the learning rate by kk and keep the other hyperparameters.

    Result 4 (why the rule works). Let l(x,w)l(x, w) be the loss of example xx. Compare kk SGD steps on mini-batches B0,…,Bk−1B_0, \dots, B_{k-1} of size nn with learning rate η\eta, against one step on their union (size knkn) with learning rate η^\hat\eta.

    1. kk small steps: wt+k=wt−η1n∑j<k∑x∈Bj∇l(x,wt+j)w_{t+k} = w_t - \eta \frac{1}{n}\sum_{j<k}\sum_{x \in B_j} \nabla l(x, w_{t+j}).
    2. One large step: w^t+1=wt−η^1kn∑j<k∑x∈Bj∇l(x,wt)\hat w_{t+1} = w_t - \hat\eta \frac{1}{kn}\sum_{j<k}\sum_{x \in B_j} \nabla l(x, w_t).
    3. If ∇l(x,wt+j)≈∇l(x,wt)\nabla l(x, w_{t+j}) \approx \nabla l(x, w_t) for j<kj < k, the two results are equal exactly when η^/(kn)=η/n\hat\eta/(kn) = \eta/n, that is, η^=kη\hat\eta = k\eta. ■\blacksquare

    The assumption in step 3 fails in two cases. Early in training the network changes fast, so the gradients at wt+jw_{t+j} and wtw_t differ. The fix is gradual warmup: increase the learning rate linearly from η\eta to kηk\eta 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 η^=k η\hat\eta = \sqrt{k}\,\eta is a common start (Malladi et al. 2022 derive it).

    Other pitfalls (Goyal et al.): normalize the loss by the global batch knkn, not by the per-worker batch nn (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.

    Quick check

    SGD with batch 256 and learning rate 0.1 works well. You move to batch 1,024. What does the linear scaling rule suggest?

    3.5 Mixed precision #

    Format Exponent bits Fraction bits pp ϵ=2−p\epsilon = 2^{-p} Largest Smallest normal
    fp32 8 23 1.19⋅10−71.19 \cdot 10^{-7} 3.4⋅10383.4 \cdot 10^{38} 1.18⋅10−381.18 \cdot 10^{-38}
    fp16 5 10 9.77⋅10−49.77 \cdot 10^{-4} 65,504 6.10⋅10−56.10 \cdot 10^{-5}
    bf16 8 7 7.81⋅10−37.81 \cdot 10^{-3} 3.4⋅10383.4 \cdot 10^{38} 1.18⋅10−381.18 \cdot 10^{-38}

    Here machine epsilon ϵ\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 2−24≈5.96⋅10−82^{-24} \approx 5.96 \cdot 10^{-8}. bf16 ("brain float") has the range of fp32 but only 8 significant bits; fp16 has more precision but a small range.

    Interactive figure: loading…

    Move xx. The top chart is the gap between neighboring numbers near xx for bf16, fp16 and fp32: a staircase that doubles at every power of 2. bf16's gap is 8 times fp16's at every xx where both are normal. Try 1e−8: fp16 underflows to 0, bf16 does not. Try 70000: fp16 overflows.

    Write ulp⁡(w)\operatorname{ulp}(w) ("unit in the last place") for the gap between the format's numbers near ww.

    Result 5 (when an update is lost). Let a format have pp fraction bits, and let ww be a number of the format with 2e≤w<2e+12^e \le w < 2^{e+1}. The spacing of the numbers there is ulp⁡(w)=2e−p\operatorname{ulp}(w) = 2^{e-p}. With round-to-nearest, w+uw + u is rounded back to ww for every positive update u<ulp⁡(w)/2=2e−p−1u < \operatorname{ulp}(w)/2 = 2^{e-p-1}.

    Why. The next number of the format above ww is w+ulp⁡(w)w + \operatorname{ulp}(w). A value between them that is closer to ww, that is, less than half a spacing above ww, is rounded to ww. ■\blacksquare

    For w=2ew = 2^e and u>0u > 0, the update is lost when u/w<2−(p+1)u/w < 2^{-(p+1)}: when the ratio w/uw/u 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 w=2ew = 2^e 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:

    1. 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, mm, vv).
    2. 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).
    3. Loss scaling (fp16 only). Many activation gradients are smaller than fp16's range and become 0. Multiply the loss by a scale SS before the backward pass. By the chain rule every gradient is multiplied by SS, so small values move into the range. Then unscale (g←g/Sg \leftarrow g/S, in fp32) before clipping and the update, so no hyperparameter changes. A fixed SS must keep S⋅max⁡∣g∣≤65,504S \cdot \max|g| \le 65{,}504.

    Dynamic loss scaling (torch.amp.GradScaler): start at S=216S = 2^{16}. If any gradient is inf or NaN, skip the optimizer step and halve SS. After 2,000 steps in a row without overflow, double SS.

    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.

    Interactive figure: loading…

    Lost updates. A weight w0w_0 gets the same update uu 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 ss until no gradient becomes 0 or inf (magenta), and switch to bf16 to see why it needs no scaling.

    Quick check

    Why does bf16 training usually work without loss scaling, while fp16 training needs it?

    Answers to section 1. (1) It stays at 1.01.0. Above 1 the fp16 spacing is 2−10≈9.8⋅10−42^{-10} \approx 9.8 \cdot 10^{-4}, and 3⋅10−43 \cdot 10^{-4} is less than half of it, so every addition rounds back to 1.0. An fp32 master weight reaches 1.31.3. (2) k=4096/128=32k = 4096/128 = 32, so try η=0.05⋅32=1.6\eta = 0.05 \cdot 32 = 1.6. 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 s↦s+a1s \mapsto s + a\mathbf{1}. Only log⁡Z\log Z grows by 50; the z-loss sees it.

    4. Worked example #

    (a) Clipping two tensors. The gradients are (2,−4)(2, -4) for WW and (4)(4) for bb, and τ=1.5\tau = 1.5.

    1. Global norm: 22+42+42=36=6>1.5\sqrt{2^2 + 4^2 + 4^2} = \sqrt{36} = 6 > 1.5.
    2. Factor: 1.5/6=0.251.5/6 = 0.25.
    3. Clipped gradients: (0.5,−1)(0.5, -1) and (1)(1). The new global norm is 0.25+1+1=1.5\sqrt{0.25 + 1 + 1} = 1.5.

    Per-tensor clipping would give WW: (2,−4)⋅1.5/20=(0.671,−1.342)(2, -4) \cdot 1.5/\sqrt{20} = (0.671, -1.342) and bb: (1.5)(1.5). That is a different direction, with total norm 1.52+1.52=1.52≈2.12\sqrt{1.5^2 + 1.5^2} = 1.5\sqrt 2 \approx 2.12.

    (b) Warmup + cosine with nanoGPT's settings. ηmax⁡=6⋅10−4\eta_{\max} = 6 \cdot 10^{-4}, ηmin⁡=6⋅10−5\eta_{\min} = 6 \cdot 10^{-5}, W=2,000W = 2{,}000, T=600,000T = 600{,}000, warmup ηmax⁡(t+1)/(W+1)\eta_{\max}(t+1)/(W+1).

    1. At t=999t = 999 (warmup): 6⋅10−4⋅1000/2001=3.0⋅10−46 \cdot 10^{-4} \cdot 1000/2001 = 3.0 \cdot 10^{-4}.
    2. At t=301,000t = 301{,}000: r=(301,000−2,000)/(600,000−2,000)=0.5r = (301{,}000 - 2{,}000)/(600{,}000 - 2{,}000) = 0.5 and cos⁡(π/2)=0\cos(\pi/2) = 0, so η=ηmin⁡+12(ηmax⁡−ηmin⁡)=6⋅10−5+2.7⋅10−4=3.3⋅10−4\eta = \eta_{\min} + \tfrac12(\eta_{\max} - \eta_{\min}) = 6 \cdot 10^{-5} + 2.7 \cdot 10^{-4} = 3.3 \cdot 10^{-4}.

    (c) Large batch. Base: batch 128, η=0.05\eta = 0.05. Target: batch 4,096.

    1. k=4096/128=32k = 4096/128 = 32, so η^=32⋅0.05=1.6\hat\eta = 32 \cdot 0.05 = 1.6.
    2. ImageNet has 1,281,167 training images, so one epoch is 1,281,167/4,096≈3131{,}281{,}167/4{,}096 \approx 313 steps, and 5 epochs of warmup are about 1,564 steps.
    3. The learning rate grows by (1.6−0.05)/1,564≈9.9⋅10−4(1.6 - 0.05)/1{,}564 \approx 9.9 \cdot 10^{-4} per step.

    (d) A lost update. fp16, w=1.0w = 1.0, u=3⋅10−4u = 3 \cdot 10^{-4}.

    1. w=20w = 2^0, so e=0e = 0; fp16 has p=10p = 10. The threshold is 2e−p−1=2−11≈4.88⋅10−42^{e-p-1} = 2^{-11} \approx 4.88 \cdot 10^{-4}.
    2. u=3⋅10−4<4.88⋅10−4u = 3 \cdot 10^{-4} < 4.88 \cdot 10^{-4} (equivalently, w/u≈3,333>2,048w/u \approx 3{,}333 > 2{,}048), so w+uw + u rounds to 1.0 at every step.
    3. The fp32 master weight reaches 1+1000⋅3⋅10−4=1.31 + 1000 \cdot 3 \cdot 10^{-4} = 1.3; cast to fp16 it is 1.2998.

    (e) Loss scaling. fp16, S=216=65,536S = 2^{16} = 65{,}536.

    1. A gradient of 2⋅10−82 \cdot 10^{-8} is below half of fp16's smallest subnormal (2−25≈2.98⋅10−82^{-25} \approx 2.98 \cdot 10^{-8}), so it becomes 0.
    2. Scaled, it is 2⋅10−8⋅65,536=1.31⋅10−32 \cdot 10^{-8} \cdot 65{,}536 = 1.31 \cdot 10^{-3}, a normal fp16 number. Unscaled in fp32 it is 1.9994⋅10−81.9994 \cdot 10^{-8}: a relative error of 3⋅10−43 \cdot 10^{-4}.
    3. A gradient of 1.5 becomes 1.5⋅65,536=98,304>65,5041.5 \cdot 65{,}536 = 98{,}304 > 65{,}504: inf. The scaler skips this step and sets S=32,768S = 32{,}768.

    (f) Logit shift and z-loss. Logits s=(10,9,8)s = (10, 9, 8), label 0.

    1. log⁡Z=10+log⁡(1+e−1+e−2)=10.408\log Z = 10 + \log(1 + e^{-1} + e^{-2}) = 10.408, and p=(0.665,0.245,0.090)p = (0.665, 0.245, 0.090).
    2. Loss: log⁡Z−s0=10.408−10=0.408\log Z - s_0 = 10.408 - 10 = 0.408.
    3. Shift by −10-10: (0,−1,−2)(0, -1, -2) gives the same pp and the same loss, but log⁡Z=0.408\log Z = 0.408.
    4. z-loss (cz=10−4c_z = 10^{-4}): 10−4⋅10.4082=0.010810^{-4} \cdot 10.408^2 = 0.0108 before the shift and 10−4⋅0.4082=1.7⋅10−510^{-4} \cdot 0.408^2 = 1.7 \cdot 10^{-5} 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 cc grows by 4; the normalized logit stays at 3.51, below the bound 64=8\sqrt{64} = 8. The notebook has these cells and the exercises.

    6. Where it shows up #

    • LLM pretraining. AdamW with β2=0.95\beta_2 = 0.95, 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 10−310^{-3}), 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 SS, 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 τ\tau 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 #

    1. The global gradient norm is 3 and max_norm is 6. What does clip_grad_norm_ do? What would value clipping at 1 do to the gradient (1.5,−2,1.5)(1.5, -2, 1.5)?
      AnswerNothing: the factor 6/(3+10−6)6/(3 + 10^{-6}) is clamped to 1. Value clipping gives (1,−1,1)(1, -1, 1), which points in a different direction.
    2. Why must you call scaler.unscale_(optimizer) before clipping in fp16 training?
      AnswerThe gradients are still multiplied by SS (for example 2162^{16}). The clip would compare S∥g∥S\|g\| with τ\tau, clip almost every step by a factor of about τ/(S∥g∥)\tau/(S\|g\|), and shrink the steps by orders of magnitude.
    3. An fp16 weight equals 64. Which positive updates survive the addition?
      AnswerIn [64,128)[64, 128) the spacing is 64⋅2−10=1/1664 \cdot 2^{-10} = 1/16. Only updates larger than half the spacing, 1/32=0.031251/32 = 0.03125, change the stored value. An update of exactly 1/321/32 is a tie: round-to-nearest-even keeps 64 (its last significand bit is 0), so it is lost too.
    4. log⁡Z\log Z 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?
      AnswerThe cross-entropy does not change (Result 3). log⁡Z\log Z grows by 50, so the z-loss grows to about 10−4⋅502=0.2510^{-4} \cdot 50^2 = 0.25, and its gradient pushes the logits back down. In bf16, numbers in [32,64)[32, 64) have spacing 32⋅2−7=0.2532 \cdot 2^{-7} = 0.25, so the logit differences, which decide pp, lose precision.
    5. Why does Goyal et al.'s gradual warmup work better than a constant low learning rate for the first 5 epochs?
      AnswerWith the constant warmup, the learning rate jumps from η\eta to kηk\eta 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.
    6. 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?
      AnswerNo. 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.
    Recall cardsAnswer in your head first, then reveal. Add the cards to your review deck to see them again just before you would forget.
    1. When is gradient descent on λ2w2\frac{\lambda}{2}w^2 stable, and what plays the role of λ\lambda in a network?

    2. Why does a learning-rate warmup help? Give one reason.

    3. Write global-norm gradient clipping with threshold τ\tau.

    4. Why does qk-layernorm bound the attention logits, and by how much?

    5. What is the z-loss, and why is it needed?

    6. Why does a lower β2\beta_2 in Adam prevent some loss spikes?

    7. State the linear scaling rule and its condition.

    8. A weight w=1w = 1 is stored in fp16. Which updates are lost?

    9. Why does mixed-precision training keep fp32 master weights?

    10. What does dynamic loss scaling do, and why does bf16 not need it?

    Open the review →

    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 β2\beta_2 if a signal points to them; change one thing at a time.
    • "Write gradient clipping." — One global norm over all tensors; factor min⁡(1,τ/∥g∥)\min(1, \tau/\|g\|); keeps the direction; with fp16, unscale first.
    • "Batch size ×8. What changes?" — Learning rate ×8 for SGD (about ×8\sqrt 8 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:

    1. Write warmup_cosine (nanoGPT's convention) and wsd, and check them against the values of the worked example.
    2. Write global-norm clipping over a list of tensors and check it against clip_grad_norm_.
    3. Write half_ulp(w, p) and test it against real fp16 and bf16 additions.
    4. Write a dynamic loss scaler and check it step by step against torch.amp.GradScaler.
    5. Compute attention logits with and without qk-layernorm and check the quadratic growth and the bound dk\sqrt{d_k}.

    11. Watch and read #

    From 1:39:38, float16, gradient scalers and bfloat16; from 2:14:55, AdamW, gradient clipping and the warmup + cosine schedule (sections 3.1, 3.2 and 3.5).
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    fp32 master weights, loss scaling and fp32 accumulation, as in section 3.5.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play
    Optimizers, Adam and learning-rate decay: the background for sections 3.1 and 3.3.
    Open on YouTube · the player loads from youtube-nocookie.com when you press play