Two number formats, used where each is safe
The middle number is the exponent, which sets the range. The last is the mantissa, which sets the precision.
Half precision halves the memory of every tensor and roughly doubles the arithmetic throughput on hardware with tensor cores.
What it loses is range, and gradients are exactly the quantity that lives near the bottom of that range.
Why a small gradient becomes zero
Anything below the smallest representable value underflows to zero, and a zero gradient is indistinguishable from a parameter that had nothing to learn.
Loss scaling is the fix: multiply the loss by a large constant before the backward pass, which multiplies every gradient by the same constant and lifts them into range.
The gradients are divided by again before the optimiser step, so the update is unchanged and only the intermediate representation moved.
cannot be fixed in advance, because too small does not solve the underflow and too large overflows to infinity.
It is chosen dynamically: raise it while steps succeed, halve it and skip the step whenever a gradient comes back as NaN or infinity.
The master copy stays in fp32
An update is typically a thousand times smaller than the parameter it modifies, and adding a tiny number to a large one in fp16 rounds straight back to the large one.
So the optimiser keeps a full precision copy of the weights and applies every update to that. The fp16 copy used in the forward pass is cast from it each step.
That costs memory rather than saving it, and it is the reason mixed precision reduces activation memory far more than parameter memory.
One step, in order
- Cast the weights to fp16 and run the forward pass in half precision.
- Multiply the loss by the current scale factor.
- Backward, producing scaled fp16 gradients.
- Unscale the gradients and check them for infinities or NaNs.
- If they are finite, step the fp32 master weights. If not, skip the step and halve the scale.
bf16, which removes most of this
Brain float keeps fp32's full 8-bit exponent and gives up mantissa bits instead, so it has the same range as fp32 and less precision.
Because the range matches, gradients do not underflow and loss scaling is unnecessary. That is why every large model trained on recent hardware uses bf16 rather than fp16.
The lower precision is tolerable because gradient descent is an averaging process, and the noise from rounding is small next to the noise from sampling batches.
Some operations still run in fp32 whatever the mode: softmax, normalisation statistics, and any sum over a long axis.
Accumulating thousands of small values is where low precision actually hurts.