Fix RuntimeError: CUDA out of memory (PyTorch)
Quick answer
RuntimeError: CUDA out of memory means the GPU ran out of room for a tensor allocation. Read the error's numbers first: if reserved memory is much larger than allocated, it is fragmentation — set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True. If allocated fills the card, you genuinely need less memory: lower the batch size, use gradient accumulation, enable mixed precision (AMP), add gradient checkpointing, or quantize. torch.cuda.empty_cache() does not reduce your peak usage, so it rarely fixes OOM.
The error that stops every GPU training run eventually:
RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB (GPU 0;
15.78 GiB total capacity; 13.52 GiB already allocated; 1.23 GiB free;
13.90 GiB reserved in total by PyTorch)Most guides jump straight to "reduce your batch size" or "call
torch.cuda.empty_cache()." The first is sometimes right; the second almost never
helps. Before you change anything, the numbers in the error tell you which
problem you actually have — and there are two very different ones, with
different fixes.
This guide reads the error properly, separates fragmentation from genuine memory pressure, then walks the memory-reduction levers in order of impact.
Quick Answer
Read the numbers. If reserved ≫ allocated, it's fragmentation — set
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True. If allocated fills the
card, you genuinely need less memory: lower batch size, use gradient
accumulation, enable mixed precision (AMP), add gradient checkpointing,
or quantize. torch.cuda.empty_cache() does not lower peak usage, so it
rarely fixes OOM.
TL;DR
- Read allocated vs reserved vs free before touching code.
- reserved ≫ allocated → fragmentation →
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True. - allocated ≈ total → genuinely out → reduce memory.
- Levers by impact: batch size ↓ → gradient accumulation → AMP → checkpointing → quantize → shard.
empty_cache()is a myth for fixing OOM in your own run.- Wrap eval in
torch.no_grad(); useloss.item(), nottotal += loss.
Read the Error First
Four numbers matter:
| Number | Meaning |
|---|---|
| total capacity | Physical GPU memory (e.g. 15.78 GiB) |
| allocated | Memory holding live tensors right now |
| reserved | Memory PyTorch's caching allocator has claimed from the GPU |
| free | Memory not yet reserved, available for a new block |
The decisive comparison is reserved vs allocated:
- reserved ≈ allocated (like
13.90vs13.52above) → the card is genuinely full. You must use less memory. - reserved ≫ allocated (e.g.
13.90reserved but only6allocated) → fragmentation. PyTorch holds lots of memory in small cached blocks, none big enough for the new allocation. Fix the allocator, not the model.
The error even hints at it: "If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation."
The Fix Decision
Step 0: Rule out another process
Before assuming your code is the problem, check who's on the GPU:
nvidia-smiIf another PID holds most of the memory (a zombie run, a notebook you forgot,
someone else's job), that's your fix — kill it or move to a free GPU with
CUDA_VISIBLE_DEVICES=1.
Step 1: Fragmentation → tune the allocator
If reserved ≫ allocated, let the allocator manage memory more flexibly. Set this
before the process starts:
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True python train.pyexpandable_segments:True→ segments grow instead of fragmenting; the modern, usually-best option.- Older alternative:
PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128caps block splitting, which also reduces fragmentation. - This can't create memory — it only helps when memory is reserved-but-unusable.
Step 2: Genuinely out → reduce memory, in order of impact
When the card is truly full, apply these from cheapest to most involved.
Lower the batch size (biggest, simplest lever)
Activation memory scales roughly linearly with batch size. Halving the batch roughly halves activation memory:
# 32 → 8 if you're OOMing
train_loader = DataLoader(dataset, batch_size=8, shuffle=True)Gradient accumulation (keep the effective batch)
Don't want to lose the large effective batch? Run small micro-batches and step the optimizer every N of them:
accum_steps = 4 # effective batch = micro_batch * 4
optimizer.zero_grad()
for i, (x, y) in enumerate(loader):
loss = model(x, y) / accum_steps # scale so grads average correctly
loss.backward()
if (i + 1) % accum_steps == 0:
optimizer.step()
optimizer.zero_grad()- Peak memory tracks the micro-batch, not the effective batch.
loss / accum_stepskeeps the gradient magnitude equivalent to a full batch.
Mixed precision (AMP) — ~half the activation memory
Run the forward/backward in float16/bfloat16 where safe:
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for x, y in loader:
optimizer.zero_grad()
with autocast(): # fp16/bf16 ops
loss = model(x, y)
scaler.scale(loss).backward() # scaled to avoid underflow
scaler.step(optimizer)
scaler.update()autocast()casts eligible ops to half precision → smaller activations.GradScalerprevents fp16 gradient underflow.
Gradient checkpointing (trade compute for memory)
Recompute activations during backward instead of storing them — big memory savings for deep models, at ~20–30% more compute:
# Hugging Face models
model.gradient_checkpointing_enable()
# Raw PyTorch
from torch.utils.checkpoint import checkpoint
out = checkpoint(expensive_block, x)Quantize or use a smaller model
For inference or QLoRA-style training, load in 8-bit/4-bit to cut weight memory dramatically:
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
bnb = BitsAndBytesConfig(load_in_4bit=True)
model = AutoModelForCausalLM.from_pretrained("model", quantization_config=bnb)If quantization fails to initialize, see fixing bitsandbytes CUDA Setup failed.
Shard across GPUs
When one model won't fit any single card, split it: FSDP
(torch.distributed.fsdp) or DeepSpeed ZeRO shard parameters, gradients, and
optimizer state across devices.
The Silent Leaks Everyone Hits
Two coding mistakes cause OOM that no batch-size change fixes.
Retaining the graph during eval
Inference and validation don't need gradients. Without no_grad, PyTorch stores
the whole graph:
# ❌ builds and keeps the autograd graph for every batch
for x, y in val_loader:
preds = model(x)
# ✅ no graph, far less memory
with torch.no_grad(): # or torch.inference_mode()
for x, y in val_loader:
preds = model(x)Accumulating tensors with grad history
total_loss += loss keeps every batch's graph alive because loss still carries
history. Detach the scalar:
# ❌ leaks — loss keeps the graph
total_loss += loss
# ✅ store the plain number
total_loss += loss.item() # or loss.detach()The same applies to appending tensors to a list for logging — call .item() or
.detach().cpu() first.
The empty_cache() myth. torch.cuda.empty_cache() returns PyTorch's cached
but unused memory to the OS. It does not lower your peak usage, so it does not
prevent an OOM that your code will hit again a moment later. It's only useful to
free memory for other processes. Don't sprinkle it in your training loop
expecting a fix.
Debugging: See What's Using Memory
import torch
print(torch.cuda.memory_allocated() / 1e9, "GiB allocated")
print(torch.cuda.max_memory_allocated() / 1e9, "GiB peak")
print(torch.cuda.memory_summary()) # full breakdown by size classmax_memory_allocated()is the number that must fit under your card's capacity.- Reset the peak between phases with
torch.cuda.reset_peak_memory_stats(). - On GPU, an OOM traceback can point at the wrong line due to async execution — see
the device-side assert guide for the same
CUDA_LAUNCH_BLOCKING=1technique.
Verification Steps
torch.cuda.max_memory_allocated()stays comfortably under total capacity.- One full epoch (train + validation) completes without OOM.
- Memory doesn't creep upward across iterations — a rising
memory_allocated()over steps means a leak (retained graph or tensors). - With
expandable_segments:True,reservedtracksallocatedclosely. nvidia-smishows only your process on the target GPU.
Green state: peak allocated fits under capacity with headroom, memory is flat
across steps (no creep), and validation runs under no_grad.
Prevention
- Wrap all eval/inference in
torch.no_grad()/inference_mode(). - Log losses with
.item(), never accumulate live tensors. - Set a batch size that leaves headroom; use accumulation for large effective batches.
- Enable AMP by default for training on modern GPUs.
- Track
max_memory_allocated()and alert if it nears capacity. - Set
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:Truefor long/large jobs. - Pin one job per GPU, or partition with
CUDA_VISIBLE_DEVICES.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
reserved ≫ allocated | Fragmentation | PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True |
allocated ≈ total | Genuinely full | Reduce batch / AMP / checkpointing |
| OOM only in validation | No no_grad | Wrap eval in torch.no_grad() |
| Memory grows each step | Retained graph/tensors | .item() / .detach(); del refs |
| OOM on a huge model | Won't fit one GPU | Quantize or shard (FSDP/ZeRO) |
| Another PID in nvidia-smi | Competing process | Kill it / CUDA_VISIBLE_DEVICES |
empty_cache() didn't help | Wrong tool | Reduce actual peak usage |
| OOM at first big batch | Batch/seq too large | Lower batch or sequence length |
Related Guides
- Fix "bitsandbytes CUDA Setup failed"
- Resolve CUDA error: device-side assert triggered
- Fix torch.cuda.is_available() returns False
- Fix "Token indices sequence length is longer than the specified maximum"
- A practical guide to 4-bit QLoRA fine-tuning
External References
- PyTorch — CUDA memory management
- PyTorch — Automatic mixed precision (AMP)
- PyTorch — torch.utils.checkpoint
- PyTorch — FSDP
FAQs
Is CUDA out of memory a bug in PyTorch? No — it means your workload's peak memory exceeded the GPU's capacity, or memory fragmented. It's a resource/config issue you resolve by using less memory or tuning the allocator, not a framework bug.
Why does the first iteration OOM but not later ones? Peak memory is usually hit on the first forward/backward when activations and the graph are largest, or on the first batch with the longest sequence. If it survives the first iteration, it typically survives the rest — unless you have a leak.
Does a smaller learning rate help OOM? No. Learning rate doesn't affect memory. Only the sizes of tensors (batch, sequence, model, activations) and whether you keep the graph do.
Should I use bfloat16 or float16 for AMP?
On Ampere and newer GPUs, bfloat16 is more numerically stable and often needs no
GradScaler. On older cards, float16 with GradScaler is the path. Both roughly
halve activation memory.
Can I catch the OOM and retry with a smaller batch?
You can, but after an OOM the CUDA context may be in a poor state; it's cleaner to
size the batch correctly up front (or use accumulation). If you do retry, call
torch.cuda.empty_cache() first — this is the one place it genuinely helps, by
releasing the failed allocation's cache before the smaller retry.
Key takeaways
- •Read the numbers: allocated vs reserved vs free tells you whether it is fragmentation or genuine pressure.
- •reserved >> allocated is fragmentation — fix it with PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True.
- •If allocated fills the GPU, reduce memory: batch size, gradient accumulation, AMP, checkpointing, quantization.
- •torch.cuda.empty_cache() returns cached memory to the OS but does not lower your peak usage — it rarely fixes OOM.
- •Wrap inference and validation in torch.no_grad() or torch.inference_mode() to avoid storing the graph.
- •Accumulating loss with += keeps the whole graph in memory — use loss.item() or .detach().
Frequently asked questions
What causes RuntimeError: CUDA out of memory in PyTorch?
The GPU could not fit a tensor allocation. Common causes are too large a batch size or input, a model too big for the card, keeping the autograd graph during evaluation, retaining tensors across iterations, or memory fragmentation where PyTorch has reserved memory it cannot reuse for the requested allocation.
Does torch.cuda.empty_cache() fix CUDA out of memory?
Usually not. It releases PyTorch's cached but unused memory back to the OS, which only helps other processes. It does not reduce your peak memory usage, so within a single run it rarely prevents OOM. Reducing batch size, using AMP, or checkpointing addresses the actual cause.
What does PYTORCH_CUDA_ALLOC_CONF do?
It tunes PyTorch's CUDA caching allocator. Setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True lets the allocator grow segments and reduces fragmentation, which is the fix when reserved memory is much larger than allocated. The older max_split_size_mb option limits split sizes to the same end.
How do I reduce GPU memory without hurting my batch size?
Use gradient accumulation: run several small micro-batches and only step the optimizer after N of them, which keeps the effective batch size while cutting peak memory. Combine it with mixed precision (AMP) and gradient checkpointing for larger savings at the cost of some speed.
Why do I get CUDA out of memory only during validation?
Because you probably did not wrap the validation loop in torch.no_grad() or torch.inference_mode(), so PyTorch stores the autograd graph for every batch. Wrap evaluation in torch.no_grad(), and avoid keeping tensors with grad history in lists.
How do I see how much GPU memory PyTorch is using?
Use torch.cuda.memory_allocated() for current allocations, torch.cuda.max_memory_allocated() for the peak, and torch.cuda.memory_summary() for a full breakdown. nvidia-smi shows total GPU usage across all processes, which reveals other jobs competing for the card.