When PyTorch loads a model, glibc allocates large memory arenas via malloc. When you unload with del model + gc.collect() + torch.cuda.empty_cache(), Python releases its references — but glibc keeps the arenas because small residual allocations pin entire chunks. Memory grows with every model switch and is never returned to the OS.
This affects anyone running long-lived inference servers, Gradio apps, ComfyUI, or any pipeline that loads/unloads multiple models.
Set these before launching Python. Forces allocations >64KB to use mmap() instead of arenas. mmap pages are returned to the OS immediately on free — no fragmentation.
Proof
Before: RSS grew ~3GB per model switch, OOM after 17 hours and 107 switches
After: RSS flat at 955MB across 107 consecutive switches between 13 different checkpoints (SDXL, Flux, PixArt, SD 1.5, Playground v2.5)
Tested with diffusers/FastAPI on an AMD RX 7800 XT (ROCm) and NVIDIA GTX 1080 Ti (CUDA)
No code changes. No hook removal. No gc hacks. Just two environment variables.
This is a great write-up — the malloc arena fragmentation issue is one of the more insidious problems in long-running PyTorch serving processes and it is underdiagnosed.
A few things to add from production experience:
Monitoring: You can track this in production by comparing torch.cuda.memory_allocated() (PyTorch’s view of GPU memory) against the RSS reported by the OS (/proc/self/status ->VmRSS). If RSS grows steadily while PyTorch’s allocator stats stay flat, the leak is in the CPU-side allocator, not the GPU side.
Alternative to MALLOC_ARENA_MAX: jemalloc or tcmalloc are drop-in replacements that handle arena fragmentation much better for long-running processes. In serving environments: