Quant Memo
Advanced

Mixed Precision and bfloat16 Training

Storing every number in a network at full 32-bit precision is mostly wasted accuracy; mixed precision training does most of the arithmetic in a cheaper 16-bit format and keeps only what genuinely needs full precision at 32 bits, roughly doubling speed and halving memory for little to no loss in final accuracy.

Prerequisites: Gradient Accumulation and Effective Batch Size

Every number in a neural network — weights, activations, gradients — is normally stored as a 32-bit floating-point number (fp32), which gives roughly 7 decimal digits of precision and a huge representable range. Most of that precision is never actually needed: a weight update that changes a value in its 6th decimal digit rarely changes the model's behavior in any meaningful way. Mixed precision training exploits this by doing the bulk of the arithmetic in a 16-bit format instead — half the memory, and roughly 2–8× the throughput on hardware built for it — while keeping a small number of precision-sensitive operations at full 32-bit accuracy.

The analogy: rounding a bill, keeping the ledger exact

A cashier can quote and compute change using prices rounded to the nearest cent all day without ever mis-balancing the till, provided the master ledger behind the scenes tracks every transaction to a finer precision so tiny rounding errors don't accumulate into a real discrepancy by the end of the day. Mixed precision training works the same way: the "storefront" arithmetic (most forward and backward computations) happens in cheap, lower-precision numbers, while a "master ledger" copy of the weights is kept in full 32-bit precision and is what actually gets updated — the low-precision copies are refreshed from it before each step, so rounding never compounds across the whole training run.

The two 16-bit formats, and why bfloat16 avoids one whole problem

Both fp16 and bfloat16 use 16 bits total, but split them differently between exponent bits (controlling range — how large or small a number can be) and mantissa bits (controlling precision — how many significant digits). fp16 uses 5 exponent bits and 10 mantissa bits: more precision, but a much narrower range than fp32, so small gradient values can underflow to exactly zero. bfloat16 uses 8 exponent bits (matching fp32 exactly) and only 7 mantissa bits: less precision per number, but the same representable range as fp32, so a value large or small enough to be a normal fp32 number essentially never overflows or underflows in bfloat16.

Worked example 1: why fp16 needs loss scaling and bfloat16 doesn't

A gradient value of 0.000030.00003 is well within fp32's range. fp16's smallest normal representable magnitude is around 6×1056 \times 10^{-5} — this gradient underflows toward zero (or to a denormal value with almost no precision), silently vanishing before it can update the weights. The standard fix is loss scaling: multiply the loss by a large constant (say, 1024) before the backward pass, which scales every gradient up by the same factor — 0.00003×1024=0.03070.00003 \times 1024 = 0.0307, safely representable — then divide the weight update back down by 1024 afterward. bfloat16's exponent range matches fp32's directly (103810^{-38} to 103810^{38}, roughly), so 0.000030.00003 is stored without any special scaling step at all — this is the main practical reason bfloat16 has become the default over fp16 for large-model training.

Worked example 2: memory and throughput, concretely

A model with 1 billion parameters stored entirely in fp32 needs 1×109×41 \times 10^9 \times 4 bytes =4= 4 GB just for the weights, plus roughly the same again for gradients and optimizer state (Adam keeps two extra moment buffers per parameter), pushing total memory well past 12 GB before activations are counted. Switching weights and gradients to bfloat16 (2 bytes each) while keeping an fp32 master copy for the optimizer update cuts forward/backward memory roughly in half and, on hardware with dedicated 16-bit tensor cores, can roughly double the raw matrix-multiply throughput — why mixed precision is close to a default setting (torch.cuda.amp, bf16=True) rather than an optional extra.

All fp32 ~12+ GB Mixed precision ~7 GB
Storing weights and gradients in bfloat16 while keeping only the optimizer's master copy in fp32 cuts memory substantially without changing what the model actually learns.
32 total bits, split differently fp32 1 sign, 8 exp, 23 mantissa bfloat16 1 sign, 8 exp (matches fp32), 7 mantissa fp16 1 sign, 5 exp (narrow range), 10 mantissa
bfloat16 keeps fp32's full exponent range (avoiding overflow/underflow) but sacrifices mantissa precision; fp16 keeps more precision but its narrower exponent range makes small gradients vulnerable to underflow.

Mixed precision training does most forward/backward arithmetic in 16-bit numbers while keeping a full-precision fp32 master copy of the weights for the actual optimizer update; bfloat16 matches fp32's exponent range exactly (avoiding underflow) at the cost of mantissa precision, which is why it has largely replaced fp16 (more precision, narrower range, needs loss scaling) as the default for large-scale training.

What this means in practice

Enabling mixed precision is typically a one-line change in modern frameworks and is close to free accuracy-wise, provided precision-sensitive operations (softmax, layer/RMS normalization, loss computation) are kept in fp32 internally, which automatic mixed precision handles by default. On financial data specifically, be alert to features with a wide dynamic range fed directly into a bfloat16 computation without normalization — bfloat16's reduced mantissa means two values differing only in their 8th significant digit become indistinguishable.

The common confusion is treating "16-bit" as one thing and assuming fp16 and bfloat16 are interchangeable drop-in options. They trade off range and precision in opposite directions, and code tuned for one (loss-scaling logic written for fp16's narrow range) is often either unnecessary or subtly wrong when swapped to bfloat16, and vice versa — always check which 16-bit format a training recipe assumes before reusing its stability tricks.

Related concepts

Practice in interviews

Further reading

  • Micikevicius et al., Mixed Precision Training (2018)
ShareTwitterLinkedIn