Repository navigation
Pool pinned host staging buffers for weight page-in (#837 item 2) - #843
Conversation
resident_mapped page-locked a fresh cuMemHostAlloc buffer and freed it on every page-in (5,535 alloc/free pairs for 16 tokens on qwen14b). Reuse a bounded free-list of pinned staging buffers instead, keyed by capacity with a reuse-if-large-enough rule and bounded to 8 buffers / 512 MiB. Return-to-pool is provably ordered after copy completion: every offload page-in copies through htod_async_elapsed_ms, which host-synchronizes the copy's end event before returning, so the buffer is no longer read by any in-flight DMA when it returns to the pool (via PooledStaging Drop, or explicit release on the non-VMM branch). No assert in Drop. Adds a deterministic pinned-allocation counter (per-instance + global) and a regression guard test asserting reuse cannot silently reallocate per page-in. Reports pinned_alloc_calls / pinned_reuses / effective_htod_gbps in profile_native. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #843 +/- ##
==========================================
- Coverage 78.26% 77.67% -0.59%
==========================================
Files 350 351 +1
Lines 142164 145346 +3182
Branches 142164 145346 +3182
==========================================
+ Hits 111265 112902 +1637
- Misses 26460 27969 +1509
- Partials 4439 4475 +36
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
…ss (#837 item 2) The fence-safety invariant (a staging buffer must not be reused/freed until its host->device copy has *completed*, not merely enqueued) was previously enforced only by a comment plus the incidental fact that htod_async_elapsed_ms host-synchronizes. Make it structural: htod_async_elapsed_ms now returns a CopyCompleted witness (a ZST mintable only inside the runtime module by a host-synchronizing copy), and the pool's reuse path (PinnedStagingPool::release / PooledStaging::retire) consumes it. PooledStaging::Drop no longer returns to the pool; it is a leak-safe fallback that frees the buffer, so a forgotten witness costs a re-allocation (caught by the pinned_alloc_calls regression guard) rather than silently corrupting weights. A future switch to a non-blocking htod_async + deferred fence now fails to compile at the reuse site until the author threads a completion witness through after awaiting the fence. No runtime behavior change; before/after measurements remain valid. Drop never asserts (STATUS_STACK_BUFFER_OVERRUN). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…PU contention; fence-safety completion-witness pattern) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ntended runs invalid Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, tracked in #851 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Validated and merging. The type-level guard is exactly right, and the reasoning behind its shape is the good part. I checked the witness for the hole I was most worried about — one witness authorizing the retire of a different buffer whose copy is still running. I also verified the blocking premise in the dependency rather than trusting the comment: Gates I ran personally on a worktree with Your pooling is not implicated by any of that, and the discriminator you supplied is why I can say so cleanly: a genuine return-to-pool-before-fence corruption would fail the bit-identical parity subtest deterministically, not crash the process. Every red anyone saw was a whole-process crash with zero token-stream divergence. Result stands: And thank you for adding the Merging as item 2 of #837. #837 stays open; the de-instrumentation half and item 3 remain. |
🔴 Benchmark Regression DetectedComparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).
Visual flags: Host infoWhat this cannot catch
|
…ile size (#853) (#856) Fixes #853. `model_weight_bytes()` measured the **on-disk size** of the external-data blob. File size is only an upper bound: a blob can contain regions no initializer references — most commonly when a re-export appends fresh tensors without truncating the original, orphaning a prefix. `qwen14b-zp` is exactly **50.0% dead space**: ``` 920 initializers, sum of declared lengths : 8,329,906,176 ( 8.330 GB) actual model.onnx.data size : 16,652,453,888 (16.652 GB) unreferenced : 8,322,547,712 (50.0%) ``` Not fragmentation — the referenced blobs are perfectly contiguous (**0 gaps between consecutive blobs, 0 trailing bytes**), but the first one begins at offset `8,322,547,712`. Cross-checked by data type: 339 UINT8 (7.397 G elements) + 581 FLOAT16 (0.467 G elements) + 2 INT64 = 8.33 GB, matching the declared lengths exactly. ### Impact The runtime believed this model had **2.00× more weights than it does**, and strategy inference (#752), budget resolution, and `fits_resolved_device_budget` all consume that number. Verified on hardware, before and after: ``` before: total_weight_bytes = 16,653,143,582 after : total_weight_bytes = 8,330,595,870 (= my independent protobuf sum + the graph file) generated text unchanged ``` Most consequentially, this model is over the resolved device budget (7.727 GB) by **0.599 GB — not by the ~8.9 GB the old figure implied**, a 14× error in the margin. "Nearly fits" and "permanently stream the whole model" are different engineering problems. It also closes the arithmetic hole I hit while building the #837 item-3 ceiling: measured traffic (4.11 GB/step) appeared to be *below* the theoretical floor for a full per-step scan (10.53 GB/step), which is impossible. With the correct `W = 8.33 GB` the floor is 2.21 GB/step and the measurement sits **1.86× above** it — so item 3 has roughly 1.9 GB/step of real headroom, where the inflated `W` made the workload model look simply broken. The 922 initializers against ~867 lookups/step also stop being a puzzle: essentially every weight is touched every step, so `W_eff ≈ W`. There was never a mystery 6.4 GB of untouched weights; there was 8.3 GB of dead file. ### Implementation `onnx_runtime_loader::weights::referenced_weight_bytes()` sums what initializers actually reference, splitting inline from external payloads. Externals use the declared `length`, falling back to the geometry implied by shape and dtype — the same rule `resolve_initializer` applies. It parses **only the graph protobuf** (a few MB even when weights are tens of GB) and never opens, maps, or reads the weight blobs, so the load-time budget path stays cheap. The graph file is still charged whole (it carries inline payloads plus the protobuf); only the external blobs are narrowed. Falls back to file size if the graph cannot be read or decoded. Warns when file size exceeds referenced bytes by more than 10%, because a materially oversized blob is a defective export and paying for it silently in budget decisions, disk, and page cache is worse than saying so. ### Tests New `unreferenced_external_regions_are_excluded_from_the_weight_total` builds a minimal ONNX graph with one 1,024-byte external initializer at offset 4,096 against a 5,120-byte blob, pinning that the orphaned 4 KiB prefix is not charged while the file-size upper bound still sees the whole file. The protobuf is hand-encoded so the test owns the exact layout without adding a `prost` dependency to this crate. Existing tests are unchanged and still pass — they use non-ONNX placeholder files, so they now also cover the file-size fallback path. ### Validation - `cargo fmt -p onnx-genai-ort -p onnx-runtime-loader -- --check` — pass - `cargo clippy --all-targets -p onnx-genai-ort -p onnx-runtime-loader -- -D warnings` — pass - `cargo test -p onnx-genai-ort --lib` — **117 passed** - Real-model verification on qwen14b-zp shown above ### Note for readers of today's other PRs #836, #841, #843 and #844 quote `total_weight_bytes = 16,653,143,582`. Their **counters remain valid** — `htod_bytes`, `page_ins`, `hit_rate`, committed bytes and token IDs were measured directly. What is wrong is any *ratio against total weights*, and the "16.6 GB of weights against a 7.7 GB budget" shorthand used throughout. Repacking the local `qwen14b-zp` export to drop the orphaned 8.32 GB is worth doing **after** this lands, so the fix keeps its only reproducer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
…ected (negative result) (#886) ## #837 item 3 — residency policy gap: attribution (the deliverable) + a rejected design, measured Refs #837 (item 3 only; items 1 & 2 landed in #842/#843). **Do not merge / do not close #837** — this is a report PR. A rejected design with its measurements attached is more useful than a verdict alone, so both are here. --- ## 1. The deliverable: the 1.158 GB/step, attributed `bypassed_page_ins` was **704** in the last measured run and had never been explained. It now is. A "bypass" is a page-in that `admit_committed_span` maps, H2D-fills, hands to the kernel transiently, then frees on `Drop` — **it is counted in `htod_bytes` but never joins the resident set**, so its bytes re-stream every decode step. I added byte-weighted accounting for it (`GlobalOffloadStats::bypassed_page_in_bytes`, `bypassed_byte_share()`, printed by `profile_native`). Measured on `qwen14b-zp`, managed path, **solo** (`nvidia-smi --query-compute-apps` verified empty immediately before the run), 16 tokens: | metric | value | |---|---| | `byte_hit_rate` | **70.16 %** | | `htod_bytes_per_token` | **2.349 GB** | | `bypassed_page_ins` | 336 (**11 %** of page-in *events*) | | `bypassed_page_in_bytes` | 16.77 GB / 16 tok | | **`bypassed_byte_share`** | **44.63 % of streamed bytes ≈ 1.05 GB/step** | | avg bypass size | 49.9 MB (large projections) | | avg retained page-in | 7.8 MB (norms / biases) | **Bypass traffic is 11 % of page-in events but 44.63 % of streamed bytes ≈ ~1.05 GB/step ≈ ~90 % of the 1.158 GB/step recoverable gap.** This converts an unexplained counter into the explanation for the whole 1.97×-above-floor figure. It is exactly the count-vs-byte trap #837 warned about: size-blind `StableResident` retains the small tensors (norms/biases) that win the first-fit race for leftover budget and re-streams the large projections transiently, every step. **This attribution stands on its own even with the policy change below abandoned, and is what redirects whoever picks item 3 up next.** --- ## 2. The obvious fix, implemented and rejected: byte-aware residency Env knob `ONNX_GENAI_WEIGHT_OFFLOAD_BYTE_AWARE` (default-**OFF**, opt-in). When enabled, admission keeps the largest tensors resident: retain an incoming tensor iff it is strictly larger than the smallest evictable resident, evicting smallest-first (strictly-smaller only, to avoid thrash when the large set alone exceeds budget). ### Before / after (managed arm, each run solo, `nvidia-smi` clear beforehand) | config | tokens | token IDs | offload engaged | `byte_hit_rate` | `htod/token` | |---|---|---|---|---|---| | byte-aware **OFF** (baseline), steady, graph ON | 16 ✓ | `[96347, 3375, 724, 11, 358, 2776, 14589, 311, 6723, 429, 498, 3003, 2581, 6617, 315, 752]` | yes (`page_ins=3008`) | 70.16 % | 2.349 GB | | byte-aware **ON**, steady, graph ON | **3 ✗** | — early EOS | yes | — (aborted) | — | | byte-aware **ON**, steady, graph OFF | **3 ✗** | — early EOS | yes | — (aborted) | — | | byte-aware **ON**, non-steady | 16 ✓ | identical to baseline | **no** (`page_ins=0`) | n/a | 0 | The "after" arm is being **abandoned**: the policy cannot be safely enabled, so **no bytes were moved**. The counters are attached because a measured rejection is the useful artifact. ### What went wrong: silent numeric corruption, not an error > **Byte-aware residency corrupts decode output — token-identity violated, generation collapses to 3 tokens — whenever offload actually engages on the managed streaming path, both with and without CUDA graph. The default-off path is unaffected.** This is **silent numeric corruption, not an admission error**, and that is *proven*, not inferred, from the profiler's error-propagation structure: `generate_with_callback` is `?`-propagated with `.context("steady measured generation")` **before** the `bail!("generation emitted N tokens")` check. The message we observed is the `bail!`, which means generation returned **`Ok` with only 3 tokens** (an early EOS from corrupted logits) — a surfaced `WeightHandleError` would have produced the `.context(...)` error instead. The runs that stayed byte-identical are exactly those where offload never engaged (`page_ins=0`), i.e. the branch never ran. **The token-identity hard constraint is what caught this.** That check exists precisely so a plausible optimisation cannot ship while silently producing wrong output — and this is the first time it has fired on a real proposal in this line of work. Without it, byte-aware would have "improved" the count-based hit-rate report while emitting garbage. ### Mechanism, as far as I got (and a defect worth flagging) A residency policy decides *what is resident*, not *what is computed*. Both the resident-hit and bypass paths fill the correct bytes at a **stable per-tensor VA**; every eviction here only targets `strong_count == 1` pages, and the admit loop **drains both the compute and copy streams before any eviction**. The only behavioural change vs baseline is the eviction *target*: `smallest_evictable` picks the smallest-by-bytes page, whereas `next_evictable_index` (used by both `Lru` and `StableResident`) picks the **front-of-order oldest** evictable page. Under those guards, a change of eviction *target* **should be value-neutral**. That it corrupts values implicates **state that depends on eviction *order*** — most plausibly the physical granule / retained-handle pool accounting — rather than the policy decision itself. Reproduction with **CUDA graph OFF** rules out captured-VA baking as the sole cause. I could **not** isolate, within this task, whether this is: - **(a)** a bug in the byte-aware admission loop, or - **(b)** a **latent defect in the existing offload path**, merely *exposed* (not caused) by reordering evictions — i.e. something downstream assumes the front-of-order eviction discipline and reads/reuses a physical granule it should not when that order changes. **Possibility (b) is the more valuable one and I cannot exclude it.** If it holds, it is a latent correctness defect independent of this knob, and should be filed and investigated separately (an instrumented run dumping the resident set + per-tensor checksums across steps under both eviction orders would decide it). Flagging rather than asserting. --- ## 3. Conclusion and where the effort should go The count-vs-byte residency gap **cannot be closed by an eviction-policy reorder**. Closing it needs the structural lever #837 deferred: a **dedicated transient staging zone** so a large tensor can be handed to the kernel *without* displacing a resident page (today even a bypass must evict a resident for physical room). That redirects effort toward **#864's hybrid** and **#750 batching** — as the issue anticipated, and as `batch-stage2a` independently corroborated (batch-N amortizes the weight stream exactly 1/N: 301→37.6 pages/token for N=1→8, and the byte counters are contention-invariant process-local EP accounting). ### #864 raises the stakes for item 3, it does not lower them Because the managed residency cache only governs Linux / forced-managed Windows / the #864 hybrid (see scope), one might think item 3 is now niche. The opposite: #880 measured the resident hot set at **~9.7× realistic / ~15–24× at large N** against ~5.6 GB/s host-mapped reads. **Which weights are resident is worth more than it was when this issue was filed** — a well-chosen resident hot set is exactly the edge the hybrid claims over the driver's blind WDDM choice. The attribution in §1 is the input to choosing that hot set well. --- ## Scope Since **#874** the **Windows default is the WDDM zero-copy path**, which has **no residency cache** (the driver chooses). So item 3 governs behaviour **only on the managed path**, which is the arm measured here (`ONNX_GENAI_MANAGED_WEIGHT_STREAMING=1`): **Linux** (managed streaming is the only way to run over-budget), **Windows with `ONNX_GENAI_MANAGED_WEIGHT_STREAMING=1`** / explicit `--vram-limit`, and prospectively the **#864 hybrid**. ## Hard constraints / gates - **Token identity:** default (byte-aware OFF) path byte-identical — `[96347, …, 752]`, matches prior baselines. (Byte-aware ON deliberately fails this; that is the finding, and it is default-OFF.) - Baseline: `captures=2` (>0), `fallbacks=0`, `oversubscribed_bytes=0`, `peak_committed_physical_bytes=7,725,907,968 < managed_limit=7,726,753,178`, `ref_underflows=0`, `byte_underflows=0`, `unaccounted_committed_bytes=0`. - No assertions added in `Drop`. - **Mandatory** `mobius_seqmajor_growth_parity_native_cuda` (`--ignored --test-threads=1`): **GREEN**, run **solo** with `nvidia-smi` verified clear (treated as binding per #851). - `cargo fmt` (per-package), `cargo clippy -p onnx-runtime-ep-cuda --features cuda -D warnings`, `cargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -D warnings`, `cargo test -p onnx-runtime-session`, `cargo test -p onnx-runtime-ep-cuda --lib weight_paging::tests` (18 pass): all green. - CPU unit tests exercise the size-greedy admission *decision function* on an abstract model and are explicitly documented **not** to validate the GPU path. ## Measurement hygiene Wall-clock is deliberately **not** headlined (WDDM paging our own memory ranges 3.9–28 tok/s here, #863). Each GPU run verified `nvidia-smi --query-compute-apps` empty immediately beforehand; GPU serialized with siblings `batch-stage2a` / `zerocopy-kernel`. One transient contender appeared during a short non-steady probe and is noted; it did not touch the steady A/B arms, which were clean. --------- Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
Refs #837 (item 2 — host→device streaming efficiency). Do not merge yet; do not close #837.
What this changes
CudaWeightResidency::resident_mapped(the hot page-in path) page-locked a freshcuMemHostAllocstaging buffer and freed it withcuMemFreeHoston everypage-in — 5,535 alloc/free pairs per 16-token window on qwen14b-zp. Those calls
take the CUDA context lock to pin/unpin OS pages and serialize against the
device, defeating the async, fence-ordered page-in the design already
implements correctly.
This adds a bounded reuse pool (
crate::pinned_pool::PinnedStagingPool) anddraws the staging buffer from it instead of allocating one per miss.
Functions/areas I touched (kept deliberately narrow so this merges cleanly
against
vmm-churn's work in the same file):crates/onnx-runtime-ep-cuda/src/pinned_pool.rsCudaWeightResidencystruct + its two constructors (new,new_leased) — one fieldCudaWeightResidency::resident_mapped— acquire-from-pool instead ofalloc_pinnedGlobalOffloadStats+profile_nativereportingNot touched:
admit_committed_span,admit,drain_copy_stream,resident_vmm_with's body (beyond passing a pooled buffer into itsfillclosure), and all VMM commit/release/mapping code (
vmm-churn's domain).ONNX_GENAI_WEIGHT_OFFLOAD_ASYNC_PAGEINsemantics are unchanged.Pooling design and bounded memory cost
Mutex<Vec<PinnedStaging>>free-list, keyed by capacity witha reuse-if-large-enough rule (smallest fit is handed out; a request larger
than every retained buffer allocates a fresh one).
host bytes; any buffer that would breach either bound is dropped (freed) on
release rather than retained.
under the residency lock, so at most one staging buffer is live at a time and
the pool converges to a single retained buffer sized to the largest page-in
(tens of MiB for qwen14b). The 8-buffer / 512-MiB ceiling only caps a
pathological worst case; it never trades unbounded host RAM for speed.
Why return-to-pool cannot happen before the copy fence (safety)
The staging buffer is the source of the H2D copy, so returning it to the
pool before the copy's DMA read completes would silently corrupt weights. This
is safe because every copy on the offload page-in path is host-blocking:
resident_mappedissue the copy throughCudaRuntime::htod_async_elapsed_ms, which records a CUDA end event after thecuMemcpyHtoDAsyncand callsevent.elapsed_ms.Event::elapsed_mscallsend.synchronize()(verified incudarc-0.19.8/src/driver/safe/core.rs), which blocks the host thread untilthe end event — and therefore the copy — has completed.
branch the
PooledStagingguard is moved into thefillclosure and droppedwhen the closure ends (strictly after the blocking copy); on the non-VMM
branch the buffer is
released afterupload_staged_async(alsohost-blocking) returns.
So on the host timeline the DMA read has provably finished before the buffer is
reusable.
PooledStaging::Dropperforms only the return-to-pool and neverasserts (an assert in
DroptriggersSTATUS_STACK_BUFFER_OVERRUNhere).htod_async+deferred fence, the return-to-pool must be re-ordered after that fence is awaited
(e.g. via
drain_copy_stream) or the safety argument no longer holds. This isdocumented at the top of
pinned_pool.rs.Regression guard
pinned_pool::tests::reused_buffers_do_not_reallocate_per_page_indrives 64same-size acquire→fill→release cycles and asserts exactly one real pinned
allocation with 63 reuses — i.e. a per-page-in pinned allocation cannot silently
return. A deterministic
pinned_alloc_callscounter (per-instance + global) isalso surfaced in
profile_nativeso the property is visible at benchmark scale(
pinned_alloc_callsmust stay far belowpage_ins).Measurements (qwen14b-zp, 16 tokens, release, CUDA_GRAPH=1, VMM=1)
This box's wall-clock is extremely noisy (an identical after-binary produced
both 0.02 tok/s under external contention and 0.55–0.59 tok/s clean). So the
result is led by the deterministic, contention-immune counter, and bandwidth is
reported from clean, uncontended matched pairs run back-to-back on an
otherwise-idle GPU (verified 0 other compute PIDs).
¹ Baseline main has no pool; the equivalent "before" is one
cuMemHostAlloc/cuMemFreeHostpair per page-in = 5,535 allocations and 5,535 frees per window.
Token IDs byte-identical across every run (baseline and pooled):
[96347, 3375, 724, 11, 358, 2776, 14589, 311, 6723, 429, 498, 3003, 2581, 6617, 315, 752]Reading the result honestly
allocations for 5,535 page-ins — the pool serves 100% by reuse. This is the
deterministic proof the defect is fixed.
and ~5.5 → 7.3 GB/s in a second — a consistent uplift, but the magnitude is
noisy and should not be over-claimed.
vram_free_mscollapsing from 16–40 s to a stable ~5.6 s is a secondaryeffect I did not target (it's
vmm-churn's item 1). The plausible mechanism:removing ~5,535
cuMemFreeHostcalls — which take the context lock andserialize against the device — stops interleaving driver-lock stalls into the
unmap path. Flagged as a finding for
vmm-churn, not a claim I optimized it.What still dominates (in scope to report, not fixed here)
The offload page-in is fully synchronous:
htod_async_elapsed_mshost-blocks on the copy's end event, so
materialize_ms(host fill, ~7–9 s) andhtod_ms(device copy, ~7–9 s) sit on the critical path back-to-back with nooverlap between host fill and device transfer, and none with compute — despite
the async/fence infrastructure existing. That serialized fill+copy, not pinned
alloc/free, is now the largest streaming term. Overlapping them (double-buffered
fill while the previous copy is in flight, and a genuinely non-blocking copy so
transfer overlaps decode compute) is the next lever, and it interacts with
vmm-churn's coarser mappings. Note the current instrumentation(
htod_async_elapsed_ms) is itself what forces the copy synchronous — measuringhonest per-copy ms and preserving overlap are in tension here.
Fence-safety made structural (review follow-up)
The invariant is: a staging buffer is the source of an asynchronous H2D
copy and must not be reused or freed until that copy has actually completed,
not merely been enqueued. Today that holds because
htod_async_elapsed_mshost-blocks: cudarc's
Event::elapsed_ms(cudarc-0.19.8/src/driver/safe/core.rs:603-611)calls
end.synchronize(), and its own doc comment says it "does block the hostuntil the DMA completes." So on this path the copy is provably finished before
the buffer can return to the pool.
Per review, that coupling is now enforced by the type system, not by a
comment, because removing exactly that blocking is the scheduled next piece of
work on this path (see "What still dominates" — the instrumentation is the
serialization):
htod_async_elapsed_msnow returns aCopyCompletedwitness — a zero-sizedtoken whose only field is private to the
runtimemodule, so it can be mintedonly by a copy primitive there that host-synchronizes the copy before
returning. Nothing outside
runtimecan fabricate one.PinnedStagingPool::releaseandPooledStaging::retire— consumes aCopyCompleted. Reuse is thereforeunreachable without presenting proof the copy finished.
htod_async+ deferred fence produces nowitness at the reuse site, so the code fails to compile until the author
threads a completion witness through after awaiting that fence. The unsafe
combination cannot be reached by accident.
Which construction and why.
Dropcannot consume an argument, so aDrop-based return-to-pool could never require the witness. I therefore madethe return an explicit method (
retire(self, CopyCompleted)) and demotedPooledStaging::Dropto a leak-safe fallback that frees the buffer (dropsthe inner pinned block) rather than returning it to the pool. Consequence: if a
future refactor forgets to
retire, the buffer is freed and re-allocated nexttime — a performance regression that the
pinned_alloc_callscounter and itsregression test catch immediately — but it can never silently reuse a buffer
whose copy is still in flight.
Dropnever asserts (STATUS_STACK_BUFFER_OVERRUNon this codebase). Unit tests that exercise the pool without a real copy use a
#[cfg(test)] pub(crate)CopyCompleted::new_for_test(), which keeps the testshonest about requiring a witness without granting non-test code the ability to
forge one.
This change is compile-time only — no runtime behavior change — so the
before/after measurements above remain valid and were not re-run for numbers
(the mandatory memory-governance parity gate was re-run and passes).
Validation (all green, local)
cargo fmt -p onnx-runtime-ep-cuda -- --check,-p onnx-genai-bench -- --checkcargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warningscargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -- -D warningscargo test -p onnx-runtime-session(all pass)cargo test -p onnx-runtime-ep-cuda --features cuda --lib pinned_pool(4/4 pass, incl. regression guard)cargo test -p onnx-genai-engine --features cuda,native-backend --test mobius_seqmajor_growth_parity_native_cuda -- --ignored --test-threads=1(2/2 pass — memory-governance parity)Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com