AI Grimoire
Sheet
statusstandard
difficultyintroductory
time
described2018
revisedtoday

Floating-Point Formats

Every format spends its bits on either range or precision. Which one you are short of decides how the run fails — silently wrong, or loudly NaN.

Standing

Current standardPart of the default recipe. Present in most frontier models trained today, and the thing a new design departs from rather than argues for.

bf16 for training and fp8 for inference is the current settled answer. fp4 is where the argument is live.

judged as of 2026-09 · what the labels mean

Theory

A floating-point number is a sign, an exponent and a mantissa. The exponent sets how far apart the largest and smallest representable magnitudes are; the mantissa sets how finely spaced the values are within that span. A format is a decision about how to split a fixed bit budget between the two, and every such decision is a bet on which kind of error the workload can tolerate.

FormatExponentMantissaMax finiteSmallest normal
fp328233.4e381.2e−38
tf328103.4e381.2e−38
bf16873.4e381.2e−38
fp1651065 5046.1e−5
fp8 e4m3434481.5e−2
fp8 e5m25257 3446.1e−5

Why bf16 displaced fp16

fp16 came first and has more precision. It also has a dynamic range that gradient magnitudes routinely leave. A gradient of 10810^{-8} is not small for a deep network; in fp16 it is zero, and the update it should have produced never happens. At the other end, an activation spike above 65 504 becomes infinity, and one infinity propagates to a NaN loss within a step.

The mitigation was loss scaling: multiply the loss by a large constant before the backward pass so that gradients land in fp16’s representable band, then divide it back out before the optimiser step. Dynamic loss scaling automates the constant by doubling it periodically and halving it whenever the gradients overflow — which means a training run that intermittently discards steps as a matter of course.

The precision loss is not free everywhere, and the exceptions are the reductions: sums over the feature dimension in normalisation, the softmax denominator, the optimiser’s second-moment accumulator. All of those are kept in fp32 in every serious implementation, which is what “mixed precision” actually means — the weights and activations are half precision, the accumulations are not.

fp8, and why it needs a scale

Four exponent bits reach 448. Activations in a large model exceed that routinely, so fp8 is not a format one simply casts into — every tensor carries a scale factor, and the value stored is x/sx / s with ss chosen so the tensor’s maximum lands near the top of the range.

The two variants split the work. e4m3 has more mantissa and less range, and holds weights and activations. e5m2 has more range and less mantissa, and holds gradients, which span more orders of magnitude and tolerate coarser steps. Getting this backwards is a real and common mistake.

x^=castfp8 ⁣(xs),s=maxxFmax\hat{x} = \mathrm{cast}_{\text{fp8}}\!\left(\frac{x}{s}\right), \qquad s = \frac{\max|x|}{F_{\max}}
eq. 1 — per-tensor scaling, chosen from the observed maximum

Because maxx\max|x| is not known until the tensor exists, production implementations use delayed scaling: keep a short history of recent maxima, predict the next one, and correct if the prediction was too low. Recomputing the scale from the current tensor is exact and costs a full extra pass over it.

Implementation

python · torch
import torch
from torch import Tensor

FP8_E4M3_MAX = 448.0


def to_fp8(x: Tensor, amax: Tensor | None = None) -> tuple[Tensor, Tensor]:
    """Scale into range, then cast. Returns the tensor and its scale."""
    amax = x.abs().amax() if amax is None else amax
    # clamp: an all-zero tensor would otherwise divide by zero.
    scale = (amax / FP8_E4M3_MAX).clamp(min=1e-12)
    return (x / scale).to(torch.float8_e4m3fn), scale


def from_fp8(x: Tensor, scale: Tensor, dtype=torch.bfloat16) -> Tensor:
    return x.to(dtype) * scale

The matmul is where the win is. An fp8 tensor-core matmul runs at roughly twice the throughput of bf16 on Hopper and later, and — more importantly for serving — halves the bytes moved, which is the actual bound. The accumulator inside the kernel is still fp32; fp8 describes the operands, never the sum.

Below eight bits

fp4 (e2m1) has six representable magnitudes. Per-tensor scaling is hopeless at that width, so the block moves to micro-scaling: a shared exponent per group of 16 or 32 elements, so the scale tracks local structure rather than the whole tensor. Blackwell implements this in hardware as MXFP4 and NVFP4.

Whether four-bit training works is genuinely open. Four-bit inference is settled — GPTQ and its successors are in production — and the distinction is that inference quantises a finished weight once, against a calibration set, while training has to keep gradients meaningful through a format with six magnitudes in it.

fp16 · 5 / 10
overflows
bf16 · 8 / 7
rounds
fp8 e4m3 · 4 / 3
needs scaling
Exponent / mantissa bits, and what breaks

Related

References

[1]Micikevicius et al. — Mixed Precision Training (2017)arXiv:1710.03740
[2]Micikevicius et al. — FP8 Formats for Deep Learning (2022)arXiv:2209.05433
[3]Kalamkar et al. — A Study of BFLOAT16 for Deep Learning Training (2019)arXiv:1905.12322