Skip to content

refactor(ep-plugin): share first native shape rules - #2064

Merged
justinchuby merged 3 commits into
mainfrom
refactor/shared-shape-rules
Aug 25, 2026
Merged

justinchuby merged 3 commits into
mainfrom
refactor/shared-shape-rules

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Rebased commit list

  1. 8dc155b58 — refactor(ep-plugin): share first native shape rules
  2. 5d1d4b80a — docs(test): name remaining shape-rule duplication
  3. 6f23d5e2e — test(ep-plugin): harden shared shape contracts

The diff against origin/main is only the incremental adapter, tests, manifests/lockfile, and related documentation: 7 files, 701 insertions, 47 deletions. It contains none of #2049's duplicated implementation commits.

Selection and fallback contract

Resolved concrete native shapes are used directly. SymbolicOrUnknown falls back because the ORT plugin must allocate before kernel execution. Rejected also falls back in this first slice, preserving historical plugin permissiveness.

Tests now discriminate:

  • Expand@7 => SymbolicOrUnknown, then compatibility fallback; Expand@8 => Resolved
  • absent version => opset 1, never a silent latest-version guess
  • foreign-domain Expand => not SharedNative
  • unregistered native op => symbolic, with a synthetic shared fallback succeeding
  • device-marked Expand shape operand => symbolic native result, then a safe plugin error without dereference
  • malformed rank-2 Tile repeats => native Rejected, while the pre-existing permissive plugin fallback still returns [6, 6]
  • every shared fallback is asserted non-SharedNative, preventing recursion

The adapter documentation explicitly records that production receives ORT Node_GetSinceVersion (the selected schema/kernel since_version), not necessarily the graph-level opset. Future version-sensitive migrations must account for that distinction.

Anti-vacuity and edge coverage

The shared-rule sweep asserts the exact independent census ConstantOfShape, Expand, Tile, then asserts the number compared. The shape-preserving sweep can no longer skip rejected/unresolved cases and asserts compared == CASES.len().

Added agreement fixtures for:

  • Expand target extent 0
  • Tile repeat 0
  • empty ConstantOfShape input => scalar

Dependency finding

Empirical cargo tree -p onnx-runtime-ep-plugin -e normal findings:

  • onnx-runtime-shape-inference is a thin direct dependency (onnx-runtime-ir + thiserror)
  • it does not introduce a loader/prost path
  • prost was already in the shipped plugin graph through onnx-runtime-ep-api -> onnx-runtime-loader / tracer before this PR
  • normalized runtime package sets are identical before/after: 48 packages; incremental package nodes = 0

Therefore no shared-rules crate split is justified for this slice: protobuf is pre-existing, not introduced by the adapter. The remaining binary-size risk is compiled shape-registry code itself, noted below.

Validation

All commands were run after restoring every mutation:

  • cargo test --manifest-path C:\Users\justinchu\dev\ng-shape-dry\Cargo.toml --locked --no-fail-fast -p onnx-runtime-ep-plugin — 356 passed, 3 ignored
  • cargo test --manifest-path C:\Users\justinchu\dev\ng-shape-dry\Cargo.toml --locked --no-fail-fast -p onnx-runtime-ep-cpu-plugin — 101 passed, 1 ignored
  • cargo test --manifest-path C:\Users\justinchu\dev\ng-shape-dry\Cargo.toml --locked --no-fail-fast -p onnx-runtime-shape-inference — 343 passed
  • cargo test --manifest-path C:\Users\justinchu\dev\ng-shape-dry\Cargo.toml --locked -p onnx-runtime-session warm_decode_seeding_admits_previously_unresolved_capture_safe_node -- --nocapture — 1 passed; this is the genuine runtime JIT-sizing proof
  • cargo clippy --manifest-path C:\Users\justinchu\dev\ng-shape-dry\Cargo.toml --locked -p onnx-runtime-ep-plugin -p onnx-runtime-ep-cpu-plugin -p onnx-runtime-shape-inference -p onnx-runtime-session --all-targets -- -D warnings — passed

Mutation matrix

Mutation Expected guard Actual result
Remove Tile from SharedNativeShapeRule::ALL exact census failed: actual two rules vs expected three
Make ALL empty anti-vacuity census failed: [] vs expected three
Make plugin Expand return the target agreement fixture failed: plugin [1, 4] vs native [3, 4]
Turn native Rejected into a hard error permissiveness fallback failed on native rank-2 Tile rejection instead of returning [6, 6]
Hardcode domain/opset selection selector/version tests failed both foreign-domain selection and absent-version-not-latest assertions

Each mutation exited 101 and was reverted; the clean suites above then passed.

Remaining risks / intentionally deferred

  • The plugin carries the native shape registry's compiled code, although it adds no new runtime package or protobuf dependency. Binary-size measurement can be added if packaging establishes a size budget.
  • Node_GetSinceVersion is not guaranteed to equal graph opset; this is documented and pinned, but every future version-sensitive migration needs its own selection fixture.
  • Compatibility rules remain duplicated as fallbacks for these three ops. All other plugin-only rules remain local; this PR does not migrate additional ops.
  • Device-resident value-carried shape operands still require host staging by an EP; absent staging, the fallback fails safely and actionably.

Copilot AI added 3 commits August 24, 2026 20:00
Route ConstantOfShape, Expand, and Tile through the native inference registry while retaining plugin fallbacks for symbolic inputs and stricter native rejections.

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

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

Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
Pin the exact shared-rule census, exercise every tri-state fallback and version/domain selector, and keep test-only hooks out of shipped builds.

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

Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
@justinchuby
justinchuby force-pushed the refactor/shared-shape-rules branch from f255ba3 to 6f23d5e Compare August 25, 2026 03:31
@codecov

codecov Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.51163% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.70%. Comparing base (eccc824) to head (6f23d5e).
⚠️ Report is 25 commits behind head on main.

Files with missing lines Patch % Lines
crates/onnx-runtime-ep-plugin/src/shared_shapes.rs 95.11% 4 Missing and 7 partials ⚠️
crates/onnx-runtime-ep-plugin/src/compute.rs 99.15% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2064      +/-   ##
==========================================
+ Coverage   80.35%   80.70%   +0.35%     
==========================================
  Files         409      427      +18     
  Lines      191136   211901   +20765     
  Branches   191136   211901   +20765     
==========================================
+ Hits       153593   171025   +17432     
- Misses      32103    35185    +3082     
- Partials     5440     5691     +251     
Flag Coverage Δ
cli-ort-linux 72.51% <ø> (?)
cli-ort-windows 72.01% <ø> (?)
mlas 85.20% <ø> (?)
offline 80.85% <96.51%> (+0.49%) ⬆️

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

Files with missing lines Coverage Δ
crates/onnx-runtime-ep-plugin/src/lib.rs 100.00% <ø> (ø)
crates/onnx-runtime-ep-plugin/src/compute.rs 83.19% <99.15%> (+0.49%) ⬆️
crates/onnx-runtime-ep-plugin/src/shared_shapes.rs 95.11% <95.11%> (ø)

... and 75 files with indirect coverage changes

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

@justinchuby
justinchuby merged commit 3087071 into main Aug 25, 2026
17 of 19 checks passed
@justinchuby
justinchuby deleted the refactor/shared-shape-rules branch August 25, 2026 05:03
justinchuby added a commit that referenced this pull request Aug 25, 2026
## Summary
- implement `ai.onnx::STFT` on the native CPU EP
- share DFT's radix-2/vDSP/scalar transform core through a reusable
per-execution plan
- route plugin output sizing through the native shape-rule adapter from
#2064
- add strict dtype/shape/value validation and dynamic native/plugin
shape tests

## ONNX contract

This implements **STFT since_version 17**:

- inputs: `signal: T1`, required scalar `frame_step: T2`, optional
rank-1 `window: T1`, optional scalar `frame_length: T2`
- at least one of `window` or `frame_length` is required; when both are
present their lengths must match, so ONNX STFT has no separate
window-length/FFT-length mode
- `onesided` is integer 0/1 and defaults to 1
- canonical signal shape is `[batch, signal_length, 1|2]`; complex input
(`...x2`) requires `onesided=0`
- output is `[batch, floor((signal_length-frame_length)/frame_step)+1,
bins, 2]`, with `bins=frame_length/2+1` for onesided and
`bins=frame_length` otherwise
- frames are complete and unpadded: no centering and no implicit
padding; a signal shorter than one frame is rejected

The schema permits f16/f32/f64/bf16. This CPU kernel deliberately claims
**f16, bf16, and f32 only**: inputs are widened once, computed in f32,
and narrowed once. f64 is rejected at claim and execution rather than
pretending f32 arithmetic is f64 support. Length inputs accept
int32/int64.

## Algorithm and allocation behavior

For `N=frame_length`, power-of-two frames use the existing radix-2 FFT
(or Accelerate vDSP where available), for `O(batch * frames * N log N)`.
Arbitrary lengths currently use the existing scalar DFT, `O(batch *
frames * N^2)`; no faster arbitrary-length claim is made.

Signal/window strided views are materialized once. The output is
allocated once. Four `N`-element scratch vectors and one DFT/vDSP plan
are reused across all frames and batches; there is no per-frame `Vec` or
vDSP-plan allocation.

## Tests

- CPU STFT focused suite: **11 passed** (real/complex, overlap, explicit
nontrivial window, strided signal/window, onesided/full conjugacy and
Nyquist, exact/short/multiple frames, arbitrary length, f16/bf16,
invalid contracts)
- existing DFT focused suite: **5 passed**
- native shape-inference STFT tests: **3 passed**
- shared native/plugin rule census + agreement: **4 passed**
- plugin shape-coverage suite: **4 passed**
- native runtime dynamic STFT sizing: **1 passed**
- CPU EP lib suite with the pre-existing Windows-only affinity test
excluded: **1780 passed, 23 ignored, 1 filtered**
- shape-inference full suite: **283 passed** plus **59
auxiliary/integration tests**
- warnings-denied Clippy: changed five crates, all targets, green
- package-scoped rustfmt check: green

Mutation checks each went red independently:

1. ignoring `window` failed the strided nontrivial-window oracle (`10`
vs `13.75` DC)
2. incrementing frames by `frame_length` instead of `frame_step` failed
the overlap test
3. dropping the final `+1` frame failed the three-frame shape assertion
4. using `N/2` instead of `N/2+1` failed the Nyquist/shape test

## CUDA reuse

A future CUDA STFT can reuse the v17 validation contract, native shape
rule, shared plugin adapter, independent f64 reference formula,
frame-count/onesided mutation tests, and the fixed-size
transform-plan/scratch ownership pattern. This PR does not touch CUDA
code or Leon's cuFFT work.

## Not verified

- CUDA STFT or GPU execution (out of scope)
- macOS/iOS vDSP execution on this Windows host; existing DFT vDSP tests
remain in place and the setup is now reusable across STFT frames
- a loaded ORT plugin end-to-end STFT session; the plugin claim
coverage, shared-rule census, concrete shared shape agreement, and CPU
execution paths are locally green
- throughput numbers; complexity and allocation behavior are
established, but arbitrary-length acceleration/benchmarking remains
follow-up work

The only excluded CPU-lib test is
`a_default_width_pool_on_leader_cpus_uses_every_core_it_was_given`,
which is Linux-affinity-specific and fails on Windows because
process-wide affinity masking is intentionally unsupported there.

Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
justinchuby added a commit that referenced this pull request Aug 25, 2026
…2058) (#2098)

Closes #2058.

21 crates are compiled and tested by CI and linted by nothing. This
derives the clippy package lists from the same source the test lanes
already use, and adds a guard so the two cannot drift apart again.

## The correction I owe first

#2058 claimed two live `clippy::unnecessary_cast` denials on `main` in
`onnx-runtime-ep-plugin/src/compute.rs`. **They are gone, and I did not
fix them.** At `bc715c329`:

```
cargo clippy --locked --all-targets -p onnx-runtime-ep-plugin -- -D warnings   ->  0
```

`git log 67d3aa5..HEAD -- crates/onnx-runtime-ep-plugin/` is three
feature/refactor commits (#2049, #2064, #2083); none mentions clippy or
the cast. The lines were deleted incidentally by a refactor.

That weakens the issue's headline and strengthens its actual point. The
gap admits defects **and releases them unobserved** — nobody knows what
is in there at any moment without running the lint themselves. The
sample was never the argument; the mechanism is.

## The rule that created the gap was false

`ci.yml` instructed: *"To add a crate, first confirm its normal+dev
dependency tree contains no ort-sys/CUDA dependency."*

That rule was already violated by the list it annotated. Measured:

```
crates in offline-linux whose normal+dev tree contains onnx-genai-ort-sys:  13
of those, already on the clippy list:  onnx-runtime-ep-cpu, onnx-runtime-ep-api
```

And it does not matter which way you resolve that, because **`cargo
clippy` only ever checks** — it never links and never runs a test
binary. The offline/ort-backed split exists for `cargo test`; it has no
force for lint. `onnx-genai-ort-sys` itself compiles here with no
network.

In #2058 I wrote that I did not know whether the premise was stale or
whether `ep-plugin` should be out of `offline-linux`, and would rather
flag it than guess. This is the measurement I said I would not
substitute a guess for: **the premise was false.**

## What changed

- All three clippy `-p` lists — **byte-identical to each other, 31
packages, repeated in three jobs** — become `$(python
.github/scripts/workspace_test_packages.py cargo-args lint)`. The new
`lint` lane is "every package some test lane compiles" (55). Windows
ARM64 takes `offline-linux` (49), still a strict superset of the 31 it
linted before, and no ORT crates on that target.
- `verify` gains a **lint-coverage half**: it fails if any tested
package is reached by no `cargo clippy` invocation anywhere in
`.github/workflows`. Generator calls are **expanded, not skipped** — a
computed `-p` list counts, and a hand-written one cannot hide behind
some other step computing one.
- A **self-test step** actually exercises the controls.

## Evidence

**The guard detects the real defect.** Run against unmodified `main`,
before the `ci.yml` change, it reports the gap by name:

```
Workspace lint coverage check failed.
Package(s) are compiled and tested by CI and linted by nothing:
  - onnx-genai            - onnx-runtime-ep-nxrt-abi      - onnx-runtime-memory-abi
  - onnx-genai-capi       - onnx-runtime-ep-nxrt-host     - onnx-runtime-memory-host
  - onnx-genai-ort        - onnx-runtime-ep-nxrt-testplugin  - onnx-runtime-memory-testplugin
  ... 21 total
```

**The fix closes it, and the pass is not an empty selection.** The exact
command `ci.yml` now runs:

```
cargo clippy --locked --all-targets $(python ... cargo-args lint) -- -D warnings   ->  RC=0
```

parsed with `--message-format=json`: **wanted 55, seen 55, MISSING:
none.** A clippy run that selected nothing would also exit 0, so the
package set is confirmed present in the lint graph rather than inferred
from the exit code.

**Mutations.**

| mutation | result |
|---|---|
| `--simulate-unlinted onnx-runtime-ep-cpu` | rc=1, names that crate |
| `--simulate-missing onnx-runtime-ir` (pre-existing half, after
refactor) | rc=1 |
| both Linux clippy steps reverted to lane `offline-linux` | rc=1, names
`onnx-genai`, `onnx-genai-capi`, `onnx-genai-ort` |
| a **comment** claiming `cargo clippy -p onnx-genai ...` added, lanes
narrowed | rc=1 — prose cannot buy coverage |
| clean tree | rc=0 |

## Two defects the mutations found in my own work

Recording both, because in each case the check was passing at the time.

**1. The scanner counted a YAML comment as an invocation.** After I
rewrote the explanatory comment — which contains the words `cargo
clippy` — the reported invocation count went `10 -> 11`. Nothing failed;
the only symptom was a number moving that I had no reason to expect to
move. A comment reading `# cargo clippy -p foo` would have granted `foo`
lint coverage. The scanner now skips comment lines, the count is back to
10, and the mutation table above has a cell for exactly this.

**2. My first self-test passed because `python` was not on PATH.** It
was written as `if cmd ...; then fail; fi` — a bare non-zero check.
Command-not-found is `127`, which is non-zero, so it read as *"the guard
correctly failed"*. It passed loudest in precisely the case where
nothing ran. It now requires **exit 1 specifically, plus the matching
failure message**:

```
interpreter present   ->  rc=0   both guards correctly fail
interpreter MISSING   ->  rc=1   ::error::verify ... exited 127, expected 1
guard neutered        ->  rc=1   ::error::verify ... exited 0, expected 1
```

This is the same shape as `QEMU_LD_PREFIX` and `| head`: **a weaker
check fails in the direction of passing.** It is also why the self-test
step exists at all — `--simulate-missing` had shipped with this guard
since it was written and CI had **never once invoked it**. A control
nobody runs is not a control.

## Scope and risk

- `Fast (Linux x86_64)` and `Rust quality` gain 24 crates each. Verified
locally: the whole 55-package set is clean at `-D warnings
--all-targets`.
- **`Rust (Windows ARM64)` gains 18 crates on `aarch64-pc-windows-msvc`,
which I cannot run locally.** If that lane goes red this is mine and I
will narrow it before merging. That lane is **not required**, which — as
of last night's two merged defects that green required checks did not
catch — is exactly the kind of lane I intend to read rather than assume.
**I will wait for it.**
- No Rust source changes. No behaviour change. Lint coverage only.

Normal `--squash --auto`. No admin bypass.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants