Repository navigation
fix(scheduler): clear SpecPrefill state after failures and cache rejects - #4035
Merged
Merged
Conversation
…corrupts The engine stopped admitting requests after a SpecPrefill request failed mid-decode or hit cache corruption. Neither fail_all_requests nor _recover_from_cache_error released the active SpecPrefill id or the offset RoPE patch, so _schedule_waiting kept deferring every request, including the corruption retry of the SpecPrefill request itself. Both paths now call _cleanup_specprefill, as abort and generation-overflow recovery already do. A retried SpecPrefill request (cache corruption and generation overflow) now prefills densely. Its indices and position offset were scored against the pre-reset cache state: after a prefix-cache hit they index the uncached suffix. The reset sets cached_tokens to 0 and does not re-run scoring, so the retry fed those suffix-relative indices into a sparse prefill of the full prompt and forwarded the wrong tokens at the wrong positions, with no prefix KV. _reset_request_for_reprefill now drops the indices, the same fallback used when sparse prefill fails.
… fails validation SpecPrefill scores a request while its prefix cache is being prepared, so the selected indices are relative to the uncached suffix and the RoPE offset is the restored cached_tokens. When _validate_cache then rejected the restored cache, the fallback reset the request to prefill the full prompt from position 0 but kept specprefill_indices. The sparse path applied the suffix-relative indices to the full prompt, so the wrong tokens were prefilled at the wrong positions, with no prefix KV and no error. Scoring does not run again for that request, so nothing corrected it. The fallback now clears specprefill_indices, and the request is prefilled densely. specprefill_position_offset is only read when indices are set, so it is left alone. The regression test stores a prefix on a tiny hybrid model through the paged SSD cache, rewrites the stored block so its recurrent layers carry a different slot count than the live model, and checks in a second engine session that the rejected restore is followed by a dense prefill of the full prompt.
tc3oliver
force-pushed
the
fix/specprefill-stale-active-id
branch
from
September 27, 2026 18:45
99936fe to
ef0ecfd
Compare
jundot
approved these changes
Oct 2, 2026
jundot
left a comment
Owner
There was a problem hiding this comment.
Each of the four paths is reachable on current main, and reverting any one hunk breaks only its matching test. I'll move the coverage into the existing scheduler tests in a follow-up. LGTM, merging.
jundot
added a commit
that referenced
this pull request
Oct 2, 2026
tc3oliver
added a commit
to tc3oliver/tc3oliver
that referenced
this pull request
Oct 2, 2026
The profile read like generated copy: a keyword ribbon under the title, em-dash separators on nearly every line, number-dense project blurbs and a technology list at the end. The intro is now a short first-person paragraph, each project is one plain paragraph, and the curated PR summaries in data/oss-contributions.toml say what broke and what the fix changed without the extra framing. PiShip, the distribution and governance layer for the Pi coding agent, was missing from the projects. It is added with its pre-release status stated as the project's own status page states it. The generator's line format used em dashes as separators; it now uses a colon, parentheses and a comma, so the weekly sync does not put them back. The regenerated record also picks up jundot/omlx#4035, merged on October 2. Local .omc/ state is ignored.
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.
Problem
If a SpecPrefill request fails after its sparse prefill, stale SpecPrefill state can stop the engine from admitting requests, while stale sparse indices can make fallback paths prefill the wrong tokens.
fail_all_requests(from the engine loop's exception handler) leaves_specprefill_active_request_idand the offset RoPE patch in place with no request behind them._schedule_waitingdefers everything from then on._recover_from_cache_errorand puts the request back in waiting, but the id stays set, so the retried request is deferred by its own leftover id and never finishes._reset_request_for_reprefillre-prefills from the full prompt withcached_tokens = 0, but scoring doesn't run again. After a prefix-cache hit the request keeps indices that point into the old suffix, so the retry prefills the wrong tokens at the wrong positions without any error._validate_cachehas the same problem. Scoring runs during the prefix-cache lookup, before validation, so when validation rejects the restored cache the fallback switches to the full prompt but keeps the suffix-relative indices. The regression test covers the recurrent-slot-count rejection path using a hybrid model and a persistent cache block whose recurrent state doesn't match the current model.Related: #2336. Its Bug B is the
_recover_from_cache_errorcase above, reported there as a hang with TurboQuant KV and SpecPrefill on a real model. Bug A (the sparse-prefilled cache not being TurboQuant-converted) is not touched here.Fix
fail_all_requestsand_recover_from_cache_errorcall_cleanup_specprefill, as abort, normal completion and generation-overflow recovery already do._reset_request_for_reprefilldrops the indices, so the retry prefills densely, like the existing fallback when sparse prefill fails. This also applies to the generation-overflow retry, which uses the same reset._validate_cacherejection fallback drops the indices scored against the rejected cache (second commit).Regression coverage
Tests reach each state through
EngineCore.generatewith tiny random-init models and the repo's mock tokenizer. The only faults are a model forward that raises once during decode and, for the_validate_cachecase, a stored block rewritten on the SSD cache between two engine sessions.On main the three test cases in
test_specprefill_lifecycle.pytime out with the engine stuck, and in the_validate_cachetest the model is given a sparse prefill of suffix tokens where the full prompt should start. Removing either index reset on the branch makes its test fail: the corruption retry, or the_validate_cachefallback, feeds the model suffix tokens instead of the full prompt.Validation
tests/test_specprefill_lifecycle.pyandtests/test_specprefill_cache_validation.py: 4 passed on the branch, 4 failed on maintests/test_specprefill*.py: 111 passedtests/test_scheduler.py tests/test_engine_core.py tests/test_prefill_oom_graceful.py tests/test_exceptions.py: 623 passedpython -m pytest: 14984 passed, 381 skipped, 86 deselected, 0 failedruff checkandblack --checkon the two new test files.omlx/scheduler.pyalready doesn't pass Black on main; the lines added here do.Not tested
I haven't run a representative real-model hardware check for this change. The tests above use tiny random-init models, including the hybrid cache-validation case, so they exercise the scheduler and persistent-cache paths but not a production-size model or the TurboQuant workload from #2336.