Fix DeepSeek-V4 Metal residency leak: materialize per-layer cache state each forward - #25
Open
snagnever wants to merge 1 commit into
Conversation
…te each forward The attention caches (PoolingCache concat-grow, RotatingKVCache slice-assign, and their Batch* variants) build un-detached per-decode-step lazy graphs, so MLX keeps every prior step's intermediate array -- and its backing Metal buffer -- resident. The live-buffer count hits the Metal residency cap (resource_limit, 499000) after ~11.3K decode tokens regardless of prompt length, aborting with `[metal::malloc] Resource limit (499000) exceeded`; the command queue then wedges until a process restart. Materialize all per-layer cache state once per forward pass in DeepseekV4Model.__call__ to detach the chains, keeping the live-buffer count bounded. One mx.eval per forward (not per cache); negligible cost, no throughput regression. Verified on M4 Max 128GB / mlx 0.31.2: forced 20K-token generation clean to 19,989 (was OOM at 11,314), 31.3 tok/s (no regression), and a 300-request knowledge-bench soak on a single long-lived server with 0 OOMs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
snagnever
added a commit
to snagnever/macstudio-local-llm
that referenced
this pull request
May 31, 2026
…-bench soak Submitted the Metal residency-leak fix upstream: - issue ml-explore/mlx-lm#1332 (root cause + reproducer + diagnostics) - PR Blaizzy/mlx-lm#25 against the #1192 head branch - heads-up comment on #1192 linking both Adds the pre-submission soak result (MMLU+GPQA+HumanEval, 300 requests on a single long-lived patched server: 0 metal::malloc, 0 errors, ~2h44m) to the writeup + PR-draft verification tables, plus the soak driver script. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jundot
added a commit
to jundot/omlx
that referenced
this pull request
Jun 1, 2026
Detach DeepSeek-V4 cache update graphs after each forward pass to avoid Metal residency growth during long decode. Apply the same materialization to the MTP override path, including MTP cache updates. This ports the fix from Blaizzy/mlx-lm#25, which addresses the DeepSeek-V4 Metal residency leak described in ml-explore/mlx-lm#1332. The upstream change materializes per-layer cache arrays so PoolingCache concatenation and RotatingKVCache slice updates do not retain prior lazy graph chains indefinitely.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the Metal residency leak in the DeepSeek‑V4 port that aborts generation with
[metal::malloc] Resource limit (499000) exceededafter ~11K decode tokens on Apple Silicon (full root cause: ml-explore#1332).Cause (short)
The attention caches build per‑decode‑step lazy graphs that are never detached:
PoolingCachegrows viamx.concatenate,RotatingKVCachevia sliced assignment (and theBatch*variants likewise). Because the cache object holds the head of the chain, MLX keeps every prior step's intermediate array — and its backing Metal buffer — resident. The count of live buffers grows ~one per layer per step until it hits the device residency cap (resource_limit, 499000), ≈ 11.3K tokens regardless of prompt length, after which the command queue wedges until a process restart.Change
Materialize all per‑layer cache state once per forward pass in
DeepseekV4Model.__call__, which detaches the chains so the resident‑buffer count stays bounded. Onemx.evalper forward (not per cache), so the cost is negligible.Verification (M4 Max 128 GB, mlx 0.31.2)
Notes
.stateaccessor) coversPoolingCache/RotatingKVCache/BatchPoolingCache/BatchRotatingKVCacheuniformly; a cache‑class‑local fix is possible but is whack‑a‑mole across four classes (two shared with other models).KVCache/RotatingKVCache._update_in_place) instead ofconcatenate, removing the chain at the source.max_tokensinstead of crashing.)