If your PyTorch process runs out of memory after hours of switching between models, the problem isn't PyTorch — it's glibc's malloc arena allocator

The problem

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.

The fix

export MALLOC_MMAP_THRESHOLD_=65536
export MALLOC_TRIM_THRESHOLD_=65536

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.

Full write-up with methodology and data: GitHub - brjen/pytorch-memory-fix: Two environment variables that fix PyTorch/glibc memory creep on Linux. Zero code changes. Zero performance cost. · GitHub

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:

# Using jemalloc

LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so python serve.py

# Using tcmalloc

LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc.so python serve.py

Both are widely used in production ML serving. jemalloc in particular is the default allocator for Redis and Rust for exactly this reason.

In my opinion for anyone diagnosing memory growth in long-running PyTorch processes, the debugging order should be:

  1. torch.cuda.memory_summary() — check for GPU-side fragmentation

  2. gc.get_objects() filtered by tensor type — check for Python-side tensor leaks

  3. RSS monitoring — check for CPU allocator fragmentation (this post)

  4. /proc/self/maps — check for memory-mapped file accumulation

Thanks for sharing this — it is the kind of operational knowledge that rarely makes it into tutorials.

/Aditya