PyTorch Memory Management Overview

How PyTorch Uses CUDA Memory

PyTorch manages CUDA memory via a caching allocator. Instead of releasing memory back to the OS immediately, it caches blocks for reuse. While this improves performance, it can cause confusion in reported memory usage.

Important APIs to monitor include:

  • torch.cuda.memory_allocated()
  • torch.cuda.memory_reserved()
  • torch.cuda.max_memory_allocated()

Memory Fragmentation and Ghost Tensors

Memory fragmentation occurs when small blocks prevent large allocations. Ghost tensors—unfreed references due to Python closures or dataloader workers—also contribute to hidden leaks.

Symptoms of Memory Leaks or Fragmentation

Common Errors

RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB
Total memory: 16.00 GiB
Reserved: 14.50 GiB, Allocated: 12.00 GiB, Free: 1.00 GiB

This error might still appear even if peak allocated memory is well below capacity.

Reproducible Scenario

Training a model with num_workers > 0 and large batch sizes across multiple epochs may show an increasing trend in memory usage, ultimately leading to a crash.

Root Causes and Diagnostics

1. Retained Tensors in Closures

Common with loss logging or validation code retaining computation graphs:

losses.append(loss)  # Dangerous if loss retains graph
del loss  # Not enough if referenced elsewhere

Fix:

losses.append(loss.item())  # Detach and store scalar
optimizer.zero_grad()

2. Memory Not Freed Due to Dataloader Forking

Workers in dataloaders use separate processes. If they retain GPU tensors or fail silently, the memory isn't reclaimed.

Fix:

DataLoader(..., pin_memory=True, num_workers=0)  # Use 0 to debug
torch.multiprocessing.set_start_method('spawn', force=True)

3. Unused Variables Retained Across Epochs

Retaining large tensors across iterations without calling detach() or del leads to cumulative memory usage.

Step-by-Step Fix Strategy

1. Monitor Live Memory Usage

import torch
print(torch.cuda.memory_summary())

This provides allocator stats and fragmentation reports.

2. Use context managers to control scope

Ensure large tensors are released:

with torch.no_grad():
    output = model(input)
    del output

3. Periodically Clear Cache

Not a fix, but helps in test environments:

torch.cuda.empty_cache()

4. Avoid Accidental Graph Retention

Use loss.item(), detach(), and avoid list/dict structures with references to graph nodes.

Long-Term Solutions and Best Practices

1. Use PyTorch Profiler

Profile memory usage per layer:

from torch.profiler import profile, record_function
with profile(activities=[...]) as prof:
    with record_function('model_inference'):
        model(input)

This identifies which operations spike memory.

2. Train with AMP (Automatic Mixed Precision)

Reduces memory footprint:

scaler = torch.cuda.amp.GradScaler()
with torch.cuda.amp.autocast():
    output = model(input)

3. Reuse Tensors with In-Place Ops

In-place ops reduce allocations but must be used carefully to avoid overwriting required tensors:

x.add_(1)  # In-place

4. Validate GPU Utilization Metrics

Use nvidia-smi or torch.cuda.memory_stats() to validate per-process usage and confirm leak trends over epochs.

Conclusion

GPU memory leaks in PyTorch are subtle and multifactorial. They often stem from retained computation graphs, dataloader multiprocessing artifacts, or improper scope handling. Developers must combine runtime profiling, good memory hygiene, and architectural considerations (e.g., batch size tuning, AMP, and model checkpointing) to ensure consistent, leak-free execution across training cycles.

FAQs

1. Why does PyTorch show high memory usage even after a model is deleted?

Because PyTorch's caching allocator reserves memory to reduce malloc overhead. Use torch.cuda.empty_cache() to release to OS.

2. How can I verify memory leaks across epochs?

Log torch.cuda.memory_allocated() at each epoch and observe trends. Persistent increases indicate leaks.

3. Are in-place operations memory efficient?

Yes, but dangerous if shared tensors are modified. Always test correctness before using in-place ops.

4. What's the safest way to log loss during training?

Use loss.item() to avoid retaining computation graphs. Never store raw loss objects for logging.

5. How can I reduce memory footprint in inference pipelines?

Use torch.no_grad(), AMP, and remove intermediate references. Batch size tuning is also critical.