Skip to content

Fix mlx_lm.server model-swap leak and dead generation thread - #1837

Merged
michalk8 merged 7 commits into
ml-explore:mainfrom
gyanu2507:fix/server-clear-cache-on-model-swap
Sep 11, 2026
Merged

michalk8 merged 7 commits into
ml-explore:mainfrom
gyanu2507:fix/server-clear-cache-on-model-swap

Conversation

@gyanu2507

@gyanu2507 gyanu2507 commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1757
Fixes #1505

ModelProvider._load dropped the old model references and immediately loaded the next one. Those tensors go into MLX's buffer pool, not back to the OS, and server.py never called mx.clear_cache() (unlike generate.py / evaluate.py). After dropping the old refs, gc.collect() then mx.clear_cache().

If _generate dies, the HTTP server kept answering /health and /v1/models with 200 while every completion waited forever on the dead thread. Wrap the generation thread, mark it failed, fail queued requests, reject new generate() calls, and return 503 from those endpoints. A request that arrives during the crash drain is rejected instead of hanging.

Tests cover both: two fake model loads asserting clear_cache, and a crashed load_default that makes generation_available() false.

Dropping the previous model only returns its weights to MLX's cache, so a
long-lived server kept every model it had loaded. Collect and clear_cache
before the next load.
An uncaught exception in _generate left HTTP workers up while every
completion hung. Catch that death, fail queued work, and make /health
and /v1/models return 503 so a probe can restart the process.
A request could land on the queue after the crash drain finished and
then hang forever. Hold one lock around the failed flag, the drain, and
generate()'s availability check plus put.
@gyanu2507 gyanu2507 changed the title Clear the MLX buffer pool when mlx_lm.server swaps models Fix mlx_lm.server model-swap leak and dead generation thread Sep 4, 2026
Main landed reset() on swap, health tied to the generation thread,
and cache teardown in that thread. Keep those, and still call
gc.collect() plus mx.clear_cache() after reset so the buffer pool
does not hold the previous weights. Enqueue still fails fast if
the generation thread has already died.
@gyanu2507

Copy link
Copy Markdown
Contributor Author

Merged current main. The overlapping pieces that landed upstream stay as they are: ModelProvider.reset(), health via is_healthy, and tearing down the model/prompt cache in the generation thread.

This branch still does two things main does not:

  • after reset(), gc.collect() and mx.clear_cache() so the previous weights leave the MLX buffer pool
  • enqueue fails immediately if the generation thread has already died, instead of hanging on the request queue

@michalk8
michalk8 self-requested a review September 11, 2026 12:56

@michalk8 michalk8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank for the PR, I did some changes:

  • /v1/models 503 dropped, since /health already reports this
  • the queue drain + lock replaced with a timeout wait, it's simpler

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants