Problem Description
During fine‑tuning of the OpenAI GPT‑4o model on a single GPU (e.g., A100 40 GiB, RTX 4090 24 GiB), the training loop crashes with a CUDA out‑of‑memory (OOM) error when the batch size or sequence length is increased. A typical failure looks like:
RuntimeError: CUDA out of memory. Tried to allocate 12.34 GiB (GPU 0; 24.00 GiB total capacity; 11.56 GiB already allocated; 0.12 GiB free; 12.34 GiB reserved in total)
Traceback (most recent call last):
File "train.py", line 152, in train_step
loss.backward()
File ".../torch/autograd/grad_mode.py", line 107, in __exit__
torch.autograd.backward(tensors, grad_tensors, retain_graph, create_graph)
File ".../torch/nn/modules/linear.py", line 115, in forward
return F.linear(input, self.weight, self.bias)
In production, the same configuration runs for two epochs and then fails during the third epoch, as reported in a fintech startup incident (“OOM after the third training epoch when batch size was set to 32 and sequence length 2048”). The error prevents any further progress and forces a manual restart.
Root Cause Analysis
GPT‑4o is a decoder‑only transformer with ≈175 B parameters. Memory consumption during fine‑tuning is driven by three major components:
- Model weights – stored once per GPU (≈12 GiB for fp16 on a 40 GiB A100).
- Activation memory – scales linearly with batch size × sequence length × hidden size. High‑resolution inputs (e.g., 4096 tokens) multiply activation size by a factor of two compared to the default 2048 limit.
- Optimizer state & gradients – duplicated per rank in Distributed Data Parallel (DDP) and per parameter when using LoRA adapters or 4‑bit quantization tricks.
The official fine‑tuning guide for GPT‑4o (OpenAI API reference) recommends a maximum batch size of 8 for sequence length 2048 on a 24 GiB GPU. Exceeding this limit triggers the OOM condition because activation memory alone can exceed the remaining capacity after weights and optimizer states are allocated.
Additional contributors identified in community reports:
- Gradient accumulation without clearing the CUDA cache (GitHub issue).
- Mixed‑precision loss‑scaling overflow that creates extra temporary tensors (Stack Overflow).
- Duplicated optimizer states across DDP ranks (Real incident).
- Flash‑attention or torch.compile generating large intermediate buffers (Reddit discussion).
Investigation and Debugging Steps
- Capture the exact error message and memory statistics.
import torch print(torch.cuda.memory_summary(device=None, abbreviated=False))Typical output shows
Allocated: 11.56 GiBandReserved: 12.34 GiB, confirming that the backward pass is the failure point. - Profile activation memory. Use
torch.utils.benchmarkor NVIDIA Nsight Systems to see per‑layer memory spikes. A quick sanity check:from torch.profiler import profile, record_function, ProfilerActivity with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA], profile_memory=True) as prof: with record_function("model_forward"): outputs = model(inputs) print(prof.key_averages().table(sort_by="self_cuda_memory_usage", row_limit=10))Layers with large
self_cuda_memory_usage(often the feed‑forward MLP) will dominate. - Verify batch size vs. sequence length scaling. The activation memory scales roughly as
batch * seq_len * hidden_dim * 4 (bytes per fp16). For GPT‑4o (hidden_dim ≈ 12288):# Approximate activation size (MiB) batch=16 seq_len=4096 hidden=12288 act_mb = batch * seq_len * hidden * 2 / (1024**2) print(f"≈ {act_mb:.1f} MiB")Result: ≈ 1,536 MiB per layer, multiplied by ~96 layers → > 140 GiB, which is impossible without checkpointing.
- Check for duplicated optimizer states. In DDP, each rank holds its own copy of
optimizer.state_dict(). Use:for name, param in model.named_parameters(): print(name, param.numel()) print("Optimizer state size:", sum(v.numel() for v in optimizer.state_dict()["state"].values()))If the state size approaches the model size, consider
torch.distributed.optim.ZeroRedundancyOptimizer. - Inspect mixed‑precision settings. Verify that
torch.cuda.amp.autocastis active and that the loss scaler is not overflowing:from torch.cuda.amp import GradScaler, autocast scaler = GradScaler() ... with autocast(): loss = model(inputs).loss scaler.scale(loss).backward() print("Current scale:", scaler.get_scale())A scale that repeatedly grows indicates overflow, which can allocate extra buffers.
Resolution Strategies
Below are concrete changes that have resolved the OOM condition in the documented incidents.
1. Reduce Effective Batch Size via Gradient Accumulation
Keep batch_size=2 per step but accumulate gradients over grad_accum_steps=8 to achieve an effective batch of 16.
# Before
batch_size = 16
# After
batch_size = 2
grad_accum_steps = 8
optimizer.zero_grad()
for i, batch in enumerate(dataloader):
with autocast():
loss = model(batch).loss / grad_accum_steps
scaler.scale(loss).backward()
if (i + 1) % grad_accum_steps == 0:
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
2. Enable Activation Checkpointing (Gradient Checkpointing)
OpenAI’s cookbook recommends model.gradient_checkpointing_enable() for GPT‑4o.
# Before
model = GPT4oForCausalLM.from_pretrained("gpt-4o")
# After
model = GPT4oForCausalLM.from_pretrained("gpt-4o")
model.gradient_checkpointing_enable()
Checkpointing reduces activation memory by ~60 % at the cost of extra compute.
3. Apply Mixed Precision with Proper Loss Scaling
Use torch.cuda.amp and clamp the scaler to avoid overflow.
scaler = GradScaler(init_scale=2.**16, growth_interval=1000, enabled=True)
# Optional: limit max scale
scaler._scale = min(scaler._scale, 2.**20)
4. De‑duplicate Optimizer States (Zero Redundancy)
from torch.distributed.optim import ZeroRedundancyOptimizer
optimizer = ZeroRedundancyOptimizer(model.parameters(),
optimizer_class=torch.optim.AdamW,
lr=5e-5,
weight_decay=0.01)
5. Use Flash‑Attention or 4‑bit Quantization for Large Sequences
When sequence length exceeds 2048, replace the default attention with flash‑attention (requires torch>=2.1) or apply 4‑bit LoRA adapters.
# Flash‑Attention flag
model.config.use_flash_attention = True
# 4‑bit LoRA (example with bitsandbytes)
from peft import LoraConfig, get_peft_model
lora_cfg = LoraConfig(r=64, lora_alpha=16, target_modules=["q_proj","v_proj"],
inference_mode=False,
lora_dropout=0.05,
quantization_config={"bits":4})
model = get_peft_model(model, lora_cfg)
6. Clean CUDA Cache Between Accumulation Steps
if (i + 1) % grad_accum_steps == 0:
torch.cuda.empty_cache()
7. Adjust NCCL Buffers for Distributed Runs
If using DDP, increase the NCCL buffer size to avoid “NCCL error: out of memory while initializing process group”.
export NCCL_BUFFSIZE=104857600 # 100 MiB
Verification
After applying the fixes, confirm that training proceeds without OOM:
# Sample log snippet after fix
[2026-09-19 12:34:56] INFO: Training step 120/500 completed, loss=0.8423
[2026-09-19 12:35:01] INFO: GPU 0 memory usage: 13.2 GiB allocated, 10.8 GiB reserved, 0.4 GiB free
[2026-09-19 12:35:01] INFO: No CUDA OOM detected for 5 consecutive epochs.
Additional checks:
- Run
torch.cuda.memory_summary()after a few steps; free memory should stay > 1 GiB. - Monitor GPU utilization (e.g.,
nvidia-smi --query-gpu=memory.used,memory.total,utilization.gpu --format=csv -l 5) to ensure stable usage. - Run a quick validation pass on a held‑out batch to verify that model outputs are unchanged.
Operational Best Practices & Prevention
- Capacity Planning: Use the activation memory formula to size batch & sequence length before launching a run.
- Automated Guardrails: Add a pre‑flight script that aborts if
torch.cuda.get_device_properties(0).total_memory< estimated required memory. - Monitoring: Set alerts on
GPU memory usage > 90%and on the appearance of “CUDA out of memory” in logs. - Version Pinning: Ensure torch, transformers, and bitsandbytes versions match those validated in the OpenAI cookbook (e.g., torch 2.2, transformers 4.41).
- Reproducibility: Store the exact
torch.cuda.memory_summary()snapshot alongside experiment metadata.
Related Topic Hub: LLM Systems Troubleshooting Hub
FAQ
- Why does increasing the sequence length from 2048 to 4096 cause immediate OOM?
Activation memory scales linearly with sequence length. Doubling the length roughly doubles the per‑layer activation size, pushing total memory beyond the GPU capacity unless checkpointing or other memory‑saving techniques are used. - Can I fine‑tune GPT‑4o with batch size 32 on a single A100?
Not directly. The official limits recommend batch size ≤ 8 for 24 GiB GPUs. To emulate a larger batch you must use gradient accumulation, checkpointing, or distributed training across multiple GPUs. - What is the difference between gradient checkpointing and de‑duplication?
Checkpointing trades compute for memory by recomputing forward activations during the backward pass. De‑duplication (e.g., ZeroRedundancyOptimizer) removes redundant optimizer state copies across DDP ranks, reducing static memory usage. - Why does mixed‑precision sometimes make OOM worse?
Automatic loss scaling can overflow, causing the scaler to allocate extra temporary tensors. If the scale grows unchecked, the runtime may allocate larger buffers, leading to OOM. Clamp the scaler or usetorch.cuda.amp.GradScalerwith a max scale. - Is flash‑attention safe for fine‑tuning GPT‑4o?
Flash‑attention reduces attention memory from O(N²) to O(N) and is supported in the OpenAI cookbook for GPT‑4o. Ensure you are on torch ≥ 2.1 and that the GPU driver supports the required CUDA kernels.