This page is the "put a number in every box" companion to Mixed precision training . The parent note taught you the three moving parts — FP32 master weights , loss scaling , and FP16 arithmetic . Here we do not add new theory. We take every kind of situation those parts can land in and grind through an actual worked number, so that when you meet one on an exam or in a real training log, you have already seen its twin.
Before any symbol appears, we agree on plain words:
Definition The words we will use
A float is a number a computer stores as ( sign ) × ( mantissa ) × 2 exponent . Picture a ruler: the exponent picks which decade of the ruler you are on (0.001 vs 1000 ), the mantissa picks the tick mark within that decade.
FP32 = 32 bits (1 sign + 8 exponent + 23 mantissa), fine ruler ticks (spacing ≈ 1 0 − 7 of the value), huge span (1 0 − 38 to 1 0 38 ).
FP16 = 16 bits (1 sign + 5 exponent + 10 mantissa), coarse ticks (spacing ≈ 1 0 − 3 of the value), short span. IEEE-754 half precision has two low-end thresholds: the smallest normal positive number is 2 − 14 ≈ 6 × 1 0 − 5 , and below it FP16 keeps going with subnormal (denormal) numbers — coarser, reduced-precision values — down to the smallest subnormal 2 − 24 ≈ 6 × 1 0 − 8 . Anything below that last subnormal rounds to 0 . Largest value ≈ 65504 .
BF16 (Brain Float 16) = 16 bits laid out like FP32 at the top and FP16 at the bottom: 1 sign + 8 exponent (same as FP32) + 7 mantissa bits. Because it copies FP32's 8 exponent bits, it inherits FP32's huge range (normal numbers down to ≈ 1 0 − 38 , and subnormals all the way to the smallest BF16 subnormal 2 − 133 ≈ 1 0 − 40 ), but with only 7 mantissa bits its relative tick is coarse, ϵ B F 16 = 2 − 7 ≈ 1 0 − 2 . Slogan: BF16 buys range by spending precision ; used on TPUs and newer GPUs.
Normal vs subnormal = between 6 × 1 0 − 5 and 65504 FP16 has full 10-bit mantissa precision (normal ); between 6 × 1 0 − 8 and 6 × 1 0 − 5 it still stores numbers but with fewer significant bits (subnormal ) — usable but lossy. (BF16 has its own subnormal tail reaching ≈ 1 0 − 40 , far below FP16's.)
ϵ F P 16 = the machine epsilon of FP16, the relative gap between 1.0 and the next representable value, 2 − 10 ≈ 1 0 − 3 . It measures how coarse the ticks are as a fraction of the value , so near a value v the absolute tick is ≈ v ϵ F P 16 .
Underflow = a number so small it lands below the smallest subnormal tick (≈ 6 × 1 0 − 8 for FP16), so it rounds to 0 .
Overflow = a number so big it lands past the last tick (65504 for FP16), so it becomes inf.
S = the loss scale , one big positive number we multiply the loss by before backprop.
η (eta) = the learning rate , the size of each step (see 3.3.05-Learning-rate-schedules ).
g = a gradient, the slope ∂ L / ∂ w telling us which way to nudge weight w .
The whole game is the picture below — read it now, we will point back to it in the examples . Its horizontal axis is number magnitude on a log scale (each tick a power of ten). The violet band is FP16's full-precision normal zone; the magenta strip to its left is the subnormal zone (numbers survive but lose bits); left of that is pure underflow; the orange strip on the right is overflow. The violet arrow shows loss scaling × S sliding a doomed tiny gradient rightward onto the safe ruler. Everything on this page is one of those two failures (fall off the left = underflow, fall off the right = overflow), or the fix.
Every situation this topic throws at you falls in exactly one of these cells. The last column names the worked example that fills it. (Recall ϵ F P 16 ≈ 1 0 − 3 is the relative tick size defined above; near a value v the absolute tick is ≈ v ϵ F P 16 .)
#
Case class
The tricky quantity
What can go wrong
Example
A
Tiny weight update vs FP16 tick
η g ≪ v ϵ F P 16
update rounds to 0 → training stalls
Ex 1
B
Gradient underflow (small g )
g < 6 × 1 0 − 8
grad → 0, dead neuron
Ex 2
C
Loss-scale overflow (too big S )
S g > 65504
grad → inf, step skipped
Ex 3
D
Dynamic scaler: skip → halve → recover → double
S over time
picking S automatically
Ex 4
E
Degenerate input: exact power of two
w = 0.5 , g = 0
no-op, must stay exact
Ex 5
F
BF16 vs FP16 trade (range vs precision)
same big/small g
range fine, precision coarse
Ex 6
G
Real-world word problem: memory budget
bytes per tensor
does the model fit?
Ex 7
H
Exam twist: gradient accumulation + scaling
scale vs accumulation order
double-count or lose scale
Ex 8
Worked example Ex 1 — Cell A: the update that vanishes
Statement. A weight is w = 0.5 . Learning rate η = 1 0 − 4 , gradient g = 3 × 1 0 − 3 . Does the update survive in FP16? Does it survive with an FP32 master?
Forecast: guess now — does w move, or is it stuck?
Compute the raw update. Δ w = η g = 1 0 − 4 × 3 × 1 0 − 3 = 3 × 1 0 − 7 .
Why this step? The step size is what we are comparing against the ruler's tick — the magenta dot's neighbourhood in the figure.
Find FP16's tick near 0.5 . The absolute tick is ≈ v ϵ F P 16 = 0.5 × 1 0 − 3 = 5 × 1 0 − 4 near 0.5 . Our update 3 × 1 0 − 7 is 1666× smaller than one tick.
Why this step? An update smaller than the local tick cannot change the stored number — it rounds to the same tick. This is the "value sits inside the violet band but the step is finer than the ruling" situation.
FP16-only result. round F P 16 ( 0.5 − 3 × 1 0 − 7 ) = 0.5 . The step is lost .
Why this step? This is exactly the "training stalls" failure — repeat N times, still 0.5 .
FP32 master result. FP32 relative tick is ≈ 1 0 − 7 , so absolute tick near 0.5 is ≈ 5 × 1 0 − 8 , smaller than 3 × 1 0 − 7 , so the update lands : w F P 32 = 0.5 − 3 × 1 0 − 7 = 0.4999997 .
Why this step? The master's fine ruler resolves the tiny step; we cast this back to FP16 next round.
Verify: 0.4999997 = 0.5 , so the master moved. Over 1 0 4 such steps the master drifts by 1 0 4 × 3 × 1 0 − 7 = 3 × 1 0 − 3 — a real, visible change. Sanity: units are all dimensionless weight units. ✓
Worked example Ex 2 — Cell B: gradient underflow, and the scale that saves it
Statement. A backprop gradient is g = 5 × 1 0 − 8 . FP16's smallest subnormal is ≈ 6 × 1 0 − 8 , below which values round to 0 . Loss scale S = 1024 . Show underflow without scaling, and recovery with it.
Forecast: will g survive FP16 on its own? What does × 1024 do to it? On the figure, which coloured zone does g start in?
Without scaling. 5 × 1 0 − 8 < 6 × 1 0 − 8 (the smallest subnormal), so it falls off the left end of the ruler → cast F P 16 ( g ) = 0 .
Why this step? Below even the last subnormal tick ⇒ rounds to zero ⇒ neuron gets zero update ⇒ never learns. This is the magenta dot in the figure, left of the whole ruler.
Scale the loss. New gradient g ′ = S g = 1024 × 5 × 1 0 − 8 = 5.12 × 1 0 − 5 .
Why this step? Multiplying the loss by S multiplies every gradient by S (chain rule), sliding them rightward — this is the violet arrow.
Check it now fits FP16. 5.12 × 1 0 − 5 lands just below the normal threshold 6 × 1 0 − 5 (so it is a high-precision subnormal, comfortably above 6 × 1 0 − 8 ) and far below 65504 ⇒ representable.
Why this step? We only need to survive the backward pass ; that is where FP16 storage happens.
Unscale in FP32 before the step. g true = g ′ / S = 5.12 × 1 0 − 5 /1024 = 5 × 1 0 − 8 .
Why this step? The optimizer runs in FP32, so restoring the true tiny value costs nothing there (see 1.3.07-Numerical-stability ).
Verify: 5.12 × 1 0 − 5 /1024 = 5 × 1 0 − 8 exactly — we got the original back. ✓
Worked example Ex 3 — Cell C: too much scaling causes overflow
Statement. Largest gradient in the batch is g = 0.8 . Someone sets S = 131072 (= 2 17 ). FP16 max is 65504 . What happens?
Forecast: loss scaling saves small gradients — can it hurt big ones? On the figure, which end do they fall off?
Scale the big gradient. S g = 131072 × 0.8 = 104857.6 .
Why this step? Scaling lifts all gradients — big ones can shoot past the top of the ruler into the orange zone.
Compare to FP16 max. 104857.6 > 65504 ⇒ result becomes inf.
Why this step? Past the last tick, FP16 gives up and stores infinity.
inf poisons everything. inf unscaled is still inf/nan; the optimizer step is skipped to avoid corrupting weights.
Why this step? One overflow anywhere aborts the whole step — this is why blind huge S is dangerous.
Find the largest safe S . We need S × 0.8 ≤ 65504 , i.e. S ≤ 81880 . The largest power of two under that is 2 16 = 65536 . Check: 65536 × 0.8 = 52428.8 ≤ 65504 ✓. So S = 65536 is the biggest safe power of two.
Why this step? Dynamic scalers keep S a power of two so scaling/unscaling is exact bit-shifts.
Verify: 131072 × 0.8 = 104857.6 > 65504 (overflow, correct), and 65536 × 0.8 = 52428.8 ≤ 65504 (safe, correct). ✓
Worked example Ex 4 — Cell D: dynamic loss scaling over time
Statement. Dynamic scaler rules: on overflow, halve S and skip the step; after 2000 good steps, double S . Start S = 65536 . Step 5 overflows; then 2000 clean steps. Trace S .
Forecast: guess the value of S after the overflow, and after the recovery streak.
Steps 1–4 clean. S stays 65536 (streak counter = 4 , not yet 2000 ).
Why this step? No overflow, no doubling threshold reached ⇒ nothing changes.
Step 5 overflows. Halve: S = 65536/2 = 32768 . Skip the weight update, reset streak to 0 .
Why this step? Halving pulls all gradients back down the ruler (leftward in the figure); skipping avoids a corrupt step.
Next 2000 steps clean. Streak hits 2000 ⇒ double once: S = 32768 × 2 = 65536 .
Why this step? A long calm streak means we have headroom; push S back up so small gradients stay safe.
Net effect. S went 65536 → 32768 → 65536 — it breathes around the right value.
Why this step? This auto-tuning is why you rarely hand-pick S in practice.
Verify: 65536/2 = 32768 and 32768 × 2 = 65536 . ✓ End value equals start value.
Worked example Ex 5 — Cell E: the degenerate no-op (exact power of two, zero gradient)
Statement. w = 0.5 (exactly 2 − 1 ), gradient g = 0 , any η . Show FP16 and FP32 both leave w bit-for-bit unchanged.
Forecast: with g = 0 nothing should move — but does casting itself sneak in an error?
Update value. w − η ⋅ 0 = 0.5 . Zero step.
Why this step? A zero gradient must be a perfect no-op; if casting corrupted it, we'd have a silent bug.
Is 0.5 exactly representable in FP16? 0.5 = 1.0 × 2 − 1 , mantissa all zeros, and 2 − 1 is deep inside the normal range — yes, exact on both rulers.
Why this step? Powers of two in the normal range are the one input where FP16 and FP32 agree to the bit.
Cast round-trip. cast F P 16 ( cast F P 32 ( 0.5 )) = 0.5 exactly, no drift.
Why this step? Confirms the degenerate case introduces no rounding — a good sanity anchor.
Verify: 0.5 − η × 0 = 0.5 for every η , and 0.5 is exact in FP16. ✓
Worked example Ex 6 — Cell F: BF16 vs FP16 on the same numbers
Statement. Two gradients: a tiny g 1 = 1 0 − 9 and a big g 2 = 70000 . Compare FP16 (subnormals down to ≈ 6 × 1 0 − 8 , max 65504 , relative precision ∼ 1 0 − 3 ) with BF16 (FP32-wide range: normals to ≈ 1 0 − 38 , subnormals to ≈ 1 0 − 40 ; relative precision ∼ 1 0 − 2 ).
Forecast: which format loses g 1 ? which loses g 2 ? do they lose the same one?
FP16 on g 1 . 1 0 − 9 < 6 × 1 0 − 8 (below even the smallest FP16 subnormal) ⇒ underflows to 0 (needs loss scaling).
Why this step? FP16's short low end, even counting subnormals, can't reach 1 0 − 9 .
BF16 on g 1 . BF16 shares FP32's 8 exponent bits, so its normal range reaches ≈ 1 0 − 38 and its subnormal tail reaches ≈ 1 0 − 40 ; 1 0 − 9 sits comfortably inside the normal range — no scaling needed for underflow.
Why this step? BF16 buys range by spending mantissa bits; range is exactly what tiny gradients need. Note: the value is stored, just with ∼ 1% relative blur.
FP16 on g 2 . 70000 > 65504 ⇒ overflow to inf.
Why this step? FP16's short high end can't reach 70000 either.
BF16 on g 2 . 70000 well inside BF16 range ⇒ fine, but stored coarsely: nearest BF16 value differs by up to ∼ 1% (≈ 700 ).
Why this step? BF16 trades precision for range — the number is present but blurry .
Verify: FP16 fails both ends (1 0 − 9 < 6 × 1 0 − 8 and 70000 > 65504 ); BF16 represents both because its range covers 1 0 − 9 (normal, above 1 0 − 38 ) and 70000 (below 1 0 38 ). ✓
Worked example Ex 7 — Cell G: real-world memory word problem
Statement. A model has P = 25 × 1 0 6 parameters, trained with Adam. Compute per-GPU memory for (a) pure FP32 and (b) mixed precision, given: FP32 = 4 bytes, FP16 = 2 bytes, Adam keeps 2 FP32 optimizer buffers per parameter. Ignore activations. By what fraction does mixed precision change this (weights+grads+optimizer) footprint?
Forecast: mixed precision uses FP16 and adds an FP32 master copy — net saving, net loss, or a wash?
Pure FP32. weights 4 P + grads 4 P + Adam 2 × 4 P = 8 P , total = 4 P + 4 P + 8 P = 16 P bytes = 16 × 25 × 1 0 6 = 4.0 × 1 0 8 B = 400 MB.
Why this step? Baseline: everything on the fine ruler costs 4 bytes each; the two Adam buffers (m and v ) are the biggest single chunk.
Mixed precision pieces. FP16 weights 2 P + FP32 master 4 P + FP16 grads 2 P + Adam FP32 8 P = 16 P bytes.
Why this step? We now hold both an FP16 and an FP32 copy of weights — that master is the price of precision.
Mixed total. 16 P = 16 × 25 × 1 0 6 = 4.0 × 1 0 8 B = 400 MB.
Why this step? For the parameter-state alone, mixed precision comes out to exactly the same 16 P as pure FP32 here.
The fraction. ( 400 − 400 ) /400 = 0 = 0% change on this slice.
Why this step? The real win (as the parent note showed) is in activations and in speed via 4.2.03-Tensor-cores , not in the weight/grad/optimizer slice. This example is the exam trap: people assume "half precision ⇒ half memory everywhere," but here it is a wash — the FP32 master and full-precision Adam buffers eat the FP16 savings.
Verify: FP32 slice = 16 P = 4 × 1 0 8 ; mixed slice = 16 P = 4 × 1 0 8 ; fraction change = ( 400 − 400 ) /400 = 0 . ✓ On this slice mixed precision is a wash; savings come from activations, which we deliberately ignored.
Worked example Ex 8 — Cell H: exam twist — scaling with gradient accumulation
Statement. You accumulate gradients over K = 4 micro-batches (see 3.3.08-Gradient-accumulation ) with loss scale S = 1024 . Each micro-batch gradient is g = 2 × 1 0 − 4 . What is the correctly unscaled, averaged gradient the optimizer must see, and where do dividing by S and by K go?
Forecast: do you divide by S before or after summing the 4 batches? and where does the 1/ K average enter?
Scaled per-batch gradient. S g = 1024 × 2 × 1 0 − 4 = 0.2048 (comfortably in FP16).
Why this step? Each micro-batch backprops the scaled loss, so each stored grad is S g .
Accumulate scaled grads. Sum over K : ∑ = 4 × 0.2048 = 0.8192 (still FP16-safe).
Why this step? Accumulation sums the scaled grads; keep S on until the end so nothing underflows mid-sum.
Unscale once, at the end. Divide by S : 0.8192/1024 = 8 × 1 0 − 4 .
Why this step? Unscaling is linear, so doing it once after the sum = doing it per batch — but once is cheaper and avoids repeated rounding.
Average by K . 8 × 1 0 − 4 /4 = 2 × 1 0 − 4 .
Why this step? Accumulation approximates a K × bigger batch, so we average to keep the effective learning rate unchanged. Final grad = g , as it should for identical micro-batches.
Verify: ( 4 × 1024 × 2 × 1 0 − 4 ) / ( 1024 × 4 ) = 2 × 1 0 − 4 = g . ✓ Order (sum, then unscale, then average) recovers the true mean gradient.
Recall Quick self-check
A weight update η g = 3 × 1 0 − 7 near w = 0.5 in FP16 rounds to what, and why? ::: To 0.5 — the update is far below FP16's local absolute tick ≈ 5 × 1 0 − 4 , so it vanishes; the FP32 master saves it.
Loss scale S = 131072 , largest gradient 0.8 — what breaks? ::: S g = 104857.6 > 65504 , so FP16 overflows to inf and the step is skipped.
Dynamic scaler on overflow does what two things? ::: Halves S and skips (does not apply) the corrupt update.
Why does BF16 usually need no loss scaling? ::: BF16 shares FP32's 8 exponent bits, so its range reaches ≈ 1 0 − 38 (subnormals to ≈ 1 0 − 40 ) and tiny gradients don't underflow — it trades mantissa (precision) for that range.
In gradient accumulation, you unscale by S how many times? ::: Once, after summing all micro-batch gradients — unscaling is linear so once suffices and rounds less.
What is the difference between FP16 normal and subnormal numbers? ::: Normal (6 × 1 0 − 5 to 65504 ) has full 10-bit precision; subnormal (6 × 1 0 − 8 to 6 × 1 0 − 5 ) still stores values but with fewer significant bits; below 6 × 1 0 − 8 everything underflows to 0 .
Mnemonic Two failures, one ruler
FP16 is a short ruler . Loss scaling slides small numbers up onto it — but slide too far and the big numbers fall off the top . That single picture explains underflow, overflow, and why S must breathe .