Gradient Checkpointing for Memory-Efficient PyTorch Training
Make this article actionable
Send the article context into Vife Agent and turn it into a plan, checklist, or draft you can keep working on.
Introduction
Training large neural networks is often limited not by algorithmic ideas but by hardware: GPU memory. Activations (intermediate tensors saved for backprop) are a common bottleneck. Gradient checkpointing is a practical technique that trades extra computation for much lower peak memory usage, letting you train larger models or increase batch size without changing hardware.
This article moves you from the research idea to concrete execution in PyTorch. You’ll get practical code, profiling recipes, workflows for integrating checkpointing into a training loop, and a decision framework that compares checkpointing with other memory-saving strategies. By the end you’ll know when to use checkpointing, how to implement it safely with AMP (automatic mixed precision), and how to validate memory savings.
Turn the useful parts into next steps
Vife Agent can convert this guide into a prioritized workflow with tasks, risks, and reusable prompts.
Quick answer
- What is it? Gradient checkpointing (aka activation checkpointing) discards some forward activations and recomputes them during backward to save memory.
- When to use it? When activations are the dominant memory cost and compute overhead (typically 10–40% more FLOPs) is acceptable.
- How to use in PyTorch? Use
torch.utils.checkpoint.checkpointorcheckpoint_sequentialfor simple models; for Hugging Face Transformers usemodel.gradient_checkpointing_enable()for module-level checkpointing. - Main caveats: avoid in-place ops in checkpointed regions, ensure reproducible random behavior, and handle AMP/
autocastcarefully.
1. Why checkpointing works (short primer)
Autograd saves intermediate activations during the forward pass so it can compute gradients during the backward pass. If you discard some of those activations after the forward pass, you can re-run the needed portion of the forward pass during backward to recreate them. That reduces peak memory usage at the cost of extra forward computations.
Key trade-offs:
- Memory saved: proportional to the activations you drop.
- Time cost: proportional to the extra forward recomputation (often 1.2×–1.5× total time depending on how much you checkpoint).
- Implementation complexity: moderate; PyTorch makes it fairly easy for typical modules.
2. Basic PyTorch examples
Here are the core tools in PyTorch’s standard library.
torch.utils.checkpoint.checkpoint(function, *args)— checkpoint an arbitrary function that takes tensors and returns tensors. The function should be stateless (or at least not mutate inputs/buffers) and must not calltorch.no_grad()internally.torch.utils.checkpoint.checkpoint_sequential(module_list, segments, *inputs)— convenient when you have ann.Sequential-style stack and want to split it intosegmentsgroups.
Minimal example using checkpoint for a single block:
import torch
from torch import nn
from torch.utils.checkpoint import checkpoint
class Block(nn.Module):
def __init__(self, in_features, out_features):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_features, out_features),
nn.ReLU(),
nn.Linear(out_features, out_features),
)
def forward(self, x):
# This forward will be checkpointed from the caller
return self.net(x)
# In your model:
class BigModel(nn.Module):
def __init__(self):
super().__init__()
self.blocks = nn.ModuleList([Block(1024, 1024) for _ in range(10)])
self.head = nn.Linear(1024, 10)
def forward(self, x):
for i, block in enumerate(self.blocks):
# wrap block's forward with checkpoint
x = checkpoint(block, x)
return self.head(x)Using checkpoint_sequential with a nn.Sequential stack:
from torch.utils.checkpoint import checkpoint_sequential
seq = nn.Sequential(*[Block(1024, 1024) for _ in range(12)])
# split into 4 segments: each segment will be checkpointed
out = checkpoint_sequential(seq, 4, input_tensor)Notes:
- The function passed to
checkpointmust accept and return tensors (Python scalars or module objects will cause issues). - Don’t use in-place operations inside checkpointed functions.
3. Measuring memory and runtime impact (practical recipe)
Before and after comparisons are essential. Use the following recipe to measure GPU memory and time for training iterations.
import torch
import time
def profile_step(model, data, target, optimizer, device):
torch.cuda.reset_peak_memory_stats(device)
start_time = time.perf_counter()
optimizer.zero_grad()
output = model(data)
loss = torch.nn.functional.cross_entropy(output, target)
loss.backward()
optimizer.step()
torch.cuda.synchronize(device)
elapsed = time.perf_counter() - start_time
peak = torch.cuda.max_memory_allocated(device)
return elapsed, peakRun several warmup steps (5–10) before measuring. Compare the baseline model and the checkpointed model with identical batch size and seeds. Report both elapsed time and peak memory.
A realistic outcome: peak memory down by 20–60% depending on how many activations you checkpoint; wallclock time increases by 10–40%.
4. Integration with AMP (mixed precision)
AMP (automatic mixed precision) reduces memory mainly for weights and some activation storage. Combining checkpointing with AMP gives the best of both worlds but requires care.
Key point: checkpointed functions are re-executed during backward and must respect the same autocast context as during forward. If you use torch.cuda.amp.autocast around your forward pass, the recompute step must also run inside an autocast context of the same dtype.
Safe pattern:
from torch.cuda.amp import autocast
from torch.utils.checkpoint import checkpoint
def checkpointed_block(block, x):
# Wrap block call inside autocast to ensure dtype consistency during recompute
with autocast():
return block(x)
# Usage in forward:
with autocast():
for block in blocks:
x = checkpoint(checkpointed_block, block, x)Alternative: write a small wrapper that accepts block and input tensors, and always calls the block under autocast so recomputation sees the same mixed precision policy.
Note: when using GradScaler with AMP, the usual pattern of scaler.scale(loss).backward() remains unchanged.
5. Checkpointing at different granularities
Where to place checkpoints matters. You can checkpoint at the layer level, block level, or between groups of layers. Three typical strategies:
- Fine-grained: checkpoint many small blocks. Memory savings high but time overhead higher due to more recomputation.
- Coarse-grained: checkpoint only big blocks (e.g., between transformer layers grouped in chunks). Lower overhead but also lower memory saving.
- Hybrid: group small layers into segments (common with
checkpoint_sequential). This gives a balance.
Decision framework (simple):
- If activations dominate and you need maximum memory reduction: use fine-grained checkpointing.
- If you only need modest memory saving: checkpoint at coarse block boundaries.
- If you have expensive small ops where recompute is cheap: fine-grained works well.
6. Checklist: Implement checkpointing safely (step-by-step)
- Identify memory hotspots: profile activations and parameters.
- Decide checkpoint granularity (per layer, per block, or sequential segments).
- Remove or rewrite in-place operations inside checkpointed regions (e.g.,
relu_()->relu()). - Wrap checkpointed calls with
autocastif using AMP. - Ensure checkpointed functions don't use
torch.no_grad(). - Verify RNG consistency: avoid non-deterministic ops inside checkpointed regions or set seeds appropriately.
- Run warmup steps then measure peak memory and throughput.
- Validate training results (loss curves) to ensure no silent errors.
7. Common mistakes and how to fix them
-
In-place operations cause runtime errors or incorrect gradients.
- Fix: replace in-place ops with out-of-place equivalents.
-
Using
torch.no_grad()inside checkpointed functions.- Fix: remove it.
checkpointrequires autograd to record operations.
- Fix: remove it.
-
Non-tensor or Python object arguments to
checkpoint.- Fix: wrap non-tensor parameters inside tensors or closures; ensure the function signature matches PyTorch requirements. Example: use a lambda that only takes tensors and captures module by closure.
-
Wrong behavior with BatchNorm or other stateful modules.
- Issue: recomputation affects running statistics if modules update buffers during forward.
- Fixes: set BatchNorm to
evalfor checkpointed segments when appropriate, or avoid checkpointing across modules that modify buffers. For training you usually want BatchNorm updates; consider alternatives like sync-BN outside checkpointed regions, or checkpoint only on stateless blocks.
-
AMP dtype mismatch during recompute causing unexpected behavior.
- Fix: always run checkpointed functions inside the same
autocastcontext used in forward.
- Fix: always run checkpointed functions inside the same
-
Forgetting to reset peak memory counters when benchmarking.
- Fix: call
torch.cuda.reset_peak_memory_stats()before each measurement.
- Fix: call
8. Comparison table: memory-reduction techniques (decision framework)
| Technique | Memory reduction | Runtime cost | Implementation complexity | Good when... |
|---|---|---|---|---|
Gradient checkpointing | Medium–High | Medium (recompute) | Medium | Activations dominate memory but compute budget has headroom |
AMP (mixed precision) | Low–Medium | Low | Low | Want quick wins; stable with most codebases |
ZeRO / optimizer sharding (DeepSpeed) | High | Low–Medium | High | Large models across multiple GPUs / multi-node |
Activation offload (CPU) | High | High (PCIe/host transfer) | High | GPU memory tight and CPU/host available |
Model parallelism | High | Medium–High | High | Model too big for single GPU; can distribute weights |
Quantization | Low–High | Low | Medium | Inference primarily; sometimes training with specialized tooling |
Use this table to pick one or more techniques. Often the best results come from combining AMP + checkpointing + sharding.
9. Realistic workflows (three recipes)
Workflow A — Quick memory relief (minimal code change)
- Add AMP (
torch.cuda.amp.autocast) to your training loop. - Run a profiling step to measure memory.
- If memory still high, identify the largest contiguous module/block and wrap it with
checkpoint. - Re-run profile and iterate.
Workflow B — Controlled tradeoff (balanced speed and memory)
- Profile activations across layers to find where activations peak.
- Group layers into segments that balance compute and memory (use
checkpoint_sequentialwith 3–6 segments). - Use
autocastand GradScaler. - Benchmark and adjust number of segments to hit memory budget with acceptable throughput.
Workflow C — Large model scaling (multi-GPU)
- Combine ZeRO stage 1/2 (optimizer-state sharding) or DeepSpeed with AMP.
- Add gradient checkpointing across transformer blocks to reduce activation footprint.
- Profile multi-GPU runs; if CPU memory is abundant, consider activation offload.
- Validate training stability across nodes.
10. Concrete example: Checkpointing a Transformer encoder stack
This example demonstrates grouping transformer layers into segments and checkpointing each segment. It’s a simplified sketch; in production you’ll adapt to your model and library.
import torch
from torch import nn
from torch.utils.checkpoint import checkpoint_sequential
class SimpleTransformer(nn.Module):
def __init__(self, layer, n_layers):
super().__init__()
self.layers = nn.ModuleList([layer for _ in range(n_layers)])
self.head = nn.Linear(layer.embed_dim, 1000)
def forward(self, x):
# Create a sequential-like container for checkpoint_sequential
seq = nn.Sequential(*self.layers)
# Choose segments based on memory/compute tradeoff
segments = 4
x = checkpoint_sequential(seq, segments, x)
return self.head(x)Notes:
- If your transformer layer is stateful (e.g., caching past key/values for autoregressive models), you’ll need a custom checkpoint wrapper that preserves and restores these buffers correctly.
- For Hugging Face Transformers,
model.gradient_checkpointing_enable()will apply module-level checkpointing for standard transformer blocks. Still follow the checklist above and test carefully.
11. When not to use checkpointing
- When compute is already the bottleneck (e.g., you run near 100% GPU utilization on forward + backward). Checkpointing would only make training slower.
- For small models where activation memory is not the limiting factor.
- When your model uses many stateful or side-effecting layers that cannot be recomputed easily.
12. Advanced tips and integration
- Combine with optimizer-state sharding such as DeepSpeed ZeRO to scale beyond single-GPU memory limits.
- For models with many small operations, grouping them into segments reduces recomputation overhead.
- Consider the impact on gradient accumulation: when using gradient accumulation, the memory per micro-batch still matters. Checkpointing reduces per-step peak memory.
- Use PyTorch’s new features and community libraries: projects like DeepSpeed and FairScale provide utilities that integrate checkpointing with sharded training.
Put This Into Practice With an AI Agent
An AI agent (like a Vife Agent) can accelerate applying checkpointing in your codebase and help you run controlled experiments. Here are practical tasks an agent can do for you:
- Automatically identify memory hotspots by inserting profiling hooks and running a warmup + measurement pass.
- Propose candidate checkpoint boundaries (e.g., split layers into N segments) based on profiling data and your target memory budget.
- Generate the exact code changes (diffs/PRs) to insert
torch.utils.checkpointcalls or to enablemodel.gradient_checkpointing_enable()where applicable. - Run benchmark experiments (baseline vs checkpointed) and produce comparison reports: peak memory, throughput, and loss curves.
- Validate correctness by running short training checks and comparing outputs/gradients between baseline and checkpointed runs.
Example agent prompt (to paste into your Vife Agent):
"Profile this training script on one GPU and suggest where to insert gradient checkpointing to reduce peak memory to X GiB with minimal speed loss. Generate a patch with unit tests and produce a benchmarking table comparing baseline and patched runs."
The agent can iterate: try 2, 4, and 8 segments, run profiling, and return the best tradeoff. This moves you from guessing to measured decisions.
Checklist: sanity tests before you train for days
- Test one training step with checkpointing and confirm no exceptions.
- Compare loss and output of a single forward/backward step between baseline and checkpointed model (same seed).
- Run 5–10 training steps and verify loss decreases similarly.
- Profile peak memory and runtime and compare against baseline.
- Check BatchNorm running stats and any stateful behavior for correctness.
- Confirm AMP behavior and GradScaler operation if used.
- Add monitoring to your long runs (memory, time per step, divergence alerts).
FAQ
Q: How much memory can I realistically save? A: It depends on model architecture and how many activations you checkpoint. Typical savings range from 20% to 60% of activation memory. If activations dominate GPU usage, savings can be larger.
Q: Does checkpointing change training results? A: It shouldn’t change numerical gradients if implemented correctly, but it can expose bugs (e.g., in-place ops, RNG differences). Always validate with short-run comparisons.
Q: Is checkpointing compatible with DataParallel or DistributedDataParallel (DDP)? A: Yes. Checkpointing works with DDP; checkpointed recomputation happens locally on each rank. Take care with stateful modules like BatchNorm — consider SyncBatchNorm or keeping BatchNorm out of checkpointed regions.
Q: Can I checkpoint arbitrary Python code?
A: Only operations that are recorded by autograd (tensor ops). Non-tensor state needs careful handling. Functions passed to checkpoint should accept and return tensors.
Q: What about in-place operations? A: In-place operations typically break checkpointing because autograd expects to see original tensors during recompute. Replace in-place ops with out-of-place versions.
Q: Are there better alternatives? A: For multi-GPU large models, ZeRO/DeepSpeed and optimizer sharding can be more effective. Activation offload can also help when host memory is available. Often you’ll combine techniques.
Common pitfalls with examples
- Broken in-place op example:
# BAD
x = x.relu_() # in-place; can break checkpointing
# GOOD
x = x.relu()- Autocast and checkpoint mismatch:
# BAD: forward under autocast but checkpointed function doesn't use autocast
with autocast():
x = checkpoint(block, x)
# BETTER: ensure block runs inside autocast on recompute too
def cp_block(block, x):
with autocast():
return block(x)
with autocast():
x = checkpoint(cp_block, block, x)- Non-deterministic ops inside checkpointed regions:
If your forward uses random dropout and your reproducibility policy depends on RNG states, recomputation will invoke the same ops again but not necessarily in the same RNG state unless you manage seeds. The usual approach is to let autograd handle it — in practice dropout behavior is consistent for recompute in the same forward/backward because the RNG state sequence is the same — but be cautious when doing custom random operations.
Conclusion
Gradient checkpointing is a practical, well-supported technique in PyTorch to trade extra computation for significantly reduced GPU memory usage. It’s especially useful when you need to train larger models or increase batch sizes without access to more GPU RAM.
Key takeaways:
- Profile first. Know whether activations are the bottleneck.
- Start conservatively: checkpoint coarse blocks or use
checkpoint_sequentialbefore moving to fine-grained checkpointing. - Combine with AMP and optimizer sharding for maximum effect.
- Watch out for in-place ops, RNG issues, and stateful modules.
If you want to move from experimentation to production faster, try automating the profiling and code changes with an AI agent. A Vife Agent can run experiments, generate diffs, and produce benchmarking reports so you can pick the configuration that hits your memory/throughput targets.
Ready to scale your training runs? Use Vife Agent to profile your codebase, propose checkpoint boundaries, and generate the exact patches to run experiments — then compare results and iterate automatically.

