Skip to content

perf(cpu-ep): give x86 Relu and Clip the vector kernel they never had - #1355

Closed
justinchuby wants to merge 6 commits into
mainfrom
squad/resch-native-relu-clip-avx2
Closed

justinchuby wants to merge 6 commits into
mainfrom
squad/resch-native-relu-clip-avx2

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 19, 2026 •

Copy link
Copy Markdown
Owner

Closed unmerged — controlled null result

This PR is correct and it does what the title says: it gives x86 a hand-written
AVX2 Relu/Clip that is bit-identical to the scalar path on NaN, ±0 and inf.
It just does not make anything faster, so it is not worth the unsafe and the
maintenance.

The measurement

Paired against latest main (68fc9480f), 1 thread pinned, 400 iters / 50
warmup, 3 reps each, ours_p50 / ort_p50:

case main this PR
bench_relu_f32_4k 1.296 / 1.275 / 1.287 1.281 / 1.287 / 1.280
bench_relu_f32_1m 1.018 / 1.013 / 1.037 1.019 / 1.012 / 1.022

The distributions overlap completely. There is no effect at either size.

Why — verified in the emitted assembly, not guessed

I dumped the release assembly for onnx-runtime-ep-cpu on main and looked at
<ReluOp as ElementwiseOp>::apply_f32_bulk directly. The "scalar" loop

for (d, &s) in dst.iter_mut().zip(src.iter()) {
    *d = if s < 0.0 { 0.0 } else { s };
}

is already auto-vectorised by LLVM into maxps xmm… — SSE, 4 floats per
instruction. This PR widens that to vmaxps ymm…, 8 floats per instruction.

Doubling the vector width buys nothing here because Relu has no arithmetic
intensity to exploit — one compare per 8 bytes of traffic (4 in, 4 out):

  • at 1 Mi the kernel is bandwidth-bound, so the width of the ALU op is
    irrelevant;
  • at 4 Ki it is dispatch-bound — total is 2.8 µs, of which ~2.5 µs is
    the fixed per-Run overhead tracked in Reduce cpu ep kernel dispatch overhead #1077. The kernel itself is ~0.3 µs,
    and 4-wide was already enough to get there.

There is no size in between where this would show up, because Relu is memory-
bound at every size large enough for dispatch not to dominate.

Disposition

Closing unmerged, same standard as #1422 and #1358: a change that adds unsafe
and a second code path has to pay for itself in a controlled measurement, and
this one does not. Recorded as a falsified direction in the #1077 ledger.

The one piece here with standalone value is the bench_relu_f32_4k case (the
existing grid only had the bandwidth-bound 1 Mi size, which cannot say anything
about the kernel). That can be re-landed on its own if wanted; it does not need
the AVX2 code.

Refs #1077

Resch and others added 5 commits August 19, 2026 00:35
`relu_f32_simd` and `clip_f32_simd` dispatch to a hand-written NEON
kernel on aarch64 and to a scalar loop everywhere else. Every other
activation in this crate got an AVX2 kernel; these two were left behind,
and it shows: `Relu` at 1 Mi is the only large-tensor case in the
benchmark grid still slower than ORT's own CPU EP, which runs MLAS's
vectorised `MlasComputeRelu` there.

Both are now eight lanes wide on x86-64, four vectors per iteration,
with the usual 8-wide remainder and scalar tail. No new dependency, no
feature gate, no MLAS: `is_x86_feature_detected!("avx2")` at the top of
the dispatcher, same shape as `simd_activations`.

The operand order is the whole correctness argument. `vmaxps` is not a
general `max` — Intel defines it as `IF SRC1 > SRC2 THEN SRC1 ELSE SRC2`
and it returns SRC2 on an unordered compare — so with the zero vector as
SRC1 and the data as SRC2 it reproduces `if x < 0.0 { 0.0 } else { x }`
exactly, NaN payloads and `-0.0` included. Written the other way round,
the way anyone would write it first, both become `+0.0`. Clip is the
same argument twice, for `vmaxps` against the lower bound and `vminps`
against the upper.

Three tests compare against the scalar form **by bit pattern**, over
both zeros, both infinities, quiet/signalling NaNs with distinct
payloads, the subnormal boundary, and every length from 0 to 114 so the
32-wide body, the 8-wide remainder and the tail each see special values;
Clip additionally over eight bound pairs including inverted and NaN
bounds. One test drives the dispatcher rather than the kernel, so a
wrong `#[cfg]` arm cannot pass.

Falsifier, run: swapping the `_mm256_max_ps` operands fails both Relu
tests at lane 1 with `0.0 (0x00000000)` against `-0.0 (0x80000000)`.

1420 `-p onnx-runtime-ep-cpu --lib` tests pass, clippy and fmt clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`Relu` had only a 1 Mi bench case. At that size the op moves 8 MiB and
runs at ~63 GB/s on one core — it is bandwidth-bound, so it reports
almost nothing about the arithmetic. 4 Ki fits in L1, is the size a
decode step actually sees, and is where every other activation in the
grid is already measured.

The first thing it says is worth knowing: on `origin/main` Relu at 4 Ki
is 1.64x ORT, the **worst** ratio in the 4 Ki grid, and the AVX2 kernel
in the previous commit does not move it (1.642 -> 1.638, 2/5 rounds).
Relu is the cheapest op in the set, so ORT's own time is smallest and
our fixed per-`Run` cost is the largest share of it. That is a dispatch
problem, not a kernel one, and it is the number to watch as #1244/#1246
and their successors land.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
From the independent review of #1355. Neither is a correctness defect;
both are gaps the review was right to name.

- The AVX2 arms were gated `target_arch = "x86_64"` while the sibling
  f16c/bf16 staging in the same file uses `any(x86, x86_64)`. 32-bit x86
  can have AVX2, so relu and clip were silently falling to the scalar
  loop there for no reason. Widened, with the matching `std::arch::x86`
  import and test gate.
- `relu_f32_avx2_is_bit_identical_to_the_scalar_form` swept
  `0..src.len()`, which stops one short of the full 114-element slice.
  `0..=` now, symmetric with the clip test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A `git add -A` while applying the review nits swept up the A/B driver,
its two result CSVs, the summariser and a scratch copy of the PR body.
None of them belong in the tree. Removed from the index; they stay on
disk as untracked working files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@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
🔴 block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 53.88 µs 134.27 µs +149.2%
🔴 block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 40.88 µs 89.31 µs +118.5%
🔴 block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 50.68 µs 104.93 µs +107.0%
🔴 matmul/small_generic_bf16_threads=1/1x256x256 32.33 µs 53.57 µs +65.7%
🔴 block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 357.34 µs 589.62 µs +65.0%
🔴 gather/large_bf16_threads=1-internal/131072 17.10 µs 24.97 µs +46.0%
🔴 matmul/small_generic_f32_threads=1/1x256x256 49.78 µs 69.05 µs +38.7%
🔴 grammar_masking/llguidance_compute_mask/32 74.43 µs 101.65 µs +36.6%
⚠️ matmul/large_generic_f32_threads=8/32x1024x1024 3.65 ms 4.69 ms +28.5%
⚠️ matmul/medium_generic_bf16_threads=8/32x512x512 457.01 µs 577.32 µs +26.3%
⚠️ qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline 3.60 ms 4.48 ms +24.4%
⚠️ kv_cache/alloc_dealloc_pages 38.73 µs 47.93 µs +23.8%
⚠️ block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 801.55 µs 983.65 µs +22.7%
⚠️ gather/medium_f16_threads=1-internal/32768 2.59 µs 3.15 µs +21.3%
⚠️ matmul/small_generic_bf16_threads=8/1x256x256 40.92 µs 49.36 µs +20.6%
⚠️ matmul/medium_generic_f16_threads=8/32x512x512 32.31 µs 38.87 µs +20.3%
⚠️ matmul/large_generic_bf16_threads=8/32x1024x1024 1.58 ms 1.88 ms +18.9%
⚠️ logit_processing/seven_processor_chain_per_step 336.25 µs 396.57 µs +17.9%
⚠️ matmul/small_generic_f16_threads=8/1x256x256 38.12 µs 44.09 µs +15.7%
⚠️ qwen3_sampling_processors/top_p_fast_after_top_k 540.74 µs 622.38 µs +15.1%
✅ qwen3_sampling_processors/top_k_top_p_full_sort_baseline 6.21 ms 7.13 ms +14.7%
✅ matmul/small_generic_f32_threads=8/1x256x256 39.02 µs 44.49 µs +14.0%
✅ qwen3_sampling_processors/top_k_partial_selection 151.14 µs 172.26 µs +14.0%
✅ qwen3_sampling_processors/top_k_top_p_fast 682.10 µs 763.10 µs +11.9%
✅ gather/large_f16_threads=1-internal/131072 19.45 µs 21.74 µs +11.8%
✅ gather/small_f32_threads=1-internal/4096 721.5 ns 804.7 ns +11.5%
✅ matmul/small_generic_f16_threads=1/1x256x256 34.40 µs 37.96 µs +10.3%
✅ gather/medium_bf16_threads=1-internal/32768 2.46 µs 2.67 µs +8.5%
✅ gather/small_bf16_threads=1-internal/4096 531.2 ns 576.2 ns +8.5%
✅ add/large_f32_threads=1-internal/4194304 728.75 µs 778.88 µs +6.9%
✅ add/small_f16_threads=1-internal/1024 511.5 ns 546.3 ns +6.8%
✅ matmul/large_generic_f16_threads=8/32x1024x1024 92.42 µs 97.82 µs +5.8%
✅ reduce_mean/medium_f32_threads=1-internal/65536 251.44 µs 265.61 µs +5.6%
✅ matmul/medium_generic_f16_threads=1/32x512x512 33.24 µs 34.72 µs +4.5%
✅ matmul/large_generic_f32_threads=1/32x1024x1024 9.52 ms 9.89 ms +3.9%
✅ matmul/large_generic_f16_threads=1/32x1024x1024 83.13 µs 85.15 µs +2.4%
✅ qwen3_sampling_processors/top_k_full_sort_baseline 2.17 ms 2.21 ms +1.9%
✅ gather/medium_f32_threads=1-internal/32768 4.94 µs 4.99 µs +1.0%
✅ matmul/large_generic_bf16_threads=1/32x1024x1024 2.10 ms 2.11 ms +0.6%
✅ add/medium_bf16_threads=1-internal/262144 112.65 µs 113.28 µs +0.6%
✅ matmul/medium_generic_bf16_threads=1/32x512x512 545.68 µs 544.61 µs -0.2%
✅ tokenization/encode_tokens_per_second 431.38 µs 425.71 µs -1.3%
✅ sampling_latency/top_p_per_token 407.48 µs 399.94 µs -1.9%
✅ sampling_latency/min_p_per_token 225.59 µs 219.87 µs -2.5%
✅ reduce_mean/large_f32_threads=1-internal/262144 1.13 ms 1.10 ms -3.2%
✅ add/large_f16_threads=1-internal/4194304 2.00 ms 1.91 ms -4.2%
✅ add/small_bf16_threads=1-internal/1024 483.0 ns 453.8 ns -6.0%
✅ add/medium_f16_threads=1-internal/262144 124.21 µs 110.56 µs -11.0%
✅ add/small_f32_threads=1-internal/1024 257.3 ns 226.0 ns -12.2%
🟢 gather/large_f32_threads=1-internal/131072 43.95 µs 36.93 µs -16.0%
🟢 matmul/medium_generic_f32_threads=1/32x512x512 2.97 ms 2.49 ms -16.2%
🟢 gather/small_f16_threads=1-internal/4096 616.3 ns 513.8 ns -16.6%
🟢 sampling_latency/greedy_per_token 4.13 µs 3.37 µs -18.4%
🟢 add/large_bf16_threads=1-internal/4194304 2.17 ms 1.76 ms -18.9%
🟢 reduce_mean/small_f32_threads=1-internal/4096 19.80 µs 15.96 µs -19.4%
🟢 tokenization/decode_tokens_per_second 8.39 ms 6.56 ms -21.8%
🟢 add/medium_f32_threads=1-internal/262144 37.82 µs 26.06 µs -31.1%
🟢 matmul/medium_generic_f32_threads=8/32x512x512 1.80 ms 1.18 ms -34.7%
🟢 sampling_latency/top_k_per_token 85.92 µs 53.42 µs -37.8%

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: { 7.02 4.55 5.73 }
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)

@codecov

codecov Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.80%. Comparing base (4a9f4ec) to head (4500658).
⚠️ Report is 80 commits behind head on main.

Files with missing lines Patch % Lines
...nx-runtime-ep-cpu/src/kernels/dense_elementwise.rs 94.73% 8 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #1355       +/-   ##
===========================================
- Coverage   82.10%   80.80%    -1.31%     
===========================================
  Files          12      377      +365     
  Lines        5471   167211   +161740     
  Branches     5471   167211   +161740     
===========================================
+ Hits         4492   135111   +130619     
- Misses        780    27253    +26473     
- Partials      199     4847     +4648     
Flag Coverage Δ
cli-ort-linux 82.60% <ø> (?)
cli-ort-windows 82.19% <ø> (+0.09%) ⬆️
offline 80.74% <94.73%> (?)

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

Files with missing lines Coverage Δ
...nx-runtime-ep-cpu/src/kernels/dense_elementwise.rs 91.27% <94.73%> (ø)

... and 364 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 added a commit that referenced this pull request Aug 19, 2026
#1376)

## What

The f16/bf16 activation path was a three-pass sandwich: widen the input
into an
`f32` scratch **the size of the whole tensor**, run the kernel over all
of it,
then narrow all of it back. On a 1 Mi tensor that is a 4 MB buffer
streamed out
to memory and read back for no reason — the intermediate is dead the
moment the
narrow has seen it.

This fuses the last two passes. When the output is a contiguous f16/bf16
tensor
and the closure declares no extra reads, `write_mapped_reading`
evaluates the
kernel ~8192 elements at a time into a 32 KiB scratch and narrows each
chunk
straight into the output, so the intermediate never leaves L1.

The code already carried the diagnosis and named this as the fix:

> The fix for f16/bf16 is to fuse widen, compute and narrow into a
single pass
> per chunk so each thread keeps its slice in L2 — a different change,
and one
> that has to be measured on its own.

This is that change, measured on its own.

## Why the chunking is bit-identical

Two separate things have to hold, and the second one is subtle enough
that the
first version of this PR got it wrong (see the review section).

**1. The closure must be a pure elementwise map.** A closure that reads
across
lanes would see a different window per chunk. The gate is structural
rather than
a convention:

- the row-broadcast callers (`BiasGelu`, `FastGelu` with a bias)
**must**
declare their bias range through `also_read`, because that is what lets
  `output_direct_write_eligible` reject an aliasing output;
- a non-empty `also_read` is exactly what keeps them on the whole-tensor
path.

A caller cannot reach the chunked arm without having already asserted it
reads
nothing but `x`. All five `write_mapped` (empty `also_read`) call sites
were
checked and are plain elementwise maps: `Gelu` ×2, `QuickGelu`,
`FastGelu`
without bias, and the `Tanh`/`Sqrt`/`Erf` arm of unary elementwise.

**2. No chunk may be shorter than `SIMD_MIN_LEN`.** `dispatch!` picks
scalar or
vector from the length of the slice it is handed, and for the
approximated
kernels **the two are not bit-identical**. A plain
`chunks(NARROW_CHUNK)` hands
the last chunk whatever is left over, so a tensor of `NARROW_CHUNK + 16`
elements would run its final 16 scalar where one pass runs them
vectorised.
`chunk_end` absorbs a trailing remainder below `SIMD_MIN_LEN` into the
chunk
before it, and keeps every other boundary on an 8-wide lane so the
vector run
lands where one pass would have put it.

This matters because the module treats bit reproducibility as a
**correctness**
property rather than a tolerance — see the `host_chunk_len` docs and the
thread-invariance tests — and bf16 results cross to the CUDA EP.

### `unsafe`

The `&mut [u16]` is built under `validate()`, `is_contiguous()`,
`numel() == n`,
and a dtype match on `Float16`/`BFloat16` (which is what makes the
element width
16 bits). Disjointness from `x` does **not** rest on access ordering —
that
would not make overlapping `&[f32]`/`&mut [u16]` sound anyway. It rests
on
`to_dense_f32_widen` always allocating a fresh buffer when it widens, so
a
16-bit output cannot be aliased by its own widened input. A
`debug_assert`
is the tripwire for a future caller that breaks that.

## Measurement

Production A/B, `plugin_path_ab_vs_plain_ort`, 1 Mi elements, **1
thread**,
60 iterations, box gated to ≥93% idle. Ratio is **ours/ORT, lower is
better**.

| case | ratio_p50 before | after | ours p50 before → after |
|---|---|---|---|
| `bench_tanh_f16_1m` | 1.408 | **1.213** | 0.752 → 0.653 ms
(**−13.2%**) |
| `bench_exp_f16_1m` | 1.206 | 1.206 | 0.718 → 0.718 ms (flat) |

Repeatability of the headline case — seven independent rounds, the last
three
taken **after** the correctness fix, which did not move it:

| round | ratio_p50 | ours p50 | ORT p50 (control) |
|---|---|---|---|
| 1 | 1.217 | 0.6458 | 0.5306 |
| 2 | 1.214 | 0.6542 | 0.5389 |
| 3 | 1.214 | 0.6525 | 0.5376 |
| 4 | 1.212 | 0.6536 | 0.5391 |
| 5 (post-fix) | 1.212 | 0.6562 | 0.5415 |
| 6 (post-fix) | 1.214 | 0.6524 | 0.5372 |
| 7 (post-fix) | 1.214 | 0.6590 | 0.5427 |

The ORT arm moves 1.0% across the whole set against a 13.2% effect, so
the
result is an order of magnitude clear of its own control.

`Exp` does not move because it never reaches `write_mapped` — only
`Tanh`,
`Sqrt` and `Erf` route through the f32 path today. Widening that routing
is the
obvious follow-up and is deliberately **not** in this PR.

Small tensors are unaffected: the scratch is `(NARROW_CHUNK +
SIMD_MIN_LEN).min(n)`,
so for any `n` below a chunk it allocates exactly the same `n` floats
the old
path did.

### Context: why f16 was the thing to fix

A full 1 Mi sweep of the activation family at 1 thread showed the native
kernels
are already ahead of ORT on **17 of 18 f32 cases** — `mish` 0.101,
`celu` 0.135,
`elu` 0.137, `selu` 0.145, `log` 0.203, `sqrt` 0.309, `erf` 0.428,
`tanh` 0.444,
`sigmoid` 0.451 — with `relu` (1.026) the only exception, and that one
is
already addressed by #1355. The two 16-bit cases were the real remaining
kernel-side losses, which is what pointed here.

## Tests

- `a_short_tail_does_not_fall_off_the_vector_path` — the one that guards
the
seam. It **searches at run time** for an input where the scalar and
vector
paths disagree *after narrowing to f16* (most f32 disagreements are
washed out
by the narrow, so checking in the f32 domain proves nothing), asserts a
witness was found, and then pins fused == unfused for tails of 1, 8, 16
and 31.
- `chunk_end_never_emits_a_short_trailing_chunk` — pins the
decomposition
itself across ~250 tensor sizes around chunk boundaries: every chunk is
either
≥ `SIMD_MIN_LEN` or the whole tensor, every interior boundary is
lane-aligned,
  and no chunk overruns the scratch.
- `fused_narrow_is_bit_identical_across_chunk_boundaries` — f16 **and**
bf16
against a single-shot reference across `Tanh`/`Erf`/`Sigmoid` for seven
tail
  lengths.
- `a_row_broadcast_closure_stays_off_the_chunked_arm` — a `BiasGelu`
with row
  width 61 (never divides a chunk) over a multi-chunk tensor.

Every one was verified non-vacuous by mutating production code and
observing the
failure. Reverting `chunk_end` to the naive form fails the seam test at
index
**8192** with f16 bits 5146 vs 5145.

Suites, on this branch:

- `onnx-runtime-ep-cpu --lib`, **debug** profile: **1451 passed, 0
failed**
- `plugin_ort_e2e` with `NXRT_REQUIRE_ORT_TESTS=1`: **55 passed, 0
failed**,
including `every_assigned_node_is_also_executed_by_this_ep` — assigned
==
  executed conformance holds.
- `cargo clippy -p onnx-runtime-ep-cpu --lib --all-targets`: clean.
- `cargo fmt -p onnx-runtime-ep-cpu -- --check`: clean.

## Review

Independently reviewed by Claude Opus 4.8 with a brief to attack the
safety
argument and verify every test by mutation. Verdict: **REQUEST
CHANGES**, one
blocker, which is the `SIMD_MIN_LEN` tail divergence described above.
The
reviewer reproduced it with a `Sigmoid` f16 tensor of 8208 elements
(bits 3455
vs 3454) and correctly observed that the original 137-element tail could
not
catch it. Fixed in `887c22e51`, along with the reviewer's point that the
aliasing justification was argued from access ordering rather than from
the
allocation that actually guarantees it.

The reviewer also confirmed: all eight call sites audited and the
structural
`also_read` gate holds; the `FnOnce`→`Fn` relaxation is safe (every
closure
captures only `Copy` state, no `Cell`/RNG/lazy-init); the guard set
matches
`write_dense_f32_narrow`; `n == 0`, `numel() != n` and non-x86 all fall
through
unchanged; and small-`n` allocation is not a regression.

## Scope

No ORT CPU fallback, no runtime or default MLAS — pure native x86, one
function
plus its tests.

---

## Re-validated on latest `main` (`e0aedd0fa`), 2026-08-19

Latest `main` merged in normally (no rebase). Re-measured paired against
`main`
from the same box, 1 thread pinned, 60 iters / 10 warmup, `ours_p50 /
ort_p50`:

| arm | reps | ours p50 (ms) | ratio |
|---|---|---|---|
| `main` | 3 | 0.7367 / 0.7375 / 0.7319 | 1.361 / 1.367 / 1.371 |
| this PR | 5 | 0.6536 / 0.6612 / 0.6577 / 0.6532 / 0.6506 | 1.213 /
1.226 / 1.226 / 1.218 / 1.217 |

**Ratio 1.367 → 1.218 (median), our own time −11%.** ORT drifted under
1%
between the arms (0.534–0.541 on `main`, 0.535–0.539 here), which is the
control. The distributions do not overlap.

## Cross-platform fix (`f9a97e853`)

The fused arm is behind `#[cfg(x86)]`, but `NarrowFn`, `chunk_end`,
`NARROW_CHUNK`, the test helper `scalar_vector_divergence` and the four
tests
that exercise chunk boundaries were **not**. An aarch64 build warned on
every
one of them, and CI builds with `-D warnings` — so this branch would
have
failed cross-platform CI while a local x86 `clippy` run stayed green.

Now gated alongside the code they belong to: **0 warnings on both x86-64
and
aarch64**. All four tests assert properties of the chunked arm, which
does not
exist off x86, so gating them loses no coverage.

## Validation on the merged base

- `cargo fmt` clean; `cargo clippy --all-targets -D warnings` clean
- **1556 `onnx-runtime-ep-cpu` tests**, debug profile (`debug_assert`s
live)
- **55 plugin conformance tests** (`NXRT_REQUIRE_ORT_TESTS=1`, release)
- `every_assigned_node_is_also_executed_by_this_ep` and
`every_fixture_loads_with_cpu_fallback_disabled` green — nothing defers,
  nothing falls back to the ORT CPU EP
- **aarch64-unknown-linux-gnu** `--all-targets` check clean, 0 warnings

---------

Co-authored-by: Resch <resch@users.noreply.github.com>
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