Account for recently reclaimed memory in adaptive prefill estimates - #2573
Conversation
|
Thanks for the deep investigation, the repro and the double counting analysis made this easy to verify. The approach is right and I'll merge this, with a small follow-up on my side for two gate ordering corners. I ran a live A/B with DeepSeek V4 Flash on a 512GB machine using a pinned custom ceiling. I could not reproduce the abort itself in context mode there, without real OS memory pressure the enforcer reclaim always wins on this box, but the mechanism shows up clearly:
Speed coming out neutral is expected, that abort comes from a single full-size chunk spike no predictor can see, so no regression on that path. The two follow-up corners, both from the gate order in
Neither blocks the merge, both are fixed by moving the sign handling above the skip gates, and I'll take care of that in the follow-up. |
Context
This follows
fix: estimate DeepSeek V4 prefill memory (#2521)(745d9c22), included in oMLX 0.5.8.dev1. That change added architecture-aware static prefill and KV-memory estimates for DeepSeek V4.While testing that path at high context, I found a separate gap in the adaptive measurement layer: MLX can release pooled memory after one chunk and allocate it again during the next, but the tracker currently forgets the release. This PR leaves the DeepSeek V4 static estimator unchanged and adds the missing dynamic signal on top of it.
The measurement fix itself is model-agnostic.
PrefillTransientTrackeris created for every scheduler, and both prefill loops feed it process-footprint deltas without an architecture check. DeepSeek V4 is the model used for the live reproduction; any model showing the same release-then-reallocation pattern can hit this path.What this changes
This PR makes the adaptive prefill tracker remember process footprint released by a chunk and account for it once when sizing the next chunk.
The change:
recent_reclaim_bytes;static prediction + recent reclaimand uses the larger value;This closes a gap where MLX can release several GiB of pooled memory after a small or boundary chunk, then allocate roughly that memory again during the next larger chunk.
Why
The existing measurement path ignores non-positive process-footprint deltas:
That is appropriate for the positive per-token EWMA, but it also means the scheduler forgets a recent pool release completely. The next prediction can only use the latest positive sample, EWMA, and static model estimate.
In one controlled run:
11.18 + 6.34 = 17.52 GiB, which closely matches that growth.Reproduction
I reproduced this on:
0.5.8.dev1, build 2159;745d9c22;Jundot/DeepSeek-V4-Flash-0731-oQ2e-mtp;The request is synthetic and deterministic. It uses one user message containing
"x "repeated 245,243 times:/v1/messages/count_tokensreports 245,248 tokens for the message. The completed/v1/messagesresponse reports 245,327 input tokens after the API template is applied.The serialized request is 490,629 bytes and has SHA-256:
4b9dcdec01ff8d56ea1bb22e3a011bdd90176f8127259e9d3dd69f4cde15ef2dRestart the server between the unmodified and patched runs so both start cold, and leave prompt caching disabled.
Before and after
prefill_memory_abortedThe successful run returned HTTP 200 with 245,327 input tokens and 1 output token. During that run the tracker saw:
A trace from the patched run shows the new path directly:
Elapsed time to roughly the same prefill position was about 5.1% higher with the change. This is only a rough safety-cost comparison: the patched measurement completed 736 tokens beyond the abort position, while the unmodified run includes hard-pressure abort deferral.
Avoiding double counting
The reclaim charge is not added blindly to the current winning prediction. The predictor uses:
For example, a second controlled high-context sequence had:
The result stays at 11.83 GiB because raw-last already covers the likely reallocation. Adding the release to raw-last would incorrectly raise the estimate to 18.69 GiB.
Tests
This PR adds:
test_adaptive_throttle_charges_recently_reclaimed_footprinttest_predicted_transient_does_not_double_count_reclaim_covered_by_rawFocused suites run:
tests/test_prefill_transient_tracker.pytests/test_prefill_oom_graceful.pytests/test_scheduler_prefill_memory_guard.pytests/test_memory_monitor.pyResult: 162 passed.
The full fast suite completed with 7,979 passed, 66 skipped, 71 deselected, and 5 failures. The same five unrelated GLM/Inkling MTP numerical-parity tests fail with the same deltas on a clean
origin/mainworktree, so they are not introduced by this change.Scope
This change does not treat adaptive headroom or large footprint samples as bugs. In 64 positive high-context measurements (
kv_len >= 180k, chunks no larger than 512 tokens), median footprint growth was 6.17 GiB and the maximum was 12.14 GiB. The large pool reallocations are real.The narrower problem is that a release was forgotten even though the next chunk could allocate that memory again. This PR keeps the existing positive-sample predictor intact and adds a one-shot safety charge for that specific case.
The reproduction uses repeated synthetic text and no prompt cache. It tests the scheduler and memory-guard behavior, not model-output quality or a particular agent workload.
The before-and-after live run has only been performed with DeepSeek V4. The regression tests cover the generic scheduler/tracker path, but I have not run the same high-context live comparison on another model family.