Start Building →
Guide

CUDA out of memory in PyTorch: 9 fixes, with measured VRAM savings

CUDA out of memory is rarely random: the error names its own cause. Read the five numbers in the message first, then work through nine fixes from free to expensive.

Author photo
packet.ai Team
November 10, 2026

CUDA out of memory means PyTorch asked for more GPU memory than was free, and in Hugging Face's published run gradient accumulation cut peak use from 14,949 MB to 8,681 MB while gradient checkpointing took it to 6,775 MB.

Key takeaways

  • The error message reports total capacity, free memory, memory held by other processes, memory allocated by PyTorch and memory reserved but unallocated. Read those five numbers before changing code.
  • Fixes 1 to 3 cost nothing: check what else holds the GPU, free cached memory, and set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True.
  • In Hugging Face's bert-large-uncased run, peak GPU memory fell from 14,949 MB to 8,681 MB with gradient accumulation and to 6,775 MB with gradient checkpointing added.
  • Gradient checkpointing is not free: that run's epoch time rose from 57.82 s to 85.47 s, and Hugging Face's rule of thumb is about 20 percent slower.
  • Mixed-precision AdamW needs 18 bytes per parameter plus activations, and 8-bit AdamW cuts optimizer state from 8 bytes to 2 bytes per parameter.
  • QLoRA reduced the average memory needed to fine-tune a 65B parameter model from more than 780 GB to 48 GB in the original paper.

CUDA out of memory stops a PyTorch job when one allocation asks for more GPU memory than is free. The right fix depends on which number in the error is too large. This guide orders nine fixes from free to expensive, so you stop changing code that is not the problem.

Every memory figure here comes from Hugging Face documentation, PyTorch documentation or the QLoRA paper. None of it is from a packet.ai benchmark run, and each figure is labelled with its source.

This post is part of our fine-tuning series. If you are still choosing a card, start with the best GPU for fine-tuning LLMs. For how much memory a specific model needs, the VRAM requirements guide covers sizing, and this post stays on debugging.

Nine PyTorch OOM fixes at a glance, cheapest first

The order follows cost: free configuration changes first, then changes that trade speed for memory, then changes that trade accuracy risk for memory. Skip ahead only if the error message points you there.

#FixCostTry it whenDocumented effect
1Check what else holds GPU memoryFreeThe error names another process using memoryFrees memory held outside your job
2Free references and cached blocksFreeNotebooks and repeated experimentsReturns cached blocks to the driver; does not raise PyTorch's own limit (PyTorch docs)
3Set expandable segmentsFreeReserved but unallocated memory is largeTargets fragmentation; the error text itself recommends it
4Lower batch size or sequence lengthFree, may change trainingAllocated by PyTorch is close to total capacityActivation memory depends on sequence length, hidden size and batch size (Hugging Face)
5Gradient accumulationSlightly slowerYou still need the larger effective batch14,949 MB to 8,681 MB in Hugging Face's run
6Mixed precisionUsually faster, memory variesLarge batch on a small modelCan raise memory at small batch sizes (Hugging Face)
7Gradient checkpointingAbout 20% slower (Hugging Face)Saved activations dominate memory8,681 MB to 6,775 MB in Hugging Face's run
8Memory-light optimizerCheck results on your taskOptimizer state is a large share8-bit AdamW: 2 bytes per parameter against 8 for AdamW (Hugging Face)
9Quantize or offload the modelSpeed and quality trade-offsThe weights themselves do not fitQLoRA: more than 780 GB to 48 GB for a 65B model (paper)

What torch.cuda.OutOfMemoryError actually tells you

Depending on the PyTorch version, you will see the exception named torch.cuda.OutOfMemoryError or torch.OutOfMemoryError, and older releases raised a plain RuntimeError. The message format is the useful part. It has five numbers, and each one points at a different fix.

torch.OutOfMemoryError: CUDA out of memory. Tried to allocate <X>.
GPU 0 has a total capacity of <T> of which <F> is free.
Process <PID> has <P> memory in use.
Of the allocated memory <A> is allocated by PyTorch,
and <R> is reserved by PyTorch but unallocated.
If reserved but unallocated memory is large try setting
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True to avoid fragmentation.
What the message showsWhat it meansGo to
Another process holds a large share of memorySomething outside your job is using the cardFix 1
Reserved but unallocated memory is largePyTorch holds cached blocks it cannot reuse for this requestFixes 2 and 3
Allocated by PyTorch is close to total capacityYour job genuinely needs less memoryFixes 4 to 9
The error appears after you rerun a notebook cellOld objects are still referencedFix 2

To see where memory goes, record the peak allocated memory around one training step. The same snippet gives you a before and after number for every fix below.

import torch

torch.cuda.reset_peak_memory_stats()
# run one training step here
peak_gib = torch.cuda.max_memory_allocated() / 2**30
print(f"Peak allocated: {peak_gib:.2f} GiB")
print(torch.cuda.memory_summary())

Free fixes: check, clear and configure GPU memory

Fix 1: check what else is using the GPU

Run nvidia-smi and compare used memory with what your own job should need. If another process holds a large share, stop it, or point your job at a different card with CUDA_VISIBLE_DEVICES.

nvidia-smi
CUDA_VISIBLE_DEVICES=1 python train.py

Fix 2: free references and cached memory

Delete variables that still hold the model, optimizer or outputs, then collect garbage and release cached blocks. The PyTorch docs are explicit that empty_cache makes memory visible to other applications and may reduce fragmentation, but does not increase the memory PyTorch can use. Hugging Face also advises restarting the Python kernel between experiments.

import gc
import torch

del model, optimizer, outputs   # drop every reference first
gc.collect()
torch.cuda.empty_cache()

Fix 3: set expandable segments when memory is fragmented

If the reserved but unallocated figure in the error is large, set the allocator option that the error message itself suggests. Set it before the process starts.

export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
python train.py

Reduce GPU memory usage: batch size, accumulation and mixed precision

Fix 4: lower the batch size or sequence length

Forward activations depend mostly on sequence length, hidden size and batch size, according to Hugging Face. Halve the batch size or shorten sequences first. Hugging Face also points out that if your preferred batch size already fits, memory-saving techniques only slow training down.

Fix 5: add gradient accumulation to keep the effective batch size

Gradient accumulation runs smaller batches and applies the optimizer step after several of them. It costs extra forward and backward passes, so training gets somewhat slower.

In Hugging Face's published run on bert-large-uncased, moving from batch size 4 to batch size 1 with 4 accumulation steps cut GPU memory from 14,949 MB to 8,681 MB, about 42% lower.

# Hugging Face Trainer
training_args = TrainingArguments(
    per_device_train_batch_size=1,
    gradient_accumulation_steps=4,
    output_dir="out",
)

# Plain PyTorch
accum_steps = 4
optimizer.zero_grad()
for step, batch in enumerate(loader):
    loss = model(**batch).loss / accum_steps
    loss.backward()
    if (step + 1) % accum_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

Rule of thumb

Keep the largest per-device batch that fits and raise the accumulation steps. For an effective batch of 64 where 4 fits, Hugging Face recommends a per-device batch of 4 with 16 steps rather than 1 with 64.

Fix 6: try mixed precision, but do not expect it to fix small batches

Mixed precision mainly speeds up training. Hugging Face notes that memory use can go up with a large model at a small batch size, because the weights then exist in both 16-bit and 32-bit form. It helps most with a small model and a large batch. Hugging Face notes that some GPU architectures, such as Ampere, offer bf16.

import torch

with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
    loss = model(**batch).loss
loss.backward()

Gradient checkpointing and memory-light optimizers

Fix 7: enable gradient checkpointing

Normally every activation from the forward pass is kept for the backward pass. Gradient checkpointing keeps a subset and recomputes the rest, trading time for memory. Use it when batch size 1 with accumulation still fails.

# Hugging Face models
model.gradient_checkpointing_enable()

# or in TrainingArguments
TrainingArguments(per_device_train_batch_size=1,
                  gradient_accumulation_steps=4,
                  gradient_checkpointing=True,
                  output_dir="out")

Adding gradient checkpointing to that Hugging Face run lowered memory again to 6,775 MB, about 22% below the accumulation run, and raised epoch time from 66.03 to 85.47 seconds.

Measured peak VRAM, fix by fix (Hugging Face run)

ConfigurationPeak GPU memoryEpoch timeSamples per secondMemory vs vanilla
Vanilla, batch size 414,949 MB57.82 s8.86Baseline
Batch size 1, 4 accumulation steps8,681 MB66.03 s7.75-42% memory, +14% time
Same, plus gradient checkpointing6,775 MB85.47 s5.99-55% memory, +48% time

How to read these numbers

These are Hugging Face's published measurements, not a packet.ai run. The model is bert-large-uncased with sequence length 512 for one epoch on a 16 GB V100, and the figures are device memory including about 1.3 GB of CUDA kernels. Absolute megabytes will differ on your GPU and model. The ratios are the useful part, and the snippet in the error section lets you measure your own.

Fix 8: switch to a memory-light optimizer

Optimizer state is a fixed cost per parameter. Hugging Face lists 8 bytes per parameter for AdamW, 4 bytes for SGD with momentum and 2 bytes for 8-bit AdamW from bitsandbytes.

Hugging Face's training guide puts mixed-precision AdamW at 18 bytes per parameter plus activations, and 8-bit AdamW cuts optimizer state from 8 bytes to 2 bytes per parameter.

import bitsandbytes as bnb

optimizer = bnb.optim.AdamW8bit(model.parameters(), lr=2e-5)

Check validation results on your own task after the switch. The QLoRA authors also use paged optimizers, available as the paged_adamw_32bit option in their repository, to absorb memory spikes.

Quantization, offload and when to move to a larger GPU

Fix 9: quantize or offload the model weights

When the weights alone do not fit, shrink them. QLoRA loads the base model in 4-bit NormalFloat and trains small adapters on top. The paper reports that this cut the average memory needed to fine-tune a 65B parameter model from more than 780 GB to 48 GB, and double quantization saves about 0.37 bits per parameter.

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)
model = AutoModelForCausalLM.from_pretrained(
    "your-model-id",
    quantization_config=bnb_config,
    device_map="auto",   # spills layers to CPU if the GPU is full
)

Offloading layers to the CPU through device_map saves GPU memory at a speed cost, so treat it as a last resort. For the full comparison of cards, see our A100 vs H100 for fine-tuning LLMs post.

When a larger GPU is the right call

Move up when batch size 1 with checkpointing and quantization still fails, or when the time cost of accumulation and checkpointing outweighs the price of more memory. A card with more VRAM also removes the need for fixes 5 to 9 on many jobs.

On packet.ai

As of 10 November 2026, the RTX 6000 Pro on packet.ai lists 96 GB of GDDR7 from $0.66 per GPU-hour on Dynamic, or $299 per month flat. Check the product page for current rates before you commit.

Sources

  • Hugging Face: efficient training on a single GPU (memory and speed figures from an older version of the guide)
  • Hugging Face: model training anatomy (bytes per parameter, V100 16 GB, sequence length 512)
  • PyTorch: CUDA memory management notes
  • Dettmers et al., QLoRA (arXiv 2305.14314)

Frequently asked questions

The failing allocation asks for one block, and the error message splits the rest of the card into other processes, memory allocated by PyTorch and memory reserved by PyTorch but unallocated. Any of those can explain the gap. Read those numbers first, then pick the matching fix instead of lowering the batch size by habit.
Delete the variables that hold the model, optimizer and outputs, run gc.collect(), then call torch.cuda.empty_cache(). The PyTorch docs say empty_cache returns unused cached blocks so other applications can see them in nvidia-smi, but it does not raise the memory PyTorch itself can use. In a notebook, restart the kernel between experiments.
Yes. Hugging Face puts the slowdown at roughly 20 percent as a rule of thumb. In its published run, epoch time rose from 66.03 to 85.47 seconds while peak GPU memory fell from 8,681 MB to 6,775 MB, so the saving costs real time.
Not always. Hugging Face notes that mixed precision mainly speeds up training, and that the memory can rise with a large model at a small batch size because the weights exist in both 16-bit and 32-bit form, about 1.5 times the original model. Savings show up with small models and large batches.
Use gradient accumulation. Hugging Face's guidance is to keep the largest per-device batch that fits and raise the accumulation steps to reach your target. For an effective batch of 64 where 4 fits, use a per-device batch of 4 with 16 accumulation steps, not a batch of 1 with 64 steps.
Often yes. Fixes 1 to 8 change how memory is used, and fix 9 shrinks the weights themselves. The QLoRA paper reports cutting the average memory needed to fine-tune a 65B parameter model from more than 780 GB to 48 GB. Move to a larger GPU only when a batch of 1 with checkpointing and quantization still fails.

Last reviewed: 10 November 2026. If a bigger card is the right call, browse available clusters on packet.ai.

Waste less compute.

Same models. Same API. Fraction of the cost. Start free — no credit card required.

Start building →

More from the blog