Distributed Training intermediate 9 min read 5 flashcards

Gradient Checkpointing, Activation Recomputation, and CPU Offload

Why activations - not weights - usually dominate training memory, and how recomputation and CPU/NVMe offload trade compute and bandwidth to fit larger models.

Most engineers' mental model of training memory is "model + optimiser state." For long sequences and large batches that picture is wrong: activations dominate. Every layer's forward output has to be retained until its backward uses it, and activation memory grows linearly with depth, batch, and sequence length. Gradient checkpointing trades extra compute for activation memory by recomputing chunks on the backward pass. When even that is not enough, ZeRO-Offload pushes parameters and optimiser state to CPU DRAM or NVMe.

Why activations dominate

For a transformer with L layers, hidden size h, sequence length s, and batch b, the activation memory for a single forward pass is roughly:

activations ~ L * s * b * h * (constant for attention + MLP intermediates)

The constant is large (Megatron's accounting puts it at 34-ish bytes per token per layer in BF16, before sequence-quadratic attention terms). For a 70B model with L=80, h=8192, s=8192, b=4, that is around 700 GB of activations - far more than the 140 GB of weights. Activations are the constraint, not parameters.

Checkpointing: save 50-80% memory at ~30% compute cost

The 2016 sublinear-memory paper made the idea explicit: pick a subset of layers (checkpoints) whose activations you save. For the layers between checkpoints, discard the activations during forward. On the backward pass, when you need those activations, recompute them from the nearest saved checkpoint.

without checkpointing:
    forward:  save activations for every layer
    backward: read saved activations
    memory:   O(L)   compute: 1x forward + 1x backward

with checkpointing every k layers:
    forward:  save activations only at checkpoint layers
    backward: recompute the k-1 intermediate layers, then backward through them
    memory:   O(L/k)   compute: 1x forward + 1x recompute-forward + 1x backward

Checkpointing every layer (most aggressive) gets a roughly 2x activation memory reduction at the cost of one extra forward pass (~33% more compute since backward is roughly 2x forward). Selective checkpointing - only checkpointing the cheap-to-recompute pieces - gets most of the saving for less of the overhead.

PyTorch ships torch.utils.checkpoint.checkpoint (per-call) and checkpoint_sequential (for nn.Sequential blocks). FSDP and DeepSpeed both wire it in at the transformer-block level by default.

from torch.utils.checkpoint import checkpoint

class Block(nn.Module):
    def forward(self, x):
        return checkpoint(self._forward, x, use_reentrant=False)

    def _forward(self, x):
        x = self.attn(x)
        x = self.mlp(x)
        return x

Selective activation checkpointing

Not every operation has the same recompute cost. Attention's softmax(QK^T / sqrt(d)) is FLOP-cheap but memory-heavy (it materialises the full s x s attention matrix). The MLP intermediates are FLOP-heavy but smaller. Selective checkpointing saves the cheap-to-recompute tensors and recomputes only the expensive ones.

Megatron-LM's selective recomputation policy saves the inputs to attention and the inputs to MLP, then recomputes both. It cuts activation memory by roughly 5x at only ~5% throughput cost (rather than 30%+ for full checkpointing). FlashAttention does its own variant - the backward kernel recomputes the attention matrix on-the-fly inside the SRAM tile, so the s x s matrix never has to be materialised in HBM at all.

ZeRO-Offload: CPU and NVMe as extra memory tiers

When GPU HBM is full even with checkpointing, ZeRO-Offload moves data to host DRAM (and optionally NVMe). The standard split:

Buffer Lives on When it moves
Forward / backward compute GPU always
Activations (post-checkpoint) GPU always
Gradients GPU during backward reduce-scattered, then moved to CPU
Optimiser state (Adam moments, FP32 master) CPU DRAM stays on CPU; optimiser step runs on CPU
Updated parameters CPU then back to GPU streamed back via PCIe

The optimiser step actually runs on the CPU. DeepSpeed ships an AVX-optimised CPU Adam kernel that is fast enough that the GPU is not idling for long, especially when the PCIe transfer overlaps with the next forward.

ZeRO-Infinity extends this by paging optimiser state to NVMe instead of DRAM. NVMe bandwidth is roughly 5-15 GB/s per drive (vs 32 GB/s for PCIe Gen4, ~3 TB/s for HBM3); it is the slowest tier and only worth it when DRAM is also full.

When offload is faster than swapping to a bigger machine

Offload sounds slow, and it is - PCIe Gen4 x16 caps at ~32 GB/s, HBM3 at ~3 TB/s, a hundred-fold difference. So why does anyone use it?

  • You already own the hardware. Buying or renting H100s with more HBM is not always an option this quarter.
  • The optimiser step is small relative to compute. A 100B-parameter model has roughly 1.6 TB of FP32 Adam state (16 bytes per param). Streaming that over PCIe at 32 GB/s takes ~50 seconds; if your training step is 30 seconds of GPU compute, you only lose ~30% throughput, not 100x.
  • Single-GPU usability. ZeRO-Offload lets a 13B model train on a single 24 GB consumer card, which has democratised fine-tuning more than any other technique.

The break-even calculation: offload wins when the cost of the extra cluster nodes (to fit the model on GPU memory) exceeds the throughput loss from PCIe/NVMe bandwidth. For research and fine-tuning on small clusters, that line moves a lot.

Trade-offs

Technique Memory saved Throughput cost Engineering cost
Full activation checkpointing ~50-80% of activation memory ~30% (extra forward) trivial (wrap modules)
Selective checkpointing ~50-70% ~5% moderate (need to pick what to save)
FlashAttention attention matrix entirely negative (faster) install + enable
ZeRO-Offload (CPU) ~80% of GPU memory 10-50% depending on step time configure DeepSpeed/FSDP
ZeRO-Infinity (NVMe) nearly all GPU memory 50-90% configure NVMe, accept slow optimiser step

When it falls down

  • CPU memory pressure. Offloading a 70B model needs ~1 TB of DRAM. Cheap nodes do not have it.
  • PCIe contention. If you are already moving training data over PCIe (image / video pipelines, network I/O), the optimiser-state stream competes.
  • Tiny models. For a 1B-parameter model, the offload setup time exceeds the saving. Just use BF16 and a single GPU.
  • Mixing offload and 3D parallelism is painful. Most production configs pick one approach. The interaction surface between FSDP-offload and pipeline parallelism is sharp; debug with nsys or expect hangs.

Further reading

Check yourself

5 flashcards for this concept

Click a card to reveal the answer.

Drill the whole track