Skip to content

style: reformat the int4 tail-block slice #1104 left unformatted - #1109

Merged
justinchuby merged 1 commit into
mainfrom
squad/roy-fmt-repair-1104
Aug 17, 2026
Merged

justinchuby merged 1 commit into
mainfrom
squad/roy-fmt-repair-1104

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

cargo fmt --all -- --check fails on main at 9b7a458 (#1104), in crates/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs:6239. Every open PR's Rust quality job therefore fails for a reason unrelated to the PR.

This is the third time (see #1089, #1102): the merge queue does not re-run formatting against the merge result, so an individually-green PR can still land unformatted main.

Pure cargo fmt -p onnx-runtime-ep-cpu output, two lines, no behaviour change.

`cargo fmt --all -- --check` fails on `main` at 9b7a458, so every open PR's
`Rust quality` job fails for a reason that has nothing to do with the PR. The
merge queue does not re-run formatting on the merge result, which is how this
keeps landing.

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

codecov Bot commented Aug 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.93%. Comparing base (91111be) to head (c36bb47).

Files with missing lines Patch % Lines
...es/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs 0.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1109      +/-   ##
==========================================
- Coverage   80.40%   79.93%   -0.48%     
==========================================
  Files         369      369              
  Lines      160217   160217              
  Branches   160217   160217              
==========================================
- Hits       128829   128064     -765     
- Misses      26655    27423     +768     
+ Partials     4733     4730       -3     
Flag Coverage Δ
cli-ort-linux 83.79% <ø> (ø)
cli-ort-windows 83.40% <ø> (ø)
mlas 84.61% <ø> (-0.11%) ⬇️
offline 79.70% <0.00%> (-0.51%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...es/onnx-runtime-ep-cpu/src/kernels/matmul_nbits.rs 75.66% <0.00%> (-6.36%) ⬇️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown

🔴 Benchmark Regression 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_f32_threads=8/32x512x512 990.34 µs 1.62 ms +64.0%
⚠️ matmul/medium_generic_f16_threads=8/32x512x512 34.84 µs 42.19 µs +21.1%
✅ matmul/small_generic_f16_threads=8/1x256x256 34.58 µs 39.34 µs +13.8%
✅ matmul/medium_generic_f16_threads=1/32x512x512 34.70 µs 39.24 µs +13.1%
✅ matmul/large_generic_f32_threads=1/32x1024x1024 10.15 ms 11.27 ms +11.1%
✅ matmul/large_generic_bf16_threads=1/32x1024x1024 2.41 ms 2.66 ms +10.3%
✅ qwen3_sampling_processors/top_k_full_sort_baseline 1.98 ms 2.10 ms +6.1%
✅ add/large_f32_threads=1-internal/4194304 752.27 µs 797.82 µs +6.1%
✅ matmul/medium_generic_f32_threads=1/32x512x512 2.60 ms 2.74 ms +5.3%
✅ sampling_latency/min_p_per_token 208.14 µs 217.01 µs +4.3%
✅ sampling_latency/greedy_per_token 3.43 µs 3.56 µs +3.7%
✅ matmul/large_generic_bf16_threads=8/32x1024x1024 2.35 ms 2.42 ms +2.9%
✅ matmul/large_generic_f16_threads=8/32x1024x1024 108.48 µs 111.52 µs +2.8%
✅ matmul/large_generic_f16_threads=1/32x1024x1024 85.27 µs 87.31 µs +2.4%
✅ qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline 3.34 ms 3.30 ms -1.3%
✅ gather/medium_f16_threads=1-internal/32768 2.30 µs 2.26 µs -1.9%
✅ matmul/medium_generic_bf16_threads=1/32x512x512 560.44 µs 549.49 µs -2.0%
✅ sampling_latency/top_k_per_token 55.18 µs 53.59 µs -2.9%
✅ add/medium_bf16_threads=1-internal/262144 114.12 µs 110.49 µs -3.2%
✅ gather/large_f16_threads=1-internal/131072 11.94 µs 11.23 µs -6.0%
✅ qwen3_sampling_processors/top_k_top_p_fast 650.80 µs 608.46 µs -6.5%
✅ reduce_mean/small_f32_threads=1-internal/4096 15.02 µs 13.98 µs -6.9%
✅ matmul/small_generic_bf16_threads=8/1x256x256 35.78 µs 33.29 µs -7.0%
✅ qwen3_sampling_processors/top_k_partial_selection 149.61 µs 138.50 µs -7.4%
✅ gather/small_bf16_threads=1-internal/4096 489.0 ns 449.1 ns -8.2%
✅ tokenization/decode_tokens_per_second 7.32 ms 6.71 ms -8.4%
✅ block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 192.63 µs 175.65 µs -8.8%
✅ matmul/small_generic_bf16_threads=1/1x256x256 35.76 µs 32.19 µs -10.0%
✅ reduce_mean/large_f32_threads=1-internal/262144 1.03 ms 918.36 µs -10.5%
✅ gather/small_f16_threads=1-internal/4096 511.9 ns 457.9 ns -10.6%
✅ qwen3_sampling_processors/top_p_fast_after_top_k 545.74 µs 484.90 µs -11.1%
✅ sampling_latency/top_p_per_token 446.76 µs 396.08 µs -11.3%
✅ add/large_bf16_threads=1-internal/4194304 1.94 ms 1.69 ms -12.8%
✅ gather/medium_f32_threads=1-internal/32768 3.98 µs 3.45 µs -13.2%
✅ gather/medium_bf16_threads=1-internal/32768 2.59 µs 2.24 µs -13.4%
✅ reduce_mean/medium_f32_threads=1-internal/65536 265.87 µs 228.04 µs -14.2%
✅ gather/small_f32_threads=1-internal/4096 723.8 ns 620.6 ns -14.3%
✅ kv_cache/alloc_dealloc_pages 43.69 µs 37.41 µs -14.4%
🟢 tokenization/encode_tokens_per_second 438.25 µs 366.94 µs -16.3%
🟢 logit_processing/seven_processor_chain_per_step 380.24 µs 308.51 µs -18.9%
🟢 add/small_f32_threads=1-internal/1024 261.0 ns 203.3 ns -22.1%
🟢 block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 709.97 µs 549.90 µs -22.5%
🟢 qwen3_sampling_processors/top_k_top_p_full_sort_baseline 6.76 ms 5.22 ms -22.9%
🟢 add/medium_f16_threads=1-internal/262144 146.46 µs 112.01 µs -23.5%
🟢 add/small_bf16_threads=1-internal/1024 576.4 ns 438.4 ns -23.9%
🟢 matmul/small_generic_f32_threads=1/1x256x256 46.28 µs 34.74 µs -24.9%
🟢 gather/large_bf16_threads=1-internal/131072 15.29 µs 11.43 µs -25.3%
🟢 matmul/large_generic_f32_threads=8/32x1024x1024 5.55 ms 4.03 ms -27.5%
🟢 gather/large_f32_threads=1-internal/131072 35.63 µs 25.27 µs -29.1%
🟢 add/large_f16_threads=1-internal/4194304 2.54 ms 1.78 ms -29.7%
🟢 matmul/small_generic_f16_threads=1/1x256x256 44.10 µs 30.35 µs -31.2%
🟢 grammar_masking/llguidance_compute_mask/32 101.99 µs 69.90 µs -31.5%
🟢 block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 95.50 µs 63.01 µs -34.0%
🟢 add/medium_f32_threads=1-internal/262144 40.81 µs 26.50 µs -35.1%
🟢 add/small_f16_threads=1-internal/1024 677.8 ns 439.8 ns -35.1%
🟢 block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 1.19 ms 767.67 µs -35.5%
🟢 matmul/medium_generic_bf16_threads=8/32x512x512 636.40 µs 389.62 µs -38.8%
🟢 matmul/small_generic_f32_threads=8/1x256x256 66.20 µs 31.65 µs -52.2%
🟢 block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 129.33 µs 54.98 µs -57.5%

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.93 3.50 5.67 }
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
justinchuby merged commit 805890e into main Aug 17, 2026
13 of 18 checks passed
@justinchuby
justinchuby deleted the squad/roy-fmt-repair-1104 branch August 17, 2026 06:32
justinchuby added a commit that referenced this pull request Aug 17, 2026
## The bug

`Kernel::set_constant_inputs` is how a kernel learns that an input is a
weight ORT owns for the life of the session. Kernels use it to decide
whether a prepack may be built once and kept, and `MatMulNBits` goes
further: `mlas_sqnbit_owns_fp32_compute(can_prepack, …)` gates the
**MLAS SQNBit path itself** on the same flag.

It was called from exactly one place in the workspace —
`onnx-runtime-session`'s executor. The ORT plugin EP never called it. So
on the only path a real ORT model takes, every kernel saw every input as
a runtime tensor: `MatMulNBits` rebuilt its packed weights on every
`Run` *and* ran the slower non-SQNBit kernel, and `QLinearMatMul`
re-packed `B` per call. A decoder paid a load-time cost per token.

## Why the existing test could not be reused

`node_inputs_all_routable` already distinguishes weights, as "no
producer and not a graph input". That is right for the whole model at
`GetCapability` time and wrong at `Compile` time: ORT hands a fused
node's subgraph over with the initializers it kept inside listed as
**graph inputs of that subgraph**, because they are inputs of the fused
node. Instrumenting the compile path on a `MatMulNBits` model prints

```
[const] op=MatMulNBits flags=[false, false, false] is_graph_input=[true, true, true] has_producer=[false, false, false]
```

`Graph_GetInitializers` still tells the two apart, so
`OutboundGraphReader` now records initializer **names** (no tensor data:
the existing `read_initializers_int64` deliberately copies only small
int64 tensors, and a 1 GB `B` must not be copied to answer a yes/no
question), and `constant_input_flags` keys against that set.

## Measurement

Interleaved A/B against **plain ORT through the ORT session API** — the
same generated single-node model loaded twice, once with this EP
registered and `session.disable_cpu_ep_fallback=1`, once with no EP
appended at all. 3 warmups, then 41 interleaved iterations each side,
p50/p90 of `Run` only (input `OrtValue`s built once). Assignment is
asserted before any timing, so a ratio is never reported for a node ORT
actually ran.

Host: AMD EPYC 9V74, 32 vCPU / 16 cores, AVX2+FMA+F16C (no
AVX-512/VNNI), ORT 1.27.0, release build **with `--features
onnx-runtime-ep-cpu/mlas`**, K=N=2048. The box is shared, so p90s are
contended; the `ours ms` columns are the load-bearing evidence and the
ORT column moves between runs.

| case | ours ms before | ours ms after | speedup | ours/ORT before |
ours/ORT after |
|---|---|---|---|---|---|
| `MatMulNBits` int4 M=1 | 1.144 | **0.425** | 2.7x | 15.0 | 2.34 |
| `MatMulNBits` int4 M=128 | 108.4 | **7.79** | 13.9x | 91.6 | 6.36 |
| `MatMulNBits` int4 f16-act M=1 | 3.659 | **0.416** | 8.8x | 16.0 |
1.80 |
| `MatMulNBits` int8 M=1 | 8.226 | **4.845** | 1.7x | 3.85 | 2.53 |
| `MatMulNBits` int8 M=256 | 47.98 | **12.04** | 4.0x | 5.03 | **1.05**
|
| `QLinearMatMul` u8 M=1 | 2.234 | **0.079** | 28x | 43.6 | 1.84 |
| `QLinearMatMul` u8 M=128 | 13.32 | **11.11** | 1.2x | 2.15 | 3.78 |
| `QLinearMatMul` i8 M=1 | 1.968 | **0.091** | 21.6x | 0.52 | **0.055**
|
| `MatMul` f32 M=1 (control) | 0.147 | 0.178 | — | 1.72 | 1.49 |
| `MatMul` f32 M=128 (control) | 9.675 | 8.698 | — | 1.36 | 1.43 |
| `MatMul` f16 M=1 (control) | 1.368 | 2.358 | — | 1.08 | 0.37 |
| `MatMul` f16 M=128 (control) | 21.04 | 22.64 | — | 7.04 | 2.84 |

The four dense cases are the control: they declare **both** operands as
graph inputs, so they have no initializer, no flag changes for them, and
they move only with host noise. Every case that moved has a constant
weight.

Cold session-creation time is reported by the same harness
(`cold_ours_ms`) and does not regress: the prepack moved from per-`Run`
to first-`Run`, not to `CreateSession`.

## Still losing after this change

Reporting all of it, per the standing rule:

- `MatMulNBits` int4 M=128 at 6.36x and M=1 at 2.34x.
- `QLinearMatMul` u8 M=128 at 3.78x (u8 M=1 is now 1.84x).
- `MatMul` f16 M=128 at 2.84x — no initializer involved, so untouched
here.
- `MatMul` f32 at 1.43-1.49x — likewise.

These are kernel gaps, not wiring gaps, and they stay open on my task
list.

**Separate finding, not fixed here:** the plugin cdylib is built
**without** the `mlas` feature by default, and
`docs/architecture/CROSS_PLATFORM.md` documents that feature as
x86-64-Linux-only. Rebuilding the same benchmark against the default
cdylib gives, after this fix, `MatMulNBits` int4 M=128 at 80x and
`QLinearMatMul` u8 M=128 at 56x ours/ORT: the pure-Rust paths are the
ones a portable build actually ships, and they are far behind. That is a
distinct piece of work and needs its own PR.

## Tests

- `constant_weights_are_reported_to_kernels_as_constant` (new,
`plugin_ort_e2e`) — reads the counter this PR exports from the very
cdylib ORT loaded and asserts symmetric int4 reports 2 constant inputs,
asymmetric int8 reports 3, and the all-graph-input dense case reports
**0**. A wiring that marks everything constant fails the third
assertion; caching a prepack of an activation is wrong, not merely slow.
Falsified by reverting `constant_input_flags` to the
producer/`is_graph_input` form: `reported 0 constant inputs, expected
2`.
- `initializers_are_constant_even_when_the_subgraph_calls_them_inputs`
and `nothing_is_constant_without_an_initializer_list` (new,
`onnx-runtime-ep-plugin` unit) — hardware-independent, pin the exact
shape ORT presents and the absent-optional-input case.
- `no_matmul_family_node_escapes_to_the_ort_cpu_ep` (extended) — every
case now runs a **second time in the same session** with the activation
rotated by one element, and is compared against ORT again. A weight
cache still matches ORT; anything activation-derived that outlived the
call does not. ORT is the oracle for whether a case can detect staleness
at all (a saturating u8 output cannot), and the suite asserts at least 5
of the 10 cases are activation-sensitive so the check cannot go vacuous.
- `plugin_path_ab_vs_plain_ort` (new, `#[ignore]` + `NXRT_MM_BENCH=1`) —
the harness that produced the table, committed so the numbers are
reproducible rather than asserted.
- `plugin_export_abi` — the two new exports are added to both the
required-symbol list and the unexpected-symbol filter.

## Verification

- `cargo test -p onnx-runtime-ep-cpu-plugin` — 51 + 9 + 6 + 1 passed, 1
ignored (the benchmark)
- `cargo test -p onnx-runtime-ep-plugin --lib` — 231 passed (was 229)
- `cargo fmt --all -- --check` clean (given #1109, which repairs `main`)
- `cargo clippy --all-targets` clean with and without `--features
onnx-runtime-ep-cpu/mlas`


---

## Post-review fix (commit 2): overridable initializers are not constant

Independent review found a correctness hole in commit 1, and it was
right.

`Graph_GetInitializers` is documented in ORT's own header as including
*"constant and non-constant initializers"*. From ONNX IR version 4, an
initializer whose name also appears in the graph's input list is only a
**default**: the caller may hand a different tensor in on any `Run`. ORT
says so at load time:

```
[W:onnxruntime:, graph.cc:1419 Graph] Initializer B appears in graph inputs and will not be
treated as constant value/weight.
```

Keying the flags on that name list alone therefore marked such a value
constant, and `MatMulNBits` would have cached a session-lifetime prepack
of the default weight and returned its answer for every later `Run` — a
wrong result, not a slow one.

**Fix:** each name from `Graph_GetInitializers` is now filtered through
`ValueInfo_IsConstantInitializer` (ORT ≥1.23, reads no tensor data).
Fail-closed: if the entry point is missing or the call errors, the
initializer is treated as **non-constant**. A false negative costs a
repeated prepack; a false positive costs correctness.

**New regression test**
`an_overridable_initializer_is_not_treated_as_a_constant_weight` builds
exactly that model — `B` and `scales` declared both as initializers and
as graph inputs — and asserts two independent things:

1. the EP reports **0** constant inputs (the classification), and
2. running the session twice with two different `B` payloads matches
plain ORT run for run, with the test first proving via ORT that the two
payloads *do* produce different outputs (the consequence — a stale
prepack would return run one's answer twice).

Falsified by deleting the `is_constant` filter and rebuilding the
cdylib:

```
assertion `left == right` failed: an initializer that is also a graph input may be replaced
on any Run, but this EP reported 2 of them as constant weights
```

The two doc comments that asserted "every initializer is constant" are
corrected, and the unit test is renamed to
`constant_initializers_are_flagged_even_when_the_subgraph_calls_them_inputs`.

### The measured win survives the stricter gate

Re-measured after the fix on the same host, same build (`--features
onnx-runtime-ep-cpu/mlas`, release, K=N=2048, 3 warmups + 41 interleaved
iterations, p50 ms):

| case | ours before PR | ours after commit 1 | ours after commit 2 |
|---|---|---|---|
| `nbits4_m1` | 1.144 | 0.425 | 0.400 |
| `nbits4_m128` | 108.4 | 7.79 | 8.80 |
| `nbits4_f16_m1` | 3.659 | 0.416 | 0.437 |
| `nbits8_m256` | 47.98 | 12.04 | 13.86 |
| `qlinear_u8_m1` | 2.234 | 0.079 | 0.092 |
| `qlinear_i8_m1` | 1.968 | 0.091 | 0.092 |

The benchmark models declare their weights as initializers only, so they
are still constant under the stricter rule; the residual movement is
host noise on a shared machine (ORT's own p50 for `nbits4_m1` moved
0.125 → 0.077 ms between the two runs, so the *ratio* column is the
noisier of the two and the absolute `ours` column is the honest
comparison here).

### Verification (re-run)

- `cargo test -p onnx-runtime-ep-cpu-plugin --test plugin_ort_e2e` — 52
passed, 1 ignored (was 51 + 1)
- `cargo test -p onnx-runtime-ep-plugin --lib` — 231 passed
- `cargo fmt --all -- --check` clean (rebased onto #1109, now merged)
- `cargo clippy -p onnx-runtime-ep-cpu-plugin -p onnx-runtime-ep-plugin
--all-targets` clean with and without `--features
onnx-runtime-ep-cpu/mlas`

---------

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.

1 participant