Systematic checklist — fast path to reproduce, isolate, identify, and fix leaks.1) Reproduce & isolate- Reproduce with a small deterministic script and fixed seed.- Run on CPU only to see if leak is Python-level vs CUDA-level (set CUDA_VISIBLE_DEVICES="" or move tensors to .cpu()).- Reduce batch size / num_workers to check resource sensitivity.2) Monitor baseline metrics- GPU: nvidia-smi -l 2 or gpustat; inspect GPU memory growth over iterations.- Process: top/htop, ps aux --sort=-rss to watch resident memory.- Python: use tracemalloc, memory_profiler (@profile) to see allocation hotspots.Example nvidia-smi watch:bash
watch -n1 --nvidia-smi
3) Inspect PyTorch GPU allocations- Query runtime stats in loop:python
import torch
print(torch.cuda.memory_allocated())
print(torch.cuda.memory_reserved())
print(torch.cuda.memory_summary())
- Use torch.cuda.reset_peak_memory_stats() between iterations to track per-iteration peaks.4) Common leak sources & concrete fixes- Retained computation graph: - Cause: calling loss.backward() multiple times without zero_grad or accumulating tensors that require_grad. - Fix: optimizer.zero_grad()/model.zero_grad(); use with torch.no_grad() or detach() when storing outputs; avoid retain_graph unless needed. - Example: saved = output.detach().cpu()- Storing tensors in lists/dicts/logs: - Cause: appending outputs/metrics directly stores GPU tensors. - Fix: .detach().cpu().numpy() or store scalars only; del references and gc.collect().- Hooks and callbacks keeping refs: - Cause: forward/backward hooks that capture tensors. - Fix: remove hooks, ensure they don't store tensors or use weakrefs.- optimizer.state growth: - Cause: optimizer.state storing transient tensors (e.g., for large per-sample grads). - Fix: check optimizer.state_dict(); use gradient accumulation carefully; clear unused state if dynamically changing param set.- DataLoader worker leaks / pin_memory: - Cause: too many workers, persistent_workers True with buggy dataset, large prefetch_factor. - Fix: reduce num_workers, set pin_memory=False, lower prefetch_factor, set worker_init_fn to reset RNG and clear caches.- retain_graph True accidentally: - Fix: only set retain_graph=True when necessary; most loops should not use it.5) Garbage collection & cache clearing- Explicit deletes and GC:python
del tensor, large_list
import gc; gc.collect()
torch.cuda.empty_cache()
- Note: torch.cuda.empty_cache() frees cached memory to OS but does not free tensors still referenced.6) Profiling tools (use in combination)- PyTorch profiler: torch.profiler (captures CPU/GPU memory and ops).- torch.utils.bottleneck for quick diagnostics.- tracemalloc for Python allocations.- memory_profiler (mprof) for line-by-line memory usage.- objgraph / Pympler / guppy for object retention graphs.- For GPU: Nsight Systems / Nsight Compute for deeper GPU traces.- For containers: docker stats, cgroups logs.7) Steps to debug iteratively- Add per-iteration prints of torch.cuda.memory_allocated() and Python RSS.- Binary search: comment half of training loop / model layers to localize offending code.- Snapshot object counts: use gc.get_objects() + collections.Counter(type(o)) to detect growing classes.- Use tracemalloc snapshots and compare: filter top frames.8) Mitigations & long-term fixes- Mixed precision (AMP) to reduce memory footprint.- Gradient checkpointing (torch.utils.checkpoint) to trade compute for memory.- Offload optimizer states or use ZeRO / DeepSpeed for large models.- Use data streaming and smaller batch sizes.- Add unit tests that run a few steps and assert memory usage stable.9) Example troubleshooting snippetpython
# check allocations per step
for step, batch in enumerate(loader):
loss = model(batch)
loss.backward()
print(step, torch.cuda.memory_allocated())
optimizer.step(); optimizer.zero_grad()
Summary: start by reproducing and isolating CPU vs GPU; monitor with nvidia-smi, torch.cuda.* APIs, tracemalloc/memory_profiler; look for retained graphs, tensors stored in containers, DataLoader worker issues, hooks, and optimizer state growth; fix by detaching/moving to CPU, deleting refs + gc.collect, using torch.no_grad(), lowering prefetch/num_workers, torch.cuda.empty_cache() for cache, and consider AMP/checkpointing/ZeRO for large models. Use PyTorch profiler, tracemalloc, objgraph, Nsight to pinpoint root cause.