BF16 vs FP16
Same 2 bytes, different split. BF16 spends 8 bits on the exponent and gets FP32’s whole range. FP16 spends 10 on the mantissa and gets 8x finer steps, but only up to 65,504. For a modern LLM, range wins: take BF16.
At a glance
| BF16 | FP16 | |
|---|---|---|
| Bits: sign · exponent · mantissa | 1 · 8 · 7 | 1 · 5 · 10 |
| Exponent bias | 127 (same as FP32) | 15 |
| Largest finite | ≈ 3.39 × 1038 | 65,504 |
| Smallest normal | 2−126 ≈ 1.18 × 10−38 | 2−14 ≈ 6.10 × 10−5 |
| Smallest subnormal | 2−133 ≈ 9.2 × 10−41 | 2−24 ≈ 5.96 × 10−8 |
| Steps per power of two | 128 | 1024 |
| Step next to 1.0 | 2−7 = 0.0078125 | 2−10 ≈ 0.000977 |
| Worst rounding error, half a step | 2−8 ≈ 0.39 % | 2−11 ≈ 0.049 % |
| From FP32 | drop the low 16 bits (or round them) | re-encode; can overflow or flush to 0 |
| Training | no loss scaling | loss scaling |
| Tensor-core hardware | NVIDIA Ampere+, AMD CDNA1+ / RDNA2+, TPU | every NVIDIA GPU since Volta, every AMD matrix core |
| PyTorch dtype | torch.bfloat16 | torch.float16 |
FP16 is IEEE 754 binary16. BF16 layout: Google's BFloat16 post (Wang, Kanwar 2019). Limits and steps: ml_dtypes 0.6.0 finfo in python:3.12-slim; Cloud TPU flushes BF16 subnormals to zero, so there the floor is the smallest normal. Rounding error as a share of the power of two below the value.
BF16 is FP32 with the bottom half cut off
A normal value is (−1)S × 1.M × 2E − bias. BF16 keeps FP32’s sign, its 8 exponent bits and bias 127, and the first 7 of its 23 mantissa bits.
- Range is the exponent. 8 bits give 254 normal binades, the same as FP32. FP16’s 5 bits give 30.
- Precision is the mantissa. 7 bits cut each power of two into 128 steps, 10 bits into 1024. FP16 is 8x finer in every binade both can reach.
- FP32 → BF16 is a bit cut. 3.3 in FP32 is
0x40533333. Keep the top half,0x4053, and you have BF16’s 3.296875; round-to-nearest-even gives the same here, because the dropped half0x3333is below0x8000. Truncation never overflows. Rounding can, but only from 3.396 × 1038 up, the last 0.2 % of FP32’s range. - FP32 → FP16 changes the exponent. It is a re-encode with a narrower exponent: anything from 65,520 up becomes inf, anything at or below 2−25 ≈ 2.98 × 10−8 becomes 0.
Worked example
| Value | BF16 | Error | FP16 | Error |
|---|---|---|---|---|
| 3.3 | 3.296875 | −0.09 % | 3.30078125 | +0.02 % |
| 0.1 | 0.10009765625 | +0.10 % | 0.0999755859375 | −0.02 % |
| 70,000 | 70,144 | +0.21 % | inf | — |
| 10−8 | 1.0012 × 10−8 | +0.12 % | 0 | −100 % |
ml_dtypes 0.6.0 and NumPy 2.5.4 in python:3.12-slim, cast from FP32, round to nearest even.
3.3 = 1.65 × 21. Between 2 and 4 BF16 steps by 2−6 = 0.015625, so the nearest neighbours are 3.28125 and 3.296875: 3.296875. FP16 steps by 2−9 = 0.001953125 and lands on 3.30078125.
finfo (Cloud TPU flushes BF16 subnormals to zero). Grid points: every BF16 and FP16 value in the window; rounding is to nearest.finfo (Cloud TPU flushes BF16 subnormals to zero). Grid points: every BF16 and FP16 value in the window; rounding is to nearest.Training: FP16 needs loss scaling, BF16 does not
Small gradients fall below FP16’s range and become 0. NVIDIA’s mixed-precision guide: “gradients may have to be scaled to move them into the range to keep them from becoming zeros in FP16”. So FP16 training multiplies the loss by a scale before the backward pass and divides the gradients after, and the scale is adjusted when an overflow shows up.
BF16 reaches down to 1.18 × 10−38 like FP32, so there is nothing to shift. Google’s TPU team: “Unlike FP16, which typically requires special handling via techniques such as loss scaling, BF16 comes close to being a drop-in replacement for FP32 when training and running deep neural networks”.
Inference: use the checkpoint’s BF16
- The weights are BF16.
torch_dtypeisbfloat16in theconfig.jsonof Qwen3 (8B, 235B-A22B), Llama 3.3 70B, DeepSeek-V2-Lite and the DeepSeek-R1 distills. DeepSeek-V3 is the exception: its weights ship in FP8 E4M3, though itstorch_dtypestill readsbfloat16. - FP16 overflows on outliers. A model trained in BF16 never had to keep activations under 65,504. When an outlier passes it, FP16 stores inf; the next inf − inf or inf × 0 in a norm or softmax is NaN, and the NaN spreads through every layer after it.
- Hardware. BF16 tensor-core math is new in NVIDIA Ampere (A100: “BF16 Tensor Core instructions at the same throughput as FP16”), and on AMD in every listed CDNA (MI100 onward) and RDNA2–4 matrix core. On Cloud TPU, XLA converts FP32 to BF16 itself, rounding to nearest even. FP16 runs on all of those and on pre-Ampere NVIDIA (V100, T4), which have no BF16 tensor cores: there FP16 is the only fast 16-bit option, and it carries the overflow risk above.
- In vLLM,
--dtype auto(the default) uses “BF16 precision for BF16 models”.half/float16is a choice you make; on a BF16 checkpoint it reintroduces the 65,504 ceiling.
For inference on a modern GPU: BF16, or auto on a BF16 checkpoint. The 16-bit KV cache is BF16 as well, 2 bytes per element. That is the baseline FP8 E4M3 vs E5M2 weighs E4M3 against: half the bytes, 16 steps between 1 and 4 instead of 256.
Every kv_cache_dtype and its bytes: KV type. The 4-bit end: KV cache: FP8 vs NVFP4.
Sources
- IEEE 754-2019, Standard for Floating-Point Arithmetic (binary16, binary32) — IEEE
- Improve your model’s performance with bfloat16 — Google Cloud TPU docs
- BFloat16: The secret to high performance on Cloud TPUs — Shibo Wang, Pankaj Kanwar (Google), 2019
- Train With Mixed Precision — NVIDIA Deep Learning Performance docs
- Mixed Precision Training — Micikevicius et al., arXiv 1710.03740
- NVIDIA Ampere Architecture In-Depth — NVIDIA Technical Blog
- Precision support, matrix cores — AMD ROCm docs
vllm serveCLI,--dtype— vLLM docs- ml_dtypes — JAX/ML numeric types for NumPy