Repository navigation
fix(engine): the interpreted path admits through the scheduler (#1891) - #1901
justinchuby wants to merge 2 commits into
Conversation
#1723 routed every package without a fused decode core through `generate_with_pipeline_callbacks`, and that drive ran the declared components without ever consulting the scheduler. Two holes followed, and the second is the one that misreports rather than merely omits: 1. An engine with no decode core had no admission path at all. The KV byte budget did not apply to those packages, so an over-budget request did its work and then failed downstream for an unrelated reason. The `#[cfg(feature = "native-backend")]` block in `generate_with_callbacks` whose comment promises "both cold and session-reusing paths still admit through the scheduler before touching the native backend" is still true of the code it describes; the `!holds_decode_core()` early return above it means that code is not reached. 2. `on_admitted()` fired unconditionally, before any admission decision existed. Anything counting admissions counted requests nothing had admitted. Admission is placed in the funnel both entry points reach, after the prompt is encoded (so the scheduler sizes the request it will actually run) and before any component is invoked (which is the ordering the guarantee is about). `on_admitted()` now fires only after the scheduler accepts. `scheduler.complete()` runs on both the success and the error path, so a request that admits and then fails does not leak its bytes or its running-batch slot. The re-entry branch at the top of the drive is left in place: it is dead only on the `generate_interpreted` path, where its own guard is the condition whose falsity routed us there. It is live for callers entering the pipeline API directly, and those callers still reach exactly one admission. Not repaired by relaxing the assertion, per the constraint on the issue. `git log -S` dates both defects to #1723 (faedea4), so the precise statement is stronger than "the guarantee moved": the interpreted path never had admission wired in. Coverage: the pre-existing rejection test now shares a fixture helper with a new positive test asserting the two facts a rejection test structurally cannot reach -- that the callback fires only after a real admission, and that the reservation is released afterwards. Both new assertions were falsified by mutation: deleting `scheduler.complete` fails the running-count assertion (left 1, right 0), and moving `on_admitted()` above admission fails "refused requests must not signal admission". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Closing as superseded by #1900, which merged at Their fix is better than mine on three counts, and I'd rather say which than wave at it:
On the duplication. I ran What survives from this branch, as the narrow test-only follow-up #1902:
One residual I am reporting rather than changing, because it's the #1900 author's scope call: on the |
Pull request was closed
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1901 +/- ##
==========================================
- Coverage 80.71% 80.33% -0.38%
==========================================
Files 415 415
Lines 204440 204440
Branches 204440 204440
==========================================
- Hits 165013 164245 -768
- Misses 33858 34620 +762
- Partials 5569 5575 +6
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Closes #1891.
What was wrong
#1723 (
faedea4d1) routed every package without a fused decode core throughgenerate_with_pipeline_callbacks, and that drive ran the declared components without ever consulting the scheduler.I verified all three of @justinchuby's claims against the source rather than taking them on trust; all three hold.
runtime.rs:977—if !self.holds_decode_core() { return self.generate_interpreted(...) }sits above the#[cfg(feature = "native-backend")]block whose comment promises "both cold and session-reusing paths still admit through the scheduler before touching the native backend". The promise is still true of the code it describes. The code is not reached.workflow_api.rs:173-176— the re-entry branch back to the admitting entry point is guarded onself.holds_decode_core(), the very condition whose falsity routed us there.workflow_api.rs:188-190—on_admitted()fired unconditionally, with no admission decision in existence.One refinement to the diagnosis.
git log -S 'holds_decode_core' -- crates/onnx-genai-engine/src/engine/workflow_api.rsdates both defects to #1723, and the drive did not previously admit either. So the precise statement is stronger than "the guarantee moved": the interpreted path never had admission wired in at all. Candidate (1) in mechanism, but it is a wiring defect, not stale budget arithmetic.Two consequences, and the second misreports rather than merely omits:
decoder_kv.0000— the symptom, not the defect).The fix
Admission goes in the single funnel both entry points reach, rather than being duplicated at each caller:
on_admitted()fires only after the scheduler accepts;scheduler.complete()runs on both the success and the error path — a request that admits and then fails must not leak its bytes or its running-batch slot.admit_generate_request_with_schedulerbecomespub(crate).scheduler_seqis the parsedsession_idor0; the workflow session counter starts at 1, so 0 is a free sentinel for "no session".The re-entry branch is deliberately left in place. It is dead only on the
generate_interpretedpath. It is live for callers entering the pipeline API directly, and on that route it returns togenerate_with_callbacks, which admits — so every route still reaches exactly one admission, never zero and never two.Not repaired by relaxing the assertion, per the constraint on the issue, and not by fixing
decoder_kv.0000, which would have greened the lane with both holes intact.Coverage, and why the existing test could not carry this alone
A rejection test passes on an engine that never admits anything — including the pre-fix engine. It is structurally blind to the positive half. The ~40-line
Enginefixture is now a helper parameterised on the byte budget, shared by the existing rejection test and a new one that asserts the two facts distinguishing a real admission from no admission: the callback fires only after the scheduler accepted, and the reservation is handed back.The fixture cannot complete a generation, so the new test's observable is scheduler state, not success — which is also the route proof that the assertion is on the path it names.
Both new assertions falsified by mutation:
self.scheduler.complete(scheduler_seq)admitted_..._releases_its_reservationleft: 1, right: 0on_admitted()above the admission callnative_generate_rejects_over_kv_byte_budget_before_backend_runValidation
cargo test --locked -p onnx-genai-engine --features native-backend— 766 passed / 0 failed across 80 binaries (lib alone 618/0/1 ignored). Baseline onmainwas 764 passed / 1 failed.cargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -- -D warnings— clean.cargo clippy --locked --all-targets -p onnx-genai-engine -- -D warnings(default features, nonative-backend) — clean. Checked deliberately: my whole local matrix would otherwise be one feature configuration, and a-D warningslane built without the feature is exactly the blind spot that has cost two people a round this week.cargo fmt --all -- --check— clean.main(6ccf7eec2) before pushing.Limitations
The interpreted path now admits every request that reaches it, including non-token workflows (diffusion, TTS) whose
prompt_tokensis 0 and which therefore reserve nothing. Empirically this caused zero regressions across the crate's 766 tests, but I am flagging that the design intent — whether those packages should pass through the scheduler at all — is inferred from the guarantee's own comment, not confirmed by anyone who owns #1723.Auto-merge armed, no
--admin; waiting onFast (Linux x86_64)andRust quality.