Why does half-precision training multiply the loss by a large scale factor before backpropagation?
answer
- The backward pass is linear in the loss
- One constant lifts the whole distribution
- Divide it back out before stepping
- Clipping thresholds care about the order
- Halve on overflow, double when calm
basics
~20 sHalf-precision gradients can be too small to represent and collapse to zero. Multiplying the loss by a large constant scales every gradient up by that factor through the chain rule; the scale is divided back out before the optimizer step.
solid answer
~50 sThe narrow 16-bit format has a limited exponent range, and a large share of a real network's gradients sit below its smallest representable magnitude, so they become exactly zero and those parameters get no signal. Loss scaling fixes it with one multiplication: scale the loss by a constant `S` before the backward pass, and by the chain rule every gradient in the graph comes out multiplied by `S`, lifting the small ones into representable territory. Before the optimizer touches anything, the gradients are divided by `S` again, so the update is mathematically unchanged -- gradient clipping and weight decay must also happen *after* the unscale, or their thresholds are wrong by a factor of `S`. Picking `S` is the real work: too small and gradients still underflow, too large and the big gradients overflow to infinity. A dynamic scaler picks it automatically by halving on overflow and doubling after a run of clean steps.
code
python · 22 linesimport math
FP16_MAX = 65504.0 # largest representable magnitude
FP16_MIN = 6e-8 # below this, values collapse to zero
def narrow(x):
if abs(x) > FP16_MAX:
return math.inf if x > 0 else -math.inf
return 0.0 if abs(x) < FP16_MIN else x
w, lr, scale, clean = 1.0, 0.1, 1024.0, 0
for g in [3e-8, 3e-8, 80.0, 4e-8]: # true 32-bit gradients
g16 = narrow(g * scale) # backward runs on the scaled loss
if math.isinf(g16):
scale, clean = scale / 2, 0 # overflow: skip the step, back off
print("skip scale=%d w=%.9f" % (scale, w))
continue
w -= lr * (g16 / scale) # unscale, then step the 32-bit master
clean += 1
if clean == 2:
scale, clean = scale * 2, 0 # a run of clean steps: try higher
print("step scale=%d w=%.9f" % (scale, w))go deeper
Recall that the loss is multiplied by a constant before the backward pass so small gradients do not become zero, and that the same constant is divided back out before the weights are updated.
Explain why one multiplication at the loss scales every gradient -- the backward pass is linear -- and where the unscale must sit relative to clipping and the optimizer step.
Show judgment on choosing the scale: what a fixed value costs when the gradient distribution shifts mid-run, and how to read a dynamic scaler's skip rate as a diagnostic rather than noise.
Own the determinism tradeoff. Skipped steps make applied-update counts numerics-dependent, so decide deliberately whether reproducibility or robustness matters more for the runs your team has to defend.
## The problem A 16-bit floating-point format with a narrow exponent field can only represent magnitudes down to a certain point; below it, values become zero. Gradients are not comfortably above that point. In a deep network, gradient magnitudes span many orders of magnitude across layers and parameters, and a substantial fraction of them — often the majority by count, concentrated in the early layers and in small-magnitude weights — fall below the threshold. In 32-bit they are tiny but real. In the narrow 16-bit format they are **exactly zero**, and the affected parameters receive no update at all. The failure is quiet. The run does not crash, the loss still goes down (the large gradients still work), and the model just ends up worse than the same recipe in full precision, for no visible reason. This is why loss scaling exists. ## The trick Backpropagation is linear in the loss. If you compute gradients of `S * L` instead of `L`, the chain rule multiplies every partial derivative in the entire graph by the same constant `S`: ``` dL/dw -> S * dL/dw for every parameter w ``` So one multiplication at the very top of the backward pass shifts the whole gradient distribution up by `S`, moving the underflowing tail into representable range. Nothing about the model, the loss, or the geometry changes — only which numbers survive the format. Then, before the optimizer does anything, each gradient is divided by `S`. The update that lands is the same one you would have computed in full precision, minus the gradients that were still too small even after scaling. ## The ordering rule that gets missed **Unscale before anything that looks at gradient magnitude.** Gradient clipping compares a norm against a threshold; if the gradients are still scaled by 1024, the norm is 1024 times too big and the clip fires on every step. Weight decay applied inside the update, adaptive optimizers' second-moment statistics, and any logging of gradient norms are all in the same boat. The canonical order is: backward on the scaled loss, check for overflow, unscale, clip, step. Getting this order wrong produces a run that trains but behaves as if the clipping threshold were absurdly tight, which is a genuinely confusing bug. ## Choosing the scale: static A **static** scale is a single constant chosen once — 1024 is a common pick — often by running a few hundred steps, histogramming the gradient magnitudes, and choosing a power of two that lifts the bulk of the distribution above the underflow threshold with headroom before the maximum. Powers of two are used because multiplying and dividing by them is exact, adding no rounding error of its own. Static scaling is free at runtime and has no surprises. Its weakness is that it is calibrated to the gradient distribution *at the moment you measured it*. Gradient magnitudes shift over a run — typically shrinking as the model converges, but also spiking at a learning-rate warmup boundary, a schedule change, or a new data stage. A scale that was correct at step 500 can be too small at step 50,000 (underflow returns silently) or too large after a spike (overflow, and now the run produces infinities). Both failures are invisible unless you are watching for them. ## Choosing the scale: dynamic A **dynamic** scaler adapts. The loop is: 1. Backward pass on the scaled loss. 2. Inspect the gradients for any infinity or not-a-number. 3. If found: **skip the weight update entirely** and halve the scale. 4. If clean: unscale, clip, step, and count the clean step. After some number of consecutive clean steps (a couple of thousand is typical), double the scale. This walks the scale up until it overflows, backs off, and repeats — keeping it just below the ceiling, which is exactly where you want it, because that is where the underflow tail is smallest. **Skipped steps are the designed cost, not a bug.** On a fresh run the scaler starts far too high on purpose, so the first steps overflow and are discarded while it calibrates down; a few percent of early steps being skipped is normal and the rate drops to near zero once it settles. A structure-prediction model trained this way might discard around 3% of its early steps and almost none later. What *is* a signal is a persistently high skip rate, or a scale that keeps ratcheting downward: that says the gradients themselves are misbehaving, and the scaler is reporting a training problem rather than causing one. ## Static versus dynamic in practice Dynamic is the default because it is robust to distribution shift and needs no calibration run. Static is defensible when you need every step to be bit-reproducible and identically shaped — skipped steps make step counts depend on numerics — or when the overflow check's cost matters, or when you already know the distribution because you have run this exact recipe before. The honest comparison is: static trades robustness for determinism and zero overhead; dynamic trades a few discarded steps and a per-step check for never having to think about the constant again. ## Interview framing The answer has three parts: *why* (underflow to zero, silently), *how* (linearity of the backward pass, one constant, unscale before the step), and *which constant* (static calibrated once versus dynamic adapting by halving and doubling, with skipped steps as the expected cost). A candidate who says "it stops gradients from vanishing" without distinguishing format underflow from the vanishing-gradient problem of deep architectures has not understood the question — these are different phenomena that happen to share a word.
- Where exactly in the step must the gradients be unscaled, and what breaks if you get it wrong?Immediately after the backward pass and the overflow check, before gradient clipping, weight decay, the optimizer's moment updates, and any gradient-norm logging. If you clip while the gradients are still scaled by 1024, the measured norm is 1024 times the real one and the clip fires on every single step, silently capping the effective learning rate. The run trains, badly, and the cause is nearly invisible.
- A run skips about 3% of its early steps under a dynamic scaler. Is something wrong?No -- that is the calibration cost. The scaler deliberately starts high, overflows, halves, and repeats until it settles just under the ceiling, and the discarded steps are the price of finding that point. The rate should fall to near zero once it stabilizes. What should worry you is a skip rate that stays high, or a scale that keeps ratcheting down over the run: that means the gradients themselves are growing, and the scaler is reporting a training problem, not creating one.
- When would you still choose a fixed scale over a dynamic one?When step-for-step determinism matters, since skipped steps make the number of applied updates depend on numerics and break exact reproducibility across runs. Also when you have already characterized this exact recipe's gradient distribution and know it is stable, or when the per-step overflow inspection is a measurable cost. The tradeoff is that a fixed scale is calibrated to one moment and goes silently wrong when the distribution shifts later in the run.
- Is loss-scaling underflow the same thing as the vanishing-gradient problem?No, and conflating them is a common error. Vanishing gradients are a property of the model: repeated multiplication by small factors through depth or time shrinks the signal, and it happens in full precision too. Underflow is a property of the number format: a perfectly healthy gradient falls below the smallest magnitude the format can hold and becomes zero. Loss scaling fixes only the second; it does nothing for the first.
It is like turning up a microphone's gain so the quiet passages clear the noise floor, then turning the recording back down by the same amount -- provided nothing loud clips on the way in.
saying these in an interview costs you the question
- Says scaling changes the effective learning rate
- Unscales after clipping instead of before
- Confuses format underflow with vanishing gradients
- Treats skipped steps as a bug to eliminate
- Thinks a bigger scale is always safer