Skip to content

EP plugin export: run our Rust execution providers inside upstream ONNX Runtime - #762

Merged
justinchuby merged 92 commits into
mainfrom
squad/ep-plugin-parity-cuda
Aug 12, 2026
Merged

justinchuby merged 92 commits into
mainfrom
squad/ep-plugin-parity-cuda

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 11, 2026 •

Copy link
Copy Markdown
Owner

EP Plugin Export — Rust Execution Providers as ORT Plugin EPs

What This PR Does

Exports our Rust execution providers through the ORT plugin-EP C ABI so upstream ONNX Runtime can load them via RegisterExecutionProviderLibrary. The full ORT lifecycle — RegisterExecutionProviderLibrary → GetEpDevices → SessionOptionsAppendExecutionProvider_V2 → CreateSession → Run — succeeds with numerically correct outputs for the CPU EP.

New crates

Crate Role
onnx-runtime-ep-plugin Shared adapter: projects any ExecutionProvider through ORT's C ABI
onnx-runtime-ep-cpu-plugin cdylib shim exporting CpuExecutionProvider
onnx-runtime-ep-cuda-plugin cdylib shim exporting CudaExecutionProvider (compiles; unvalidated on hardware)
onnx-runtime-ep-nxrt-abi Native nxrt dynamic ABI: #[repr(C)] types, vtable definitions, export_nxrt_ep_factories! macro
onnx-runtime-ep-nxrt-host Host loader for nxrt plugins (dlopen → negotiate → factories)
onnx-runtime-ep-nxrt-testplugin Fixture cdylib; genuine workspace member

Review History — Six Rounds, Three Late-Breaking Blockers

This PR went through six independent adversarial review rounds. The first two established the basic adapter and conformance suite. Rounds 3–6 found and fixed three blocking defects that would have caused silent wrong results or undetectable fallback:

Blocker 1 — Optional slot fidelity (found round 3, fixed 6ce94f033–fbd565160)

Omitted optional outputs were compacted with filter_map, so canonical SkipLayerNormalization(output,"","",sum) silently wrote mean into sum. Absent optional inputs aliased to input 0. Both now preserve positional slots; absence is explicit (NodeInputSource::Absent, TensorView::absent()).

Blocker 2 — LayerNorm axis resolution (found round 3, fixed 168e40c3e)

The raw axis was pre-resolved against a rank truncated by filter_map(as_static) which drops symbolic dims — [B,S,H] became [H]. The raw axis is now preserved and resolved at runtime against actual input rank.

Blocker 3 — Absent-output representation (found round 5, fixed 38625fb38)

An earlier fix used a name-based sentinel (__absent_output_*) — forgeable from model content. Replaced with an out-of-band HashSet<ValueId>; ValueId is an arena index the reader assigns, so model content cannot forge membership.

Additional fixes since round 2

  • Rank preservation. Both truncating call sites now carry Vec<Option<usize>>, so length always equals rank; build_conv fails closed when a required dim is unknown.
  • Dtype guessing removed. Three unwrap_or(DataType::Float32) fallbacks deleted — short dtype vectors now fail closed.
  • ABI correctness (94bbbe545): NxrtStatus code carried as u32 with a checked conversion (unknown enum discriminant from a foreign .so was UB); struct_size honoured before vtable access; CUDA diagnostic's status now survives to ORT.

Test Story — The Big Change

Previously a passing test could not distinguish "our EP ran it" from "ORT's built-in CPU EP ran it" — and in one case a shipped fix turned out to be dead code for exactly that reason.

How assignment is now proven

  1. session.disable_cpu_ep_fallback=1 — forbids ORT's silent fallback to its own CPU EP. If our EP cannot handle an op, the session fails instead of silently succeeding through a different code path.

  2. Session_GetEpGraphAssignmentInfo (ORT ≥ 1.24) — directly queries which EP was assigned which ops. 14 tests now assert specific ops are owned by cpu_ep:

    • Add, Mul, MatMul (2D and batched-ND), Cast, Where
    • LayerNormalization, RMSNormalization
    • f16 Add, bf16 Add
  3. Verified falsifiable: asserting an unclaimed op yields Expected op 'Relu' assigned to cpu_ep, but assignment was: [("cpu_ep", "Add")].


Verified Test Numbers

EP crates: 269 passed, 0 failed across ep-plugin, cpu-plugin, cuda-plugin, nxrt-abi, nxrt-host.

All five EP crates are clippy-clean under -D warnings and cargo fmt --check clean.

Workspace (cargo test --workspace --no-fail-fast): 4580 passed / 20 failed / 436 ignored. The 20 failures are pre-existing on base 675b697bc — across onnx-genai-server, onnx-runtime-session, onnx-genai-engine (base: 910 passed / 21 failed; head: 911 / 20). No EP crate is among the failures. Note: plain cargo test --workspace fail-fasts and silently truncates totals.

~30 dead-code clippy warnings in crates/onnx-genai-bench are pre-existing at base and untouched by this PR.


What Is NOT Proven — CUDA Hardware

The CUDA EP compiles and type-checks; it is unvalidated on hardware. There is no GPU and no CUDA toolkit on the CI host, and no self-hosted GPU runner exists.


Architecture

ORT Plugin Adapter (onnx-runtime-ep-plugin)

  • export_ep_factories! macro generates CreateEpFactories/ReleaseEpFactory with full catch_unwind panic containment
  • AtomicPtr for ORT API storage (no static mut)
  • Shape inference: 22 rules, fail-closed Declined for unresolvable shapes
  • Compute: CreateState / Compute / ReleaseState with topological kernel execution
  • Device surfaces: DeviceAllocator, DeviceSyncStream, DeviceDataTransferFull — all projecting through ORT's vtable ABI
  • Optional slot handling preserves positional semantics; absent inputs/outputs are explicit

CUDA Ownership Model (B1 fix)

Components hold Arc<Mutex<Box<dyn ExecutionProvider + Send>>> clones via EpRef::Shared. Each use locks briefly for the EP method call duration. No raw pointers from MutexGuards.

Native nxrt ABI (onnx-runtime-ep-nxrt-abi)

  • NxrtNegotiate / NxrtCreateEpFactories — major/minor version negotiation
  • struct_size forward-compatibility on all vtables
  • Fail-closed on unknown capability bits
  • Pure-value NxrtStatus (inline [u8; 256] buffer — no cross-module free)
  • export_nxrt_ep_factories! macro with panic containment

Security

Holden's audit verdict: 🟡 YELLOW — May ship. All CRITICAL/HIGH findings resolved. Two LOW advisories recorded for post-merge. See docs/EP_PLUGIN_EXPORT_SECURITY_AUDIT.md.


Files Changed

55 commits; primary changes in crates/onnx-runtime-ep-plugin/, crates/onnx-runtime-ep-cpu-plugin/, crates/onnx-runtime-ep-cuda-plugin/, crates/onnx-runtime-ep-nxrt-abi/, crates/onnx-runtime-ep-nxrt-host/, crates/onnx-runtime-ep-nxrt-testplugin/, and docs/.

justinchuby and others added 14 commits August 10, 2026 21:12
… session

Baseline commit of the outbound ORT plugin-EP export workstream recovered
from an interrupted session. Contains the shared adapter crate skeleton,
the CPU plugin shim, inventory/ABI docs, and decision inbox drops.

Known-broken at this commit (fixed in follow-ups):
- crates/onnx-runtime-ep-plugin does not compile (OrtEp field mismatch)
- 4 CRITICAL/HIGH FFI security findings outstanding
- compute path returns NOT_IMPLEMENTED

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

Makes the outbound ORT plugin-EP export adapter compile, be panic-safe, and
actually execute kernels instead of returning ORT_NOT_IMPLEMENTED.

FFI hardening (Nabil):
- Fix OrtEp initializer: ValidateCompiledModelCompatibilityInfo belongs to
  OrtEpFactory, not OrtEp (which has GetCompiledModelCompatibilityInfo).
- catch_unwind on all extern "C" callbacks (no UB across the C ABI).
- HOST_ORT_API as AtomicPtr with Release/Acquire (was `static mut`).
- Null-check incoming ORT pointers; return ORT_INVALID_ARGUMENT.
- Remove unsound `unsafe impl Send/Sync` on OutboundGraphReader.
- check() now reports the real ORT error string via GetErrorMessage.

Compute path (Deckard):
- Real CreateState/Compute/ReleaseState lifecycle.
- OrtKernelContext <-> TensorView bridge with dtype/shape marshaling.
- Output shapes via ElementwiseBroadcast / SameAsInput inference; fails
  closed with an actionable message when a shape cannot be resolved.

Conformance harness (Pris):
- L1 exported-symbol assertions (nm -D, readelf --dyn-syms).
- L2 dlopen/dlsym factory lifecycle + fail-closed bad-API-version.
- L3 real ORT 1.27.0 vtable drive (3 tests ignored pending device fix).
- New unsupported-op fixture (nonzero_1x4).

Docs (Roy): corrected stale "v1 implemented" claims, pinned the exact ORT
compatibility boundary, resolved the onnxruntime-mlx scope question.

Validation: cargo build/clippy clean; 23 adapter unit tests pass;
8 cpu-plugin conformance tests pass, 3 ignored on a known blocker
(GetSupportedDevices returns 0 devices -> ORT segfaults at registration).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Upstream ONNX Runtime 1.27.0 now genuinely loads, registers, and executes
our Rust CPU execution provider as a plugin EP.

Device enumeration (Nabil):
- GetSupportedDevices filters ORT's discovered hardware for CPU devices,
  creates OrtEpDevice via OrtEpApi::CreateEpDevice, and registers CPU
  memory info via EpDevice_AddAllocatorInfo. Bounded by max_ep_devices.
- Previously returned 0 devices, which made ORT segfault inside
  RegisterExecutionProviderLibrary.

Verified end-to-end against real ORT 1.27.0:
  CreateEnv -> RegisterExecutionProviderLibrary -> GetEpDevices (finds
  'cpu_ep') -> SessionOptionsAppendExecutionProvider_V2 -> CreateSession
  -> Run -> outputs [6.0, 8.0, 10.0, 12.0] (correct).

Shape inference (Deckard):
- 22 inference rules: MatMul (1-D/2-D/batched-ND broadcast), Gemm, Concat,
  Transpose, Gather/GatherND/GatherBlockQuantized, Reshape, Slice, Squeeze/
  Unsqueeze, Shape, reductions, Conv, LayerNorm family, MHA/GQA/RoPE.
- Data-dependent shapes (Reshape, Slice) read their control tensors at
  Compute time, honoring -1 inference and allowzero.
- Unmodeled ops return Declined -> hard error, never a guessed shape.
- Multi-node fused subgraphs thread intermediates in topological order.

Adapter unit tests: 66 passed (up from 23).

Known follow-ups (tracked, not shipped): ep.rs still calls for_op rather
than for_node, so attribute-aware rules are not yet wired into the live
path; Holden's re-audit returned RED on three panic-safety//dim-validation
findings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gs, broaden conformance

Closes the integration gap that left the richer shape-inference rules dead,
resolves Holden's ship-blocking audit findings, and turns a single smoke
test into a real upstream-ORT conformance suite.

Capability + shape-inference integration (Nabil):
- ep.rs now calls ShapeInference::for_node (was for_op), so the 22
  attribute-aware rules are live instead of always returning Declined.
- GetCapability filters out nodes whose shape cannot be inferred, so we
  stop over-claiming ops we cannot execute (NonZero now falls back to ORT).
- SubgraphRouting wired for multi-node fused subgraphs.
- graph_reader reads node attributes and int64 initializers, copying them
  into owned Rust data during Compile (no OrtGraph/OrtNode pointer caching,
  per ORT's lifetime contract). Opset-13 Unsqueeze/Squeeze axes-as-input
  is resolved from initializers.

Security remediation of Holden's RED verdict:
- N2 (HIGH, Leon): dims from ORT are validated - negative dims rejected by
  index and value instead of wrapping -1i64 to usize::MAX; element-count
  and byte-length use checked_mul; zero dims accepted as legal.
- N3 (MEDIUM, Isidore): macro-generated CreateEpFactories/ReleaseEpFactory
  are panic-guarded. ReleaseEpFactory's return type corrected to void per
  the ORT ABI. Exported symbols unchanged.
- N1 (CRITICAL): compute_execute panic guard verified present and covered
  by a regression test.

Reviewer Rejection Protocol was enforced: Holden nominated the original
authors as fixers, so the coordinator reassigned N1/N2 to Leon and N3 to
Isidore under strict lockout.

Conformance suite (Pris) - all against real ORT 1.27.0:
- broadcast, dynamic/symbolic dim, int32, multi-node chain (A+B)*C+D,
  MatMul 2-D, and a mixed graph that forces ORT to partition between our
  EP and its own CPU EP. Each asserts independently computed values.

Validation: 82 adapter unit tests pass; cpu-plugin suite 16 passed,
0 failed, 3 ignored on a known device-descriptor lifetime bug.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes a use-after-free that corrupted our advertised EP device and left ORT
unable to find an allocator at Run time. BUG 1 and BUG 2 shared one root cause.

1. Use-after-free: OrtMemoryInfo was released immediately after
   EpDevice_AddAllocatorInfo, but ORT stores the raw pointer rather than
   copying it. Across repeated register/Run/unregister cycles the freed
   memory was reused, so the device descriptor decayed into garbage
   (DeviceType:-112 MemoryType:-85). ORT now owns the memory info via the
   EpDevice; we only release it on the failure path.

2. Wrong API: CreateCpuMemoryInfo is the pre-1.22 entry point and does not
   populate the OrtMemoryInfoDeviceType/OrtDeviceMemoryType fields that the
   EP device system reads, yielding DeviceType:64 MemoryType:28 and an
   allocator lookup miss at Run. Replaced with CreateMemoryInfo_V2 using
   explicit OrtMemoryInfoDeviceType_CPU / OrtDeviceMemoryType_DEFAULT.

Reviewer Rejection Protocol enforced: Pris rejected the device-enumeration
work, so the fix was reassigned from Nabil (original author, locked out)
to Deckard.

Validation against real ORT 1.27.0:
- ort_loads_our_ep_and_runs_model: all stages pass (register -> GetEpDevices
  -> CreateSession -> Run -> correct outputs).
- conformance_multiple_run_calls: passes; repeated cycles no longer corrupt
  the descriptor.
- No regressions in the previously passing conformance tests.

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

Leon found that validate_dims() was never actually called: read_inputs still
cast ORT dims with `d as usize`, so every ORT-supplied dimension bypassed
validation and a -1 dynamic sentinel would still have wrapped to usize::MAX.
The helper is now wired into read_inputs, closing the path for real.

Pris (conformance):
- conformance_two_sessions was a test bug, not an implementation bug:
  EpDevice_EpName returns OrtEpFactory::GetName ("cpu_ep"), not the key
  passed to RegisterExecutionProviderLibrary. Assertion corrected.
- stress_register_run_unregister_cycles: 25 full register/Run/unregister
  cycles, 4x the former corruption threshold, locking in the OrtMemoryInfo
  use-after-free fix so it cannot silently regress.
- conformance_matmul_batched_nd: [2,3,4]x[2,4,2] -> [2,3,2] through real ORT
  dispatch, with hand-computed expected values.
- All three previously ignored tests now run.

Holden's final re-audit: YELLOW (may ship). N1/N2/N3 all RESOLVED with
evidence; the factory.rs ownership fix confirmed correct (ORT owns the
memory info on success, we release only on the failure path, so the paths
are mutually exclusive and double-free is impossible). Two LOW advisories
recorded as post-merge follow-ups.

Validation: 82 adapter unit tests pass; cpu-plugin suite 21 passed,
0 failed, 0 ignored, stable across repeated runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Isidore (lib.rs): drop redundant mut on out_num; replace a type-ascribed
panic! with a bare panic! statement so it is no longer a diverging
sub-expression. Panic guards, null checks, and the fail-closed ORT API
version check are untouched.

Deckard (ep.rs): replace the 1usize as *mut OrtEp test sentinel with
std::ptr::dangling_mut, the idiomatic form for an intentional non-null,
never-dereferenced placeholder. Cosmetic lint, confined to test code;
the pointer never crossed the ABI.

cargo clippy --all-targets -D warnings is now clean and rustfmt is clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Roy: rewrote docs/EP_PLUGIN_EXPORT_PR.md against verified reality. The
earlier draft was written mid-flight and understated the result - it still
described conformance_two_sessions as ignored, the OrtEpDevice corruption
as open, a mutex-poison cascade that no longer occurs, and Holden's verdict
as pending. All corrected; every number re-observed before being quoted.

Also documented honestly: end-to-end f16/bf16 remains unproven because the
EP implements no GetKernelRegistry, so type-constraint metadata is never
registered with ORT (our CPU kernels do implement f16/bf16; no fake test
was written). CUDA EP is blocked on both halves - no toolkit/GPU on this
host, and genuine adapter design work remains for allocators, streams, and
device pointers crossing the ABI.

Scribe: merged 34 decision drops into .squad/decisions.md, wrote 16
orchestration-log entries and the session log, and compacted Deckard's
history.

Two ABI contracts recorded prominently, since both caused real bugs here:
- OrtMemoryInfo passed to EpDevice_AddAllocatorInfo must outlive the
  EpDevice; ORT stores the pointer and does not copy it.
- OrtGraph*/OrtNode* must not be cached beyond Compile; copy what you need
  into owned Rust data.

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

Standing directive from @justinchuby: push progress to origin frequently,
open draft PR(s) early rather than waiting for completion, and convert to
ready for review once implementation and validation are complete.

Captured as a decision drop because this session demonstrated the failure
mode it prevents: a lost coordinator session left the entire EP plugin
export workstream uncommitted, and the recovered branch then accumulated
local-only commits that were invisible from GitHub because no push
credentials exist on this host.

Also adds the corollary that a blocked push must be reported immediately
and loudly rather than deferred to a final summary, since it silently
breaks this contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, f16/bf16 marshaling

Milestone 2, stacked on squad/ep-plugin-export. Extends the outbound plugin
adapter beyond the host-only CPU path.

Device surfaces (Nabil) - crates/onnx-runtime-ep-plugin/src/device.rs:
- device_type_to_ort_hardware maps our DeviceType to OrtHardwareDeviceType
  (Cuda/Rocm -> GPU, Qnn/CoreML/MLX -> NPU, Cpu -> CPU).
- DeviceSupport declares which hardware types an EP serves, plus allocator
  name, vendor id, and stream-awareness.
- DeviceAllocator projects ExecutionProvider allocate/deallocate through the
  OrtAllocator vtable; DeviceSyncStream projects sync() through
  OrtSyncStreamImpl. Both panic-guarded and null-checked.
- Fail-closed validators for device mismatch, allocator type mismatch, and
  stream creation on a non-stream-aware EP.
- ~26 unit tests driven by MockGpuEp/MockCpuEp doubles, so the device paths
  are exercised on a machine with no GPU.

New crate crates/onnx-runtime-ep-cuda-plugin: feature-gated behind `cuda`,
workspace member but not a default member, so `cargo check --workspace`
still succeeds without a CUDA toolkit.

Kernel registry (Deckard): GetKernelRegistry callback, OrtKernelRegistryHolder,
build_ort_kernel_registry, and create_ep_factories_with_registry. Confirmed
from the ORT header that GetKernelRegistry coexists with Compile rather than
replacing it, so the working compile-based path is preserved.

NEW-2 resolved (Deckard): ORT's ownership contract for OrtNodeComputeInfo on
a mid-loop Compile failure is unspecified in the header, so ep_compile_inner
now frees and nulls already-written entries. That is safe under both readings
- if ORT calls ReleaseNodeComputeInfos it sees nulls, and if it does not, we
have already freed.

NEW-1 resolved (Leon): compute_release_state is now panic-guarded; it was the
last unguarded extern "C" callback. f16/bf16 dtype mapping verified against
the generated bindings (FLOAT16=10, BFLOAT16=16, 2 bytes each) and covered by
the existing checked_mul overflow guards.

Validation: 120 adapter unit tests pass (was 82); cpu-plugin conformance
still 21 passed, 0 failed, 0 ignored.

Known-broken at this commit, fixed in follow-ups: the new trait/C-ABI parity
test does not compile (DataType::Float should be Float32; KernelMatch lacks
Debug), and device.rs trips clippy's forget_non_drop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ts, prove trait parity

Trait <-> C-ABI parity (Pris): 9 integration tests in
crates/onnx-runtime-ep-plugin/tests/trait_cabi_parity.rs now compile and
pass, giving §524's Rust-trait half real evidence rather than assertion.

The committed parity rule holds, but the Declined set is smaller than
assumed: ShapeInference::for_node can infer Squeeze (empty axes), ReduceMean,
and Conv from input shapes, so those are NOT declined. The confirmed real
declined case is opset>=13 Unsqueeze with axes as a data-dependent input.
Tests were corrected to use that real example rather than the assumed one.

Also fixed a latent test-harness bug: the graph builder never set
opset_imports, so effective_opset() returned 0 and supports_op declined
everything - which would have made the parity assertions vacuous.

Device generalization (Nabil): factory.rs now filters via
DeviceSupport::serves(hw_type) instead of a hard-coded CPU constant.
IsStreamAware reads support.stream_aware; CreateAllocator builds a
DeviceAllocator for device EPs and defers to ORT's default for CPU;
CreateSyncStream builds a DeviceSyncStream for stream-aware EPs and fails
closed otherwise. The OrtMemoryInfo ownership fix is preserved and still
guarded by the 25-cycle stress test.

The mem::forget flagged by clippy in device.rs was dead code, not an
ownership bug: DeviceBuffer has no Drop, and the allocation lives in the EP's
allocator until device_free calls ep.deallocate().

Type constraints (Deckard): build_cpu_registry_with_descriptors() in
crates/onnx-runtime-ep-cpu/src/kernels/mod.rs derives supported dtypes by
recording every register() call through the same path that builds the live
OpRegistry, then classifying by the kernel's actual dispatch macro. This
projects the Rust registry rather than hand-maintaining a list that could
drift. Ops whose dtype support cannot be determined advertise f32 only -
fail closed, so we never claim a dtype we cannot honor.

Validation: 127 adapter unit tests, 9 parity tests, 21 cpu-plugin
conformance tests. f16/bf16 end-to-end remains honestly ignored pending
verification that ORT consults the kernel registry for compile-based EPs.

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

f16 and bf16 models now execute through our EP inside upstream ONNX Runtime,
verified with exact bit-pattern assertions:
  conformance_add_float16  -> [0x4000, 0x4400, 0x4600, 0x4800]
  conformance_add_bfloat16 -> [0x4000, 0x4080, 0x40C0, 0x4100]
Both were previously ignored. This is our EP claiming and executing the
nodes, not ORT falling back - the distinction was checked explicitly.

Dtype-aware claiming (Deckard): ep_get_capability_inner now runs
node_passes_dtype_filter() on every candidate node, rejecting any node whose
input/output element types we do not support, whose dtype is Undefined, or
whose op has no registry entry. Previously we claimed nodes without checking
element type at all - the same over-claiming class already fixed for NonZero,
where claiming denies ORT the chance to fall back.

The claim predicate and the advertised type constraints consume the same
Vec<KernelRegistryEntry> from one call to build_kernel_registry_entries(), so
they cannot drift apart. Drift here would mean advertising a dtype we cannot
honor, i.e. silently wrong results inside ORT.

Test hardening (Pris): the unused `ep` binding flagged by clippy was hiding a
weak assertion - error_parity_declined_shape_inference_is_cabi_only checked
shape inference twice and never verified the trait/C-ABI divergence it
documented. Rewritten to prove all three steps: the trait claims Unsqueeze,
for_node returns Declined, and the C-ABI filter yields empty.

Ownership cleanup (Nabil): the two remaining mem::forget sites were in
#[cfg(test)] mock EPs and were no-ops - DeviceBuffer has no Drop, so the
manual dealloc was already correct. Replaced with a discard binding. Holden
independently confirmed this assessment.

Security (Holden, milestone-2 audit): YELLOW - may ship. NEW-1 and NEW-2 both
RESOLVED. One MEDIUM found: factory_create_sync_stream leaks the EP because
stream_release drops only the DeviceSyncStream and never reclaims the
Box::into_raw'd ep, unlike factory_release_allocator which does. Tracked as
M2-1 and reassigned away from the author under strict lockout.

Validation: 132 adapter unit tests, 9 trait/C-ABI parity tests, 23 cpu-plugin
conformance tests (6 + 17), 0 ignored. cargo check --workspace clean.

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

M2-1 (MEDIUM, Holden): factory_create_sync_stream handed out an EP via
Box::into_raw, but stream_release only dropped the DeviceSyncStream and never
reclaimed it, leaking a full ExecutionProvider per sync-stream creation inside
a long-lived host process. stream_release now reclaims the EP with
Box::from_raw behind a null guard.

Double-free was ruled out from the header rather than assumed:
onnxruntime_ep_c_api.h:207-216 specifies ORT calls OrtSyncStreamImpl::Release
exactly once per stream, and no other path reclaims that EP (the allocator
path owns a separate instance). New regression test
stream_release_reclaims_owned_ep_no_leak uses an EP with a Drop counter and
asserts the drop actually runs, so the leak cannot silently return.

M2-2 (LOW): the DeviceAllocator::memory_info comment claimed "Owned; freed on
drop" when the pointer is borrowed from ORT and must not be freed. Corrected -
an inaccurate ownership comment is a real hazard here, since a lifetime
misjudgement in exactly this area caused milestone 1's use-after-free.

Reviewer Rejection Protocol enforced again: Holden found both in Nabil's file
and explicitly recommended reassignment, so Leon owned the fix under strict
lockout.

Also: removed two needless borrows in ep.rs test helpers (Deckard, who
confirmed the surrounding dtype-filter assertions are genuinely meaningful
rather than vacuous), and updated the docs for both milestones.

Final validation on this branch:
- cargo clippy -p onnx-runtime-ep-plugin --all-targets -D warnings: clean
- cargo test -p onnx-runtime-ep-plugin: 133 lib + 9 parity, 0 failed
- cargo test -p onnx-runtime-ep-cpu-plugin: 23 (6 + 17), 0 failed, 0 ignored
- cargo check --workspace: clean
- rustfmt: clean

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

Third and final accuracy pass. The previous draft still listed the clippy
regression, M2-1 (EP leak in stream_release) and M2-2 (misleading ownership
comment) as open pre-merge blockers; all three were resolved in 3ab0ded.

Corrections: removed the pre-merge blocker paragraph, moved M2-1/M2-2 into
resolved findings with Leon as fixer and the regression test as evidence,
refreshed counts (133 lib + 9 parity = 142; 23 cpu-plugin, 0 ignored), and
marked the engineer status table complete.

Holden's milestone-2 verdict is retained as YELLOW with an explicit note that
his findings are resolved but a re-verification pass was not separately run -
deliberately not upgraded to GREEN on his behalf.

Kept honest: the native nxrt dynamic ABI half of the extension contract is
still unimplemented, and CUDA remains blocked on both missing hardware and
real device-pointer/data-transfer work.

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

Copy link
Copy Markdown
Owner Author

PR structure note (supersedes the two-stacked-PR recommendation in the description)

Per @justinchuby's direction, this is a single draft PR tracking the whole EP-compatibility milestone, rather than the two stacked PRs recommended in the body above. Subsequent milestone commits will be pushed to this same draft.

It will stay in draft until the broader EP compatibility milestone is complete and validated.

The base branch squad/ep-plugin-export (M1, 9 commits) is also pushed to origin and remains independently green, so it can still be split out into its own PR later if a smaller review surface is preferred.

Current state at 10ae8b493

  • cargo clippy -p onnx-runtime-ep-plugin --all-targets -- -D warnings — clean
  • cargo test -p onnx-runtime-ep-plugin — 133 lib + 9 trait/C-ABI parity, 0 failed
  • cargo test -p onnx-runtime-ep-cpu-plugin — 23 conformance vs real ORT 1.27.0, 0 failed, 0 ignored
  • cargo check --workspace — clean

Known incomplete (why this is still a draft)

  • Native nxrt dynamic ABI — the second half of the extension contract is not implemented.
  • CUDA EP — blocked on both missing toolkit/GPU on the dev host and real device-pointer/data-transfer work. Device/allocator/stream surfaces and the feature-gated shim crate exist and are mock-tested, which is not the same as a working CUDA EP.

Push blocker cleared - gh is authenticated, so both squad/ep-plugin-export
(M1, 9 commits) and squad/ep-plugin-parity-cuda (M2, 5 commits) are now on
origin and no longer local-only.

Delivered as a single draft PR (#762, base main) tracking the whole EP
compatibility milestone, per @justinchuby's direction. This supersedes the
two-stacked-PR recommendation in docs/EP_PLUGIN_EXPORT_PR.md; a PR comment
records the deviation so reviewers are not misled by the description.

Stays in draft until the broader milestone is complete: the native nxrt
dynamic ABI is unimplemented, and CUDA is blocked on both hardware and real
device-pointer/data-transfer work.

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

codecov Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 47.17160% with 2232 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.21%. Comparing base (d7db6f2) to head (49d3988).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
crates/onnx-runtime-ep-plugin/src/factory.rs 0.61% 647 Missing ⚠️
crates/onnx-runtime-ep-plugin/src/ep.rs 34.48% 635 Missing and 7 partials ⚠️
crates/onnx-runtime-ep-plugin/src/device.rs 67.07% 237 Missing and 4 partials ⚠️
.../onnx-runtime-ep-nxrt-host/src/provider_adapter.rs 0.00% 188 Missing ⚠️
crates/onnx-runtime-ep-nxrt-host/src/loader.rs 34.69% 159 Missing and 1 partial ⚠️
crates/onnx-runtime-ep-nxrt-abi/src/vtable.rs 43.55% 89 Missing and 3 partials ⚠️
crates/onnx-runtime-ep-nxrt-testplugin/src/lib.rs 0.00% 83 Missing ⚠️
crates/onnx-runtime-ep-cpu-plugin/src/lib.rs 0.00% 60 Missing ⚠️
crates/onnx-runtime-ep-cpu/src/kernels/mod.rs 90.64% 23 Missing and 19 partials ⚠️
crates/onnx-runtime-ep-nxrt-abi/src/testing.rs 86.87% 18 Missing and 3 partials ⚠️
... and 6 more
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main     #762       +/-   ##
===========================================
- Coverage   83.55%   78.21%    -5.34%     
===========================================
  Files          12      350      +338     
  Lines        5192   141311   +136119     
  Branches     5192   141311   +136119     
===========================================
+ Hits         4338   110522   +106184     
- Misses        677    26386    +25709     
- Partials      177     4403     +4226     
Flag Coverage Δ
cli-ort-linux 83.50% <ø> (ø)
cli-ort-windows 82.99% <ø> (ø)
mlas 81.33% <ø> (?)
offline 77.95% <47.17%> (?)

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

Files with missing lines Coverage Δ
crates/onnx-runtime-ep-api/src/kernel.rs 83.87% <ø> (ø)
crates/onnx-runtime-ep-api/src/provider.rs 64.76% <ø> (ø)
crates/onnx-runtime-ep-nxrt-abi/src/version.rs 100.00% <100.00%> (ø)
crates/onnx-runtime-ep-plugin/src/compute.rs 73.30% <ø> (ø)
crates/onnx-runtime-ep-plugin/src/graph_reader.rs 0.00% <ø> (ø)
crates/onnx-runtime-ep-plugin/src/kernel_ctx.rs 87.46% <ø> (ø)
crates/onnx-runtime-ep-plugin/src/lib.rs 100.00% <ø> (ø)
crates/onnx-runtime-ep-plugin/src/status.rs 90.74% <ø> (ø)
crates/onnx-runtime-ep-plugin/src/transfer.rs 62.38% <ø> (ø)
...s/onnx-runtime-ep-cpu/src/kernels/contrib_fused.rs 85.51% <66.66%> (ø)
... and 15 more

... and 313 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

github-actions Bot commented Aug 11, 2026 •

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
🔴 gather/medium_f16_threads=1-internal/32768 2.92 µs 4.12 µs +41.3%
🔴 matmul/large_generic_bf16_threads=1/32x1024x1024 2.41 ms 3.20 ms +32.8%
⚠️ matmul/large_generic_f16_threads=8/32x1024x1024 122.55 µs 147.39 µs +20.3%
⚠️ qwen3_sampling_processors/top_k_top_p_fast 702.25 µs 836.31 µs +19.1%
✅ sampling_latency/top_p_per_token 476.62 µs 540.09 µs +13.3%
✅ matmul/large_generic_f16_threads=1/32x1024x1024 94.44 µs 106.15 µs +12.4%
✅ add/small_bf16_threads=1-internal/1024 16.76 µs 18.74 µs +11.9%
✅ sampling_latency/min_p_per_token 240.10 µs 263.44 µs +9.7%
✅ kv_cache/alloc_dealloc_pages 45.73 µs 48.23 µs +5.5%
✅ matmul/small_generic_f16_threads=1/1x256x256 40.13 µs 41.85 µs +4.3%
✅ sampling_latency/top_k_per_token 65.56 µs 67.46 µs +2.9%
✅ gather/medium_f32_threads=1-internal/32768 7.02 µs 7.12 µs +1.4%
✅ sampling_latency/greedy_per_token 3.78 µs 3.83 µs +1.3%
✅ qwen3_sampling_processors/top_p_fast_after_top_k 571.02 µs 572.49 µs +0.3%
✅ matmul/large_generic_f32_threads=1/32x1024x1024 10.85 ms 10.78 ms -0.6%
✅ matmul/small_generic_bf16_threads=8/1x256x256 43.02 µs 42.71 µs -0.7%
✅ reduce_mean/small_f32_threads=1-internal/4096 18.66 µs 18.47 µs -1.0%
✅ add/medium_bf16_threads=1-internal/262144 3.15 ms 3.12 ms -1.1%
✅ reduce_mean/large_f32_threads=1-internal/262144 1.15 ms 1.12 ms -3.1%
✅ matmul/medium_generic_f32_threads=1/32x512x512 2.83 ms 2.73 ms -3.6%
✅ gather/medium_bf16_threads=1-internal/32768 2.99 µs 2.87 µs -3.8%
✅ tokenization/decode_tokens_per_second 7.78 ms 7.47 ms -4.0%
✅ logit_processing/seven_processor_chain_per_step 406.54 µs 389.49 µs -4.2%
✅ matmul/large_generic_bf16_threads=8/32x1024x1024 2.34 ms 2.24 ms -4.6%
✅ qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline 4.39 ms 4.14 ms -5.5%
✅ block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 532.67 µs 501.11 µs -5.9%
✅ matmul/small_generic_f16_threads=8/1x256x256 40.95 µs 38.37 µs -6.3%
✅ matmul/medium_generic_bf16_threads=1/32x512x512 633.09 µs 591.50 µs -6.6%
✅ qwen3_sampling_processors/top_k_top_p_full_sort_baseline 6.45 ms 6.02 ms -6.6%
✅ qwen3_sampling_processors/top_k_full_sort_baseline 2.56 ms 2.39 ms -6.7%
✅ reduce_mean/medium_f32_threads=1-internal/65536 308.66 µs 283.43 µs -8.2%
✅ matmul/small_generic_bf16_threads=1/1x256x256 42.63 µs 38.71 µs -9.2%
✅ add/large_f16_threads=1-internal/4194304 53.26 ms 47.75 ms -10.4%
✅ add/medium_f16_threads=1-internal/262144 3.36 ms 3.00 ms -10.9%
✅ matmul/medium_generic_f32_threads=8/32x512x512 1.67 ms 1.49 ms -11.1%
✅ block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 93.44 µs 82.40 µs -11.8%
✅ add/small_f16_threads=1-internal/1024 18.14 µs 15.66 µs -13.6%
✅ add/medium_f32_threads=1-internal/262144 3.32 ms 2.86 ms -13.8%
✅ gather/small_bf16_threads=1-internal/4096 708.2 ns 608.7 ns -14.0%
✅ matmul/medium_generic_f16_threads=8/32x512x512 50.56 µs 43.40 µs -14.2%
🟢 matmul/medium_generic_f16_threads=1/32x512x512 45.86 µs 38.72 µs -15.6%
🟢 gather/large_f16_threads=1-internal/131072 20.59 µs 17.25 µs -16.2%
🟢 block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 131.22 µs 109.17 µs -16.8%
🟢 add/large_bf16_threads=1-internal/4194304 62.37 ms 51.70 ms -17.1%
🟢 grammar_masking/llguidance_compute_mask/32 131.25 µs 108.21 µs -17.6%
🟢 add/large_f32_threads=1-internal/4194304 58.07 ms 47.00 ms -19.1%
🟢 gather/large_f32_threads=1-internal/131072 46.65 µs 35.98 µs -22.9%
🟢 matmul/medium_generic_bf16_threads=8/32x512x512 651.49 µs 479.57 µs -26.4%
🟢 block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 109.87 µs 80.79 µs -26.5%
🟢 matmul/small_generic_f32_threads=1/1x256x256 62.79 µs 45.89 µs -26.9%
🟢 gather/small_f16_threads=1-internal/4096 718.4 ns 513.5 ns -28.5%
🟢 matmul/large_generic_f32_threads=8/32x1024x1024 6.48 ms 4.48 ms -30.8%
🟢 gather/small_f32_threads=1-internal/4096 1.04 µs 712.0 ns -31.5%
🟢 qwen3_sampling_processors/top_k_partial_selection 247.01 µs 161.34 µs -34.7%
🟢 block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 1.26 ms 816.54 µs -35.2%
🟢 tokenization/encode_tokens_per_second 666.71 µs 424.91 µs -36.3%
🟢 matmul/small_generic_f32_threads=8/1x256x256 76.86 µs 48.75 µs -36.6%
🟢 gather/large_bf16_threads=1-internal/131072 24.01 µs 13.07 µs -45.6%
🟢 add/small_f32_threads=1-internal/1024 468.2 ns 243.7 ns -47.9%

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: { 9.45 5.49 7.41 }
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 and others added 9 commits August 11, 2026 00:37
…ring

Advances the EP-compatibility milestone on three fronts. The nxrt half is
committed as WIP because it has a real integration gap, documented below.

Native nxrt ABI (Nabil) - crates/onnx-runtime-ep-nxrt-abi:
- Exports NxrtNegotiate + NxrtCreateEpFactories with #[repr(C)] vtables
  projecting ExecutionProvider.
- Version negotiation: major mismatch is a hard reject; minor is min(plugin,
  host) with plugin-newer-than-host rejected; every struct carries struct_size
  for forward compatibility; unknown capability bits are rejected via
  NXRT_CAP_KNOWN_MASK (fail closed).
- Ownership made explicit rather than implied: host owns factory/EP/kernel/
  allocator and must call vtable.release; borrowed pointers are valid only
  within the callback frame. This is the contract whose absence caused the
  OrtMemoryInfo use-after-free on the ORT side.
- Panic containment on every extern "C" entry. 19 unit tests.

nxrt host loader (Isidore) - crates/onnx-runtime-ep-nxrt-host:
- dlopen + eager symbol resolution, version check, adapts a loaded plugin back
  to dyn ExecutionProvider. Library lifetime enforced structurally via
  Arc<Library> held by every derived object, so an EP cannot outlive its
  library. Negative paths return actionable errors.

Device transfer (Leon) - crates/onnx-runtime-ep-plugin/src/transfer.rs:
- Projects ExecutionProvider copy/copy_async/sync through ORT's
  OrtDataTransferImpl. Copy-direction matrix implemented with honest CanCopy:
  host<->device and same-device supported, cross-device and host->host
  declined rather than accepted-then-failed.
- Device pointers are tagged non-host-accessible so the adapter never
  dereferences them on the host.

CUDA wiring (Deckard) - the cuda-plugin now constructs a real
CudaExecutionProvider behind the `cuda` feature and advertises GPU device
support, instead of panicking. `cargo check -p onnx-runtime-ep-cuda-plugin
--features cuda` passes here because cudarc uses dynamic loading.
prefetch_lazy_weight is deliberately left as Ok(false): CudaWeightResidency
has no try_without_eviction API and the standing directive forbids a prefetch
that may evict.

KNOWN INTEGRATION GAP (fixed in follow-up, do not read this as working):
- onnx-runtime-ep-nxrt-host does NOT depend on onnx-runtime-ep-nxrt-abi. It
  carries a private duplicate abi_contract.rs expecting nxrt_abi_version /
  nxrt_create_ep, while the ABI crate exports NxrtNegotiate /
  NxrtCreateEpFactories. The two halves are incompatible.
- crates/onnx-runtime-ep-nxrt-testplugin declares its own [workspace], so
  cargo check --workspace never builds it.
Consequently the whole tree compiles only because the incompatible pieces are
never linked together. §524's native ABI is NOT yet proven end to end.

CUDA remains UNVALIDATED: no GPU or driver on this host. Compiling is not
working, and nothing here should be read as a working CUDA EP.

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

Makes §524's native nxrt dynamic ABI real end to end. The previous commit
shipped two halves that each compiled but were never linked; this connects
them and proves the round trip.

The gap (found by Pris and Roy, confirmed by inspection):
- onnx-runtime-ep-nxrt-host had NO dependency on onnx-runtime-ep-nxrt-abi. It
  carried a private duplicate contract in abi_contract.rs expecting
  nxrt_abi_version / nxrt_create_ep, while the shipped ABI exports
  NxrtNegotiate / NxrtCreateEpFactories.
- onnx-runtime-ep-nxrt-testplugin declared its own [workspace], so
  cargo check --workspace never built it.
The tree therefore compiled green while nothing was connected - the
"instruments that lie" failure mode this team has recorded.

Resolution: Nabil's ABI is the single source of truth (it has the versioned
struct_size forward-compat, major/minor negotiation, and NXRT_CAP_KNOWN_MASK
fail-closed capability handling that §524 requires).
- Isidore deleted abi_contract.rs; the host now depends on the ABI crate and
  implements the real negotiate -> create_factories -> create_ep -> release
  sequence, copying borrowed name pointers into owned Strings rather than
  retaining them.
- Nabil shipped export_nxrt_ep_factories! plus override macros
  (NxrtNegotiateOverride / NxrtCreateFactoriesOverride) so deliberately broken
  plugins can be built for negative testing without hand-rolling a duplicate
  ABI - closing the hole that invited the duplicate in the first place.
- Pris rebuilt the fixture on the shipped ABI and made it a real workspace
  member.

Round trip now proven against the shipped ABI: negotiate -> create factories
-> create EP -> query -> release, with Drop counters returning to zero.
Negative paths all fail closed: incompatible major, plugin minor newer than
host, unknown capability bits, missing symbol, missing library, factory
error, zero devices, and panic containment.

Two test-integrity fixes along the way:
- Fixture resolution followed the cdylib into the shared workspace target/,
  honoring CARGO_TARGET_DIR and profile, with auto-build and a loud failure
  rather than a silent skip.
- An ENV_MUTEX now serializes the tests that set NXRT_TEST_PANIC /
  NXRT_TEST_FACTORY_ERROR; without it a parallel run could contaminate the
  lifecycle test (Roy hit exactly this).
Duplicate round-trip suites were collapsed into one authoritative suite so
they cannot drift apart.

CUDA remains UNVALIDATED. scripts/cuda_conformance_runner.sh correctly exits
2 (UNVALIDATED) on this GPU-less host rather than reporting success.

Validation: clippy --all-targets -D warnings clean across all four EP crates;
rustfmt clean; cargo check --workspace clean; nxrt-abi 30, nxrt-host 4+10,
ep-plugin 154+9, cpu-plugin 6+17 - all passing, 0 failed.

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

CI (Rust quality, job 93646716235) failed with:
  NXRT_ABI: documented in EP_PLUGIN_EXPORT_PR.md but no crate reads it.

NXRT_ABI is not an environment variable. It was matched out of the
documentation filename docs/NXRT_ABI.md, which EP_PLUGIN_EXPORT_PR.md links
to, because ENV_PATTERN could not distinguish a doc filename from a variable:
  \b(?:ONNX_GENAI|NXRT)_[A-Z0-9_]+\b

This is a false-positive class rather than a one-off: any doc named NXRT_*.md
or ONNX_GENAI_*.md trips the gate as soon as another doc links to it.

Fix (Gaff): add a negative lookahead so a match immediately followed by a
file extension is not treated as an env var. A real env var is never written
with a .md suffix, so the gate keeps its full power. The module docstring
records the exclusion and the reasoning so it is not "fixed" back later.

Deliberately NOT done: adding NXRT_ABI to KNOWN_UNIMPLEMENTED. That list
means "a real env var we knowingly do not read"; NXRT_ABI is not an env var,
so listing it would be a false statement inside the honesty gate itself.
Also not done: deleting the docs/NXRT_ABI.md links to dodge the checker,
which would leave the landmine armed for the next doc.

Gate re-proved rather than assumed: injecting a fake NXRT_FAKE_KNOB into a
scratch doc still fails the check (exit 1); scratch file removed.

Docs (Roy): swept EP_PLUGIN_EXPORT_PR.md and siblings for stale claims.
Removed the push-blocked language (no GH_TOKEN / no SSH key / branches
local-only / "a user must open the PR") - PR #762 has been open on origin
since credentials landed. Removed "1 test failing", "9/10 passing", and
"Pris's fixture isolation fix not yet landed"; the nxrt round-trip suite is
10/10 and stable. Updated stale SHAs 99560c8 and 3ab0ded to 087d348.

CUDA status unchanged and still honest: UNVALIDATED, nothing has run on a
GPU, and there is no self-hosted GPU workflow in this repo, so CI cannot
validate it. scripts/cuda_conformance_runner.sh exits 2 (UNVALIDATED) here.

Validation:
- python3 .github/scripts/verify_documented_env_vars.py -> exit 0,
  "63 documented, 9 known-unimplemented, all accounted for"
- python3 .github/scripts/verify_cuda_test_honesty.py -> exit 0,
  385 tests/51 targets, 381 fail-loud, 0 passed on this no-CUDA host
- cargo fmt --all -- --check clean; cargo metadata --locked resolves
- clippy --all-targets -D warnings clean across the four EP crates
- 230 tests passing, 0 failed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second failure in PR #762's Rust quality job. The env-var honesty gate now
passes; this one was the "Run clippy on all offline crates" step.

crates/onnx-runtime-ep-cpu/src/kernels/mod.rs:2293 used
`delta >= 0 && delta < 50`, which trips clippy::manual_range_contains. It
appears only as a warning in a plain local run, but the CI job sets
RUSTFLAGS: "-D warnings" at job level, so it is a hard error there. Replaced
with `(0..50).contains(&delta)`.

Checked rather than assumed that the assertion is sound: `delta` is a static
count difference between two in-memory registry maps built from compiled
code - no wall clock, no I/O, no concurrency - so the bound cannot flake on a
loaded runner. Worth confirming, since a timing-based assertion hidden behind
a lint would have been a worse problem than the lint.

Validation now runs the CI lane exactly rather than only the touched crates:
- RUSTFLAGS="-D warnings" cargo clippy --locked --all-targets over all 31
  offline-lane crates -> clean
- same flags over the four EP crates -> clean
- verify_documented_env_vars.py -> 63 documented, 9 known-unimplemented
- verify_cuda_test_honesty.py -> 381 fail-loud, 0 passed on this no-CUDA host
- cargo fmt --all -- --check -> clean

CUDA remains UNVALIDATED; no GPU runner exists in this repo.

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

Fixes the five CI lanes (Fast Linux, Rust Windows ARM64, Rust coverage on
Linux/Windows/macOS) failing at "Test Linux offline crates":

  Could not find libonnx_runtime_ep_cpu_plugin.so
  Searched: [".../target/debug/...", ".../target/release/..."]
  test result: FAILED. 2 passed; 4 failed

The cpu-plugin tests hardcoded target/debug and target/release. They passed
locally only because an earlier manual `cargo build` had left the .so in
target/debug. Moving that file aside reproduced the CI failure exactly, so
the suite was never reproducible from a clean tree - our headline "23
conformance tests passing" was an artifact-dependent false pass. That is the
same instruments-that-lie class as the duplicated nxrt ABI, hiding inside the
number we were reporting.

Fix: extract crates/onnx-runtime-ep-cpu-plugin/tests/cdylib_resolve.rs as a
shared helper used by both plugin_export_abi.rs and plugin_ort_e2e.rs, rather
than duplicating the lookup a second time - duplicated resolution logic is
what let these two paths drift apart to begin with.

Resolution order: NXRT_CPU_PLUGIN_PATH override, then CARGO_TARGET_DIR with
the active profile, then the workspace target dir, then auto-build via
`cargo build -p onnx-runtime-ep-cpu-plugin`, panicking loudly if that build
fails. It never silently skips.

Also fixes a latent cause of the Windows and macOS failures specifically: the
library name was hardcoded to .so, so those lanes could not have found the
artifact under any search path. The helper now selects .so / .dylib / .dll
via cfg!(target_os).

Audited every other test that loads a built artifact by path: the nxrt
round-trip suite already had this pattern, trait_cabi_parity loads none, and
onnx-genai-bench's profile_native has an env override with a graceful skip.
No other test can pass on a stale artifact.

Verified from genuinely clean state (artifact deleted first, not a warm
tree): 6 + 17 = 23 passing, with the cdylib rebuilt by the test run itself.
Also clean: clippy -D warnings on the four EP crates, rustfmt, the env-var
honesty gate, and workspace_test_packages verify.

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

Fixes the Rust (Windows ARM64) lane failing at "Test cross-platform offline
crates" with 12 instances of:

  error[E0308]: mismatched types
     --> crates\onnx-runtime-ep-cpu-plugin\tests\plugin_ort_e2e.rs:227:13
     |       ep_path_c.as_ptr(),
     |       ^^^^^^^^^^^^^^^^^^ expected `*const u16`, found `*const i8`

ONNX Runtime's path-taking APIs use ORTCHAR_T, which is wchar_t (UTF-16,
*const u16) on Windows and char (*const c_char) on Unix. The tests built
paths with CString, which is Unix-only, so the ORT end-to-end suite has never
compiled on Windows - it only ever ran on Linux. Same shape as the stale
artifact bug: a suite quoted as green that was not green everywhere it
claimed to run.

Fix: crates/onnx-runtime-ep-cpu-plugin/tests/ort_path.rs provides OrtPathBuf,
which encodes to a NUL-terminated Vec<u16> via OsStrExt::encode_wide under
cfg(windows) and to a CString otherwise, exposing as_ptr() at the type the
bindings expect. Applied at all 12 path call sites - six
RegisterExecutionProviderLibrary and six CreateSession.

OrtPathBuf owns its buffer and every use is bound to a named local that
outlives the FFI call, so there is no as_ptr()-on-a-temporary dangling
pointer. That is the same ownership trap behind the OrtMemoryInfo
use-after-free earlier in this workstream.

Registration names are deliberately left as CString: those parameters are
char* on every platform, unlike the ORTCHAR_T paths.

Audited the rest: no other test passes a path into an ORT C API, and both the
nxrt round-trip and plugin_export_abi suites go through libloading, which
takes an OsStr and is already portable.

Windows compilation is UNVERIFIED locally - this host is Linux, and
`cargo check --target x86_64-pc-windows-msvc` fails in bindgen for lack of
Windows SDK headers. The Windows branch is type-correct by construction
against the generated bindings; CI must confirm it.

Linux validation, from a clean artifact state: 6 + 17 passing, clippy
-D warnings clean, rustfmt clean, and no regression in ep-plugin (154 + 9),
nxrt-abi (30), or nxrt-host (4 + 10).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes the last two CI failures in the cpu-plugin suite:

  Missing model fixture: .../tests/fixtures/add_1x4/model.onnx
  called `Result::unwrap()` on an `Err` value: PoisonError { .. }

.gitignore line 3 is `*.onnx`, and git check-ignore confirmed it matched
every fixture. `git ls-files` on tests/fixtures returned only
generate_fixtures.py: all 11 models existed on the developer's disk and in no
commit, so the ORT end-to-end suite was never runnable from a fresh clone.

This is the third instance this session of one failure class - stale build
artifact, Windows-only compile error, now untracked fixtures - each of which
was green locally and impossible in CI. The rule worth keeping: a test asset
that is not tracked by git does not exist in CI.

Fix follows the convention already in .gitignore, which carries explicit `!`
negations for the 15 .onnx fixtures the repo already tracks. Added one
negation per fixture (11 files, 44K total) so the broad `*.onnx` ignore keeps
protecting against committing real model weights.

The PoisonError was a cascade, not a second bug: the first test panicked
while holding the shared ORT lock, so unrelated tests then failed with a
misleading error and pointed at the wrong test. A lock_ort_ep() helper now
recovers via PoisonError::into_inner() and reports the real cause, so one
failure can no longer masquerade as several.

generate_fixtures.py was missing generators for 4 of the 11 committed models;
added them so the script is once again the complete source of truth and the
fixtures can be regenerated byte-identically.

Audited every *.onnx/*.bin/*.pb/*.npz under crates/*/tests against
git ls-files: no other untracked test asset exists.

Validation from a clean artifact state: cpu-plugin 6 + 17, ep-plugin 154 + 9,
nxrt-abi 30, nxrt-host 4 + 10 - all passing. clippy -D warnings clean,
rustfmt clean.

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

Fixes the three remaining non-Linux lanes (Rust Windows ARM64, Rust coverage
Windows, Rust coverage macOS arm64). All Linux lanes are already green.

BUG 1 - undefined behaviour that passed on Linux by luck.
plugin_export_abi.rs declared:
  type ReleaseEpFactory = unsafe extern "C" fn(*mut OrtEpFactory) -> *mut OrtStatus;
but the real export returns nothing (onnx-runtime-ep-plugin/src/lib.rs:163),
because ReleaseEpFactory is void in the ORT ABI. Calling a void function
through a pointer typed as returning *mut OrtStatus reads whatever is in the
return register. On x86-64 Linux that garbage was null so the assertion
passed; on arm64 it is non-null, producing:
  ReleaseEpFactory returned non-null status (error)

So the test was asserting on uninitialised memory and had never actually
verified anything. Corrected the type to the void signature and removed the
meaningless status assertion.

Audited every other extern "C" fn-pointer type in the test suites -
CreateEpFactories, CreateFn, GetApiBaseFn - all match their real exports. No
further mismatches.

BUG 2 - the L1 symbol checks used ELF-only tooling. `readelf` does not exist
on macOS or Windows, and `nm -D` is ELF-specific ("File format has no dynamic
symbol table" on Mach-O). Replaced with:
- l1_required_symbols_resolve: resolves both exports through libloading
  (dlsym/LoadLibrary) on every platform, which is a stronger and more
  portable assertion than parsing nm output.
- l1_no_symbol_leakage: keeps the "nothing else is exported" property via
  `nm --dynamic` on Linux, skipping with an explicit message elsewhere rather
  than silently passing or failing for a missing tool.

That is four instances this session of one failure class - stale artifact,
Windows-only compile error, untracked fixtures, and now a signature mismatch
- each green on Linux and impossible or wrong elsewhere.

Verified on Linux from a clean artifact state: 6 + 17 passing, ep-plugin
154 + 9, nxrt-abi 30, nxrt-host 4 + 10, clippy -D warnings clean, rustfmt
clean. macOS and Windows cannot be exercised on this host; CI must confirm.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The four tests added for build_cpu_registry_with_descriptors each
constructed the full 166-op registry, and RecordingOpRegistry builds it a
second time to record descriptors, so the suite performed roughly eight full
constructions. They now share a OnceLock-backed fixture. Every assertion is
preserved verbatim.

Context: Rust (Windows ARM64) is the last red lane, failing
spmd_adaptive_calibrated_decode_is_bit_identical_to_flat in matmul_nbits.rs -
a file this branch does not touch. Its child subprocess printed its test
header and then nothing at all: no assertion, no stderr, which is the
signature of a killed or timed-out process rather than a failed check. The
hypothesis was that our extra registry-building tests raised memory/CPU
pressure enough to starve that child on the most constrained runner.

Measured rather than assumed, and the numbers refute it:
  wall clock 32.20s -> 32.25s, peak RSS 254 MB -> 253 MB.

So the consolidation is not the fix; the Windows ARM64 failure looks like a
pre-existing flake on a constrained runner. Recorded honestly rather than
claimed as resolved. The change is kept because building the registry once
instead of eight times is correct regardless.

Deliberately not done: touching matmul_nbits.rs or ignoring the SPMD test.
Editing an unrelated, untouched test to turn CI green is the exact
dishonesty this workstream has spent the session correcting. Recommend to its
owner that the SPMD parity test gain a timeout and diagnostics - an
empty-output failure gives no actionable information.

Validation on Linux: ep-cpu green, clippy -D warnings clean, rustfmt clean,
cpu-plugin 6 + 17, ep-plugin 154 + 9, nxrt-abi 30, nxrt-host 4 + 10.
Windows ARM64 cannot be reproduced on this host; CI must confirm.

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

Copy link
Copy Markdown
Owner Author

GPU validation is tracked in #768 — this PR stays draft

Confirmed external hardware blocker: this repo has zero self-hosted runners (GET /repos/justinchuby/onnx-genai/actions/runners → total_count: 0), and the owner is a User account, so there is no org-level GPU pool. Hosted runners have no NVIDIA GPU. CUDA cannot be validated by CI as configured, only by a human on a CUDA host.

#768 carries the exact prerequisites, the scripts/cuda_conformance_runner.sh exit-code contract (0 VALIDATED / 1 FAILED / 2 UNVALIDATED), and the acceptance evidence required.

Status of this PR

Everything except GPU validation is green — all CI lanes pass, including Rust quality, Miri, Windows ARM64, macOS, and Linux coverage.

docs/CUDA_EP_STATUS.md keeps VALIDATED-ON-HARDWARE empty for all 17 rows. Nothing here should be read as a working CUDA EP; cargo check --features cuda passing is compile coverage, not evidence it runs.

Marking ready-for-review is gated on #768 returning exit 0 with evidence.

@justinchuby

Copy link
Copy Markdown
Owner Author

CI baseline comparison

This PR is being marked ready with red checks. That is deliberate, and here is the evidence that none of the red is caused by this branch.

Every failing job shares one root cause

All five failing jobs — Fast (Linux x86_64), CLI ORT (Linux x86_64), CLI ORT (Windows x86_64), Rust (Windows ARM64), Rust quality — fail on the same assertion:

crates/onnx-genai-server/src/registry.rs:1020
assertion failed: error.contains("committed bytes")
test result: FAILED. 182 passed; 1 failed; 2 ignored

It fails identically on main

I checked out origin/main at 9b2414fb5 in a clean worktree and ran the same test:

thread 'registry::tests::failed_runtime_shrink_preserves_policy_and_ledger_limit'
  panicked at crates/onnx-genai-server/src/registry.rs:1020:9:
assertion failed: error.contains("committed bytes")
test result: FAILED. 0 passed; 1 failed; 184 filtered out

main is currently red on this test independently of this branch.

This branch does not touch that crate

git diff --name-only origin/main...HEAD -- crates/onnx-genai-server/
(no output)

What this branch's own code does

check result
EP crates (ep-plugin, ep-cpu-plugin, ep-cuda-plugin, nxrt-abi, nxrt-host) 283 passed, 0 failed
cargo clippy --all-targets -- -D warnings on all five EP crates clean
cargo fmt --check clean
cargo miri test -p onnx-runtime-ep-plugin 173 lib tests pass

The remaining workspace clippy errors are in onnx-runtime-cuda-memory, which I confirmed also fails on origin/main and which CI's clippy allowlist does not lint.

Reproducing the Rust quality job locally

Every step in that job passes here: check_publish_order, workspace_test_packages verify, check_platform_naming, check_dispatch_reachability, check_dispatch_manifest, check_feature_gate_coverage, verify_documented_env_vars, check_cross_compile.sh, cargo fmt --all --check.

One note on measurement method

cargo test --workspace fail-fasts and silently truncates its totals — it once reported "1555 passed" where the true figure was 4598. Every count above uses --no-fail-fast. Similarly, test binaries were rebuilt before counting; a stale binary reported 42/1 where a fresh build gave 41/2 on a sibling PR.

If a maintainer would rather this wait until main is green, say so and I will return it to draft.

justinchuby and others added 8 commits August 12, 2026 07:35
…on/session logs

- Merged 6 inbox drops (rachael, coco, isidore, gaff, freysa, iran) into decisions.md
  with 5 durable lessons from the PR #762 ready-for-review wave
- Deleted processed inbox files; README.md preserved
- Wrote orchestration logs for Rachael, Coco, Isidore, Gaff, Freysa (2026-08-12T08-00-00Z)
- Wrote session log 2026-08-12T08-00-00Z-762-ready.md
- Appended #762-ready entries to all five agent history files
- Compacted Rachael (11→3 dated entries), Coco (9→2), Isidore (10→2) into history-archive.md
- decisions.md: 24043→26588 bytes (well under 51200 limit, no archive needed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merged decisions/inbox/batty-32003-ptx.md into decisions.md
- Appended PR #32003 narrative-fix + PTX lessons to decisions.md
- Wrote orchestration-log/2026-08-12T08-30-00Z-batty.md
- Wrote log/2026-08-12T08-30-00Z-32003-ready.md
- Updated agents/batty/history.md with PR #32003 completion

PR #32003 marked ready for review. 12/12 PTX pairs byte-identical.

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

- decisions.md: merged 3 inbox drops (resch-32001-validation.md,
  holden-32001-focused.md, zhora-32001-dedupe.md); added PR #32001 wave
  entry with 5 durable lessons on copy-drift, dead code, parse-time
  validation, error messages, and body-vs-code agreement; 29 216 → 32 531 bytes
- orchestration-log: 3 new entries (resch, holden, zhora) at 2026-08-12T09-45-00Z
- log: 2026-08-12T09-45-00Z-32001-ready.md
- history.md: appended #32001 update to resch, holden, zhora
- history compaction: resch (12 → 4 live entries) and zhora (10 → 4 live entries)
  older entries moved to respective history-archive.md
- inbox: deleted 3 processed drops; README preserved

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…NaN/QEMU evidence

- decisions.md: merged 2 inbox drops (iran-31993-nan-runtime.md,
  sapper-31974-ab.md); added 5 durable lessons on QEMU evidence,
  NaN payload portability, compiles-vs-runs, undisclosed type
  changes, and verifying 'no-behaviour-change' claims
- orchestration-log: iran and sapper entries for final-review-wave
- log: session log 2026-08-12T10-15-00Z-final-review-wave.md
- iran/history.md: compacted (8 dated entries → archive);
  2026-07-27 through 2026-08-11 moved to history-archive-2026-08-12T10-15-00Z.md;
  live file now holds role summary + 2026-08-12 entries (5,415 bytes)
- sapper/history.md: appended PrePack A/B + PR body correction entry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…arks, x86 wording

Merged 3 inbox drops (batty-31973-n1.md, challenger-31973-delta.md,
deckard-31973-wording.md) into decisions.md. Appended history updates
for Batty, Challenger, and Deckard.

Decisions recorded:
- Gate arch-specific test assertions with a predicate mirroring the
  production #if; never restate thresholds as literals
- Name and scope-bound benchmark baselines (Welford fp32 mirrors
  layer_norm_impl.cc; per-row in-process microbenchmark)
- Readability edits can narrow accuracy (AMD64/IX86 → x86-64 → x86)
- This host (AVX2/FMA) can measure x86 performance honestly

Board: #762 ready · #31985 merged · #31973 ready · #31974 ready ·
       #32001 ready · #32003 ready · #31993 draft · #31988 draft

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

- decisions.md: appended #31973 evidence-accuracy wave (Mariette B1/B2,
  Gaff review, Coordinator correction); 5 durable lessons; also merged
  prior-wave inbox drops (coco-32001-lint, pris-31993-ctest,
  rachael-32001-crosstarget, holden-final-two)
- agents/mariette/history.md: appended 2026-08-12 evidence-accuracy entry
- agents/gaff/history.md: appended 2026-08-12 evidence-accuracy review entry
- Pre-existing staged changes from prior wave also included
  (coco, holden, pris, rachael history updates)
- decisions.md: 37050 → 40970 bytes (under 51200 gate; no archive)
- No history compaction: both files < 8 dated entries

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

- Merged 2 inbox drops (leon-31974-prepack, coco-31974-regression) into decisions.md
- Added #31974 regression wave section with 4 durable lessons
- Wrote orchestration logs for Leon and Coco (2026-08-12T14-00-00Z)
- Wrote session log: 2026-08-12T14-00-00Z-31974-regression.md
- Appended PR #31974 regression entry to leon/history.md and coco/history.md
- Compacted leon/history.md (9 dated entries → 2 current + archive pointer)
- Archived 7 older Leon entries to leon/history-archive.md
- decisions.md: 40,970 → 43,278 bytes (under 51,200 gate; no archiving needed)

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

Syncs with main (11 commits) so CI's automatic PR-merge preview builds
against current upstream, and pulls in the test_support cfg-gating bug
(#807) that this branch needs to fix forward (see follow-up commit).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby and others added 2 commits August 12, 2026 14:54
cfg(test) does not propagate to integration test crates — they link the
library as a regular dependency. The cfg gate `any(test, feature =
"gpu-tests")` therefore never activates for tests/*.rs binaries unless
gpu-tests is explicitly passed on the CLI. This broke CI's honesty-checker
base (cuda-only) build for 7 of PR #762's CI jobs (Rust coverage x3,
Rust ARM64, CUDA compile Linux, Rust quality, Fast Linux), all sharing the
identical compile error at vmm_kv_layout_residency_gpu.rs:38.

Fix: narrow the lib.rs gate to `feature = "gpu-tests"` and conditionally
compile the import + body in the one integration test that uses TestStream.
The test binary remains in inventory in both configurations (416 tests /
66 targets), and without gpu-tests the test is #[ignore]d with an empty
body, matching this crate's existing cfg_attr(not(feature = "gpu-tests"))
skip convention.

The remaining 2 of the original 9 failed jobs (CLI ORT Linux/Windows) fail
on an unrelated, pre-existing flaky assertion in
registry::tests::failed_runtime_shrink_preserves_policy_and_ledger_limit
(documented in PR #758 as pre-existing multi-threaded VRAM-ledger
flakiness) and are out of scope for this fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per task instructions this branch must not mutate .squad/ state files.
The decision-record file added in afbd2c1 belonged to a sibling effort
on squad/fix-cuda-memory-test-support-gate, not this PR; drop it here so
this branch only carries the product-code CI fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby
justinchuby enabled auto-merge (squash) August 12, 2026 15:07
@justinchuby
justinchuby merged commit e9c0ab6 into main Aug 12, 2026
11 of 16 checks passed
@justinchuby
justinchuby deleted the squad/ep-plugin-parity-cuda branch August 12, 2026 15:10
justinchuby added a commit that referenced this pull request Aug 12, 2026
Adds a PyPI publish pipeline for the two ORT plugin-EP cdylibs
introduced in #762.

## Packages
- **`nxrt-ep-cpu`** (`import nxrt_ep_cpu`) — bundles
`libonnx_runtime_ep_cpu_plugin.{so,dll,dylib}`.
- **`nxrt-ep-cuda`** (`import nxrt_ep_cuda`, experimental) — bundles the
CUDA plugin cdylib built with `--features cuda`, **CUDA 13** runtime
deps (`nvidia-*-cu13>=13`). Hardware validation tracked in #768.

Both expose `get_library_path() -> str` and
`register(session_options=None)` for ORT's
`register_execution_provider_library`. The cdylibs are pure C-ABI
(`CreateEpFactories`/`ReleaseEpFactory`) and **do not link
libonnxruntime** — the ORT API is passed through the plugin-EP ABI at
load time, so the wheels carry no ORT dependency.

## Build
setuptools + plain `cargo` (not maturin — the cdylib is a C-ABI lib, not
a PyO3 extension). `build_py` runs `cargo build --release -p <crate>`,
vendors the `.so`, and `bdist_wheel` tags it `py3-none-<platform>`
(auditwheel/delvewheel repair to manylinux/win). Validated locally: CPU
wheel builds, installs, `get_library_path()` returns a real file, and
`register()` accepts it against onnxruntime 1.28.

## Workflow — `.github/workflows/publish-ep-plugins.yml`
- **Trusted Publishing (OIDC)**, environment `pypi`, no API tokens.
- `workflow_dispatch` inputs `publish_cpu` / `publish_cuda` /
`testpypi`; also `push` tag `nxrt-ep-v*`.
- **`cpu` and `cuda` jobs are fully independent** (no cross-`needs:`).
PyPI blocks two pending trusted publishers from one repo at once, so
`nxrt-ep-cpu` publishes first and `nxrt-ep-cuda` later — the CUDA job
never touches PyPI unless `publish_cuda=true`.
- `publish-cpu needs:[cpu]` / `publish-cuda needs:[cuda]`: a failed
build blocks the corresponding publish.

No publish happens on merge — releases are triggered manually via
`workflow_dispatch`.

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 12, 2026
Fast-follow to #762. A test-review of the EP-plugin suite found **no
product correctness bugs** but 3 places where a test claimed more than
it proved. All fixed; every affected test runs and passes locally (ORT
available — nothing skipped).

## 1. Tautological CUDA diagnostic test → asserts real output
`cuda_fail_closed.rs::cuda_plugin_diagnostic_message_is_actionable`
previously asserted against string literals defined inside the test body
(always passed). Now the plugin exposes `fail_closed_diagnostic()`
returning its *actual* fail-closed message (shared
`NO_CUDA_FEATURE_DIAGNOSTIC` const, also used by `CreateEpFactories`),
and the test asserts the real string contains the actionable substrings.

## 2. Six non-reproducible fixtures → generators restored
`generate_fixtures.py` only reproduced 22 of 28 fixtures; the 6 missing
were the BL1/BL2/BL3 regression models (opaque blobs). Added generators
for all 6 and wired them into `__main__`. **All 28 fixtures now
regenerate byte-for-byte identical** to the committed models (`git diff`
empty).

## 3. Value-blind "no_overflow" tests → real oracles
`layer_norm_{f16,bf16}_absent_output_no_overflow` asserted only
non-null. They now decode `Y` and compare against a hand-computed
LayerNorm oracle (tol 0.02 f16 / 0.05 bf16), matching the SkipLayerNorm
f16/bf16 siblings.

## Validation
`cargo test -p onnx-runtime-ep-cpu-plugin` (40 passed / 0 failed / 0
skipped) · `cargo test -p onnx-runtime-ep-cuda-plugin` (12 passed) ·
fixtures regenerate identical · `cargo fmt` + `clippy --tests` clean.

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

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

Justin asked to update `docs/PROGRESS.md` with the latest progress and
drop stale info. It had grown to **1166 lines** of day-by-day changelog
spanning 2026-07-15 → 2026-07-27, with the entire recent arc missing.

## Changes (1166 → 303 lines)
- **Added recent milestones (2026-07-28 → 2026-08-12), newest-first:**
- EP plugins run inside ORT via the plugin-EP C ABI (#762) and ship on
PyPI — **`nxrt-ep-cpu` + `nxrt-ep-cuda` 0.1.0.dev5 both LIVE**
(#819/#824/#820).
  - Absent-optional-output memory-safety wave (#762 review; Miri-clean).
- VMM / KV-layout / offload / batching residency wave — managed no-spill
VMM default, layout-governed KV floor (768× reduction), offload+capture
coexist, #736 over-reservation audit (#755–#814).
- Issue triage + autonomous fixes (#784/#785/#786/#788/#789); CPU-EP
TopK correctness+perf (#774/#775).
- Qwen3.5/3.6-27B hybrid Gated-DeltaNet native CUDA (#779); GLM-4-9B +
DeepSeek-V2-Lite native + ORT 1.28 3-config fairness (#766/#770/#771).
- Fused QMoE decode kernel + QMoE arc concluded; CUDA-graph capture
trilogy, version-selectable CUDA (#708/#728/#757/#760), megakernel study
(#769).
- **Compressed stale detail:** the ~40 dated July day-by-day sections +
verbose diffusion/LoRA/ORT2 narrative collapsed into a ~35-line
"Foundational milestones" spine; superseded perf-wave numbers dropped;
§1–§40 table condensed to a compact grid.
- **Header refreshed:** last-updated → 2026-08-12; Published line now
lists both EP plugins on PyPI + Miri CI.
- **Preserved & updated** the DeepSeek/GLM known-gaps checklists and
added new open items.

Docs-only; full narrative remains in `.squad/decisions-archive/` and
`docs/benchmarks/`.

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

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

Documentation-only sync of five memory/KV design docs to the merged,
executable-test-backed facts about VMM commit geometry, KV layout,
weight offload, CUDA-graph capture, and plugin export. No code or tests
changed.

`MEMORY_ARCHITECTURE.md` is treated as the single source for KV geometry
and the audit table; the other four link into it rather than restating
(per #791).

## Most misleading claim found

`MEMORY_ARCHITECTURE.md` — the document explicitly designated the single
source of truth — still asserted the engine *"explicitly invalidates the
graph on every KV bucket growth for both layouts"* and that *"the engine
does not yet use that fact to retain the captured graph,"* with a table
showing seq-major at `captures=4, invalidations=4`. That was falsified
by **#811**: conditional invalidation has landed, seq-major fixed-stride
growth now **keeps** the captured graph (growth-attributable
invalidations **3 → 0**, `captures=1/invalidations=1`, matching the
no-growth control), verified by
`mobius_seqmajor_growth_parity_native_cuda.rs` including a negative
oracle. Because this doc is the single source others link to, the stale
claim had the widest blast radius.

## Changes

- **MEMORY_ARCHITECTURE.md** — correct the #811 conditional-invalidation
prose + table (verified against `cuda.rs`
`binding_growth_signature`/`record_growth_decision` and the parity
test); add the ORT-backend KV-routing + #762 plugin-export note
(verified `allocator.rs:232` `for_session_device` selects by device, not
implementation); thread #822 (granule queried), #812 (engine-side
consumer gate), and a #800/#802 record correcting this doc's earlier
fp16 sizing of Phase-2a scores (verified `attention.rs:368` uses
`dtype.element_size()`).
- **CUDA_GRAPH_CAPTURE.md** — replace the mutual-exclusion result with
#796 (stable VA removes it), #798 (managed no-spill default), #811
(conditional invalidation), and the #804/#807 `captures=0`
process-artifact correction.
- **WEIGHT_OFFLOAD.md** — correct the "mutually exclusive today" claim
(#796) and the north-star status for the managed no-spill default
(#798).
- **KV_INSERTION_DESIGN.md** (most stale, 2026-07-15) — add a
synchronization banner linking to `MEMORY_ARCHITECTURE.md`; note the
layout line post-dates the memo, prefix sharing is a landed primitive
(#777), and block-table paged attention remains a proposal (verified: no
`block_table` in any CUDA attention kernel).
- **DESIGN.md** — forward-pointer in the already-superseded §39.5 noting
layout (seq-major/token-major, #787/#792) is now the primary residency
lever.

## Method

Every retained performance claim cites the PR that measured it; measured
fact / design intent / not-yet-built are distinguished; superseded
designs are labelled, not deleted. The "still open" item (engine commits
flat buckets, not the dense live-prefix ranges `kv_commit.rs` computes,
so the `layers×2` floor is not yet realised through the engine) is
described as in-flight, not delivered.

## Note on #762

The task framing had #762 (EP plugin export) as open; `gh pr view 762`
shows it **merged** today. It is described accurately per that ground
truth: the plugin-EP mechanism landed for the CPU EP (validated
end-to-end), while the CUDA plugin shim compiles but is unvalidated on
hardware — so plugin-EP-hosted VMM KV remains in-progress design intent,
not a shipped capability.

## Validation

Documentation only — `git diff --stat` touches exactly the five `.md`
files; working tree otherwise clean. **Not merged; no issue closed.**

Refs #736, #762, #776, #777, #783, #787, #791, #792, #794, #796, #797,
#798, #801, #802, #804, #807, #811, #812, #822

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 12, 2026
…-up to #762's CI fix) (#818)

## Context

PR #762 merged into `main` at `e9c0ab6a4` and already carries the fix
for the
CI-wide compile break introduced by #807:
`crates/onnx-runtime-cuda-memory/src/lib.rs`
gates `pub mod test_support;` on `#[cfg(feature = "gpu-tests")]` only
(previously
`#[cfg(any(test, feature = "gpu-tests"))]`), and the one integration
test that
uses `TestStream` (`tests/vmm_kv_layout_residency_gpu.rs`) cfg-gates its
import
and body accordingly.

**Root cause recap:** `cfg(test)` is only set when a crate compiles as
its own
unit-test binary. Integration tests under `tests/*.rs` link the crate as
a
regular (non-`--test`) dependency, so `cfg(test)` never applies to them.
The
old gate `any(test, feature = "gpu-tests")` therefore never actually
exposed
`test_support` to `tests/vmm_kv_layout_residency_gpu.rs` unless
`gpu-tests` was
passed explicitly — which is exactly what broke CI's honesty-checker
base
(cuda-only, no `gpu-tests`) build for 7 of 9 failed jobs on #762's own
CI run.

## What this PR does

Since the functional fix already reached `main` via #762, **this PR is a
minimal, standalone doc correction**: `test_support.rs`'s module doc
still said
the module was compiled "behind the `gpu-tests` feature (and the crate's
own
`cfg(test)`)" — stale wording that contradicts the actual (correct) gate
in
`lib.rs` and could mislead a future reader into re-introducing the same
bug by
assuming `cfg(test)` alone helps here. Updated the doc to explicitly
state the
gate is `gpu-tests`-only and explain why `cfg(test)` doesn't apply to
integration tests.

**No functional/behavioral change.** No `.squad/` state included.

## Delta vs. Opus-approved fix (afbd2c1 on #762)

`git diff afbd2c1 origin/main --
crates/onnx-runtime-cuda-memory/src/lib.rs
crates/onnx-runtime-cuda-memory/tests/vmm_kv_layout_residency_gpu.rs`
is **empty** — main already has byte-identical content for both files
via the
#762 merge. The only delta introduced by this branch beyond what Opus
already
reviewed is the `test_support.rs` doc-comment correction (7 insertions /
3
deletions), which was never part of the reviewed diff. Flagging this so
a tiny
follow-up look is optional but available before merge.

## Validation (local, CPU-only host, matching CI's commands)

- `cargo test --locked -p onnx-runtime-cuda-memory --features cuda
--tests --no-run` — compiles clean (base/no-gpu-tests config)
- `cargo test --locked -p onnx-runtime-cuda-memory --features
cuda,gpu-tests --tests --no-run` — compiles clean (gpu-tests config)
- `python .github/scripts/verify_cuda_test_honesty.py` — passes: 416
tests/66 targets identical in both configs (416 ignored without
gpu-tests; 412 fail-loud/4 ignored/0 passed with gpu-tests on this
no-CUDA host)
- `cargo clippy --locked -p onnx-runtime-cuda-memory --features cuda
--all-targets -- -D warnings` — clean
- `cargo clippy --locked -p onnx-runtime-cuda-memory --features
cuda,gpu-tests --all-targets -- -D warnings` — clean
- `cargo check --locked -p onnx-runtime-ep-cuda -p onnx-runtime-python
--features onnx-runtime-python/cuda` — clean
- `cargo test --locked -p onnx-runtime-ep-cuda --features cuda --test
capture_sync_contract` — 1 passed
- `cargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings`
— clean
- `cargo fmt --all -- --check` — clean

## Notes

- #762 (branch `squad/ep-plugin-parity-cuda`) temporarily carried this
same
cfg-gating fix directly on its own branch (to unblock its own CI) and
has
now merged into `main`, so `main`'s CI should already be green going
forward.
  This standalone branch exists per explicit request so the repo-wide CI
  unblock has its own paper trail, separate from #762's larger diff.
- Kept scope minimal: no CI workflow changes, no other files touched.
- Leaving as **Draft** until CI is confirmed green on this PR's own run.

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

## Summary

Cherry-pick of commit `dd6ab807f` which missed the #762 merge window by
30 minutes and is stranded on the already-merged branch.

**This fixes CURRENTLY-RED lanes on main:**
- Rust Windows ARM64
- Rust coverage Windows x86_64  
- Rust coverage macOS arm64

The root cause: 7 hardcoded `libonnxruntime.so` joins in
`plugin_ort_e2e.rs` that fail on Windows (`onnxruntime.dll`) and macOS
(`libonnxruntime.dylib`).

**Fix:** Route all ORT library path construction through
`ort_discovery::ort_lib_name()` which returns the correct
platform-specific filename. Also switches `diag_ort_ep_api_nullcheck` to
use the `skip_if_missing!` macro.

**Verification (Linux x86_64):**
- `cargo test -p onnx-runtime-ep-cpu-plugin --no-fail-fast`: 24 passed,
0 failed, 0 ignored
- `cargo clippy -p onnx-runtime-ep-cpu-plugin --all-targets -- -D
warnings`: clean
- No hardcoded lib names outside `ort_discovery.rs`

**Windows/macOS correctness is BY CONSTRUCTION** (via `ort_lib_name()`
returning the right filename per `cfg!(target_os)`) and must be
confirmed by CI — we have no such hardware in this environment.

Original commit: `dd6ab807f` on branch `squad/ep-plugin-parity-cuda` (PR
#762, merged 2026-08-12T15:10:54Z; commit dated 30 min after merge).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby added a commit that referenced this pull request Aug 13, 2026
Merge 3 inbox drops into one "Decode latency-floor: node-collapse arc"
section in decisions.md. Batch-1 native CUDA int4 decode is confirmed at
its launch-amortized latency floor from three independent directions:
megakernel NO-GO (#898), glue-collapse +0.9% ceiling (#899/#900),
skip-RMSNorm fold -1.5% regression (#903).

- #899 (Sebastian): glue node-collapse GO under graph replay (74-75%).
- #900 (Batty): bf16 SiLU/SwiGLU-mul glue collapse SHIP (+0.9%, byte-exact,
  SiLU-fusion bf16 portability fix under Rule 11).
- #903 (Sebastian): bf16 skip-RMSNorm KERNEL byte-exact SHIP; standalone
  FOLD -1.5% NO-SHIP (opt-in behind ONNX_GENAI_CUDA_ENABLE_SKIP_RMSNORM_FUSION,
  default OFF).

Size gate: archived 2026-08-12 VMM/offload + #762 durable-lessons narratives
to decisions-archive/2026-08.md to keep decisions.md under 50 KB (49,833 B).
Appended cross-agent history for sebastian + batty; delete 3 processed drops.

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