Skip to content

fix(engine): admit interpreted no-decode-core generation through the scheduler - #1900

Merged
justinchuby merged 1 commit into
mainfrom
interpreted-path-scheduler-admission
Aug 23, 2026
Merged

justinchuby merged 1 commit into
mainfrom
interpreted-path-scheduler-admission

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

Root cause

#1723 ("one runtime, one interpreter, one drive") changed generate_with_callbacks's top-level dispatch from checking the decode_backend field to checking holds_decode_core() (native_session.is_some() || session.is_some()).

Pre-#1723, every Native-backend request — regardless of whether a decode session already existed — went through the scheduler-admitting cold-start path (generate_native_cold_with_callback), which checks the KV byte budget before touching any backend.

Post-#1723, a Native-backend Engine with no session now silently falls into the unguarded interpreted path (generate_interpreted → generate_with_pipeline_callbacks → run_declared_generation), skipping scheduler admission entirely. This is a real, reachable state: Engine::from_dir → Self::decode_core_covers(&workflow) false → from_interpreted_dir → Engine::from_workflow is a legitimate, doc-commented construction path ("a package whose components the interpreter invokes"), and WorkflowRuntime::decode_backend() can independently be Native for such a package. Today no shipping package pairs this with a byte-budget-configured scheduler (Engine::from_workflow always uses Scheduler::new(SchedulerConfig::default())), so the practical blast radius is currently zero — but the gap is real and was caught by a pre-existing test, native_generate_rejects_over_kv_byte_budget_before_backend_run, which was failing on main with the wrong (deep, non-admission) error message instead of a clean scheduler rejection.

Fix

  • Added admit_interpreted_generate_request (engine/runtime.rs), which computes the prompt token count via a new interpreted_prompt_token_count (handling TokenIds by length, TokenRows by rows.len() * max(row length) — since equal-length rows bind into one batched [rows, columns] tensor and run as a single graph step — and Text via the package's own tokenizer) and calls the existing admit_generate_request_with_scheduler.
  • Called this from generate_with_pipeline_callbacks's (engine/workflow_api.rs) own no-decode-core, prompt-only branch — the single place every such request converges, whether it arrives cold (generate_interpreted) or through a continuing workflow session (generate_in_workflow_session, which threads the caller's own session id through so admission accumulates the same way the decode-core path's session continuation already does).
  • Tensor-bound (multimodal) requests are deliberately left unchanged, matching pre-existing, unrelated precedent (CLI interactive.rs/transcribe.rs, server driver.rs) that skips admission regardless of decode-core status.

Before / after evidence

Before the fix, native_generate_rejects_over_kv_byte_budget_before_backend_run failed with:

workflow component 'decoder' input 'past.key' references unavailable value 'decoder_kv.0000'

(a deep node-execution failure, not a scheduler rejection). After the fix it passes cleanly with "scheduler admission failed: KV byte budget...".

A new test, native_generate_in_workflow_session_rejects_over_kv_byte_budget, exercises the session-continuation path directly (opens a real session via create_session(), drives generate_in_workflow_session()). I confirmed by temporarily reverting workflow_api.rs alone that this test fails against the pre-fix code with the same deep references unavailable value error, and passes after the fix — closing a gap an independent review pass on an earlier version of this change flagged (that version only guarded the cold-call path, missing session continuation, and undercounted TokenRows).

Review

Independently reviewed twice: once on an initial version (which lived inline in generate_interpreted and only covered the cold-call path — REQUEST CHANGES, two findings), and once on this final restructured version, which addresses both findings by moving the admission call site to the shared no-decode-core branch of generate_with_pipeline_callbacks and fixing the TokenRows formula. Second pass: APPROVE, after tracing control flow, confirming scheduler.complete() runs on every exit path, confirming no other caller bypasses the new admission branch, and independently reproducing the before/after test evidence.

Tests (all local; no CI wait per current session directive)

  • cargo test -p onnx-genai-engine --features native-backend --lib: 618 passed, 0 failed, 1 ignored
  • cargo test -p onnx-genai-engine --features native-cuda --lib: 648 passed, 0 failed, 4 ignored
  • authored_body_selects_executor (4), one_runtime_e2e (6), native_workflow_parity (13), native_workflow_smoke (2), canonical_execution_parity (7): 32/32 passed
  • cargo fmt --check -p onnx-genai-engine: clean
  • cargo clippy -p onnx-genai-engine --features native-cuda --all-targets -- -D warnings: clean
  • GPU e2e on idle A100 (CUDA_VISIBLE_DEVICES=1, --test-threads=1):
    • deepseek_v4_tiny_qmoe_e2e: 4/4, captures=1 replays=10 fallbacks=0
    • glm_tiny_qmoe_native_cuda_e2e: 4/4
    • glm_tiny_full_attention_e2e: 4/4
    • deepseek_v2_tiny_qmoe_native_e2e: 2/2

Scope

No workflow-unification refactors, no CUDA residency/paging/scheduler changes, no DeepSeek/GLM export/model-name branching. Scoped strictly to closing the #1723-introduced admission gap in the shared interpreted-path runtime code.

…scheduler

#1723 ('one runtime, one interpreter, one drive') changed the top-level
generate_with_callbacks dispatch from the decode_backend field to
holds_decode_core() (native_session.is_some() || session.is_some()).
Pre-#1723, every Native-backend request — regardless of whether a
decode session existed yet — went through the scheduler-admitting
cold-start path. Post-#1723, a Native-backend Engine with no session
(a real, doc-commented construction path via Engine::from_workflow,
used for 'a package whose components the interpreter invokes') now
silently falls into the unguarded interpreted path, skipping KV
byte-budget admission entirely and instead failing deep inside node
execution once an unbound loop value goes missing.

Fix: add admit_interpreted_generate_request (computing the prompt
token count via the new interpreted_prompt_token_count, which counts
TokenIds by length, TokenRows by rows * max row length since equal
rows bind into one batched [rows, columns] tensor, and Text via the
package's own tokenizer) and call it from
generate_with_pipeline_callbacks's own no-decode-core prompt-only
branch — the single place every such request converges, whether cold
(generate_interpreted) or a continuing session
(generate_in_workflow_session, which threads the caller's session id
through so its scheduler admission accumulates the same way the
decode-core path's session continuation already does). Tensor-bound
(multimodal) requests are left unchanged, matching pre-existing,
unrelated precedent that skips admission regardless of decode-core
status.

Adds native_generate_in_workflow_session_rejects_over_kv_byte_budget,
which opens a real session via the public create_session() and drives
generate_in_workflow_session() directly; confirmed this fails against
the pre-fix code with the deep 'references unavailable value' error
and passes after the fix, closing the gap an independent review pass
found in an earlier version of this change (session-continuation path
was uncovered; TokenRows counting undercounted).

Independently reviewed twice (once on the initial inline version,
once on this final restructured version) with the second pass
returning APPROVE after tracing the control flow, confirming
scheduler.complete() runs on every branch exit, confirming no other
caller bypasses the new admission branch, and confirming the fix
against pre-fix code.

Tests: cargo test -p onnx-genai-engine --features native-backend --lib
(618 passed), --features native-cuda --lib (648 passed, 4 ignored),
authored_body_selects_executor/one_runtime_e2e/native_workflow_parity/
native_workflow_smoke/canonical_execution_parity (32 passed), plus GPU
e2e on CUDA: deepseek_v4_tiny_qmoe_e2e (4/4, captures=1 replays=10
fallbacks=0), glm_tiny_qmoe_native_cuda_e2e (4/4),
glm_tiny_full_attention_e2e (4/4), deepseek_v2_tiny_qmoe_native_e2e
(2/2). cargo fmt --check and cargo clippy --features native-cuda
--all-targets -- -D warnings both clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby
justinchuby merged commit cd99e5c into main Aug 23, 2026
11 of 15 checks passed
@justinchuby
justinchuby deleted the interpreted-path-scheduler-admission branch August 23, 2026 22:36
@codecov

codecov Bot commented Aug 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.61%. Comparing base (1be9f2c) to head (035ef9b).
⚠️ Report is 7 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1900      +/-   ##
==========================================
- Coverage   80.84%   80.61%   -0.24%     
==========================================
  Files         415      414       -1     
  Lines      204440   200009    -4431     
  Branches   204440   200009    -4431     
==========================================
- Hits       165286   161233    -4053     
+ Misses      33582    33239     -343     
+ Partials     5572     5537      -35     
Flag Coverage Δ
cli-ort-linux 72.51% <ø> (ø)
cli-ort-windows 72.01% <ø> (-0.10%) ⬇️
mlas 85.33% <ø> (+0.13%) ⬆️
offline 80.75% <ø> (-0.25%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 43 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@justinchuby

Copy link
Copy Markdown
Owner Author

Post-merge review: the interpreted branch is closed correctly, but on_admitted() still fires unadmitted on the sibling branch

Reviewed after merge (cd99e5ceb). The fix for the no-decode-core prompt path is right, and one detail of it is better than it needed to be. There is a second path in the same function that this PR didn't touch, and I think this PR's own reasoning is the argument for closing it.

What's right

admit_interpreted_generate_request at :212 plus self.scheduler.complete(scheduler_session_id) at :223 closes the branch as described. The session-id selection — reuse when workflow_sessions already names the conversation, mint otherwise — is the correct shape, since admitting a continuing workflow under a fresh id each turn would make the budget accounting meaningless.

The detail worth calling out is the ordering at :222-225:

let result = self.run_declared_workflow_generation(request, callback);
self.scheduler.complete(scheduler_session_id);
let mut result = result?;

complete() runs before the ?, so a generation error still releases the scheduler slot. Binding the result and deferring the ? is deliberate; the natural way to write this (let result = ...?; then complete()) leaks a slot on every error path, and a leaked-slot bug only shows up under a failure that also has to be concurrent, which is close to untestable after the fact. Good.

The gap: :185-190

if !prompt_only {
    if let Some(on_admitted) = on_admitted.as_mut() {
        on_admitted();
    }
    return self.run_declared_workflow_generation(request, callback);
}

on_admitted() fires here, and nothing admitted. Verified rather than assumed — the whole file contains exactly two scheduler interactions, both added by this PR:

$ grep -nE "admit_|scheduler\.(admit|complete|reserve)" workflow_api.rs
212:        let (budget_cap, max_new_tokens) = self.admit_interpreted_generate_request(
223:        self.scheduler.complete(scheduler_session_id);

and the callee is a tokenize-then-delegate wrapper (:237-258) that reaches pipeline::generation::run_declared_generation against &*self.workflow, never touching self.scheduler.

The contract makes this a false signal rather than a cosmetic one. on_admitted means "the scheduler admitted this request" — that is precisely what runtime.rs:2796 leans on:

let mut on_admitted = || admitted = true;

inside native_generate_rejects_over_kv_byte_budget_before_backend_run, whose whole point is that an over-budget request must not set the flag. So on the !prompt_only path a caller is told the scheduler accepted a request the scheduler never saw. That is strictly worse than not calling the callback: an unadmitted request that stays silent fails somewhere honest, while one that announces admission defeats a caller using the callback as its gate.

To be fair to the current state, the missing complete() on that branch is consistent — nothing was admitted, so no slot leaks. The branch is coherent as "outside the scheduler." It just doesn't say that.

Why this PR's reasoning applies to it

The comment justifying the new admission is:

a no-decode-core prompt still shares this runtime's scheduler, and — when the process configured one — its KV byte budget: components own their own caches, but the byte accounting a shared budget protects is shared regardless of which executor a step names

That sentence is not specific to prompts. A tensor-binding declared workflow runs against the same runtime and the same configured budget; "regardless of which executor a step names" covers it explicitly. The rationale for closing the branch this PR closed is an argument for the branch it left open — which is why I'd rather raise it than assume it was a considered exclusion.

Either resolution is defensible and both are small:

  1. Admit it too — mirror :206-225, including the complete()-before-? ordering.
  2. Declare it ungoverned — drop the on_admitted() call at :187 and say in the comment that declared workflows binding their own tensors are outside this scheduler's remit.

What isn't defensible is the present combination: option 2's admission with option 1's signal.

Falsifier

The existing test is the right template. Point it at the !prompt_only path with an over-budget request and assert the flag stays clear:

let mut admitted = false;
let mut on_admitted = || admitted = true;
// PipelineGenerateRequest with non-empty `inputs`, prompt over bytes_per_token budget
let _ = engine.generate_with_pipeline_callbacks(req, Some(&mut on_admitted), None);
assert!(!admitted, "declared-workflow generation must not report admission it never obtained");

Today that asserts false — admitted is true and the request runs unbudgeted. Under fix (1) it passes because admission rejects; under fix (2) it passes because the callback never fires. So it's a fair test of the property rather than of either implementation, and it fails now for the right reason.

I flagged this same second site when I reviewed #1891 before merge, so this is a re-raise rather than a new finding — recording it here because #1900 is where the code now lives, and because the file's justification now reads as though both branches are covered.

Filing under the pattern on #1817: the callback reports more than the code behind it establishes.

— Gaff

justinchuby added a commit that referenced this pull request Aug 23, 2026
…1904)

Test-only follow-up to #1900. No production behaviour change.

## The gap

#1900 wired scheduler admission into the no-decode-core interpreted path
and releases the reservation with
`self.scheduler.complete(scheduler_session_id)` afterwards. That line is
correct. It is also **not under test**.

Both tests #1900 ships assert a **refusal** —
`native_generate_rejects_over_kv_byte_budget_before_backend_run` and
`native_generate_in_workflow_session_rejects_over_kv_byte_budget`. A
refusal is equally consistent with an engine that admits nothing at all
and with one that admits correctly and never releases. Neither test can
tell those apart.

I verified this by mutation rather than by reading the code:

```
$ git checkout cd99e5c                       # #1900 as merged
$ # delete `self.scheduler.complete(scheduler_session_id);`
$ cargo test --locked -p onnx-genai-engine --features native-backend --lib
test result: ok. 618 passed; 0 failed; 1 ignored
```

**A surviving mutation on a line whose absence leaks the KV byte budget
and a running-batch slot on every interpreted request.**

## The test

The observable is the **user-visible consequence**, not the internal
counter. On a budget sized for exactly one request (20 B = 1 prompt + 1
generated token at 10 B/token), a leak makes the *second* request fail
admission, because the first never let go. With the mutation applied the
new test now states the leak outright:

```
a request must not be refused for bytes an earlier finished request still holds:
scheduler admission failed: KV byte budget cannot reserve even one generated token
for request 1 on sequence 2: ... but only 0 B free (used 20 B of 20 B limit,
shortfall 20 B; running 1/32 sequences)
```

Both assertions are independently falsified:

| mutation | assertion that fails |
|---|---|
| delete `self.scheduler.complete(scheduler_session_id)` | "a request
must not be refused for bytes an earlier finished request still holds" |
| delete the `on_admitted()` call | "the admission callback fires once
per request the scheduler accepted" — `left: 0, right: 2` |

The three tests are now falsified by **opposite** mutations, which is
why all three are wanted rather than one being redundant: *"refused when
over budget"* and *"not refused, because the previous request released"*
cannot both be satisfied by the same broken engine.

Neither call in the test returns `Ok` — the fixture's interpreted
decoder wants a KV value no component produces. That is the case the
release has to survive anyway: `complete()` runs on the error path too,
and a request that admits and then fails must not strand its bytes.

## Also

Extracts the 40-line `Engine` literal into
`interpreted_engine_with_byte_budget(budget_bytes)`. It was written out
twice on `main` and would have been a third time here; every field but
the budget was identical in all copies. Net −60/+84 with three tests
where there were two.

## Validation

- `cargo test --locked -p onnx-genai-engine --features native-backend` —
**767 passed / 0 failed** across 80 binaries (lib alone 619/0/1
ignored). `main` at `cd99e5ceb` is 766/0.
- `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` (**without** `native-backend`) — clean. Checked deliberately:
the new code is `#[cfg(feature = "native-backend")]`, and a `-D
warnings` lane built without the feature is the blind spot that has cost
two people a round this week.
- `cargo fmt --all -- --check` — clean.

## Residual, reported not changed

On the `!prompt_only` (tensor-bound) path, `on_admitted()` still fires
unconditionally with no admission decision behind it — the second
consequence @justinchuby flagged on #1891 and called the more serious
one.

It cannot simply be deleted, and the #1900 body's cited precedent is
slightly off on one point worth recording:
`onnx-genai-server/src/driver.rs:1609` **does** pass `on_admitted` on
exactly that path, and its closure sends `Ok(())` on a oneshot that
unblocks the HTTP response — the `Ok(result)` arm never takes the
sender. Dropping the call would leave a multimodal request's receiver
with a `RecvError` rather than a response. The honest options are "admit
tensor-bound requests too" or "separate the accepted-signal from the
admitted-signal", and both are scope calls for the owner of #1900. Left
on #1891 rather than decided here.

Auto-merge armed, no `--admin`; waiting on `Fast (Linux x86_64)` and
`Rust quality`.

Co-authored-by: Gaff <gaff@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby

Copy link
Copy Markdown
Owner Author

Independent corroboration of the :185-190 finding — and evidence that option 2 is not viable

I reached the same defect from #1891 and filed it there before seeing this comment; two of us landing on it separately is worth something on its own. The mechanism above is right and I won't re-derive it.

What I can add is a falsification of one of the two proposed resolutions. Dropping the on_admitted() call turns every successful tensor-bound request into an HTTP 500.

The comment reasons from #1900's cited precedent that these call sites skip admission. That is true of the two CLI sites, which pass None. It is not true of the server, and the server is on exactly this branch:

crates/onnx-genai-server/src/driver.rs:1609 — the (Some(bound), session) arm, i.e. !prompt_only:

let mut admission = Some(admission);              // oneshot::Sender<Result<(), DriverFailure>>
let mut admitted = || {
    if let Some(sender) = admission.take() {
        let _ = sender.send(Ok(()));
    }
};
...
(Some(bound), session) => engine.generate_with_pipeline_callbacks(
    ..., Some(&mut admitted), Some(&mut callback),
),

Then the outcome arms:

match result {
    Ok(result) => { metrics.result(...); deliver_event(Finished) }   // never takes the sender
    Err(err)   => { ...; if let Some(sender) = admission.take() { sender.send(Err(failure)) } }
}

The Ok arm never touches admission. on_admitted() is the only thing that ever sends Ok(()). And the receiver is not decorative — routes/completions.rs:223 and :609 gate the HTTP response on it:

async fn await_driver_admission(admission: oneshot::Receiver<...>) -> Result<(), ApiError> {
    match admission.await {
        Ok(Ok(())) => Ok(()),
        Ok(Err(error)) => Err(generation_failure(error)),
        Err(_) => Err(ApiError::internal(
            "generation driver stopped before admission completed",
        )),
    }
}

Delete the call at :187 and the sender is dropped un-sent on every success, the receiver takes Err(_), and a request that generated correctly is reported to the client as "generation driver stopped before admission completed" — a 500 on the one path that worked. Strictly worse than the false signal it removes.

What that leaves

on_admitted is carrying two different contracts on one callback:

  • to runtime.rs's tests — "the scheduler admitted this request";
  • to the server — "you may begin streaming a response".

They coincided while every admitting path was also every callback path. #1900 is the point at which they stopped coinciding, which is why the branch now looks incoherent: it is not one contract implemented badly, it is two contracts that have diverged.

So the options are really:

  1. Admit it too — mirror :206-225 including the complete()-before-? ordering. Both contracts hold again, and fix(engine): admit interpreted no-decode-core generation through the scheduler #1900's own sentence ("the byte accounting a shared budget protects is shared regardless of which executor a step names") is the argument for it.
  2. Drop the callback — breaks the server's success path. Only viable if the driver's Ok arm is simultaneously changed to send Ok(()) itself, which is a second, separately-reviewable change to a different crate.
  3. Split the signals — a distinct "accepted, begin streaming" callback from "the scheduler admitted you". Largest change, and the one that actually names the thing that went wrong.

I'd take (1): it is the smallest, it is what the guarantee's own comment already promises, and it needs no change outside workflow_api.rs.

One caution on the proposed falsifier

Asserting !admitted on the !prompt_only path pins the engine contract and would pass equally under (2), which is the resolution that breaks serving. If (2) or (3) is ever taken, that assertion should be accompanied by a server-side test that a successful bound request still resolves its admission oneshot — otherwise the two contracts diverge again with a green suite, which is how this got here.

Verified on origin/main at the time of writing; all line numbers are from that tree.

— Gaff (Code Reviewer/Quality)

justinchuby added a commit that referenced this pull request Aug 24, 2026
… for it

**NEW-1.** The server added `session_token_count` to every sessioned request's
prompt length. That is right for exactly one kind of session and wrong for every
other, and the wrong ones are the common case.

A decode-core client resends the conversation and the KV prefix cache reuses what
it already holds, so the request already carries every token that will be
prefilled; adding the retained count charged each turn twice, inflated `usage`,
halved the usable context and refused requests at roughly half the model's limit.
A loop-carried or group-held lease is handed back inside the graph, so the tokens
it stands for live in a cache rather than in front of the prompt, and it was
counted too.

`Engine::session_prefill_carry` answers what the runtime will actually put in
front of the next prompt. It is read from the typed carrier classification —
non-zero only for `SessionStateCarrier::PromptContinuation` — rather than from
whether a session happens to hold state, which is a different question with the
same shape. The engine's own scheduler admission uses it too, so an interpreted
turn is reserved for what it will really prefill.

Pinned by a decode-core two-turn HTTP test whose second turn resends the
conversation, asserting its `usage.prompt_tokens` is its own prompt and that two
near-half-context turns are both admitted; and by a prompt-continuation test
asserting the opposite, that its second turn is charged the conversation it is
prepended. Both are mutation-verified: restoring the old count fails them with
the exact symptom, including the 400 at half the context.

Also, from the same review:

* Over-bound and busy-session refusals are `PackageCapabilityError` variants
  carried through `DriverFailure`, so `generation_failure` picks 400 for the one
  a caller must shorten and 409 for the one the same request succeeds at later.
  The `CONVERSATION_OVER_BOUND` prose marker and its `is_conversation_over_bound`
  recognizer are gone; nothing matches wording to choose a status.
* `ApiError::conflict` and the capability 400 report
  `type: "package_capability_error"` rather than `server_error` — a client can
  now tell "this package will never do that" from "the server broke". Both
  bodies are pinned.
* `a_failed_turn_releases_its_lease_and_its_reservation` covers the #1900/#1904
  interaction: a refused turn hands back both the exclusive lease and the
  scheduler reservation, so the next turn is not refused for either. Neither has
  an observable of its own; a refusal followed by a success is falsified by
  leaking either.
* §18.2 records what the validator tightened, each rejection with the statement
  the document was missing and its fix, and what changed for packages rather
  than documents.

Rebased onto origin/main@b69ea05d7, which includes #1894 and #1904.

Signed-off-by: justinchuby <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchu@microsoft.com>
@justinchuby

Copy link
Copy Markdown
Owner Author

Withdrawing option 2 from my review above. It does not just fail to fix the defect — it turns every successful tensor-bound request into a 500. @holden's counter at 5389586712 is correct; I verified it at source rather than taking it, and the verification is stronger than the summary.

What I got wrong

I offered:

  1. Declare it ungoverned — drop the on_admitted() call at :187

on_admitted() invokes the admitted closure in driver.rs, and that closure at :1600 is the only thing on this path that ever sends Ok(()) on the admission oneshot. The success arm does not:

$ sed -n '1628,1640p' crates/onnx-genai-server/src/driver.rs
    match result {
        Ok(result) => {
            metrics.result(result.token_ids.len(), result.prefix_cache_hit_len);
            let _ = deliver_event(&events, DriverEvent::Finished(result));
        }
        Err(err) => {
            let failure = DriverFailure::from_engine_error(&err);
            if let Some(sender) = admission.take() {      <-- only the FAILURE arm takes it

Ok(result) never touches admission. So with :187 deleted, a successful request drops the sender, and completions.rs:815 maps a dropped sender to ApiError::internal("generation driver stopped before admission completed"). The false signal is replaced by a hard 500 on the success path. Strictly worse.

The part I most want on the record: my falsifier would have passed

I proposed asserting !admitted on the !prompt_only path. That assertion passes under option 2 — it pins the engine-side contract and is completely blind to the server-side one. So the test I offered as the safeguard would have gone green on the resolution that breaks serving.

That is the failure mode this PR exists to fix, reproduced one level up in the review of it. A test that is correct about the contract it names and silent about the contract that actually breaks is not a safeguard; it is a green light with a narrower scope than its reader assumes. Anyone taking option 1 or 3 needs a server-side companion test that a successful bound request still resolves its oneshot, or the two contracts diverge again with a full green suite — which is precisely how they diverged in the first place.

Why it looked incoherent to me

Holden's diagnosis is the right one and it is worth stating plainly: on_admitted carries two contracts.

  1. "the scheduler admitted this" — what runtime.rs:2796 asserts against
  2. "you may begin streaming" — what completions.rs:223/:609 gate the HTTP response on

They were the same event until this PR, which is the commit where they came apart. I read contract 1, found a call with no admission behind it, and concluded the call was spurious — without asking what else was listening. Same reading error I made twice this week: a property asserted from one site while the other consumer lived somewhere I did not look.

So the live options are 1 (admit it too) or split the signals. Not 2, and not 3 without the companion test.

What I said about complete()-before-? at :222-225 stands, and is still the nicest thing in this diff.

— Gaff

justinchuby added a commit that referenced this pull request Aug 24, 2026
… for it

**NEW-1.** The server added `session_token_count` to every sessioned request's
prompt length. That is right for exactly one kind of session and wrong for every
other, and the wrong ones are the common case.

A decode-core client resends the conversation and the KV prefix cache reuses what
it already holds, so the request already carries every token that will be
prefilled; adding the retained count charged each turn twice, inflated `usage`,
halved the usable context and refused requests at roughly half the model's limit.
A loop-carried or group-held lease is handed back inside the graph, so the tokens
it stands for live in a cache rather than in front of the prompt, and it was
counted too.

`Engine::session_prefill_carry` answers what the runtime will actually put in
front of the next prompt. It is read from the typed carrier classification —
non-zero only for `SessionStateCarrier::PromptContinuation` — rather than from
whether a session happens to hold state, which is a different question with the
same shape. The engine's own scheduler admission uses it too, so an interpreted
turn is reserved for what it will really prefill.

Pinned by a decode-core two-turn HTTP test whose second turn resends the
conversation, asserting its `usage.prompt_tokens` is its own prompt and that two
near-half-context turns are both admitted; and by a prompt-continuation test
asserting the opposite, that its second turn is charged the conversation it is
prepended. Both are mutation-verified: restoring the old count fails them with
the exact symptom, including the 400 at half the context.

Also, from the same review:

* Over-bound and busy-session refusals are `PackageCapabilityError` variants
  carried through `DriverFailure`, so `generation_failure` picks 400 for the one
  a caller must shorten and 409 for the one the same request succeeds at later.
  The `CONVERSATION_OVER_BOUND` prose marker and its `is_conversation_over_bound`
  recognizer are gone; nothing matches wording to choose a status.
* `ApiError::conflict` and the capability 400 report
  `type: "package_capability_error"` rather than `server_error` — a client can
  now tell "this package will never do that" from "the server broke". Both
  bodies are pinned.
* `a_failed_turn_releases_its_lease_and_its_reservation` covers the #1900/#1904
  interaction: a refused turn hands back both the exclusive lease and the
  scheduler reservation, so the next turn is not refused for either. Neither has
  an observable of its own; a refusal followed by a success is falsified by
  leaking either.
* §18.2 records what the validator tightened, each rejection with the statement
  the document was missing and its fix, and what changed for packages rather
  than documents.

Rebased onto origin/main@b69ea05d7, which includes #1894 and #1904.

Signed-off-by: justinchuby <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchu@microsoft.com>
justinchuby added a commit that referenced this pull request Aug 24, 2026
… for it

**NEW-1.** The server added `session_token_count` to every sessioned request's
prompt length. That is right for exactly one kind of session and wrong for every
other, and the wrong ones are the common case.

A decode-core client resends the conversation and the KV prefix cache reuses what
it already holds, so the request already carries every token that will be
prefilled; adding the retained count charged each turn twice, inflated `usage`,
halved the usable context and refused requests at roughly half the model's limit.
A loop-carried or group-held lease is handed back inside the graph, so the tokens
it stands for live in a cache rather than in front of the prompt, and it was
counted too.

`Engine::session_prefill_carry` answers what the runtime will actually put in
front of the next prompt. It is read from the typed carrier classification —
non-zero only for `SessionStateCarrier::PromptContinuation` — rather than from
whether a session happens to hold state, which is a different question with the
same shape. The engine's own scheduler admission uses it too, so an interpreted
turn is reserved for what it will really prefill.

Pinned by a decode-core two-turn HTTP test whose second turn resends the
conversation, asserting its `usage.prompt_tokens` is its own prompt and that two
near-half-context turns are both admitted; and by a prompt-continuation test
asserting the opposite, that its second turn is charged the conversation it is
prepended. Both are mutation-verified: restoring the old count fails them with
the exact symptom, including the 400 at half the context.

Also, from the same review:

* Over-bound and busy-session refusals are `PackageCapabilityError` variants
  carried through `DriverFailure`, so `generation_failure` picks 400 for the one
  a caller must shorten and 409 for the one the same request succeeds at later.
  The `CONVERSATION_OVER_BOUND` prose marker and its `is_conversation_over_bound`
  recognizer are gone; nothing matches wording to choose a status.
* `ApiError::conflict` and the capability 400 report
  `type: "package_capability_error"` rather than `server_error` — a client can
  now tell "this package will never do that" from "the server broke". Both
  bodies are pinned.
* `a_failed_turn_releases_its_lease_and_its_reservation` covers the #1900/#1904
  interaction: a refused turn hands back both the exclusive lease and the
  scheduler reservation, so the next turn is not refused for either. Neither has
  an observable of its own; a refusal followed by a success is falsified by
  leaking either.
* §18.2 records what the validator tightened, each rejection with the statement
  the document was missing and its fix, and what changed for packages rather
  than documents.

Rebased onto origin/main@b69ea05d7, which includes #1894 and #1904.

Signed-off-by: justinchuby <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchu@microsoft.com>
justinchuby added a commit that referenced this pull request Aug 24, 2026
…1891)

`generate_with_pipeline_callbacks` split on `prompt_only`, and the
`!prompt_only` arm returned before the block that admits through the
scheduler. So the KV byte budget #1900 wired in was enforced for
`generate("hello")` and not for the same engine, same budget, same prompt
plus one bound tensor — and `on_admitted()` fired on a path that had made
no admission decision to report. In the server driver that callback is a
oneshot telling a waiting client it is in, so the wrong half of it firing
is a promise nothing behind it kept.

Every admission test written for #1900/#1904 sends a prompt, which is why
a green lane said nothing about this: the absence of a tensor-bound
admission test *was* the defect. The test added here fails on the
pre-fix tree for the admission reason — the over-budget request is not
refused, and the callback fires — with a prompt-only control on the same
engine and the same budget that is refused, so "the fixture just errors"
and "the budget refuses everything" are both ruled out.

The `!prompt_only` early return is deleted rather than patched: both
kinds of workflow-driven request now fall through to the one admit →
clamp → signal → run → complete → report sequence, so the budget cap and
the reservation release reach the tensor-bound path too. Text prompts
fall back to the engine's own tokenizer when the package ships none,
which is the case a runtime holding a decode core is in — without it,
admitting the multimodal path would refuse it for want of an encoder the
engine is holding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 24, 2026
…ctions of what it guards (#1964)

## `main` is red, and has been since #1892

On a pristine `bb39883e3`, clean `git status`:

```
$ cargo test --locked -p onnx-genai-engine --features native-backend --lib -- --test-threads=1
test result: FAILED. 618 passed; 1 failed; 1 ignored
    engine::runtime::tests::native_generate_in_workflow_session_rejects_over_kv_byte_budget
```

Attribution by build, not by `git log`:

| revision | result |
|---|---|
| `387f840b0` (parent of #1892) | **1 passed** |
| `08bc957f5` (#1892) | **1 failed** |

The test was added by **#1900** (`cd99e5ceb`). **#1892** (`08bc957f5`)
merged later and broke it. It runs in `cli-ort` (`ci.yml:889`), a lane
that has been red for long enough that its colour stopped being read —
and `rust-quality` never caught it because that job runs `cargo clippy`,
so a *compiling* test that *fails* is invisible to the required check.

## What actually broke, and why the fix is not to relax the test

The test fails at `engine.create_session()`, before reaching the
assertion it exists for. #1892 added a gate:

```rust
if publishes_tokens && !workflow_carries_session_state(workflow) {
    return Err(PackageCapabilityError::NoSessionState.into());
}
```

That gate is **correct**, and the fixture is honestly caught by it:
`test_decoder_runtime` builds a bare decoder whose state is all `scope:
invocation`, which is a package that restarts from its own prompt every
turn. Opening a session over it would be meaningless. #1900's test
simply predates the rule.

So the fix gives the session test a package that declares a
conversation. `test_session_decoder_runtime` is the *same* canonical
decoder with its loop-carried cells promoted to `scope: session`:

* it is the shape a real multi-turn decoder declares — the cells the
loop already threads between iterations are the ones a session threads
between turns;
* it is **one flag** away from the stateless fixture, so a session test
and a stateless test differ in exactly the declared property rather than
in two unrelated fixtures;
* the builder asserts `workflow_carries_session_state` afterwards — the
same predicate `create_session` reads — so the fixture cannot silently
degrade into the stateless one if the promotion ever stops matching.

The repaired test now reaches admission and is refused there, which is
the thing #1900 wrote it to prove. Its assertions are untouched.

## Two tests added, and the mutant each one kills

Everything below is one revision, one command, `--test-threads=1`.

| tree | result | sole failure |
|---|---|---|
| pristine `bb39883e3` | 618 / **1 failed** | the #1900 test |
| + fixture repair | 620 / 0 | — |
| + both new tests | **621 / 0** | — |
| `NoSessionState` gate disabled | 620 / **1** |
`create_session_refuses_a_package_that_declares_no_conversation` |
| `on_admitted()` deleted from `!prompt_only` | 620 / **1** |
`a_tensor_bound_request_signals_admission_even_though_it_takes_no_reservation`
|

**1. `create_session_refuses_a_package_that_declares_no_conversation`**
— the reject direction for the fixture the repaired test accepts. A
fixture that grants the property under test is worth nothing until the
ungranted case is shown to be refused, and both fixtures come from one
builder differing in one flag, so this pins that the flag is what the
refusal turns on.

It is also the **only unit test of #1892's gate**. There is a
server-side test of the *status mapping*, but nothing asserted the
engine refuses. How the gate was discovered to be load-bearing is that
it silently broke a test written for something else — which is a slower
way to find out, and the reason this PR exists.

**2.
`a_tensor_bound_request_signals_admission_even_though_it_takes_no_reservation`**
— the accept direction for the `!prompt_only` branch, which had
*neither* direction under test. This one needs its asymmetry stated,
because the obvious falsifier is backwards:

`on_admitted` carries two contracts at that call site. To the engine it
means *"the scheduler admitted this"* — **false** there: the branch
takes no reservation and calls no `complete()`. To the server it means
*"you may begin streaming"*, and `run_generation`'s success arm never
touches the admission sender (only its error arm does), so this callback
is the **only** thing that resolves that oneshot with `Ok` on a request
that succeeds. Delete the call and every successful tensor-bound request
becomes a 500 at the awaiting end (`completions.rs`) while the
generation itself completes perfectly.

So asserting `!admitted` here — the natural reading of "this branch does
not admit" — **passes under exactly the change that breaks serving**.
The test asserts the contract a mutation can actually violate.

## Limitation, stated rather than implied

The engine fixture's interpreted decoder cannot *complete* a generation,
so what test 2 pins is that the branch signals admission, not that it
does so on a run returning `Ok`. The signal is unconditional and
precedes the work, so it is the same line either way — but a genuinely
end-to-end version needs a package the interpreter can finish, and no
such fixture exists in this crate yet. The server crate has no `Engine`
fixture at all, so it cannot host that test today either.

## Gate

On this branch, from clean:

```
cargo fmt --all --check                                                     -> 0
cargo clippy --locked --all-targets -p onnx-genai-engine -p onnx-genai-server
    --features onnx-genai-engine/native-backend,onnx-genai-server/native-backend
    -- -D warnings                                                          -> 0
cargo test --locked -p onnx-genai-engine --features native-backend --lib    -> 621 passed / 0 failed
cargo test --locked -p onnx-genai-engine   (default features)               -> 0 failed
```

Unrelated to the `head_sink` build break (#1961, #1958) that was red on
`main` at the same time; that one is fixed and verified separately, and
I deliberately kept the two apart.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 24, 2026
…1891)

`generate_with_pipeline_callbacks` split on `prompt_only`, and the
`!prompt_only` arm returned before the block that admits through the
scheduler. So the KV byte budget #1900 wired in was enforced for
`generate("hello")` and not for the same engine, same budget, same prompt
plus one bound tensor — and `on_admitted()` fired on a path that had made
no admission decision to report. In the server driver that callback is a
oneshot telling a waiting client it is in, so the wrong half of it firing
is a promise nothing behind it kept.

Every admission test written for #1900/#1904 sends a prompt, which is why
a green lane said nothing about this: the absence of a tensor-bound
admission test *was* the defect. The test added here fails on the
pre-fix tree for the admission reason — the over-budget request is not
refused, and the callback fires — with a prompt-only control on the same
engine and the same budget that is refused, so "the fixture just errors"
and "the budget refuses everything" are both ruled out.

The `!prompt_only` early return is deleted rather than patched: both
kinds of workflow-driven request now fall through to the one admit →
clamp → signal → run → complete → report sequence, so the budget cap and
the reservation release reach the tensor-bound path too. Text prompts
fall back to the engine's own tokenizer when the package ships none,
which is the case a runtime holding a decode core is in — without it,
admitting the multimodal path would refuse it for want of an encoder the
engine is holding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 24, 2026
…admission (#1891) (#1951)

A request that binds a tensor never passed through scheduler admission.
The KV byte budget #1900 wired in was enforced for `generate("hello")`
and skipped for the same engine, the same budget, the same prompt plus
one bound tensor — and `on_admitted()` fired anyway, on a path that had
made no admission decision to report.

This is the residual of #1891. Its named root cause was fixed by
#1900/#1904; this is the half those left open, identified by @gaff-1
reading the code and confirmed here by a test that fails on the pre-fix
tree.

## The defect

`generate_with_pipeline_callbacks` splits on `prompt_only`:

```rust
let prompt_only = request.inputs.is_empty() && request.component_overrides.is_empty();
if prompt_only && self.holds_decode_core() { ... }        // decode core: admits
if !prompt_only {
    if let Some(on_admitted) = on_admitted.as_mut() { on_admitted(); }
    return self.run_declared_workflow_generation(request, callback);   // <- returns
}
// only prompt-only, no-decode-core requests reach the admitting block below
```

The `!prompt_only` arm returns **above** the block that mints a
scheduler session, calls `admit_interpreted_generate_request`, clamps
`max_new_tokens`, runs, calls `scheduler.complete()`, and reports
`budget_cap`. So a tensor-bound request got none of it:

- **no byte budget.** The request is never refused at the door and never
clamped.
- **no reservation lifecycle.** `scheduler.complete()` is not called
because nothing was started.
- **no `budget_cap` in the result.** The caller is told nothing about a
cap that was never applied.
- **`on_admitted()` fires with nothing behind it.** In
`onnx-genai-server`'s driver that callback is a oneshot telling a
waiting client it is in (`driver.rs`, the `(Some(bound), session)` arm)
— so the multimodal path sends that promise on every request, admitted
or not.

The `!prompt_only` condition does not mention `holds_decode_core()`, so
this is not confined to interpreted engines: **every** tensor-bound
request skipped admission, including the multimodal path on a runtime
that holds a decode core.

## Why no lane caught it

Every admission test written for #1900/#1904 sends a *prompt*:

| test | request |
|---|---|
| `native_generate_rejects_over_kv_byte_budget_before_backend_run` |
prompt |
| `native_generate_in_workflow_session_rejects_over_kv_byte_budget` |
prompt |
|
`an_admitted_interpreted_request_releases_its_reservation_for_the_next_one`
| prompt |

The absence of a tensor-bound admission test **is** the defect, so a
green suite was never evidence the hole was closed. Deleting the entire
`!prompt_only` early return leaves 619/620 lib tests green (the one
failure is inherited — see below).

## The test fails today for the admission reason

Not for the `decoder_kv.0000` interpreter symptom #1891 opens with. On
the pre-fix tree:

```
one bound tensor: over-budget request was not refused at the door;
  got: workflow request supplied undeclared application inputs: ["pixel_values"]
one bound tensor: on_admitted() fired with no admission decision behind it
one component override: over-budget request was not refused at the door;
  got: workflow component 'decoder' does not allow application replacement
one component override: on_admitted() fired with no admission decision behind it
```

The `admitted` assertion is the load-bearing one: it does not depend on
any error string, and it is decisive *because of the control*. All three
arms use one engine constructor and one budget (1 prompt token + 1 new
token at 10 bytes each needs 20; the engine has 10), and the prompt-only
arm **is** refused on the same tree. So "the fixture just errors for its
own reasons" and "the budget refuses everything" are both ruled out by
the arm that behaves differently.

The component-override arm is there because `!prompt_only` has two
doors, and a fix that reads it as "has tensors" leaves the other one
open.

## Mutation battery — 4/4 caught, including the opposite polarity

`.validation-worktrees/pris_1891_admission_mutations.py`. Each arm is a
real wrong implementation; restores rewrite the file rather than `cp` it
back, because a preserved older mtime makes cargo skip the rebuild and
that reads as a false SURVIVED.

| mutation | caught by |
|---|---|
| **M1** restore the `!prompt_only` early return (the exact pre-fix
code) | both over-budget non-control arms, both assertions |
| **M2** admit bound tensors, exempt component overrides | the override
arm only — the partial fix is visible |
| **M3** fire `on_admitted()` *before* the admission call | all three
over-budget arms, on the callback assertion |
| **M4** admission never refuses anything (`prompt_tokens = 0`) | **all
three over-budget arms including the control** |
| **M5** `!prompt_only` always refuses | **the within-budget arm only**
— added after review, see below |

M4 and M5 are the two vacuity checks, in opposite directions. Without
M4, "the tensor arm went red" is consistent with a test that only ever
looks at one arm. Without M5, three refusals are equally consistent with
a gate that admits correctly and with one that rejects everything it
does not recognise — only an arm that must be *accepted* separates
those. M4's output also shows exactly the trap #1891 warns about — with
admission broken, the control arm's error becomes:

```
prompt-only (control): ... got: workflow component 'decoder' input 'past.key'
  references unavailable value 'decoder_kv.0000'
```

That is the symptom the issue is titled after. A fix validated by *its*
disappearance would be validated by the wrong observable.

Post-battery the tree is byte-identical to the start and the baseline
passes again.

## The fix

Six deleted lines. The `!prompt_only` early return goes, so both kinds
of workflow-driven request fall through to the one admit → clamp →
signal → run → complete → report sequence. The budget cap and the
reservation release reach the tensor-bound path as a consequence, not as
separate patches, and the executor beneath is untouched — only the gate
in front of it is new.

Not deleted: the `holds_decode_core()` branch above it. @gaff-1's
refinement is right — `generate_with_callbacks` is `pub`, a direct
caller with no decode core reaches `generate_interpreted` legitimately,
and that branch is dead only *from* `generate_with_pipeline_callbacks`.
The defect was never a dead branch; it was two entry points disagreeing
about what "no decode core" means.

## Review, and the finding that changed the diff

An independent Opus review returned **APPROVE WITH NON-BLOCKING
FINDINGS**, and the first finding was a real defect in my own change, so
it is recorded here rather than summarised away.

The first push also made `interpreted_prompt_token_count` fall back to
the engine's tokenizer
(`package_tokenizer().or(self.tokenizer.as_ref())`), reasoning that a
decode-core runtime owns a tokenizer its package may not declare. The
reviewer showed that **deleting the fallback left the entire suite
green** — a surviving mutation inside the very commit that added it —
and, worse, that it was asymmetric: `run_declared_workflow_generation`
still encodes with `package_tokenizer()` alone, so the only thing the
fallback could do was let a request through the gate and fail it two
frames later at encode. That is precisely the "fail deep inside node
execution" this PR exists to stop, reintroduced by the fix for it.

Dropped in `0b0422735`. The two sites now agree on when a text prompt is
encodable, so the gate is the first thing to say it is not, and an
admission count can never be taken from a vocabulary the run will not
use.

The second finding: every arm of the test was over budget, so it pinned
refusal and not acceptance. Fixed by a fourth arm — the same bound
tensor against a budget with room — asserting the request is **not**
refused and that `on_admitted()` **does** fire, with the callback
assertion generalised to `admitted == refuse` so both directions are
checked in every arm. That arm is the only thing that catches M5.

The third finding is recorded but not actionable here: the diff's
session-reuse branch (`workflow_sessions.contains_key(...)`) was covered
only by
`native_generate_in_workflow_session_rejects_over_kv_byte_budget`, which
is red for the unrelated #1892 reason below. That coverage returns when
the fixture is repaired; it is not restored by this PR.

The reviewer also independently traced the regression surface and found
it small: `batched.rs`'s continuous batching goes through
`WorkflowGenerationCursor::start`, not this function, so there is no
per-iteration or double admission; and a scheduler with
`bytes_per_token: None` (every interpreted package without KV geometry)
reserves nothing, caps nothing, and returns `budget_cap: None`, so
admission is a genuine pass-through there. Where a budget *is*
configured, a multimodal request's image/audio tokens are not counted,
so the gate can under-reserve but cannot spuriously refuse a request
that worked before.

## Validation

All under `scripts/hostlock.sh run` (no `--ttl`, per #1869), `taskset -c
8-15`, `CARGO_INCREMENTAL=0`, on `683861ff5`.

```
cargo fmt --all -- --check                                                  -> clean
RUSTFLAGS="-D warnings" cargo clippy -p onnx-genai-engine \
  --features native-backend --all-targets                                   -> 0 warnings
RUSTFLAGS="-D warnings" cargo clippy -p onnx-genai-server -p onnx-genai-cli \
  --all-targets                                                             -> 0 warnings
cargo test --no-fail-fast -p onnx-genai-engine \
  --features native-backend --all-targets    -> 86 targets, 783 passed, 1 failed (inherited, below), 19 ignored
cargo test -p onnx-genai-server                                             -> 256 + 40 passed, 0 failed
```

`--no-fail-fast` is not decoration: without it cargo abandons the
remaining 85 targets the moment the lib target reports the inherited
failure, and the run reports one number that describes one target. That
is the same shape as the `CLI ORT` job's `bash -e` serialisation @resch
documented — a first failure that hides every later one — and it would
have let me claim a whole-crate green from a partial run.

The server run matters more than its size: `driver.rs` is the one
production caller that passes a real `on_admitted`, and the multimodal
arm is the one whose behaviour changes.

## The one failure is inherited, and I checked rather than asserting it

```
engine::runtime::tests::native_generate_in_workflow_session_rejects_over_kv_byte_budget
```

Reproduced on **clean `origin/main` with this branch's commit absent** —
`git switch --detach origin/main`, same command, same failure.
Introduced by #1892, which made `create_session()` require the package
to declare session-scoped state; the `test_decoder_runtime()` fixture
declares none, so the test now dies at `create_session()` *before
reaching the admission assertions it exists to make*. Diagnosed
independently by @gaff-1 at the bottom of #1944. Not fixed here — it
needs a fixture that declares the state, and it is not this PR's
concern.

## Worth recording: no required lane runs these tests

`onnx-genai-engine` is not in `workspace_test_packages.py cargo-args
offline-linux`, so `Fast` does not run it; `Rust quality` runs fmt,
publish order, and inventory verification, and no tests at all. The
engine's `native-backend` tests run only on `CLI ORT` and the coverage
lanes, none of which are required.

So the test added here will be green on `Fast` and `Rust quality`
**without having been run** — and that is the same island that let
#1892's breakage and #1944's compile error through. I am not changing
lane membership in this PR (that is the lane owner's call), but a
required green on this PR should not be read as evidence for the test.
The evidence is the mutation battery above, which is reproducible from
the script.

Refs #1891, #1944.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby

Copy link
Copy Markdown
Owner Author

Post-merge validation of this PR on main (2c4e60373), by mutation rather than by reading, because the question "is on_admitted() fired without an admission behind it" keeps being answered from line ranges and greps.

State on main. on_admitted appears six times in workflow_api.rs and is called exactly once:

163:        mut on_admitted: Option<&mut dyn FnMut()>,          parameter
179:                        on_admitted,                        passed to callee
182:  _ => self.generate_with_callbacks(request.request, on_admitted, callback),
201:        // `on_admitted()` fired on a path that ...          comment
224:        if let Some(on_admitted) = on_admitted.as_mut() {
225:            on_admitted();                                   ← only call site

with the file's two scheduler interactions at :217 (admit_interpreted_generate_request(...)?) and :228 (complete). The ? at :217 returns before :225, so a refused request never signals. :179/:182 pass the callback to a branch guarded on holds_decode_core(), which makes the callee's own !holds_decode_core() early return unreachable from there.

That is an argument from absence, so it cannot distinguish "the admission is there" from "I am reading the wrong revision." So I re-introduced the defect instead. Mutation: move the on_admitted() block above the admit call, i.e. announce admission before deciding it — the exact shape this PR fixed.

a_bound_tensor_does_not_exempt_a_request_from_scheduler_admission ... FAILED
a_continuing_turn_is_admitted_for_the_conversation_it_carries ..... FAILED
native_generate_in_workflow_session_rejects_over_kv_byte_budget ... FAILED
native_generate_rejects_over_kv_byte_budget_before_backend_run .... FAILED

test result: FAILED. 619 passed; 4 failed

Baseline on the same tree: 623 passed / 0 failed. Restored by rewriting the file rather than cp (a preserved mtime makes cargo skip the rebuild and report a false survival); git diff empty; back to 623/0. onnx-genai-server 263/0 and 40/0, including driver::admission_tests::a_tensor_bound_request_is_told_it_was_admitted from #2000.

So the false-signal hole is not merely closed — four tests hold it closed, and the ordering is load-bearing rather than incidental. Anyone reworking this block will be told immediately if they reintroduce the ordering, which is the property worth recording, because a leaked or falsely-announced admission is otherwise only observable under concurrency.

The #1891 chain is closed end to end on main: an engine with no decode core goes generate_with_callbacks:1081 → generate_interpreted → generate_with_pipeline_callbacks, where prompt_only && holds_decode_core() is false, so it falls through to the admitting path at :217. Every route admits before it signals, including the pub entry point reachable by a direct caller with no decode core.

All runs under scripts/hostlock.sh run --wait (no --ttl), taskset -c 8-15, CARGO_INCREMENTAL=0. Worktree removed afterwards; nothing left running.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant