Mixed precision training
What Problem Does This Solve?
Training large neural networks faces three bottlenecks:
- Memory: Modern models (GPT, Vision Transformers) have billions of parameters. Each FP32 parameter takes 4 bytes. A175B parameter model needs 700GB just to store weights!
- Compute: Matrix multiplications dominate training time. GPUs have specialized hardware (Tensor Cores) that runs FP16 ops 2-8× faster than FP32.
- Communication: In distributed training, gradients must sync across GPUs. Halving precision halves bandwidth requirements.
But naively switching to FP16 causes gradient underflow: tiny gradients round to zero, and training diverges.
The key insight: gradients span a huge dynamic range. Early layers might have gradients around , while later layers have . FP16's limited range can't represent both.
How Mixed Precision Training Works

The algorithm has three core components:
1. FP32 Master Weights
Why: Weight updates are cumulative. If learning rate is and gradient is , the update is —smaller than FP16's precision (). The update would round to zero, and weights never change.
How: Maintain two copies of weights:
- FP16 copy forward/backward passes (fast compute)
- FP32 master copy for accumulating updates (preserve precision)
Derivation from First Principles:
Consider a single parameter updated by gradient descent:
In FP16, if , the update becomes: (the small update rounds to zero)
After steps with zero updates, we've lost of accumulated change—training stalls.
Solution: Store master weights in FP32: Even if individual updates are tiny, they accumulate in the higher-precision master. Then cast to FP16 for the next forward pass:
2. Loss Scaling
Why: Gradients in early layers often fall below FP16's minimum normal value (). These underflow to zero, creating "dead neurons" that never learn.
What: Multiply the loss by a large scalar (typically to ) before backprop. This shifts gradients into FP16's representable range. After computing gradients, divide by to restore true values before updating master weights.
Derivation:
The chain rule for gradients gives:
If (common in deep networks), FP16 cannot represent it—underflows to zero.
Loss scaling trick: Define a scaled loss where . Then:
Now if and , then , which FP16 can represent!
After backprop, unscale:
Why This Step?: Scaling happens at the loss (top of computational graph) so it automatically propagates through the entire backward pass. This is more efficient than scaling each layer's gradients individually.
Dynamic Loss Scaling: Start with large (e.g., ). If gradients overflow (produce inf or nan), halve and retry. If training is stable for steps (e.g., ), double . This adapts to each model's gradient distribution.
3. FP16 Arithmetic for Most Operations
Why: Matrix multiplications (GEMM) dominate training time—often 90%+ of compute. Modern GPUs have Tensor Cores that execute FP16 ops at 2-16× the throughput of FP32.
What: Store activations, gradients, and weights in FP16 during forward/backward passes. Only the optimizer state and master weights stay in FP32.
How:
- Cast weights from FP32 master to FP16 at the start of each forward pass
- Compute activations in FP16:
- Compute loss and scale it
- Backward pass in FP16 (gradients accumulate in FP16)
- Cast gradients back to FP32 for optimizer
Common Pitfalls and Fixes
Implementation Pseudocode
# Initialization
model_fp16 = model.half() # FP16 copy for compute
master_weights = model.parameters() # FP32 master copy
optimizer = Adam(master_weights)
scale = 2**16 # Initial loss scale (6536)
for batch in dataloader:
# Forward pass in FP16
with autocast(dtype=torch.float16):
output = model_fp16(batch.x)
loss = criterion(output, batch.y)
# Scale loss to prevent gradient underflow
scaled_loss = loss * scale
# Backward pass (gradients computed in FP16, but scaled)
scaled_loss.backward()
# Unscale gradients
for param in master_weights:
if param.grad is not None:
param.grad = param.grad / scale
# Clip gradients (on unscaled values!)
torch.nn.utils.clip_grad_norm_(master_weights, max_norm=1.0)
# Optimizer step on FP32 master weights
optimizer.step()
optimizer.zero_grad()
# Copy updated master weights back to FP16 model
for param_fp16, param_fp32 in zip(model_fp16.parameters(), master_weights):
param_fp16.data.copy_(param_fp32.data)
# Dynamic loss scaling: check for overflow
if any(torch.isnan(p.grad).any() for p in master_weights if p.grad is not None):
scale = scale / 2 # Halve scale on overflow
elif step % 2000 == 0:
scale = min(scale * 2, 2**16) # Double scale if stableTheoretical Guarantees
Convergence: Mixed precision training provably converges to the same optimum as FP32 training under mild conditions:
- Assumption: Gradients satisfy (bounded) and loss scale ensures \cdot g_{\min} > \epsilon_{FP16}$ (no underflow).
- Result: If learning rate (standard SGD condition), expected distance to optimum decreases at the same rate as FP32: where depends on (loss smoothness) but not precision.
Proof sketch: Master weights accumulate updates exactly (no precision loss). FP16 forward/backward introduce rounding errors at each operation. But modern GPUs use round-to-nearest mode, so (unbiased). The error variance is , which vanishes as due to averaging. Loss scaling ensures is measured relative to scaled gradients, keeping small.
When to Use Mixed Precision
Use when:
- Training models with >10M parameters (memory savings matter)
- Using GPUs with Tensor Cores (V100, A100, RTX 20/30 series)
- Distributed training (gradient communication is a bottleneck)
- Inference is memory-bound (can deploy FP16 models directly)
Don't use when:
- Debugging numerical issues (hard to tell if bug is yours or precision-related)
- Working with datasets where values span >5 orders of magnitude (e.g., scientific computing with huge ranges)
- Using custom CUDA kernels not tested with FP16
Real-world impact: GPT-3 training used mixed precision and would have been infeasible without it (175B parameters → ~700GB FP32, but fits in ~350GB with mixed precision and activation checkpointing).
Recall Explain Like I'm 12
Pretend you're playing a video game, and you have a pigy bank to store your gold coins. Each coin can be stored in two ways:
- Small pouches (FP16): Hold coins faster, take less space, but can only count coins from 1 to 100.
- Big chests (FP32): Slower, heavier, but can count from 0.001 to 1,000,000.
Most of the time you're running around collecting and spending coins, so you use small pouches—it's quick! But every night, you pour all your pouches into a big chest to add up your total wealth. That way, even tiny coins (like 0.01 gold) don't get lost.
Now, sometimes you find really tiny treasure (0.001 gold). If you put it straight in a small pouch, it rounds to zero and disappears! So you use a trick: multiply all treasure by 1000 when you collect it. Now0.0001 becomes 0.1, which the pouch can hold. Later, when you pour into the big chest, you divide by 1000 to get the real value back.
Mixed precision training is exactly this: use fast, small storage (FP16) for most work, but keep a careful ledger (FP32 master weights) so tiny updates don't vanish. Scale up gradients so they don't disappear. This makes training huge AI models 2× faster and use 30% less memory—like beating the video game boss with a speed potion!
Connections
- 3.3.08-Gradient-accumulation – Mixed precision is often combined with gradient accumulation to fit large batches
- 3.3.05-Learning-rate-schedules – Loss scaling interacts with LR: both affect gradient magnitude
- 4.2.03-Tensor-cores – Hardware motivation for mixed precision
- 3.3.10-Gradient-checkpointing – Another memory optimization, often used together
- 2.4.06-Batch-normalization – Requires special handling (FP32 running stats)
- 5.1.02-Model-parallelism – Mixed precision reduces communication in distributed training
- 1.3.07-Numerical-stability – Fundamental tradeoffs between speed and precision
#flashcards/ai-ml
What are the three core components of mixed precision training? :: (1) FP32 master weights for accurate updates, (2) Loss scaling to prevent gradient underflow, (3) FP16 arithmetic for forward/backward passes to speed up compute.
Why can't we just train entirely in FP16?
Derive why loss scaling prevents gradient underflow :: If gradient (FP16 min), it underflows to 0. Scale loss by : backward gives (representable). Unscale: recovers true value in FP32 before optimizer step.
What is the memory formula for mixed precision training?
When should you unscale gradients—before or after gradient clipping?
Why must batch normalization running statistics stay in FP32?
What is dynamic loss scaling and why is it needed?
inf/nan), halve the scale. If training is stable for steps, double the scale. Needed because different models have gradient distributions spanning 1000× in magnitude—no single static scale works universally.Compare FP16 vs BF16 :: FP16: 5exp bits, 10 mantissa bits. Range to , precision . BF16: 8 exp bits, 7 mantissa bits. Same range as FP32 ( to ), less precision. BF16 has fewer overflow issues but less accuracy; preferred on TPUs and newer GPUs.
What is the speedup from mixed precision training on modern GPUs?
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Chalo simple tarike se samajhte hain. Jab hum bade neural networks train karte hain, tab do badi problems aati hain — memory kam padti hai (billions of parameters store karne hote hain) aur compute slow hoti hai. Iska smart solution hai mixed precision training. Iska core idea bilkul aisa hai jaise checkbook balance karna — zyada tar transactions ke liye tumhe dollars aur cents kaafi hain, par kuch critical calculations ke liye extra precision chahiye. Waise hi, hum jyada computations FP16 (lower precision) me karte hain taaki fast aur memory-efficient ho, par jahan numerical accuracy zaroori hai wahan FP32 (higher precision) use karte hain. Yeh cheating nahi, balki smart engineering tradeoff hai.
Ab problem yeh hai ki agar tum sidha sab kuch FP16 me kar do, toh gradient underflow ho jaata hai — matlab bahut chote gradients (jaise ) zero me round ho jaate hain, aur weights kabhi update hi nahi hote, training stall ya diverge ho jaati hai. Isko solve karne ke liye do main tricks hain. Pehla, FP32 master weights rakho — forward/backward pass toh FP16 me fast chalta hai, par actual weight updates ek FP32 master copy me accumulate hote hain, taaki chote updates zero na ban jaayein. Dusra, loss scaling — loss ko ek bade number se multiply karke gradients ko FP16 ki representable range me shift kar dete hain, aur baad me se divide karke original value wapas le lete hain.
Yeh matter kyun karta hai? Aaj ke real-world models — GPT, Vision Transformers — inko bina mixed precision ke train karna practically impossible ya bahut mehnga hota. Tumhe 2-8x speedup milta hai GPU ke Tensor Cores ki wajah se, memory aadhi ho jaati hai, aur distributed training me communication bandwidth bhi kam lagti hai. Toh agar tum future me ML engineer ya researcher banna chahte ho, yeh concept samajhna zaroori hai — kyunki har modern deep learning framework (PyTorch, TensorFlow) me yeh feature by default use hota hai. Yeh sirf theory nahi, ekdum practical skill hai jo tum daily kaam me apply karoge.