Skip to content

fix(server): update the native-backend test call site for generate's third argument - #1944

Merged
justinchuby merged 1 commit into
mainfrom
squad/gaff-fix-cli-ort-driver-generate-arity
Aug 24, 2026
Merged

justinchuby merged 1 commit into
mainfrom
squad/gaff-fix-cli-ort-driver-generate-arity

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

main does not compile with native-backend enabled. This is one of the two things keeping the CLI ORT lane red; the other is separate and described at the bottom.

The failure

error[E0061]: this method takes 3 arguments but 2 arguments were supplied
    --> crates/onnx-genai-server/src/tests.rs:2601:29
     |
2601 |     let generation = driver.generate(Some(session_id), request).await.unwrap();
     |                             ^^^^^^^^--------------------------- argument #3 of type
     |                                                                 `Option<MultimodalInput>` is missing

EngineDriver::generate grew an input: Option<MultimodalInput> parameter. The one call site in tests.rs was not updated. Passing None restores exactly what the call did before, since it supplied no multimodal input.

This is a compile error, not a test failure — it takes the lane down at the last step, cargo clippy -p onnx-genai-cli -p onnx-genai-server --features onnx-genai-cli/native-cuda,onnx-genai-server/native-cuda --all-targets -- -D warnings, because native-cuda implies native-backend.

Why it survived

The enclosing test is #[cfg(feature = "native-backend")], and no default lane compiles it. Fast, Rust quality, and the coverage lanes are all green on main right now with this error present. The only job that enables the feature is CLI ORT, so an API change to a pub(crate) method could not be caught anywhere near the point it was made.

That is worth noting beyond this one fix: a pub(crate) signature change is normally caught by the compiler, which is why it needs no test. That guarantee silently does not hold for call sites behind a feature no routine lane builds.

Validation

Same commit (59f6c6cde), baseline and fix measured back to back, with a touch between them so neither run could reuse the other's artifacts:

clippy exit
unmodified main 101 — error[E0061]
with this fix 0
cargo test --locked -p onnx-genai-server --features native-backend --lib -- --test-threads=1
  -> 262 passed; 0 failed; 2 ignored
cargo fmt -p onnx-genai-server --check  -> exit 0

The previously-uncompilable test itself now runs and passes:

native_driver_sessions_generate_through_server_path ... ok

This does not fully green the lane

Being explicit so the PR is not read as more than it is. CLI ORT has a second, unrelated blocker in a different step:

---- engine::runtime::tests::native_generate_in_workflow_session_rejects_over_kv_byte_budget ----
Error: this package publishes a token stream but declares no session-scoped workflow state
       anything carries, so a session could not continue a conversation ...

Deterministic on 59f6c6cde, introduced by #1892, which made create_session() require the package to declare session-scoped state. The test's fixture does not, so it now fails at create_session() before reaching the admission assertions it exists to make — the same "fixture quietly lost its premise" shape as #1879. Filing that separately; it needs a fixture that declares the state, not a relaxed assertion.

Also worth recording against #1891: the test that issue named, native_generate_rejects_over_kv_byte_budget_before_backend_run, passes on current main. That root cause is fixed. #1891's own warning — "it may be a different red" — turned out to apply to itself twice over.

Refs #1891.

…third argument

`EngineDriver::generate` grew an `input: Option<MultimodalInput>` parameter, but
the one call site in `tests.rs` behind `#[cfg(feature = "native-backend")]` was
not updated, so `onnx-genai-server` fails to compile with that feature:

    error[E0061]: this method takes 3 arguments but 2 arguments were supplied
      --> crates/onnx-genai-server/src/tests.rs:2601:29

This is a compile error on `main`, not a test failure. It takes down the
`CLI ORT` lane at its last step, `cargo clippy -p onnx-genai-cli
-p onnx-genai-server --features .../native-cuda --all-targets -- -D warnings`,
because `native-cuda` implies `native-backend`.

It survived because no default lane compiles this test: the feature is off
everywhere except `CLI ORT`, so an API change to a `pub(crate)` method could
not be caught at the point it was made. Passing `None` restores the previous
behaviour of the call, which supplied no multimodal input.

Measured, same commit, `59f6c6cde`:

    unmodified   clippy exit 101   error[E0061]
    with fix     clippy exit 0
    -p onnx-genai-server --features native-backend --lib: 262 passed, 0 failed

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

codecov Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.30%. Comparing base (0a668d5) to head (4d81ed9).
⚠️ Report is 14 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1944      +/-   ##
==========================================
+ Coverage   80.19%   80.30%   +0.10%     
==========================================
  Files         400      422      +22     
  Lines      186471   206747   +20276     
  Branches   186471   206747   +20276     
==========================================
+ Hits       149540   166020   +16480     
- Misses      31557    35098    +3541     
- Partials     5374     5629     +255     
Flag Coverage Δ
cli-ort-linux 72.51% <ø> (?)
cli-ort-windows 72.01% <ø> (?)
mlas 85.20% <ø> (?)
offline 80.42% <ø> (+0.23%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 67 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
justinchuby merged commit a917b98 into main Aug 24, 2026
14 of 17 checks passed
@justinchuby
justinchuby deleted the squad/gaff-fix-cli-ort-driver-generate-arity branch August 24, 2026 07:10
justinchuby added a commit that referenced this pull request Aug 24, 2026
…t missed it (#1961)

## `main` does not compile with `native-backend`

Two independent breaks, both in code that **no default lane builds**.
`Fast`,
`Rust quality` and every coverage lane are green right now with both
present.

### 1. `paged_gqa.rs` — the urgent one

#1956 threaded a `head_sink: Option<f32>` parameter through
`sdpa_decode_row` and `sdpa_decode_row_accessor`. Two call sites in
`crates/onnx-genai-engine/src/native_decode/paged_gqa.rs` were not
updated:

```
paged_gqa.rs:164: error[E0061]: this function takes 10 arguments but 9 were supplied
paged_gqa.rs:247: error[E0061]: this function takes 11 arguments but 10 were supplied
```

**`None` is not a guess.** `PagedGqaConfig` has no sink term,
`head_sink`
appears nowhere in this crate, and `None` reproduces exactly what these
calls
did before the parameter existed.

It also matters that this is right rather than merely compiling: this
paged path
is the **parity oracle** for the flat one, so a wrong sink value would
silently
desynchronise the two while still building. `paged_matches_flat_*` is
the test
that would catch that, and all seven pass.

Note this is the **third** call site #1956 missed — #1958 is fixing the
ep-cpu
bench, and neither it nor any other open PR touches `paged_gqa.rs`.

### 2. The check that should have caught #1944 covered only half the
surface

The "Check the native backend compiles" step builds `-p
onnx-genai-engine`
alone. A `#[cfg(feature = "native-backend")]` test in
`onnx-genai-server`
called `EngineDriver::generate` with its pre-third-parameter arity, and
`main` sat uncompilable while every default lane stayed green (#1944).
The only
job that did compile it is `CLI ORT`, which is slow and has been red for
unrelated reasons — so the signal existed but never arrived anywhere
useful.

Add the server crate to the same step. That step's own comment already
names
this exact failure mode:

> *onnx-genai-engine's native decode path only compiles when this
feature is on.
> Without this step a refactor can leave it uncompilable while every
other job
> still passes.*

The gap was never the idea — only its coverage.

## Verification

Locally on a CUDA host, reproducing before and confirming after:

| Command | before | after |
|---|---|---|
| `clippy --locked --all-targets -p onnx-genai-engine --features
native-backend` (today's CI step, unchanged) | 2x `E0061` | **exit 0** |
| the same, widened to `-p onnx-genai-server` (this PR's step) | 2x
`E0061` | **exit 0** |
| `cargo test --locked -p onnx-genai-engine --features native-backend
paged` | — | **12 passed**, including all seven `paged_matches_flat_*` |

`ci.yml` parses under `yaml.safe_load`.

## What I did not verify

- I did not measure what the widened step adds to that lane's wall time.
The
server crate's dependency tree overlaps the engine's heavily, so the
marginal
  cost should be small, but that is reasoning rather than a measurement.
- I did not audit the rest of the workspace for further #1956 call sites
beyond
the two here and the bench in #1958. The widened step will now surface
any
that live behind `native-backend`; ones behind other unbuilt features
would
  not be covered by this change.
- This does not address the general class — features that no default
lane
compiles. It closes the two instances that are breaking `main` today and
the
  one lane gap that let the second through.

Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
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>
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.

2 participants