Repository navigation
fix(engine): reset shared model caches when the CLI opens a sequence - #2542
Conversation
Models that cannot batch and keep their KV caches inside the wrapper (ExaOne 4, GPT-OSS, Ministral 3, Step 3.5) get a model-owned pool entry, which never calls make_caches(), and they implement no prepare_sequence_state. Since #2225 moved `mlxcel generate` onto DirectEngine, nothing reset those caches between sequences, so the timed run prefilled the prompt on top of the one-token warmup's KV. The output was wrong (exaone4 greedy text differed from the CxxGenerator build from the first token) and the stale-offset prefill loaded about 250 ms of cuBLAS kernels inside the timed window, which `[Generated ... tok/s]` read as a 12% decode loss at -n 384. DirectEngine::open_sequence now calls make_caches() for a model whose supports_batching() is false, the rule the scheduler already applies at prefill time and the retired loop applied through reset_with_model. It covers generate, loglikelihoods and the drafter loop. On GB10 (test-fast, interleaved, null arm) exaone4-1.2b-4bit goes from 179.28 to 202.04 tok/s against 202.73 on d379d2f; llama-3.2-1b and granite-4.0-h-tiny stay within the null floor. Record: docs/benchmark_results/exaone4-engine-decode-gb10-2026-10-22.md. Closes #2494
Implementation Review SummaryIntent
Findings AddressedNo CRITICAL or HIGH findings, so nothing was changed. Remaining Items
Checks
Verification
|
Security and performance reviewNo CRITICAL or HIGH findings, so no fix commits were made. State isolation. Every in-process path that drives a non-batching family now starts from fresh caches. Error handling. Performance. The reset runs once per open, never per step. Batching families skip it: their pool entry already called LOW, not fixed.
Tests. The |
Mellum and Laguna keep their caches in a RefCell the same way, and --prompt-lookup refuses models that cannot batch, so its output is not affected.
Bilingual report on the open-time cache reset for models that cannot batch: the fixed-cost mechanism behind the exaone4 CLI decode loss, the placement in DirectEngine::open_sequence, the GB10 measurements from the record, and the open items (per-token slope, release profile, families without a real checkpoint, pre-existing NemotronNAS and LongCat-Flash-Ngram behavior).
Summary
The exaone-4.0 CLI decode loss after #2225 was a fixed per-call cost from stale state, not a slower engine step. ExaOne 4 cannot batch and keeps its KV caches inside the wrapper; its model-owned pool entry never calls
make_caches(), soDirectEnginenever reset them.mlxcel generate's timed run prefilled the prompt on top of the one-token warmup's KV: the output was wrong, and the stale-offset prefill loaded about 290 ms (under nsys) of cuBLAS kernels (gdb oncuLibraryLoadData) inside the timed window.DirectEngine::open_sequencenow callsmake_caches()whensupports_batching()is false, the ruleBatchSchedulerapplies at prefill time and the retired loop applied throughreset_with_model. GPT-OSS, Ministral 3, Step 3.5, Mellum and Laguna hold their caches the same way. Server path unchanged.Behavior change:
mlxcel generate(andloglikelihoods,MlxInferenceSession) on these families now decodes from fresh caches, so its text changes: exaone4 greedy text now matches the d379d2f CxxGenerator build; gpt-oss-20b no longer opens with a different reply; laguna-xs-2.1-nvfp4 greedy text with a warmup now equals its--profiletext without one (checked in review).--prompt-lookuprefuses models that cannot batch, so it is unaffected.Measurement
GB10 sm_121, kernel 7.0.0-1019-nvidia, driver 580.178.04,
MLX_CUDA_ARCHITECTURES=121, test-fast; ref d379d2f, main 8116e26, fix, null = fix binary again; rotated interleaving.exaone fix vs ref paired -0.72% (-1.14..+0.82), null floor +0.11% (-1.48..+1.46). Fit
time = fixed + n * per_token: main 302 ms + 4.85 ms/tok vs ref 70 ms + 4.77 ms/tok; fix 57 ms + 4.97 ms/tok vs ref 70 ms + 4.89 ms/tok in the same block. granite main vs fix +2.14% sits inside its null spread (+1.55% median, same binary). Full record:docs/benchmark_results/exaone4-engine-decode-gb10-2026-10-22.md.Test plan
engine::shared_cache_tests(2): fail with the reset disabled, pass with it-p mlxcel-core --lib engine::(70),--lib exaone(3),granitemoehybrid(17),lfm2(72),--test-threads=1, CUDAcargo clippy --profile test-fast --features cuda -p mlxcel-core --lib --tests -- -D warnings,cargo fmt --check--show-reasoning -n 48text: exaone4 fix == ref; granite-4.0-h and ministral-3b fix == mainUnverified
Ministral3text wrapper on a real checkpoint (none local; ministral-3b-4bit loads as amistral3VLM)--imageprefill on a non-batching wrapper not exercised; it also goes throughopen_sequence, and the server applies the same reset before its embedding prefillCloses #2494