7.7 — Activation checkpointing (gradient checkpointing)
Goal: Derive activation checkpointing as a compute-memory trade: store activations at every √L blocks instead of every block; recompute the missing ones during the backward pass. Quantify the trade (memory ↓ √L, compute ↑ 33%) and identify where it sits in the modern training stack.
1. Why this session exists
Activation memory dominates the per-step training budget (7.3 §5.5, 7.4 §7). For a 7B-parameter model at in bf16, activations are tens of GB per layer; across layers, the total exceeds any single GPU's memory by an order of magnitude.
7.6 gave us one tool — gradient accumulation — that limits batch size without changing per-batch memory. Activation checkpointing is the complementary tool: it limits per-batch activation memory without changing batch size.
The idea is classical autodiff 2.11: the backward pass needs every intermediate activation, but we can choose to store some and recompute others. The trade-off is compute vs memory. Pick the right balance and you can train models with activation memory smaller at the cost of more compute.
Introduced by Chen, Xu, Zhang & Guestrin, 2016, Training Deep Nets with Sublinear Memory Cost (arXiv:1604.06174) — sometimes called "gradient checkpointing" or "activation recomputation." Universal in modern LLM training.
2. The default: store everything
Recall the backward pass (2.11 §3). For a forward chain with , the chain rule produces
where the Jacobian requires both (to evaluate the Jacobian function) and the weights of . All must be available during the backward pass.
In a standard implementation, the forward pass stores every as it goes. Memory cost:
where is the size of one activation tensor (a function of , etc., in fp16/bf16). For GPT-2-small at : MB; over layers, MB. For LLaMA-7B at : GB; over layers, GB. For the activations inside each block (attention scores especially) the numbers are an order of magnitude larger; 7.3 §5.5 gave the full table.
The naive approach is impossible at scale.
3. The other extreme: recompute everything
Imagine the opposite: store only . To get at layer , re-run the forward pass from up to (using the now-stored gradient at ). After computing , discard everything except .
This costs recomputations for each of layers — total forward FLOPs on top of the original . Memory drops to per layer's activations, but compute blows up quadratically.
Modern training cannot afford either extreme: store-everything blows out memory; recompute-everything blows out compute. The sweet spot is in between.
4. The trick
Chen et al. (2016)'s[1] key idea. Partition the layers into groups of layers each. Store only the activations at the boundaries of each group; recompute the inner activations of each group during backward.
4.1 The recipe
- Forward pass. Run normally; store the activations at . Discard everything else.
- Backward pass. When you need the gradient at some intermediate , identify which group belongs to. Re-run the forward from the previous checkpoint (e.g., from ) up to , regenerating the missing activations. Compute the gradient. Then discard the regenerated activations and move on.
4.2 Memory and compute
Memory. Stored boundary activations: entries, each in size. In-flight recomputation buffer: at most activations from one group, since we recompute one group at a time. Total:
Down from by a factor of . For (GPT-3 depth), this is roughly less memory.
Compute. The naive forward is block-forwards. Checkpointing adds one extra forward for each group of blocks during the backward — total groups, each contributing block-forwards. So the extra compute is block-forwards.
The backward itself is roughly the forward in compute. So total cost without checkpointing is forward-equivalents per step. With checkpointing, it's forward-equivalents. That is 33% more compute.
The boxed trade:
For (GPT-3): memory ÷ , compute × . An excellent trade.
5. The general checkpoint pattern
We don't have to use groups; any partition with works. With checkpoints:
For uniform , optimising memory (minimising the sum) gives — the §4 choice.
Modern implementations often go further: checkpoint every block. That's , with no intra-block recomputation needed (each block recomputed fully during its own backward). Memory: — only the residual stream is stored, not the intermediate FFN hidden / attention scores. Compute overhead: still .
This is the standard PyTorch idiom:
from torch.utils.checkpoint import checkpoint
def block_forward(X):
# one residual-stream → residual-stream block
X = X + attn(ln1(X))
X = X + ffn(ln2(X))
return X
# In the model's forward pass:
for block_module in self.blocks:
X = checkpoint(block_module, X, use_reentrant=False)
checkpoint(module, X) calls module(X) during forward but does not store the internal activations. During backward, it re-runs module(X) to regenerate them on demand.
6. Why this is the right granularity
Why checkpoint at the block boundary rather than at every layer (FFN, LN, attention)?
Inside one block, the biggest activations are the attention scores (size , growing with ) and the FFN hidden . Both are dropped when we re-run the block from its residual-stream input. So checkpointing the block sheds the worst-case attention-score and FFN-hidden buffers — what we want.
The cost of recomputing one block is: one MHA forward + one FFN forward + two LNs extra over the original forward (the inner detail: backward is forward; adding one extra forward makes total , hence ).
You could checkpoint at finer granularity (per-layer within a block) but the memory savings diminish: the residual-stream is anyway preserved across the block, and the per-layer activations within a block aren't the biggest things. Block-level checkpointing is the sweet spot.
7. Selective checkpointing
A 2023+ refinement: instead of checkpointing whole blocks, checkpoint only the most-expensive-to-store activations. The clearest example: attention scores.
Korthikanti et al., 2022[2], Reducing Activation Recomputation in Large Transformer Models (arXiv:2205.05198) describe selective activation recomputation in Megatron-LM. Their key insight:
- Storing the residual stream + FFN hidden + LN inputs is cheap (linear in ).
- Storing the attention scores is expensive (quadratic in , ).
- The attention scores are also cheap to recompute (one extra matmul).
So: store everything except the attention scores; recompute scores during backward. This shaves the memory term while adding only the cost of one matmul — much less than 33% overhead.
For LLaMA-7B at : selective recomputation drops activation memory from GB to GB at extra compute, vs full block-level checkpointing which drops to GB at extra compute. Different sweet spots; both are used in production.
7.1 FlashAttention as the natural fit
FlashAttention (Dao et al., 2022)[3] is a fused attention kernel that never materialises the score matrix in HBM — it tiles the and softmax into SRAM-friendly blocks. Selective recomputation of attention scores is essentially "what FlashAttention already does for free." The two ideas converge.
In practice: any modern LLM trained with PyTorch's scaled_dot_product_attention (with the is_causal flag and SDPA-backend selection) gets attention-score recomputation automatically. You don't need to manually checkpoint the attention sub-layer; it's handled at the kernel level. Block-level checkpointing then only covers the FFN and residual-stream activations.
We come back to FlashAttention in Cap 10.5 (inference side, but the same kernel is used in training).
8. Interaction with gradient accumulation
Checkpointing and gradient accumulation are independent and additive.
- Gradient accumulation: reduces peak memory by processing fewer samples per micro-batch (7.6).
- Activation checkpointing: reduces per-micro-batch peak memory by recomputing instead of storing activations.
Combined: you can train a 7B model at 4M-token effective batch on relatively modest hardware. A typical recipe:
- Per-device micro-batch: sequences, .
- Per-block activation checkpointing: ~5 GB activation memory per device.
- Accumulation: steps to reach 16 micro-batches × devices × M tokens-per-step.
- Total memory: GB for non-activations + GB activations = GB. Fits on a single H100 (80 GB) with comfortable headroom.
Without checkpointing, the activations would be GB; with the optimiser state added, training would not fit. Checkpointing is the load-bearing memory optimisation.
9. Why it always works (and a tiny exception)
The mathematical guarantee: as long as the forward pass is deterministic given its inputs, recomputing it produces the same result. Backward gradients are correct to floating-point precision.
The one exception is dropout. Dropout uses a random mask per forward pass; if you re-run the forward, you get a different dropout mask, which produces inconsistent gradients between forward and backward.
The fix: when checkpointing a block that contains dropout, save the random state during the original forward; restore it before the recomputation. PyTorch's torch.utils.checkpoint does this automatically when preserve_rng_state=True (the default).
You almost never have to think about this manually. Just be aware: if dropout-containing blocks are checkpointed and the RNG state isn't preserved, you get silent gradient miscomputation. PyTorch warns you if this case looks suspicious.
10. What we keep going forward
Three things to keep:
-
The trick: store checkpoints, recompute the rest. Memory ÷ , compute × . Block-level checkpointing achieves this with minimal code: wrap each block in
torch.utils.checkpoint. -
Selective recomputation: don't store the attention-score matrix (quadratic in ); recompute it during backward. Combined with FlashAttention, the attention contribution is handled automatically.
-
Activation checkpointing + gradient accumulation = practical pretraining recipe. The two compose: accumulation manages effective batch size; checkpointing manages per-micro-batch activation memory. Together they let a 7B-class model train comfortably on – H100s at 2–4M tokens-per-step.
This closes the single-machine training story. Next session (Cap 7.8 — Data parallelism, in Part 2) (Part 2 of this chapter) starts the distributed-training story: data parallelism, then tensor and pipeline parallelism, then FSDP/ZeRO, and finally scaling laws.
(a) Verify the memory analysis. For , what is the memory ratio between naive (store everything) and -checkpointed training? Quantify the compute overhead.
(b) A 7B model has blocks, , . The attention scores at one block are entries. In bf16, this is GB. With selective recomputation, this GB per block is not stored. How many GB does this save across all 32 blocks?
(c) Trace the backward pass with block-level checkpointing for a stack. Identify, step by step, when each block's forward is run. (You should run it twice each.)
(d) Why is dropout a special case for activation checkpointing? Argue why PyTorch's preserve_rng_state=True is the right fix.
(e) Selective recomputation reduces memory by per block at compute cost of one extra matmul. Argue when this is preferable to full block-level checkpointing. (Hint: think about the ratio of attention-score memory to other-activation memory at long context.)
References
- [1]
Chen, T., Xu, B., Zhang, C., & Guestrin, C. (2016). "Training deep nets with sublinear memory cost". arXiv:1604.06174 — Activation checkpointing / gradient checkpointing (7.7 §4).
↩ - [2]
Korthikanti, V., Casper, J., Lym, S., et al. (2022). "Reducing activation recomputation in large transformer models". arXiv:2205.05198 — Selective recomputation in Megatron-LM (7.7 §7).
↩ - [3]
Dao, T., Fu, D. Y., Ermon, S., Rudra, A., & Ré, C. (2022). "FlashAttention: Fast and memory-efficient exact attention with IO-awareness". arXiv:2205.14135 — Tiled attention kernel; in-SRAM softmax (7.7 §7.1; full treatment Cap 10.5).
↩