Quiz 2

Mixed Precision Training: FP16/FP32, GradScaler, and AMP

1042 words
5 min read
Python Week 1: the first filter for runtime behavior
Visual companion
Python
Type and operator map

Python Week 1: the first filter for runtime behavior

View
Revision summary

What this note is really saying

Short form

# Mixed Precision Training: FP16/FP32, GradScaler, and AMP ## 🎯 Learning Objectives - Understand FP16 (half-precision) and its trade-offs - Implement Automatic Mixed Precision (AMP) training - Use GradScaler to prevent underflow in FP16 gradients - Benchmark throughput improvements from mixed precision - Handle edg...

Mixed Precision Training: FP16/FP32, GradScaler, and AMP

🎯 Learning Objectives

  • Understand FP16 (half-precision) and its trade-offs
  • Implement Automatic Mixed Precision (AMP) training
  • Use GradScaler to prevent underflow in FP16 gradients
  • Benchmark throughput improvements from mixed precision
  • Handle edge cases in mixed precision training

📋 Prerequisites

  • PyTorch basics (Week 1): Training loops
  • Distributed Training (Week 5): Optional but helpful

1. 📖 Core Content

1.1 Intuition: Why Mixed Precision?

Deep learning models are typically trained in FP32 (32-bit floating point). Each number uses 32 bits. FP16 uses 16 bits — half the memory. Benefits:
  • 2× less GPU memory → train larger models or batches
  • Up to 2× faster training on modern GPUs (Tensor Cores)
  • 2× faster data transfer (CPU → GPU) The problem: FP16 has smaller range and precision.
FormatRangePrecisionUse Case
FP32~3e-38 to ~3e38~7 decimal digitsDefault training
FP16~6e-8 to ~6e4~3 decimal digitsNeeds gradient scaling
BF16~3e-38 to ~3e38~3 decimal digitsBetter range, same precision
Mixed precision: Use FP16 for compute-intensive operations (matrix multiplies, convolutions) and FP32 for precision-sensitive operations (loss, softmax, batchnorm).

1.2 Automatic Mixed Precision (AMP) in PyTorch

python
# runnable
import torch
import torch.nn as nn
# Traditional FP32 training
model = nn.Linear(100, 10).cuda()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()
inputs = torch.randn(32, 100).cuda()
targets = torch.randint(0, 10, (32,)).cuda()
# Mixed precision training
scaler = torch.cuda.amp.GradScaler()
for step in range(10):
    optimizer.zero_grad()
    # AMP context: FP16 for forward pass
    with torch.cuda.amp.autocast():
        outputs = model(inputs)
        loss = loss_fn(outputs, targets)
    # Scale loss, backward, unscale, step
    scaler.scale(loss).backward()
    scaler.step(optimizer)  # Unscales gradients internally
    scaler.update()          # Updates scale for next iteration
    # Optional: gradient clipping
    # scaler.unscale_(optimizer)  # Must call before clip
    # torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    # scaler.step(optimizer)
    print(f"Step {step}: Loss = {loss.item():.4f}")

1.3 How GradScaler Works

GradScaler prevents gradient underflow — FP16 gradients becoming zero because they're too small.
python
# runnable
import torch
# Initialize scaler with default settings
scaler = torch.cuda.amp.GradScaler(
    init_scale=2.**16,      # Initial scale factor: 65536
    growth_factor=2.0,       # Double scale when no inf/nan
    backoff_factor=0.5,      # Halve scale when inf/nan detected
    growth_interval=2000     # Check every N steps
)
# Scale factor dynamics:
# - Normally: increases by 2× every 2000 steps (up to 2^24)
# - On gradient overflow: decreases by 2×, optimizer step is skipped
print(f"Initial scale: {scaler.get_scale():.1f}")

1.4 When to Use Mixed Precision

ScenarioFP32 OnlyMixed Precision
Small model (< 100M params)✓ Works fine1.5-2× slower if no Tensor Cores
Medium model (100M-1B)✓ Works✅ 2-3× faster
Large model (> 1B params)❌ May OOM✅ Required to fit in GPU
Batch size limited by memory❌ Can't increase✅ Can increase 2×
NVIDIA V100/A100/H100 GPU✓ Works✅ 2-3× faster (Tensor Cores)
Older GPU (GTX 1080, no Tensor Cores)✓ Works⚠️ Modest speedup

1.5 Edge Cases

python
# runnable
# 1. Loss functions that aren't AMP-safe
# Softmax, log_softmax, cross_entropy: OK in FP16
# Some custom losses may need FP32
# 2. BatchNorm in FP16
# PyTorch's BatchNorm automatically uses FP32 internally
# No special handling needed
# 3. Gradient accumulation
scaler = torch.cuda.amp.GradScaler()
for micro_batch in range(4):  # Accumulate over 4 micro-batches
    with torch.cuda.amp.autocast():
        loss = model(data) / 4  # Average across micro-batches
    scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
# 4. Model with FP32-only layers
# Set dtype for specific layers to FP32
model.half()  # Convert full model to FP16
# Or keep specific layers in FP32:
for layer in model.fp32_layers:
    layer.float()

1.6 Why This Matters

Mixed precision is standard practice for training large models:
  • GPT-3 (175B params): trained with mixed precision (wouldn't fit in FP32)
  • Stable Diffusion: uses AMP for faster training
  • BERT Large: 2-3× speedup with AMP on V100 GPUs Every modern training framework (PyTorch Lightning, HF Trainer, JAX) enables mixed precision by default.

2. 📐 Key Formulas / Concepts

ConceptImplementationEffect
AMP contexttorch.cuda.amp.autocast()FP16 for compute ops
Gradient scalingGradScaler().scale(loss).backward()Prevents underflow
Scaler.stepscaler.step(optimizer)Unscale + clip + step
Scaler.updatescaler.update()Adjust scale factor

3. ⚠️ Common Pitfalls

Pitfall 1: Using AMP Without Tensor Cores

Mistake: Expecting 2× speedup on GTX 1080 or older GPUs. Why: Tensor Cores (Volta+ architecture) provide the FP16 speedup. Older GPUs emulate FP16 in software, which can be slower than FP32. Fix: Check torch.cuda.get_device_capability() ≥ 7.0 for Tensor Core support.

Pitfall 2: Forgetting scaler.update()

Mistake: Calling scaler.step() but not scaler.update(). Why: The scale factor never changes, so it can't adapt to gradient magnitudes. Fix: Always call scaler.update() after scaler.step().

Pitfall 3: Gradient Clipping Before Unscaling

Mistake: Calling clip_grad_norm_ without unscaling first. Why: Scaled gradients have different norms. Clipping before unscaling clips at the wrong threshold. Fix: Call scaler.unscale_(optimizer) before clip_grad_norm_().

4. 📝 Practice Questions

Q1: You're training a ResNet-50 on a V100 GPU. FP32 training takes 4 hours. FP16 with AMP takes 1.5 hours. What causes the 2.67× speedup?
Speedup sources:
  1. Tensor Cores: V100 has Tensor Cores that perform FP16 matrix multiply at 8× the rate of FP32. Not all ops use Tensor Cores, so the real speedup is 2-4×.
  2. Memory bandwidth: FP16 uses half the memory, reducing data transfer time.
  3. Larger batch: If batch size was doubled (same memory), fewer iterations per epoch.
The actual speedup depends on how much of the computation uses Tensor Cores. ResNet-50 has many convolutions (Tensor Core-friendly), so 2.67× is reasonable. Q2: Your mixed precision training produces NaN losses every 50 steps. What's happening and how do you fix it?
Cause: Gradient overflow — the scaled gradients exceed FP16's range (~6e4). The scaler detects these as inf/nan and skips the step.
Fixes:
  1. Reduce initial scale: GradScaler(init_scale=2.**10) starts with a smaller scale
  2. Increase backoff_factor: backoff_factor=0.25 reduces scale more aggressively on overflow
  3. Check model architecture: Some operations (softmax with large logits, exp of large numbers) are prone to overflow in FP16
  4. Use BF16 (if available on Ampere+ GPUs): Better range eliminates most overflow issues
  5. Keep problematic layers in FP32: with torch.cuda.amp.autocast(enabled=False): for unstable operations
The scaler should handle this automatically by reducing the scale. If it happens too frequently, investigate the root cause.

5. 🔗 Cross-References

Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.