Navigation
🏋️ Training Systems· 12 min read

Mixed Precision Training

FP32, BF16, FP8, Transformer Engine — which tensors at which precision and why, with the mechanics of loss scaling.

mixed-precisionbf16fp16fp8transformer-engineloss-scaling

Mixed precision training keeps the model accurate while halving (or quartering) memory and doubling (or quadrupling) throughput. The key is knowing which tensors need which precision — get it wrong and training diverges.


1. Why Mixed Precision?

PrecisionBytesTensor Core TFLOPS (H100)Memory BW savings
FP32467Baseline
TF324 (stored) / 19-bit (compute)495Same memory, faster compute
BF1629892×
FP1629892×
FP8 (E4M3)11,9794×

BF16 gives 2× memory savings AND 15× compute speedup over FP32. FP8 doubles that again on Hopper+.


2. Number Format Comparison

BF16 vs FP16

PropertyFP32BF16FP16
Exponent bits885
Mantissa bits23710
Dynamic range±3.4×1038\pm 3.4 \times 10^{38}±3.4×1038\pm 3.4 \times 10^{38}±6.5×104\pm 6.5 \times 10^{4}
Precision~7 decimal digits~2.4 digits~3.3 digits
Underflow threshold1.2×10−381.2 \times 10^{-38}1.2×10−381.2 \times 10^{-38}6.1×10−56.1 \times 10^{-5}

BF16 wins for training because it has the same dynamic range as FP32 — gradients and activations rarely overflow or underflow. FP16’s limited range (±65504\pm 65504) causes gradient underflow without loss scaling.

FP8 Formats

Hopper introduces two FP8 formats:

FormatExponentMantissaRangeUsed for
E4M343±448\pm 448Forward pass (weights, activations)
E5M252±57344\pm 57344Backward pass (gradients — need more range)

E4M3 has higher precision (more mantissa bits) — better for weights and activations where values are bounded. E5M2 has higher range (more exponent bits) — better for gradients which have wider dynamic range.


3. The Mixed Precision Recipe

BF16 Mixed Precision (Standard)

Mixed Precision Flow

Working precision:  BF16 (forward, backward, communication)
Master weights:     FP32 (optimizer updates)
Optimizer states:   FP32 (m, v in Adam)
Gradient accumulation: BF16 or FP32

Memory: 16Ψ (2 BF16 params + 2 BF16 grads + 12 FP32 optimizer)

The key operation is the optimizer step:

# Simplified mixed-precision training loop
for data in dataloader:
    # Forward in BF16
    with torch.autocast(device_type='cuda', dtype=torch.bfloat16):
        loss = model(data)
    
    # Backward in BF16 (gradients are BF16)
    loss.backward()
    
    # Optimizer step: BF16 grads → FP32 update → BF16 params
    optimizer.step()  # internally:
    # 1. Cast BF16 gradients to FP32
    # 2. Update FP32 master weights using FP32 Adam states
    # 3. Cast FP32 master weights back to BF16 for next forward
    
    optimizer.zero_grad()

FP16 Mixed Precision (Legacy)

FP16 requires loss scaling because small gradients underflow to zero:

scaler = torch.cuda.amp.GradScaler()

for data in dataloader:
    with torch.autocast(device_type='cuda', dtype=torch.float16):
        loss = model(data)
    
    # Scale loss to prevent gradient underflow
    scaler.scale(loss).backward()
    
    # Unscale gradients, check for inf/nan, then step
    scaler.step(optimizer)
    scaler.update()  # adjust scale factor

Loss scaling multiplies the loss by a large factor (e.g., 1024) before backward, so small gradients are amplified into FP16’s representable range. After backward, gradients are divided by the same factor.

BF16 doesn’t need loss scaling because its dynamic range matches FP32. This is why BF16 is universally preferred on Ampere+ GPUs.

FP8 with Transformer Engine

NVIDIA’s Transformer Engine (TE) handles FP8 automatically:

import transformer_engine.pytorch as te

# Replace nn.Linear with TE's FP8-aware version
model = te.Linear(hidden_size, ffn_size)

# TE handles:
# 1. Per-tensor scaling (calibrate FP8 range for each tensor)
# 2. Delayed scaling (use previous step's statistics)
# 3. E4M3 for forward, E5M2 for backward
# 4. FP32 master weights for optimizer

Per-tensor scaling: Each weight matrix and activation tensor gets its own scaling factor, computed from the max absolute value. This maximizes the use of FP8’s limited range.

Delayed scaling: Instead of computing scales from the current tensor (which requires an extra pass), use scales from the previous training step. Works because tensor statistics change slowly between steps.


4. Which Tensors at Which Precision

TensorBF16 TrainingFP8 TrainingWhy
Forward activationsBF16FP8 (E4M3)Compute-bound, benefits from Tensor Cores
Weights (working copy)BF16FP8 (E4M3)Stored alongside activations
Backward gradientsBF16FP8 (E5M2)Wider range needed
Master weightsFP32FP32Accumulated small updates
Adam m, vFP32FP32Statistical accuracy
LayerNormFP32FP32/BF16Numerically sensitive
SoftmaxFP32FP32Numerically sensitive
Loss computationFP32FP32Precision matters for convergence
AllReduce communicationBF16BF16Bandwidth-sensitive

Rule: Compute-heavy operations (GEMM) use low precision. Numerically-sensitive operations (norms, softmax, loss) use high precision. Optimizer always uses FP32.


5. Memory Impact

ConfigurationBytes/param7B totalSpeedup vs FP32
FP32 everything20140 GB1×
BF16 mixed (standard)16112 GB~2×
FP8 mixed (Hopper+)1498 GB~3×
8-bit Adam (bitsandbytes)1070 GB~2×

FP8 saves less than you’d expect because the optimizer states (12Ψ) are still FP32. The savings come from parameters and gradients: 2Ψ + 2Ψ (BF16) → 1Ψ + 1Ψ (FP8), saving 2Ψ bytes.


6. Common Pitfalls

PitfallSymptomFix
Training with FP16 without loss scalingLoss goes to 0, gradients vanishUse GradScaler() or switch to BF16
LayerNorm in low precisionTraining instability, loss spikesForce FP32 for LayerNorm
Gradient accumulation precisionAccumulated gradients lose precisionAccumulate in FP32, not BF16
FP8 without proper scalingNaN loss immediatelyUse Transformer Engine’s delayed scaling
Mixing autocast scopesSome layers compute in wrong precisionEnsure entire forward pass is in autocast

For how mixed precision interacts with distributed training, see FSDP.


Self-Check

  • Compare BF16 vs FP16: dynamic range, precision, and why BF16 is preferred
  • Explain the FP8 E4M3 vs E5M2 format choice for forward vs backward
  • Write a mixed-precision training loop with torch.autocast
  • Explain loss scaling: why FP16 needs it and BF16 doesn’t
  • Identify which tensors must remain in FP32 and why
  • Calculate memory savings from BF16 vs FP32 mixed precision

References