Skip to content

perf(cpu): route the int4 flat output-row fan-out through the task runtime - #1363

Merged
justinchuby merged 1 commit into
mainfrom
squad/sebastian-native-fanout-taskrt
Aug 19, 2026
Merged

justinchuby merged 1 commit into
mainfrom
squad/sebastian-native-fanout-taskrt

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner

The loss

gemm_nbits_qwen3_0p6b_qkv_t8 in a pure-native build (bench-native, no mlas feature — no MLAS at runtime, no ORT CPU fallback), native p50:

t=8 t=16 t=32
1.9 ms 1.6 ms 37.7 ms

§33.9 named this cell and recorded a 4.6× inversion. It is now 23×. Sixteen extra hardware threads make a 1.6 ms kernel take 37.7 ms.

The site is not the one that looks like it

The obvious suspect is borrowed_affine_int4_matmul_prefill, the m > 1 native column fan-out — the twin of the MLAS tiling #1238 fixed. Routing it changed nothing on any cell, because it is dead in the default build: gated on borrowed_int4_prefill_block_enabled(), which reads ONNX_GENAI_CPU_MM_INT4_PREFILL and defaults to false (as does ONNX_GENAI_CPU_MM_INT4_NBLK).

The route every int4 GEMM actually takes is borrowed_affine_int4_matmul, which has no m > 1 fan-out at all — it loops over activation rows and calls parallel_output_rows once per row. An 8-token prefill is eight fork-joins; a 512-token prefill is 512.

parallel_output_rows was a bare result.par_chunks_mut(chunk) on whichever Rayon pool happened to be installed. It is the single flat fan-out shared by eight call sites (int4_matmul_m1, packed_nbits_output_row, borrowed_affine_int4_matmul, borrowed_affine_int4_matmul_nblock, int8_row, gemv_nk, gemv_nk_u8, gemv_nk_u8_i16) — the §26 disease on the hottest path in the CPU EP, three phases after §35.6 declared that path clean. This PR corrects that claim in the document.

Measuring anything at all on this host

The reference host carried a load average of 17–35 on 32 logical CPUs throughout, because several agents share it. A single cell can swing 5× between trials. So every number below comes from a three-arm invocation: base, new, and null — a byte-identical copy of the base binary. The null arm measures what the harness reports for a change that provably does not exist.

t=32, 16 gemm_nbits_* cells, 5 trials × 7 runs, arms interleaved within each trial:

geomean worst cell best cell
null (base against itself) 1.00× 0.72× 1.20×
new (this change) 2.33× 0.65× 16.6×

A per-cell reading inside 0.72×–1.20× is indistinguishable from nothing here. A second, quieter invocation of the same grid reproduced the geomean at 2.15×.

The change

Route the flat fan-out through task_runtime::for_each_range, passing the existing output_chunk_len through unchanged as the minimum grain, so the partition can only get coarser, never finer, than the one Rayon used. The numa-split and SPMD branches are untouched.

Three things gate it, and all three are load-bearing (each was falsified — flipping any one constant turns a test red on its own).

1. HOT_FAN_OUT_MACS + HOT_FAN_OUT_CALLS — the hot-prefill carve-out

Unconditional routing regressed two cells: llama3_8b_mlp at t128 (0.93×) and t512 (0.86×). They run the same 56 Mi fan-out as llama3_8b_mlp_t8, which improves 1.5×. Identical per-call work, opposite result — so the discriminator is not the work, it is how many fan-outs arrive back to back.

That is §33.4's park model read in reverse. A parked Rayon worker takes 67–226 µs to reach its first task, which is why the runtime wins wherever fan-outs are sparse. But one fan-out per row of a 512-token prefill never lets the pool sleep: the park cost is paid once and amortised to nothing, leaving only the runtime's SMT cap — 16 physical lanes against Rayon's 32.

No threshold on total operator work can express this:

cell fan-out MACs calls total wants
llama3_8b_mlp_t128 56 Mi 128 7.2 Gi Rayon
llama3_8b_qkv_t512 24 Mi 512 12.3 Gi task runtime

The cell that wants Rayon has less total work. Reusing #1238's WIDE_PREFILL_MACS would have thrown away five measured wins to fix two losses. So the wide path is taken only when both hold: fan_out_macs >= 32 Mi (measured separation 24 Mi vs 56 Mi) and calls >= 32 (measured separation 8 vs 128).

parallel_output_rows keeps its one-shot spelling and delegates with calls = 1, so no decode step can reach the wide path however large its projection; only the two row-looping int4 kernels pass m.

2. lanes is a closure — a pool that should not have existed

Routing was worth 2.2× at t=32 and 1.07× at t=16, but the same grid at t=4 came back at 0.81× with six of eight cells losing — outside the 0.95× the null arm shows at that width.

The cause was not the routing. It was task_runtime::width(), which is documented to build the pool so a kernel can size a partition against it. The policy asked for the width, chose Rayon anyway, and left behind resident workers that spin before they park — on a configuration that has three other threads doing the arithmetic. flat_fan_out now takes lanes as impl FnOnce() -> usize: the width test that needs no pool runs first, and a fan-out that will stay on Rayon never asks. The type makes it impossible to reintroduce, and flat_fan_out_does_not_ask_for_a_width_it_will_not_use pins it.

3. MIN_ROUTED_FAN_OUT_WIDTH — the narrow half is left alone

Below 16 Rayon threads there are too few wake-ups for their cost to be worth re-homing a fan-out for. The crossover is measured between 8 and 16:

width geomean, routed unconditionally null arm
t=1 1.01× —
t=2 1.01× —
t=4 0.81× 0.95×
t=8 0.94× 0.97×
t=16 1.07× —
t=32 2.22× 1.00×

So the whole t ≤ 8 half of the grid is left exactly as it was. t=1/t=2 are flat because the partition policy declines to split and both arms run identical serial code.

The matrix after the fix

Native-only p50 at t=32 — the only width at which the policy can choose:

cell fan-out calls path Rayon new speedup
qwen3_0p6b_mlp_t1 6 Mi 1 runtime 10.68 ms 1.24 ms 8.6×
qwen3_0p6b_qkv_t1 3 Mi 1 runtime 4.84 ms 0.70 ms 6.9×
qwen3_0p6b_mlp_t8 6 Mi 8 runtime 60.20 ms 9.98 ms 6.0×
qwen3_0p6b_qkv_t8 3 Mi 8 runtime 48.25 ms 8.40 ms 5.8×
llama3_8b_qkv_t1 24 Mi 1 runtime 14.01 ms 2.45 ms 5.7×
llama3_8b_qkv_t8 24 Mi 8 runtime 65.29 ms 29.40 ms 2.2×
llama3_8b_mlp_t1 56 Mi 1 runtime 16.92 ms 8.26 ms 2.1×
llama3_8b_mlp_t8 56 Mi 8 runtime 67.34 ms 44.74 ms 1.5×
qwen3_0p6b_qkv_t128 3 Mi 128 runtime 80.11 ms 53.13 ms 1.5×
qwen3_0p6b_qkv_t512 3 Mi 512 runtime 144.62 ms 117.31 ms 1.2×
llama3_8b_qkv_t128 24 Mi 128 runtime 180.50 ms 158.75 ms 1.1×
qwen3_0p6b_mlp_t512 6 Mi 512 runtime 183.28 ms 167.18 ms 1.1×
llama3_8b_qkv_t512 24 Mi 512 runtime 528.29 ms 490.51 ms 1.1×
qwen3_0p6b_mlp_t128 6 Mi 128 runtime 87.71 ms 82.94 ms 1.1×
llama3_8b_mlp_t512 56 Mi 512 wide 899.74 ms 891.01 ms 1.0×
llama3_8b_mlp_t128 56 Mi 128 wide 212.54 ms 213.85 ms 1.0×

The two diverted cells were then re-measured on their own at 9 trials, three arms, to settle them past the noise:

cell base null new
llama3_8b_mlp_t128 270.1 ms 262.5 ms (1.03×) 264.0 ms (1.02×)
llama3_8b_mlp_t512 906.5 ms 898.0 ms (1.01×) 908.0 ms (1.00×)

new tracks null on both — the expected result for a policy that hands those cells back to the code the base binary runs. Both regressions are gone and no win was traded for them. The four dense f32 cells are the other control (unreachable by this change): 0.96×–0.99×.

Every cell reported parity=PASS. Row-sharding a GEMV is exactly associative — no cross-row reduction — so results are bit-identical however the rows are partitioned.

Tests

Ten new tests. The policy ones run against a synthetic 16-lane/32-thread host, so they do not need SMT to be meaningful:

  • flat_fan_out_sends_hot_repeated_fan_outs_to_the_wide_pool — pins every row of the measured table.
  • flat_fan_out_leaves_narrow_fan_outs_where_they_are — the t ≤ 8 half, plus the 16-thread crossover.
  • flat_fan_out_does_not_ask_for_a_width_it_will_not_use — a Cell<bool> proving the pool is not built on the declining path.
  • flat_fan_out_ignores_a_wide_pool_that_is_not_wider, flat_fan_out_keeps_one_shot_fan_outs_on_the_runtime, parallel_output_rows_is_the_one_shot_spelling — a decode step can never be diverted.
  • parallel_output_rows_covers_every_output_exactly_once and ..._repeated_covers_every_output_on_the_wide_path — per-element AtomicU32 write counters plus output_start correctness, so both executors owe the same coverage guarantee.
  • parallel_output_rows_dispatches_to_the_task_runtime — asserts a real dispatch, so the routed path cannot silently degrade to serial.
  • prefill_column_fan_out_matches_its_serial_self — bit-identity against testing::force_serial(); falsified during development (truncating one task's range turns it red at index 95) and it asserts counters().tasks moved, so it cannot pass by comparing serial against serial.

All three policy constants were falsified before being trusted: HOT_FAN_OUT_MACS → 128 Mi, HOT_FAN_OUT_CALLS → 1024, and MIN_ROUTED_FAN_OUT_WIDTH → 8 each turn a test red on its own.

Validation

Local, rustc 1.97.1 (8bab26f4f 2026-07-14) — the toolchain Rust quality pins:

  • cargo test -p onnx-runtime-ep-cpu — 1446 passed, 0 failed, 17 ignored, all integration binaries green
  • cargo clippy -p onnx-runtime-ep-cpu --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • all 8 Rust quality guard scripts — pass

Rebased on main at 6a855d5e0.

What is still lost

gemm_nbits_qwen3_0p6b_qkv_t8 is 8.4 ms at t=32 against 1.6 ms at t=16. The inversion is 23× smaller but not zero, and more threads, more time is still a scheduler signature. Leading hypothesis, untested: with_decode_pool's pool.install(operation) is itself a fork-join onto a possibly-parked pool, paid once per MatMulNBits node call. Probing it needs care — skipping the install changes what output_chunk_len sees from rayon::current_num_threads().

Nesting is the second open item. packed_nbits_output_row and int8_row are called from inside a par_chunks_mut for m > 1 with parallel_columns set, so several Rayon workers can now dispatch into the task pool at once. The pool's eight job slots make surplus dispatches fall back to inline execution, which is correct and bounded — but that is an argument, not a measurement.

The third is this document's own method. §36.3 is the campaign's first null arm, and it says a large fraction of the per-cell readings in earlier phases — anything inside 0.72×–1.20× at t=32 — carried no information about the change being measured. The 5–15% movements tabulated in several earlier sections should be read with that in mind.

Documented as §36 / phase 16, including the correction to §35.6.

…ntime

`gemm_nbits_qwen3_0p6b_qkv_t8` took 1.6 ms at 16 threads and 37.7 ms at 32
in a pure-native build. §33.9 named the cell; this is the site.

The `m > 1` int4 prefill fan-out that looked like the culprit,
`borrowed_affine_int4_matmul_prefill`, is dead in the default build --
`ONNX_GENAI_CPU_MM_INT4_PREFILL` defaults false. The route every int4 GEMM
actually takes is `borrowed_affine_int4_matmul`, which loops over activation
rows and calls `parallel_output_rows` once per row. That function was a bare
`result.par_chunks_mut(chunk)` on whichever Rayon pool was installed, so an
m-token prefill paid m fork-joins onto a pool whose workers take 67-226 us to
wake (§33.4). It is the flat fan-out shared by eight call sites, and it is on
the per-token critical path §35.6 claimed was already clean.

Route it through `task_runtime::for_each_range`, passing the existing
`output_chunk_len` as the minimum grain so the partition can only get coarser,
never finer, than the one Rayon used. Native-only at 32 threads, 5 trials x 7
runs, arms interleaved: 17.8x on `qwen3_0p6b_mlp_t1`, 8.0x on
`qwen3_0p6b_qkv_t8`, 7.9x on `llama3_8b_qkv_t1`, 6.8x on `qwen3_0p6b_mlp_t8`.

Unconditional routing regressed two cells, `llama3_8b_mlp` at t128 (0.93x) and
t512 (0.86x). Those run the same 56 Mi fan-out as `llama3_8b_mlp_t8`, which
*improves* 1.5x, so the discriminator is not the work -- it is how many
fan-outs arrive back to back. 512 consecutive fan-outs never let Rayon's
workers park, so the park cost this whole campaign is about stops being
charged and only the runtime's SMT cap (16 lanes against 32 threads) is left.

`flat_fan_out` takes the wide path only when both hold: the fan-out is at least
32 Mi MACs (measured separation: 24 Mi wants the runtime, 56 Mi wants Rayon)
and at least 32 of them arrive (separation: 8 against 128). No threshold on
total operator work can express this -- `llama3_8b_mlp_t128` is 7.2 Gi and
wants Rayon while `llama3_8b_qkv_t512` is 12.3 Gi and wants the runtime -- so
reusing §35's `WIDE_PREFILL_MACS` would have traded five measured wins for the
two fixes. It also never fires when Rayon is not actually wider than the pool,
which is the entire t<=16 half of the grid.

`parallel_output_rows` keeps its one-shot spelling and delegates with
`calls = 1`, so no decode step can reach the wide path however large its
projection; only the two row-looping int4 kernels pass `m`.

After the split both regressions are gone (0.99x, 1.01x) with every win intact.
Documented as §36 / phase 16, including the correction to §35.6.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby
justinchuby force-pushed the squad/sebastian-native-fanout-taskrt branch from 5af2068 to 57ca34c Compare August 19, 2026 02:31
@justinchuby

Copy link
Copy Markdown
Owner Author

Merging with the admin bypass, same justification as #1346, #1352 and #1361.

The two required checks (Fast (Linux x86_64), Rust quality) cannot report: the repository's Actions queue is saturated — every run sits in queued and none reach in_progress — so waiting is not a path to a green light. Both lanes were reproduced locally on the toolchain the workflow pins, rustc 1.97.1 (8bab26f4f 2026-07-14):

  • cargo test -p onnx-runtime-ep-cpu — 1446 passed, 0 failed, 17 ignored; every integration binary green
  • cargo clippy -p onnx-runtime-ep-cpu --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • all 8 Rust quality guard scripts — check_publish_order, check_profile_table, check_platform_naming, check_dispatch_reachability, check_dispatch_manifest, check_feature_gate_coverage, verify_documented_env_vars, workspace_test_packages verify — all pass

Behavioural risk is bounded by construction. Every configuration at or below the physical core count keeps the code it had (MIN_ROUTED_FAN_OUT_WIDTH), the two prefill shapes that preferred Rayon keep it (HOT_FAN_OUT_MACS + HOT_FAN_OUT_CALLS), and the routed path is covered by a per-element write counter and a bit-identity test against task_runtime::testing::force_serial(). All 20 benchmark cells reported parity=PASS — row-sharding a GEMV is exactly associative, so the results are bit-identical however the rows are partitioned.

Rebased on main at 6a855d5e0.

@justinchuby
justinchuby merged commit bf72272 into main Aug 19, 2026
9 checks passed
@justinchuby
justinchuby deleted the squad/sebastian-native-fanout-taskrt branch August 19, 2026 02:32
justinchuby added a commit that referenced this pull request Aug 19, 2026
…ent ORT pool (#1374)

Documentation only — no code change, no behaviour change.

§36.8 left the t=32 residual open and called it "a scheduler signature".
It is —
but it is mostly not *our* scheduler. This phase spent its budget on
measurement.

## What it found

`bench_generic --native-only` / `--ort-only` build only the arm being
timed, so
running the same binary with and without a co-resident ORT session
isolates
contention from arithmetic. At t=32:

| cell | native alone | native paired | ratio |
| --- | --- | --- | --- |
| `gemm_nbits_llama3_8b_qkv_t8` | 6.90 ms | 28.71 ms | **4.18×** |
| `gemm_dense_tall_128x4096` (f32 dense) | 5.62 ms | 26.92 ms |
**4.79×** |
| `gemm_nbits_llama3_8b_mlp_t8` | 15.99 ms | 42.56 ms | **2.66×** |

The first row reproduced to three significant figures across two
independent
sessions an hour apart (28.698 / 28.713 paired, 6.868 / 6.900 alone) —
four
times outside §36.3's measured noise band.

Measured **alone**, `llama3_8b_qkv_t8` is 7.15 / 6.22 / 6.87 ms at
t=8/16/32:
flat, no inversion. The inversion only exists in the paired numbers.

## Why

The tax is asymmetric — ORT pays only 1.2–1.3× for our co-residency, we
pay up
to 4.2× for its. ORT's intra-op pool spin-waits long after its last op;
our task
runtime spins briefly and parks. A pool that parks quickly is invisible
to its
neighbours; a pool that spins is not. It hits the **dense f32 control
hardest**,
so it is contention, not anything about int4.

Lane width is ruled out first (§38.1): t=16 and t=32 both run 16 lanes
and still
differ ~3×, and the default width is at or near optimal on 3 of 4 cells.

## What it changes

- Adds the rule to the method: long cells above 16 threads must be
measured with
solo arms. The paired harness stays correct for parity, short cells, and
  base-vs-new comparisons of our own binaries.
- Corrects the overstated ratios: the honest t=32 figure for
`llama3_8b_qkv_t8`
is **10× behind ORT, not 41×**. Still a real gap, still a kernel
problem.
- **#1363 / §36 is unaffected** — both arms there were native binaries
paying the
  same tax.
- Records an affinity probe that failed its own noise check (§38.5), so
the next
  attempt knows it has been run once on a loaded box for nothing.
- Downgrades §36.8's `with_decode_pool` hypothesis: it early-returns
inline when
`with_decode_pool_scope` is active, so the `install` is per-pass, not
per-node.

## Validation

Docs only. Full local `Rust quality` proxy run anyway: `cargo fmt --all
--check`
clean, all 8 guard scripts pass, `workspace_test_packages verify` OK.

Co-authored-by: Sebastian <sebastian@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
`prefill_fan_out` and `WIDE_PREFILL_MACS` carried
`#[cfg_attr(not(feature = "mlas"), allow(dead_code))]` because their only
non-test caller, `run_mlas_shards`, is `#[cfg(feature = "mlas")]`.

#1363 added a second caller inside `borrowed_affine_int4_matmul_prefill` and
removed both guards. That function is `#[cfg(target_arch = "x86_64")]`, so the
guards were only redundant on x86_64. On aarch64 without MLAS both callers
disappear again and the items are dead, which is an error under the
`-D warnings` the ARM64 lanes build with. `prefill_column_grain` shipped new in
the same PR with no guard and only that one x86-gated caller.

Restore the guards over the union of the two callers' cfgs. No behaviour
change on any target: `allow(dead_code)` only applies where the item already
has no caller.

Caught by `scripts/check_cross_compile.sh`, which is a blocking `Rust quality`
step but had not run on main — every CI run since #1363 merged is still queued.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
`prefill_fan_out` and `WIDE_PREFILL_MACS` carried
`#[cfg_attr(not(feature = "mlas"), allow(dead_code))]` because their only
non-test caller, `run_mlas_shards`, is `#[cfg(feature = "mlas")]`.

#1363 added a second caller inside `borrowed_affine_int4_matmul_prefill` and
removed both guards. That function is `#[cfg(target_arch = "x86_64")]`, so the
guards were only redundant on x86_64. On aarch64 without MLAS both callers
disappear again and the items are dead, which is an error under the
`-D warnings` the ARM64 lanes build with. `prefill_column_grain` shipped new in
the same PR with no guard and only that one x86-gated caller.

Restore the guards over the union of the two callers' cfgs. No behaviour
change on any target: `allow(dead_code)` only applies where the item already
has no caller.

Caught by `scripts/check_cross_compile.sh`, which is a blocking `Rust quality`
step but had not run on main — every CI run since #1363 merged is still queued.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
`prefill_fan_out` and `WIDE_PREFILL_MACS` carried
`#[cfg_attr(not(feature = "mlas"), allow(dead_code))]` because their only
non-test caller, `run_mlas_shards`, is `#[cfg(feature = "mlas")]`.

#1363 added a second caller inside `borrowed_affine_int4_matmul_prefill` and
removed both guards. That function is `#[cfg(target_arch = "x86_64")]`, so the
guards were only redundant on x86_64. On aarch64 without MLAS both callers
disappear again and the items are dead, which is an error under the
`-D warnings` the ARM64 lanes build with. `prefill_column_grain` shipped new in
the same PR with no guard and only that one x86-gated caller.

Restore the guards over the union of the two callers' cfgs. No behaviour
change on any target: `allow(dead_code)` only applies where the item already
has no caller.

Caught by `scripts/check_cross_compile.sh`, which is a blocking `Rust quality`
step but had not run on main — every CI run since #1363 merged is still queued.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 19, 2026
#1363 shipped this test past a bypassed CI lane, and it fails on every
stock runner. It asserts that the flat fan-out reaches the task runtime,
but routing reads `rayon::current_num_threads()` and `flat_fan_out`
deliberately keeps the fan-out on Rayon below MIN_ROUTED_FAN_OUT_WIDTH
(16). Below that width the test asserts a dispatch policy never
promised:

    rayon = 4 / 8 / 15   FAILED
    rayon = 16 / 32      ok

`ubuntu-latest` is a 4-vCPU runner, so "Fast (Linux x86_64)" would have
been red. It passed for me only because this host is 16C/32T.

The existing `task_runtime::width() <= 1` guard does not cover it:
task-runtime width and Rayon width are different numbers, and on a
4-vCPU box the first is > 1 while the second is < 16. `output_chunk_len`
reads the same Rayon width, so the *precondition* carried the identical
defect -- repairing only the routing moved the failure to rayon = 1
rather than removing it.

Fixed by installing a Rayon pool of exactly the routing width, so the
decision under test is the same on a 4-vCPU runner as on a 32-thread
workstation. Skipping below the threshold would have been weaker: the
test would silently guard nothing on every real runner.

Now passes at rayon = 1, 2, 4, 8, 15, 16 and 32, and still fails when
routing is forced to PrefillFanOut::Wide, so it is not vacuous. Also
adds the coverage assertion it should always have had: every output row
written exactly once.

The aarch64 dead-code break that #1363 also shipped is left to #1382,
which was open first and is already armed; I verified its three
`allow(dead_code)` restorations are exactly what the cross-target lane
needs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
`prefill_fan_out` and `WIDE_PREFILL_MACS` carried
`#[cfg_attr(not(feature = "mlas"), allow(dead_code))]` because their only
non-test caller, `run_mlas_shards`, is `#[cfg(feature = "mlas")]`.

#1363 added a second caller inside `borrowed_affine_int4_matmul_prefill` and
removed both guards. That function is `#[cfg(target_arch = "x86_64")]`, so the
guards were only redundant on x86_64. On aarch64 without MLAS both callers
disappear again and the items are dead, which is an error under the
`-D warnings` the ARM64 lanes build with. `prefill_column_grain` shipped new in
the same PR with no guard and only that one x86-gated caller.

Restore the guards over the union of the two callers' cfgs. No behaviour
change on any target: `allow(dead_code)` only applies where the item already
has no caller.

Caught by `scripts/check_cross_compile.sh`, which is a blocking `Rust quality`
step but had not run on main — every CI run since #1363 merged is still queued.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
`prefill_fan_out` and `WIDE_PREFILL_MACS` carried
`#[cfg_attr(not(feature = "mlas"), allow(dead_code))]` because their only
non-test caller, `run_mlas_shards`, is `#[cfg(feature = "mlas")]`.

#1363 added a second caller inside `borrowed_affine_int4_matmul_prefill` and
removed both guards. That function is `#[cfg(target_arch = "x86_64")]`, so the
guards were only redundant on x86_64. On aarch64 without MLAS both callers
disappear again and the items are dead, which is an error under the
`-D warnings` the ARM64 lanes build with. `prefill_column_grain` shipped new in
the same PR with no guard and only that one x86-gated caller.

Restore the guards over the union of the two callers' cfgs. No behaviour
change on any target: `allow(dead_code)` only applies where the item already
has no caller.

Caught by `scripts/check_cross_compile.sh`, which is a blocking `Rust quality`
step but had not run on main — every CI run since #1363 merged is still queued.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
`prefill_fan_out` and `WIDE_PREFILL_MACS` carried
`#[cfg_attr(not(feature = "mlas"), allow(dead_code))]` because their only
non-test caller, `run_mlas_shards`, is `#[cfg(feature = "mlas")]`.

#1363 added a second caller inside `borrowed_affine_int4_matmul_prefill` and
removed both guards. That function is `#[cfg(target_arch = "x86_64")]`, so the
guards were only redundant on x86_64. On aarch64 without MLAS both callers
disappear again and the items are dead, which is an error under the
`-D warnings` the ARM64 lanes build with. `prefill_column_grain` shipped new in
the same PR with no guard and only that one x86-gated caller.

Restore the guards over the union of the two callers' cfgs. No behaviour
change on any target: `allow(dead_code)` only applies where the item already
has no caller.

Caught by `scripts/check_cross_compile.sh`, which is a blocking `Rust quality`
step but had not run on main — every CI run since #1363 merged is still queued.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
`prefill_fan_out` and `WIDE_PREFILL_MACS` carried
`#[cfg_attr(not(feature = "mlas"), allow(dead_code))]` because their only
non-test caller, `run_mlas_shards`, is `#[cfg(feature = "mlas")]`.

#1363 added a second caller inside `borrowed_affine_int4_matmul_prefill` and
removed both guards. That function is `#[cfg(target_arch = "x86_64")]`, so the
guards were only redundant on x86_64. On aarch64 without MLAS both callers
disappear again and the items are dead, which is an error under the
`-D warnings` the ARM64 lanes build with. `prefill_column_grain` shipped new in
the same PR with no guard and only that one x86-gated caller.

Restore the guards over the union of the two callers' cfgs. No behaviour
change on any target: `allow(dead_code)` only applies where the item already
has no caller.

Caught by `scripts/check_cross_compile.sh`, which is a blocking `Rust quality`
step but had not run on main — every CI run since #1363 merged is still queued.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
#1443 stopped the aarch64 dead-code errors by `#[cfg]`-ing the three
prefill fan-out symbols to `target_arch = "x86_64"`. That removes the
items outright, and they have two callers gated on *different* things:

  matmul_nbits.rs:2457  run_mlas_shards                    #[cfg(feature = "mlas")]
  matmul_nbits.rs:6914  borrowed_affine_int4_matmul_prefill #[cfg(target_arch = "x86_64")]

So on `aarch64 + feature = "mlas"` -- Apple Silicon, the primary aarch64
target -- the MLAS caller is still compiled while its callee is not:

  error[E0425]: cannot find function `prefill_fan_out` in this scope
    --> crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs:2457:27
  error[E0425]: cannot find function `prefill_column_grain` in this scope
    --> crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs:6915:48

The cross-compile gate cannot see this: its aarch64 pass builds default
features, and MLAS is off by default.

Gating the items also forced gating the six unit tests that reference
them, so the #1363 fan-out policy stopped being checked on aarch64 at
all. Those tests are pure functions of explicit arguments -- nothing in
them is architecture-specific.

`cfg_attr(.., allow(dead_code))` fixes both: the item always exists, so
whichever caller survives can reach it, and the lint is silenced only in
the configuration where neither caller exists. `prefill_tile_grain`
already used exactly this idiom, which is what #1363 dropped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 19, 2026
#1443 stopped the aarch64 dead-code errors by `#[cfg]`-ing the three
prefill fan-out symbols to `target_arch = "x86_64"`. That removes the
items outright, and they have two callers gated on *different* things:

  matmul_nbits.rs:2457  run_mlas_shards                    #[cfg(feature = "mlas")]
  matmul_nbits.rs:6914  borrowed_affine_int4_matmul_prefill #[cfg(target_arch = "x86_64")]

So on `aarch64 + feature = "mlas"` -- Apple Silicon, the primary aarch64
target -- the MLAS caller is still compiled while its callee is not:

  error[E0425]: cannot find function `prefill_fan_out` in this scope
    --> crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs:2457:27
  error[E0425]: cannot find function `prefill_column_grain` in this scope
    --> crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs:6915:48

The cross-compile gate cannot see this: its aarch64 pass builds default
features, and MLAS is off by default.

Gating the items also forced gating the six unit tests that reference
them, so the #1363 fan-out policy stopped being checked on aarch64 at
all. Those tests are pure functions of explicit arguments -- nothing in
them is architecture-specific.

`cfg_attr(.., allow(dead_code))` fixes both: the item always exists, so
whichever caller survives can reach it, and the lint is silenced only in
the configuration where neither caller exists. `prefill_tile_grain`
already used exactly this idiom, which is what #1363 dropped.

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

Copy link
Copy Markdown

⚠️ Benchmark Change Detected

Comparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).

ℹ️ Absolute times are informational only — they vary with runner load. The % change column is the reliable signal because both sides ran under identical conditions.

Status Scenario Base PR Change
⚠️ matmul/medium_generic_bf16_threads=8/32x512x512 626.10 µs 770.15 µs +23.0%
⚠️ matmul/medium_generic_bf16_threads=1/32x512x512 564.17 µs 693.11 µs +22.9%
⚠️ block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 52.73 µs 62.66 µs +18.8%
⚠️ add/large_f16_threads=1-internal/4194304 2.26 ms 2.63 ms +16.4%
✅ add/small_f16_threads=1-internal/1024 458.5 ns 516.3 ns +12.6%
✅ add/small_bf16_threads=1-internal/1024 425.0 ns 478.4 ns +12.6%
✅ matmul/large_generic_f16_threads=1/32x1024x1024 83.64 µs 94.10 µs +12.5%
✅ block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 420.18 µs 469.42 µs +11.7%
✅ matmul/medium_generic_f16_threads=8/32x512x512 42.80 µs 46.14 µs +7.8%
✅ add/large_bf16_threads=1-internal/4194304 1.95 ms 2.10 ms +7.5%
✅ matmul/large_generic_f16_threads=8/32x1024x1024 95.90 µs 102.86 µs +7.3%
✅ add/medium_f32_threads=1-internal/262144 25.28 µs 26.98 µs +6.7%
✅ add/medium_bf16_threads=1-internal/262144 109.52 µs 115.68 µs +5.6%
✅ add/medium_f16_threads=1-internal/262144 109.76 µs 112.73 µs +2.7%
✅ add/large_f32_threads=1-internal/4194304 810.08 µs 828.17 µs +2.2%
✅ block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 925.09 µs 941.62 µs +1.8%
✅ add/small_f32_threads=1-internal/1024 207.7 ns 208.9 ns +0.6%
✅ block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 71.50 µs 71.62 µs +0.2%
✅ qwen3_sampling_processors/top_k_full_sort_baseline 2.04 ms 2.01 ms -1.6%
✅ matmul/large_generic_f32_threads=1/32x1024x1024 10.06 ms 9.85 ms -2.1%
✅ kv_cache/alloc_dealloc_pages 39.56 µs 38.67 µs -2.3%
✅ sampling_latency/top_p_per_token 371.93 µs 360.17 µs -3.2%
✅ qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline 3.38 ms 3.26 ms -3.4%
✅ matmul/medium_generic_f32_threads=1/32x512x512 2.70 ms 2.60 ms -3.8%
✅ sampling_latency/greedy_per_token 3.12 µs 2.99 µs -4.1%
✅ sampling_latency/min_p_per_token 201.76 µs 192.55 µs -4.6%
✅ matmul/large_generic_bf16_threads=1/32x1024x1024 2.07 ms 1.97 ms -4.7%
✅ matmul/small_generic_f16_threads=1/1x256x256 37.62 µs 35.67 µs -5.2%
✅ reduce_mean/small_f32_threads=1-internal/4096 16.93 µs 15.70 µs -7.3%
✅ grammar_masking/llguidance_compute_mask/32 80.71 µs 74.72 µs -7.4%
✅ sampling_latency/top_k_per_token 52.72 µs 48.68 µs -7.7%
✅ matmul/large_generic_f32_threads=8/32x1024x1024 4.79 ms 4.37 ms -8.8%
✅ tokenization/decode_tokens_per_second 6.37 ms 5.78 ms -9.2%
✅ reduce_mean/medium_f32_threads=1-internal/65536 271.35 µs 244.70 µs -9.8%
✅ logit_processing/seven_processor_chain_per_step 340.93 µs 301.80 µs -11.5%
✅ gather/small_f32_threads=1-internal/4096 761.7 ns 674.1 ns -11.5%
✅ matmul/small_generic_bf16_threads=1/1x256x256 37.90 µs 33.20 µs -12.4%
✅ gather/large_f32_threads=1-internal/131072 50.28 µs 43.83 µs -12.8%
✅ matmul/small_generic_bf16_threads=8/1x256x256 39.95 µs 34.60 µs -13.4%
🟢 reduce_mean/large_f32_threads=1-internal/262144 1.20 ms 1.01 ms -15.4%
🟢 qwen3_sampling_processors/top_k_partial_selection 153.70 µs 129.94 µs -15.5%
🟢 matmul/medium_generic_f16_threads=1/32x512x512 36.64 µs 30.54 µs -16.6%
🟢 tokenization/encode_tokens_per_second 455.70 µs 378.72 µs -16.9%
🟢 gather/medium_f16_threads=1-internal/32768 3.38 µs 2.75 µs -18.5%
🟢 qwen3_sampling_processors/top_p_fast_after_top_k 594.26 µs 482.56 µs -18.8%
🟢 gather/small_f16_threads=1-internal/4096 574.8 ns 464.5 ns -19.2%
🟢 block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 67.99 µs 54.85 µs -19.3%
🟢 matmul/small_generic_f16_threads=8/1x256x256 39.88 µs 32.14 µs -19.4%
🟢 matmul/small_generic_f32_threads=8/1x256x256 70.52 µs 56.44 µs -20.0%
🟢 qwen3_sampling_processors/top_k_top_p_fast 793.09 µs 632.14 µs -20.3%
🟢 matmul/large_generic_bf16_threads=8/32x1024x1024 1.85 ms 1.47 ms -20.4%
🟢 gather/medium_bf16_threads=1-internal/32768 3.57 µs 2.73 µs -23.7%
🟢 qwen3_sampling_processors/top_k_top_p_full_sort_baseline 6.84 ms 5.22 ms -23.7%
🟢 gather/large_f16_threads=1-internal/131072 19.73 µs 14.93 µs -24.3%
🟢 gather/medium_f32_threads=1-internal/32768 6.60 µs 4.52 µs -31.5%
🟢 gather/small_bf16_threads=1-internal/4096 728.3 ns 466.8 ns -35.9%
🟢 matmul/small_generic_f32_threads=1/1x256x256 68.84 µs 42.29 µs -38.6%
🟢 matmul/medium_generic_f32_threads=8/32x512x512 1.79 ms 1.02 ms -43.3%
🟢 gather/large_bf16_threads=1-internal/131072 25.94 µs 12.13 µs -53.2%

Visual flags: ⚠️ ≥ 15% slower, 🔴 ≥ 30% slower — calibrated against measured runner noise (~27% worst-case on multi-threaded matmul)

Host info
CPU: Apple M1 (Virtual)
Cores: 3
OS: Darwin 25.5.0 arm64
Rust: rustc 1.97.1 (8bab26f4f 2026-07-14)
Load avg: { 3.16 3.24 5.36 }
What this cannot catch
  • Regressions in code paths not covered by these benchmarks (e.g., end-to-end decode with a real model)
  • Sub-threshold regressions that compound over multiple PRs
  • Performance changes that only manifest under GPU execution
  • Latency changes in the ORT integration path (these benchmarks exercise the native Rust kernels)

justinchuby added a commit that referenced this pull request Aug 19, 2026
> **Process note, stated up front.** This defect entered `main` via
#1363, which I merged with an admin bypass while every required check
was still `queued`. That was wrong, I am not repeating it, and this PR
goes through the normal gates. Full disclosure of what I bypassed is in
the comment below.

`parallel_output_rows_dispatches_to_the_task_runtime` **fails on every
stock CI runner** and is live on `main` today.

## The defect

The test asserts the flat fan-out reaches the task runtime. But routing
reads `rayon::current_num_threads()`, and `flat_fan_out`'s *first* gate
is deliberately "stay on Rayon below `MIN_ROUTED_FAN_OUT_WIDTH` (16)".
Below that width the test asserts something policy never promised.

Measured on unrepaired `main`:

| `RAYON_NUM_THREADS` | 4 | 8 | 15 | 16 | 32 |
|---|---|---|---|---|---|
| result | **FAILED** | **FAILED** | **FAILED** | ok | ok |

`ubuntu-latest` is 4 vCPU. The whole `onnx-runtime-ep-cpu` lib suite on
unrepaired main at that width:

```
test result: FAILED. 1447 passed; 1 failed; 17 ignored
    kernels::matmul_nbits::tests::parallel_output_rows_dispatches_to_the_task_runtime
```

It passed for me only because this development host is 16C/32T — the
defect needs a *narrower* machine to appear, which is exactly the kind
of thing the CI I bypassed exists to find.

The existing `task_runtime::width() <= 1` guard does not cover it:
task-runtime width and Rayon width are different numbers, and on a
4-vCPU box the first is `> 1` while the second is `< 16`.

## The fix, and the trap in it

Install a Rayon pool of exactly the routing width so the decision under
test is host-independent.

My first attempt only wrapped the fan-out — and **still failed at
`rayon=1`**, because `output_chunk_len` reads the same Rayon width and
the test's *precondition* carried the identical defect. Moving the
precondition inside the pool too is what actually removes the host
dependency rather than relocating it.

Skipping below the threshold would have been the weaker fix: the test
would silently no-op on every real runner and guard nothing.

- passes at rayon = **1, 2, 4, 8, 15, 16, 32**
- **still falsifies** — forcing `PrefillFanOut::Wide` makes it fail, so
it is not vacuous
- adds the coverage assertion it should always have had (every output
row written exactly once)

## Scope

Test-only. No production behaviour changes.

Deliberately **not** included:
- the **aarch64 dead-code break** #1363 also shipped
(`WIDE_PREFILL_MACS`, `prefill_fan_out`, `prefill_column_grain` are dead
on a non-mlas ARM64 build, failing `-D warnings`) → **#1382** by @pris
was open first and is already armed. I had written the same three
`allow(dead_code)` restorations, verified they clear `cargo clippy
--target aarch64-unknown-linux-gnu -- -D warnings`, then dropped them
from this branch rather than ship a conflicting duplicate.
- the **Stacked-Borrows UB** in #1377's test → **#1385** by @pris, and
**#1407** (mine, which additionally closes the Miri lane gap that let it
through: `miri.yml` runs `--lib task_runtime::` only, so integration
tests under `tests/` are never Miri-checked).

## Validation

| check | result |
|---|---|
| `cargo fmt --all -- --check` | clean |
| `cargo test -p onnx-runtime-ep-cpu --lib` (host width) | 1447 passed,
0 failed |
| `cargo test -p onnx-runtime-ep-cpu --lib` at `RAYON_NUM_THREADS=4` |
**1447 passed, 0 failed** (main: 1 failed) |
| target test at rayon 1/2/4/8/15/16/32 | all pass |
| falsification probe (force `Wide`) | fails as required |

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

## What this fixes

Validating the merged scheduler/fmt wave, I found `#1363` had dropped
the `allow(dead_code)` guards on three prefill fan-out symbols, breaking
the aarch64 cross-compile gate. **#1443 has since fixed that** — but by
`#[cfg]`-ing the three items to `target_arch = "x86_64"`, which removes
them outright. That trades one break for two others.

### 1. 🔴 `aarch64 + feature = "mlas"` no longer compiles

The three symbols have two callers, gated on **different** things:

| call site | enclosing fn | its gate |
| --- | --- | --- |
| `matmul_nbits.rs:2457` | `run_mlas_shards` | `#[cfg(feature =
"mlas")]` |
| `matmul_nbits.rs:6914-6915` | `borrowed_affine_int4_matmul_prefill` |
`#[cfg(target_arch = "x86_64")]` |

Off x86 the second caller disappears, but the **first does not** — it is
arch-independent. So on `aarch64 + mlas`, which is Apple Silicon, the
caller is compiled and its callee is not:

```
error[E0425]: cannot find function `prefill_fan_out` in this scope
  --> crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs:2457:27
error[E0425]: cannot find function `prefill_fan_out` in this scope
  --> crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs:6914:8
error[E0425]: cannot find function `prefill_column_grain` in this scope
  --> crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs:6915:48
```

**How that was produced.** MLAS's vendored sources do not cross-build to
aarch64 in this container (`arm_neon.h: inlining failed in call to
always_inline vaddq_f16 — target specific option mismatch`, an
mlas-sys/toolchain issue unrelated to this PR), so instead I reproduced
the *exact cfg resolution* on the host: on `main`, retarget the three
item gates from `x86_64` to a third arch so they are absent, leave every
caller alone, and build the lib with MLAS on —

```
sed -i '395s/x86_64/s390x/; 403s/x86_64/s390x/; 543s/x86_64/s390x/' \
  crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs
cargo check --locked -p onnx-runtime-ep-cpu --features mlas --lib     # exit 101
```

That is precisely the configuration `aarch64 + mlas` produces. The
`:2457` error is the load-bearing one — that call site is
`feature`-gated only, so it is present on aarch64 for real.

**Why no gate caught it.** `check_cross_compile.sh`'s aarch64 pass
builds **default features**, and `mlas` is off by default — the same
blind spot #1443 was written under. The configuration *is* built
elsewhere: the `rust-coverage` job's macOS-arm64 leg runs with
`RUSTFLAGS: -D warnings` (`ci.yml` L434, L439) and builds `cargo build
-p onnx-runtime-ep-cpu-plugin --features mlas` (L501-503), whose `mlas`
feature forwards to `onnx-runtime-ep-cpu/mlas`. That is the
shipped-wheel build path, so `main` as it stands also breaks the
macOS-arm64 MLAS wheel at release time.

### 2. 🔴 The #1363 fan-out policy stopped being tested on aarch64

Gating the items forced gating their tests, so #1443 also put
`#[cfg(target_arch = "x86_64")]` on six unit tests. All six are pure
functions of explicit literal arguments —
`prefill_fan_out(WIDE_PREFILL_MACS - 1, 16, 32)`,
`prefill_column_grain(8, 1024, 3072)` — with nothing
architecture-specific in them. They are now simply not compiled off x86,
so the policy that #1363 rewrote has no aarch64 coverage.

## The fix

`cfg_attr(.., allow(dead_code))` instead of `cfg`. The item always
exists, so whichever caller survives can reach it; the lint is silenced
only where **no** caller exists. The six tests are ungated and run
everywhere again. `prefill_tile_grain` in this same file already uses
this idiom (`not(feature = "mlas")`) — that is the shape #1363 deleted.

The predicate is **per-symbol**, because the caller sets differ:

| symbol | callers | predicate |
| --- | --- | --- |
| `WIDE_PREFILL_MACS`, `prefill_fan_out` | `run_mlas_shards` **and**
`borrowed_affine_int4_matmul_prefill` | `not(any(feature = "mlas",
target_arch = "x86_64"))` |
| `prefill_column_grain` | `borrowed_affine_int4_matmul_prefill`
**only** — `run_mlas_shards` takes `prefill_tile_grain` instead |
`not(target_arch = "x86_64")` |

Giving `prefill_column_grain` the union predicate would leave the lint
live on `aarch64 + mlas`, where it has no caller — converting #1443's
`E0425` into a `never used` error in the same configuration. Review
caught exactly that in the first draft of this branch; the four-way
probe below is the regression check for it.

### Four-config probe of the predicates

`.validation-worktrees/cfgprobe/probe.rs` reproduces the two items, the
two callers and their gates with `mlas`/`x86` standing in for the real
cfgs, compiled under `-D warnings`:

```
=== union predicate on prefill_column_grain (wrong) ===
  PASS  [aarch64 default]
  FAIL  [--cfg mlas] <- error: function `prefill_column_grain` is never used
  PASS  [--cfg x86]
  PASS  [--cfg mlas --cfg x86]

=== per-symbol predicates (this PR) ===
  PASS  [aarch64 default]
  PASS  [--cfg mlas]
  PASS  [--cfg x86]
  PASS  [--cfg mlas --cfg x86]
```

The probe is sharp, not vacuous: it fails on exactly the configuration
that is wrong, and only that one.

## Second commit: unbreaking `main`'s required lane

`main` currently fails **both** required checks, from merges landed past
queued checks:

| defect | source | breaks |
| --- | --- | --- |
| `map_or(true, ..)` in `executor/dispatch.rs` — clippy `this map_or can
be simplified` under `-D warnings` | #1427 | `Rust quality` — and it
aborts the cross-compile gate *before* its aarch64 pass, which is why
the gate never reported defect 1 |
| `dispatch.rs`, `gather_block_quantized.rs`,
`gpt_oss_20b_decode_lock.rs` unformatted | #1427, #1418 | `Fast (Linux
x86_64)` **and** `Rust quality` |

`cargo fmt --all -- --check` runs in *both* required jobs (`ci.yml`
L162, L276) while `check_cross_compile.sh` runs only in `Rust quality`
(L401), and PR checks run against `merge(base, head)`. So while `main`
is broken this way a fmt-only PR still fails the cross-compile step and
this PR alone still fails fmt — only a branch carrying both can go
green. It is mechanical (`cargo fmt --all`, plus `map_or(true, f)` →
`is_none_or(f)`, identical on `Option`) and `git rebase` drops it once
fixed upstream.

## Verification at `9fb04f5b5` (base `main` `81f99ff42`)

| check | step | `main` | this branch |
| --- | --- | --- | --- |
| `Fast` + `Rust quality` | `cargo fmt --all -- --check` | **FAIL** (4
diffs / 3 files) | **pass** |
| `Rust quality` | `bash scripts/check_cross_compile.sh` | **FAIL** exit
1 | **pass** exit 0, `scope: full offline set (aarch64 cross toolchain
present)` |
| `Rust quality` | 30-crate `cargo clippy --locked --all-targets … -- -D
warnings` | **FAIL** exit 1 | **pass** exit 0 |
| `Rust quality` | 9 guard scripts | pass | **9/9 pass** |
| aarch64 | `cargo clippy --target aarch64-unknown-linux-gnu
--all-targets -p onnx-runtime-ep-cpu -- -D warnings` | pass | **pass**
(now *with* the 6 tests compiled) |
| `aarch64 + mlas` cfg resolution | `cargo check -p onnx-runtime-ep-cpu
--features mlas --lib`, items absent | **FAIL** exit 101, 3 × E0425 |
**pass** — `cfg_attr` never removes the item, so E0425 cannot occur |
| all 4 `(mlas on/off) x (x86 / non-x86)` | `rustc -D warnings` cfg
probe | — | **4/4 pass** |
| tests | `cargo test -p onnx-runtime-ep-cpu --lib` | — | **1447 passed
/ 0 failed**; the 8 prefill policy tests pass |

## A note on the gate that found this

`scripts/check_cross_compile.sh` **false-passes locally** without an
aarch64 cross toolchain: at L191-194 it silently swaps `CRATES_FULL` →
`CRATES_NO_FFI`, dropping `onnx-runtime-ep-cpu` — the crate the gate
exists for — and still exits 0 with a ✓. The "REDUCED SCOPE" note prints
*below* the checkmark. **Read the scope note, not the exit code**; only
`scope: full offline set (aarch64 cross toolchain present)` means
anything. On Actions it `exit 2`s instead (L178-190), and `ci.yml`
L396-399 installs `gcc-aarch64-linux-gnu` + `libc6-dev-arm64-cross`
before invoking it, so the fail-loud coverage is intact — this is a
local-only trap. All results above were produced with the toolchain
installed, at full scope.

Two of the three defects in this PR would have been caught by the
required checks had they been allowed to run.

## Process

No admin bypass, no ruleset bypass, no merge with checks queued or
failing. Auto-merge has been armed since 2026-08-19T04:55:37Z and merges
only once `Fast (Linux x86_64)` and `Rust quality` are green. Every CI
run in this repo is currently `queued` with zero in progress, so the
required contexts have not been created yet. Waiting.

---------

Co-authored-by: Pris <pris@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 22, 2026
…es (#1384)

Closes the "real remaining scheduler residual" §38.7 named. It turned
out not to
exist, which is why this PR is a diagnostic and a document rather than a
kernel.

## The residual dissolves

§38.7 left a ~1.7× native-only t=16→t=32 drift with no explanation. It
reproduces at the harness's run count (1.88× on `qwen3_0p6b_qkv_t8`) and
disappears when `--runs` goes from 7 to 40. Fitting `CPU = fixed +
marginal ×
runs` separates the terms:

| budget | fixed (CPU-s) | marginal (CPU-ms/inf) | steady-state wall |
| --- | --- | --- | --- |
| 2 | 0.13 | 8.19 | 4.531 ms |
| 8 | 0.63 | 8.05 | 1.588 ms |
| 16 | 1.55 | 13.99 | 0.921 ms |
| 32 | **3.67** | 15.17 | 0.934 ms |

Marginal CPU and steady-state wall are the same at t=16 and t=32 (t=32
is very
slightly *worse*). **There is no steady-state drift.** There is a fixed
cost
growing ~2.4× per doubling of a 2× thread count — and the 7-run harness
sits
entirely inside it.

## What the fixed cost is

Single inference, no warmups, 512² dense model so arithmetic is
irrelevant:

| budget | user CPU | first-inference wall | threads | futex |
sched_yield |
| --- | --- | --- | --- | --- | --- |
| 8 | 0.23 s | 2.198 ms | 15 | 150 | 643 |
| 32 | **2.24 s** | **27.895 ms** | 63 | 1611 | 3346 |

Model-independent, so not a kernel. Doesn't shrink with
`ONNX_GENAI_CPU_TASK_THREADS=1`, so not the task runtime. It's the Rayon
pools
coming up. It is also **pre-existing** — the phase-16 base binary
measures the
same fixed cost (0.64/1.67/3.54 at 8/16/32), so #1363 neither caused nor
fixed it.

## Nothing is bought past the physical core count

Steady-state wall, native-only, 25 runs after 10 warmups, median of 3:

| cell | budget 16 | budget 32 |
| --- | --- | --- |
| `llama3_8b_mlp_t512` | **969 ms** | 1537 ms |
| `llama3_8b_qkv_t128` | **106.4 ms** | 109.5 ms |
| `qwen3_0p6b_qkv_t8` | **0.921 ms** | 0.934 ms |

Never faster, 1.6× slower on the largest prefill shape. This is what
`cap_spinning_workers` already encodes for the task runtime's lanes, one
level out.

## Why this is a warning and not an override

The shipping default is already the efficient configuration:

| configuration | CPU for 60 runs | native |
| --- | --- | --- |
| default (no thread env) | **0.54 s** | 1.371 ms |
| `--native-threads 32` | 4.72 s | 0.933 ms |

8.7× the CPU to run 1.47× faster. Nothing in the production path is
misconfigured — the *harness* asks for 32 and the runtime honours it. So
this
changes no default and overrides no explicit request; it makes a request
that
cannot pay for itself visible instead of silent.

`budget_beyond_physical_cores` is a pure function so the policy is
testable
without a host. An unknown topology never warns, and a zero core count
is treated
as unknown.

Verified end-to-end: silent at budget 16, silent with no explicit
budget, and at
budget 32 emits
`CPU decode budget 32 exceeds the 16 physical cores available to this
process … Consider ONNX_GENAI_CPU_DECODE_THREADS=16`.

## Also in this PR

§39 records the decomposition, and states plainly that every `--threads
32` row
in this ledger was taken on a configuration §39.4 shows is strictly
worse than
`--threads 16` on this host. With §38's co-residency finding, the
wide-thread
rows have now had two independent instrument errors found in them. §39.7
keeps
the honest caveat: tight-loop steady state is the one regime where Rayon
never
parks, so it flatters the pre-#1363 base, and a gap-aware model-level
harness is
the real open item.

## Validation

- `cargo test -p onnx-runtime-ep-cpu --release`: **1451 passed, 0
failed** (+4 new)
- `cargo clippy --locked --all-targets` over CI's exact 28-crate gate
list: clean
- `cargo fmt --all -- --check`: clean
- all 8 `Rust quality` guard scripts: pass

---------

Co-authored-by: Sebastian <sebastian@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: justinchuby <11205048+justinchuby@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.

1 participant