GPU memory management

PyTorch · Expert · 16 min

Out-of-memory errors are the most common blocker when scaling models; fixing them needs a model of where memory goes.

Training memory has four parts: parameters, gradients, optimizer state and activations saved for backward. Activations scale with batch size and sequence length and usually dominate.

PyTorch uses a caching allocator: freed blocks stay reserved for reuse instead of returning to the driver, because cudaMalloc is slow. So torch.cuda.memory_allocated() (live tensors) is lower than memory_reserved() (what the process holds and what nvidia-smi shows). torch.cuda.empty_cache() releases unused cached blocks to other processes; it does not free live tensors and rarely fixes an OOM.

Fragmentation happens when free memory is split into blocks too small for a request. Setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True lets segments grow and often reduces it.

To see what holds memory, record a memory snapshot with torch.cuda.memory._record_memory_history() and _dump_snapshot(), then open it in the visualiser at pytorch.org/memory_viz, which shows every allocation with its stack trace.

Fixes, roughly in order: smaller batch with gradient accumulation, mixed precision, activation checkpointing (torch.utils.checkpoint.checkpoint(block, x, use_reentrant=False), which drops a block's activations and recomputes them during backward, about 30% more compute), fused optimizers, then sharding with FSDP.

Example

import torch
from torch import nn
from torch.utils.checkpoint import checkpoint

assert torch.cuda.is_available(), "needs a CUDA GPU"
torch.cuda.memory._record_memory_history(max_entries=100_000)

block = nn.Sequential(nn.Linear(4096, 4096), nn.GELU(), nn.Linear(4096, 4096)).cuda()
x = torch.randn(64, 4096, device="cuda", requires_grad=True)

torch.cuda.reset_peak_memory_stats()
y = checkpoint(block, x, use_reentrant=False)   # keep only the block's input
y.sum().backward()                              # recompute activations here

mib = 2 ** 20
print(f"peak      {torch.cuda.max_memory_allocated() / mib:.0f} MiB")
print(f"allocated {torch.cuda.memory_allocated() / mib:.0f} MiB")
print(f"reserved  {torch.cuda.memory_reserved() / mib:.0f} MiB")

torch.cuda.memory._dump_snapshot("mem.pickle")  # drag into pytorch.org/memory_viz
torch.cuda.memory._record_memory_history(enabled=None)

Remember

  • Allocated = live tensors; reserved = cached by the allocator
  • Snapshots show exactly which allocations fill the GPU
  • Activation checkpointing trades ~30% compute for activation memory

Watch out for

  • Calling torch.cuda.empty_cache() in the training loop, which slows training and does not free live tensors.

Check yourself

  1. nvidia-smi shows 20 GB used but torch.cuda.memory_allocated() reports 8 GB. What explains the gap?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: pytorch.org/docs/stable/notes/cuda.html#memory-management

Builds on

Unlocks

Nothing builds on this yet.