Skip to content

#837 item 3: attribute residency streaming gap; byte-aware policy rejected (negative result) - #886

Merged
justinchuby merged 2 commits into
mainfrom
squad/residency-policy-gap
Aug 13, 2026
Merged

justinchuby merged 2 commits into
mainfrom
squad/residency-policy-gap

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 13, 2026 •

Copy link
Copy Markdown
Owner

#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 Latent gap: seq-major capture-retention proof covers KV binding pointers only, not weight buffers (original flaky-gate claim RETRACTED — contended measurement) #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.

… policy

Attribution (managed streaming, qwen14b-zp, solo, nvidia-smi clear):
add byte-weighted accounting for "bypassed" page-ins - misses that are
mapped, H2D-filled, handed to the kernel transiently, then freed without
ever joining the resident set, so their bytes re-stream every decode step.

  bypassed_page_ins        336  (11% of page-in events)
  bypassed_page_in_bytes   16.77 GB over 16 tokens
  bypassed_byte_share      44.63% of streamed bytes  (~1.048 GB/step)

That accounts for ~90% of the 1.158 GB/step recoverable gap: the
size-blind scan-resistant policy retains small tensors (avg 7.8 MB:
norms/biases) that win the first-fit race and re-streams the large
projections (avg bypass 49.9 MB) transiently every step.

New instrumentation (safe, default path byte-identical):
- GlobalOffloadStats::bypassed_page_in_bytes + bypassed_byte_share()
- profile_native prints both in weight_offload_cache

Byte-aware residency (EXPERIMENTAL, default-OFF, measured UNSAFE):
env knob ONNX_GENAI_WEIGHT_OFFLOAD_BYTE_AWARE keeps the largest tensors
resident (evict smallest-first, strictly-smaller only). On-device A/B
shows that whenever it actually engages (offload active) it violates the
token-identity hard constraint - greedy decode collapses to 3 tokens vs
16 - both with and without CUDA-graph capture. The failure is an early
EOS, not an admission error, so size-greedy eviction corrupts decode
numerics: the scan-resistant recency discipline is load-bearing for
correctness on the managed path, not just for hit rate. Kept default-OFF
and wired only so the rejected approach and its evidence are reviewable.

Conclusion: the count-vs-byte residency gap cannot be closed by an
eviction-order change alone; it needs the structural transient-staging
zone deferred by #837 (hand a large tensor to the kernel without evicting
a resident page). CPU unit tests confirm the size-greedy decision on an
abstract model but explicitly do not validate the GPU path.

Mandatory mobius_seqmajor_growth_parity gate: GREEN solo.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sharpen the byte-aware knob doc with the proven finding: the failure is
SILENT numeric corruption, not an admission error. Proven by the
profiler's error-propagation structure - generate_with_callback is
?-propagated with .context("steady measured generation") before the
bail!("generation emitted N tokens") check, so the observed bail! means
generation returned Ok with 3 tokens (early EOS), not a surfaced
WeightHandleError.

Record why a residency policy can alter values at all: under the
documented guards (evict only strong_count==1 pages after draining both
compute and copy streams; stable per-tensor VA; both hit and bypass fill
correct bytes) a change of eviction TARGET should be value-neutral, so
the corruption implicates state that depends on eviction ORDER - most
plausibly the physical granule / retained-handle pool accounting. Graph
OFF reproduction rules out captured-VA baking as the sole cause. Whether
this is a bug in the byte-aware loop or a latent defect in the existing
offload path (exposed, not caused, by reordering evictions) was not
isolated and warrants separate investigation.

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

Copy link
Copy Markdown
Owner Author

Validated and merging. Gates on a worktree with main merged in: cargo fmt -p onnx-runtime-ep-cuda -p onnx-genai-bench -- --check pass, cargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings pass (the exact CI gate), byte-aware unit tests 3 passed.

The attribution is the deliverable, and it is a good one. 11% of page-in events carrying 44.6% of streamed bytes is the count-vs-byte trap in its purest form: size-blind StableResident retains 7.8 MB norms and biases that win the first-fit race for leftover budget, while 49.9 MB projections re-stream transiently every step. That converts a counter nobody had explained into ~90% of the 1.158 GB/step gap. Adding bypassed_page_in_bytes / bypassed_byte_share so it stays visible is the right move — the next person does not have to rediscover it.

The negative result is stronger than a null result, because you isolated why. This table is what makes it conclusive:

The only runs where byte-aware ON stays byte-identical are those where offload never engaged (page_ins=0) — i.e. the branch never ran. Every run where it did engage collapsed generation to 3 tokens.

That rules out flakiness and rules out capture (it reproduces graph-on and graph-off), and identifying the failure as a normal early-EOS from profile_native::main rather than an admission error is what pins it on decode numerics rather than on the allocator. A weaker report would have said "byte-aware didn't help".

And the mechanism is the part with lasting value: the scan-resistant recency discipline is load-bearing for correctness on the managed path, not merely for hit rate — reordering evictions by size corrupts output. I would not have predicted that, and it is exactly the kind of thing that would have been shipped by someone optimizing the count-based hit rate in good faith.

Two judgements I want to endorse explicitly:

The conclusion redirects effort correctly. "The count-vs-byte gap cannot be closed by an eviction-order reorder alone — it needs a transient staging zone so a large tensor can be handed to the kernel without displacing a resident page" is a real finding about the shape of the remaining work, not a shrug. It also raises the value of #864's hybrid, where a zero-copy cold read is precisely a transient hand-off that displaces nothing — measured at ~5.6 GB/s against ~100–133 GB/s resident in #880.

Scope caveat correctly stated: since #874 the Windows default is WDDM zero-copy with no residency cache, so this governs Linux, forced-managed Windows, and the hybrid. All measurements on the managed arm, which is the honest arm to measure.

Merging as a report. #837 stays open; item 3's remaining lever is now named (transient staging), not vague.

@justinchuby
justinchuby merged commit 7fa8df8 into main Aug 13, 2026
13 checks passed
@justinchuby
justinchuby deleted the squad/residency-policy-gap branch August 13, 2026 14:55
@codecov

codecov Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.79%. Comparing base (ab75a12) to head (2198ad4).
⚠️ Report is 12 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #886      +/-   ##
==========================================
- Coverage   77.79%   77.79%   -0.01%     
==========================================
  Files         354      354              
  Lines      148003   148003              
  Branches   148003   148003              
==========================================
- Hits       115137   115134       -3     
- Misses      28375    28377       +2     
- Partials     4491     4492       +1     
Flag Coverage Δ
cli-ort-linux 83.42% <ø> (ø)
cli-ort-windows 83.01% <ø> (ø)
mlas 80.96% <ø> (-0.14%) ⬇️
offline 77.53% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 1 file with indirect coverage changes

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

@github-actions

Copy link
Copy Markdown

🔴 Benchmark Regression Detected

Comparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).

ℹ️ Absolute times are informational only — they vary with runner load. The % change column is the reliable signal because both sides ran under identical conditions.

Status Scenario Base PR Change
🔴 matmul/medium_generic_f16_threads=8/32x512x512 37.12 µs 52.89 µs +42.5%
🔴 sampling_latency/min_p_per_token 235.70 µs 324.76 µs +37.8%
🔴 matmul/medium_generic_bf16_threads=1/32x512x512 539.89 µs 734.71 µs +36.1%
⚠️ gather/medium_f32_threads=1-internal/32768 4.48 µs 5.37 µs +19.9%
⚠️ sampling_latency/top_k_per_token 59.76 µs 70.01 µs +17.1%
⚠️ sampling_latency/top_p_per_token 463.78 µs 539.39 µs +16.3%
⚠️ qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline 3.80 ms 4.41 ms +16.0%
✅ gather/small_bf16_threads=1-internal/4096 516.7 ns 583.9 ns +13.0%
✅ qwen3_sampling_processors/top_k_partial_selection 152.34 µs 170.51 µs +11.9%
✅ matmul/large_generic_bf16_threads=1/32x1024x1024 2.15 ms 2.35 ms +9.6%
✅ qwen3_sampling_processors/top_k_full_sort_baseline 2.41 ms 2.54 ms +5.2%
✅ sampling_latency/greedy_per_token 3.60 µs 3.75 µs +4.0%
✅ matmul/medium_generic_f16_threads=1/32x512x512 36.06 µs 36.46 µs +1.1%
✅ kv_cache/alloc_dealloc_pages 41.32 µs 41.31 µs -0.0%
✅ tokenization/decode_tokens_per_second 7.32 ms 7.26 ms -0.7%
✅ gather/small_f16_threads=1-internal/4096 565.7 ns 554.2 ns -2.0%
✅ block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 85.32 µs 83.24 µs -2.4%
✅ qwen3_sampling_processors/top_p_fast_after_top_k 576.31 µs 561.60 µs -2.6%
✅ tokenization/encode_tokens_per_second 459.35 µs 446.24 µs -2.9%
✅ block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 66.79 µs 64.57 µs -3.3%
✅ grammar_masking/llguidance_compute_mask/32 84.22 µs 79.37 µs -5.8%
✅ qwen3_sampling_processors/top_k_top_p_full_sort_baseline 6.41 ms 6.04 ms -5.9%
✅ logit_processing/seven_processor_chain_per_step 351.74 µs 327.84 µs -6.8%
✅ matmul/large_generic_f32_threads=1/32x1024x1024 10.57 ms 9.78 ms -7.5%
✅ add/medium_bf16_threads=1-internal/262144 3.26 ms 2.99 ms -8.4%
✅ add/large_f32_threads=1-internal/4194304 48.83 ms 44.72 ms -8.4%
✅ add/medium_f16_threads=1-internal/262144 3.04 ms 2.78 ms -8.5%
✅ matmul/small_generic_f32_threads=8/1x256x256 50.86 µs 46.45 µs -8.7%
✅ add/large_f16_threads=1-internal/4194304 49.48 ms 44.97 ms -9.1%
✅ matmul/small_generic_f16_threads=1/1x256x256 35.86 µs 32.11 µs -10.5%
✅ add/small_f16_threads=1-internal/1024 15.50 µs 13.85 µs -10.7%
✅ reduce_mean/medium_f32_threads=1-internal/65536 289.45 µs 256.08 µs -11.5%
✅ matmul/small_generic_f32_threads=1/1x256x256 43.74 µs 38.65 µs -11.6%
✅ matmul/large_generic_f16_threads=8/32x1024x1024 104.15 µs 91.36 µs -12.3%
✅ reduce_mean/large_f32_threads=1-internal/262144 1.21 ms 1.06 ms -12.7%
✅ matmul/large_generic_bf16_threads=8/32x1024x1024 1.98 ms 1.73 ms -12.9%
✅ gather/small_f32_threads=1-internal/4096 805.9 ns 698.7 ns -13.3%
✅ matmul/small_generic_bf16_threads=1/1x256x256 38.43 µs 33.31 µs -13.3%
✅ add/medium_f32_threads=1-internal/262144 3.26 ms 2.81 ms -13.7%
✅ qwen3_sampling_processors/top_k_top_p_fast 732.50 µs 631.99 µs -13.7%
✅ matmul/small_generic_bf16_threads=8/1x256x256 42.90 µs 36.91 µs -14.0%
✅ matmul/medium_generic_f32_threads=1/32x512x512 2.73 ms 2.33 ms -14.5%
✅ matmul/small_generic_f16_threads=8/1x256x256 39.58 µs 33.78 µs -14.7%
🟢 gather/medium_f16_threads=1-internal/32768 2.78 µs 2.35 µs -15.3%
🟢 matmul/large_generic_f32_threads=8/32x1024x1024 6.01 ms 5.06 ms -15.8%
🟢 block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 513.28 µs 429.89 µs -16.2%
🟢 add/small_bf16_threads=1-internal/1024 16.76 µs 14.02 µs -16.4%
🟢 matmul/large_generic_f16_threads=1/32x1024x1024 106.72 µs 86.49 µs -19.0%
🟢 gather/medium_bf16_threads=1-internal/32768 2.88 µs 2.32 µs -19.4%
🟢 add/large_bf16_threads=1-internal/4194304 54.82 ms 44.12 ms -19.5%
🟢 gather/large_bf16_threads=1-internal/131072 18.55 µs 14.87 µs -19.9%
🟢 reduce_mean/small_f32_threads=1-internal/4096 20.16 µs 16.06 µs -20.3%
🟢 add/small_f32_threads=1-internal/1024 330.1 ns 240.4 ns -27.2%
🟢 gather/large_f32_threads=1-internal/131072 46.05 µs 32.99 µs -28.4%
🟢 matmul/medium_generic_bf16_threads=8/32x512x512 613.67 µs 439.42 µs -28.4%
🟢 block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 936.44 µs 656.57 µs -29.9%
🟢 matmul/medium_generic_f32_threads=8/32x512x512 1.31 ms 915.82 µs -30.3%
🟢 gather/large_f16_threads=1-internal/131072 21.27 µs 14.75 µs -30.7%
🟢 block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 92.95 µs 60.00 µs -35.5%

Visual flags: ⚠️ ≥ 15% slower, 🔴 ≥ 30% slower — calibrated against measured runner noise (~27% worst-case on multi-threaded matmul)

Host info
CPU: Apple M1 (Virtual)
Cores: 3
OS: Darwin 25.5.0 arm64
Rust: rustc 1.97.1 (8bab26f4f 2026-07-14)
Load avg: { 4.30 4.13 5.86 }
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 added a commit that referenced this pull request Aug 13, 2026
## Investigation of #888 — which explanation, with evidence

**Verdict: explanation (1).** Decode correctness does **not** depend on
eviction order. The #886 byte-aware corruption is caused by its
**retain-vs-bypass flip**, not by reordering evictions. #886's
speculation that "the scan-resistant recency discipline is load-bearing
for correctness" is **refuted** — recency is load-bearing for *hit rate*
only. The defect is in the *rejected* change, not the shipped path.

This is an investigation PR. It changes **no shipped/default
behaviour**; every knob added is default-OFF and byte-identical on the
default size-blind path (mirroring how #886 kept byte-aware wired for
reviewability). **Do not close #888.** Do not merge without a squad
review.

### The discriminating experiment

A residency policy changes two independent things: *which* weights are
resident, and *in what order they evict*. Byte-aware changes **both** at
once — it starts **retaining** large tensors the shipped path streams
transiently, **and** it evicts smallest-first. To separate them,
`ONNX_GENAI_WEIGHT_OFFLOAD_EVICT_ORDER` changes only the eviction
**victim** while keeping the shipped always-bypass decision.

Runs: qwen14b-zp, `ONNX_GENAI_MANAGED_WEIGHT_STREAMING=1`, `--tokens 16
--steady`, each arm solo with `nvidia-smi` verified clear beforehand.
Baseline tokens `[96347, 3375, 724, 11, 358, 2776, 14589, 311, 6723,
429, 498, 3003, 2581, 6617, 315, 752]`.

| arm | tokens | byte_hit_rate | evictions | page_ins | verdict |
|---|---|---|---|---|---|
| baseline (LRU, byte-aware OFF) | **16 ✓** | 70.16% | 2672 | 3008 |
reference |
| evict order = **MRU** (reverse recency) | **16 ✓ identical** | 79.15%
| 1952 | 2160 | clean |
| evict order = **Smallest** (byte-aware's exact victim) | **16 ✓
identical** | 71.77% | **10192** | — | clean |
| byte-aware ON (graph ON) | **3 ✗** early EOS | — | — | >0 | corrupt |
| byte-aware ON (graph OFF) | **3 ✗** early EOS | — | — | >0 | corrupt |

Two independent, still-correct eviction *orders* — including
byte-aware's **exact** smallest-first victim under extreme churn (10,192
evictions, hit rate driven down to ~22% in a harsher variant) — stay
**byte-identical** with clean ledgers. **If any non-recency order
corrupted, that would be evidence for (2); none does.** Changing
eviction order alone is value-neutral. The corruption tracks *only* the
retain flip.

### Mechanism — what it is NOT

- **Not captured-VA baking** — graph-OFF corrupts too.
- **Not a copy/compute fence / WAR hazard** —
`ONNX_GENAI_WEIGHT_OFFLOAD_SYNC_BEFORE_FILL=1` (full drain of both
compute and copy streams before every page-in fill) does **not** fix it.
(The `htod_async_elapsed_ms` fill already host-syncs every page-in — the
class of confusion flagged in #837/#851 — so fences are not the issue.)
- **One concrete secondary bug found and confirmed to occur:** a
*slotted* key that later bypasses gets `stable_slot = true` (Drop
decommits-but-keeps-VA) yet is never re-inserted into `pages` (observed:
key=20, 4.4 MB). `ONNX_GENAI_WEIGHT_OFFLOAD_RETAIN_SLOTTED=1` closes it
— but closing it does **not** stop the corruption. So it is
real-but-**secondary**; unreachable on the shipped size-blind path.

### Honest boundary

The exact value-corruption *line* is not isolated. The primary path is
byte-aware retaining/re-admitting **large stable-slot tensors served as
hits** (no re-fill) across steps — most plausibly granule-ownership
under multi-granule decommit + reused-slot recommit. Settling it needs
**granule-level checksums across steps**; `compute-sanitizer` is
unavailable here (CUDA runtime via pip wheels, no toolkit), which is the
honest limit of what counters + targeted sync + code reading can prove.
What *is* proven is the discrimination above: the shipped path's
correctness is order-independent.

### Impact on #864 (and #866 / #750)

The shipped size-blind path is **safe** — it never retains large
tensors, so the buggy path is unreachable, and the probe confirms it
stays byte-identical under reorder + heavy churn. Therefore **#864 is
not blocked by an eviction-order invariant.** A hybrid that pins a
**static** hot set (retain once, never evict/re-admit; zero-copy the
cold remainder — exactly #864's stated shape) does not exercise the
corrupting retain-then-churn path. The hazard is specifically **evicting
and re-admitting large stable-slot residents**; any dynamic scheme that
churns large weight pages (byte-aware; potentially #866 elastic reclaim,
#750 admission) must validate token identity and should prefer a pinned
non-churning hot set.

### What changed (all default-OFF, byte-identical on default path)

- `weight_paging.rs`: `EvictOrderProbe` (lru|mru|smallest|largest) + env
parse + victim selection wiring; `SYNC_BEFORE_FILL` and `RETAIN_SLOTTED`
diagnostic knobs; one-shot slotted-bypass diagnostic; updated
`WEIGHT_OFFLOAD_BYTE_AWARE_ENV` doc with the #888 resolution; 1 unit
test.
- `lib.rs`/`provider.rs`/`memory_strategy.rs`: export + wire the probe
from env.
- `profile_native.rs`: env-echo at start of `main`.
- `.squad/decisions/inbox/copilot-eviction-order-correctness.md`:
decision note for #864/#866/#750 owners.

### Gates

fmt (per-package `--check`), clippy `-p onnx-runtime-ep-cuda --features
cuda -D warnings`, clippy `--locked --all-targets -p onnx-genai-engine
--features native-backend -D warnings`, `weight_paging::tests` (all
green incl. new test), and **`mobius_seqmajor_growth_parity_native_cuda
--ignored --test-threads=1` run SOLO with GPU clear → GREEN**. Final
baseline re-run with the final binary is byte-identical with
`captures>0`, `fallbacks==0`, `oversubscribed_bytes==0`,
`peak_committed_physical_bytes < managed_limit_bytes`, and
`ref_underflows/byte_underflows/unaccounted_committed_bytes == 0`.

Refs #888.

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

Four measured findings about the CUDA weight-residency policy lived only
in issue threads and PR bodies. Two of them cost a full investigation
each, and all four are the kind a future change would otherwise
rediscover the hard way. Adding them to §3.5, where someone editing
admission or eviction will actually see them.

**1. Decode correctness does *not* depend on eviction order
(#888/#892).** Default LRU, MRU (reverse recency) and smallest-first all
produce **byte-identical tokens** — the last driven to 10,816 page-ins
with the count hit rate at 22%. The earlier speculation that *"the
scan-resistant recency discipline is load-bearing for correctness"* is
**refuted**; recency is load-bearing for hit rate only.

**2. But evicting and *re-admitting* large stable-slot residents does
corrupt output.** A byte-aware policy (#886) silently collapsed greedy
generation 16 → 3 tokens whenever offload actually engaged, with **and
without** graph capture. The corruption tracks the **retain flip**, not
the reorder — isolated by a probe that changes only the eviction victim
while keeping the shipped always-bypass decision. Not captured-VA baking
(graph-OFF corrupts too), not a fence (a full drain of both streams
before every fill does not fix it). The exact value-corruption line is
**not** isolated, and the doc says so along with what would settle it.

The design consequence is the part worth having written down: a
**static** hot set — retain once, never evict, never re-admit — provably
does not exercise the corrupting path, which is why #864's hybrid is
safe *in that shape*. Any **dynamic** scheme that churns large weight
pages (#866's elastic reclaim, #750 admission, any future byte-aware
attempt) must **assert token identity rather than assume it**.

**3. The residency gap is an *admission* problem, not an eviction
problem (#837/#886).** ~90% of the recoverable 1.158 GB/step is bypass
traffic — 11% of page-in events carrying 44.6% of streamed bytes —
because `StableResident` bypasses on `!can_fit(bytes)`, which is
arrival-order first-fit. Small tensors (avg 7.8 MB) win the free budget;
large projections (avg 49.9 MB) then fail `can_fit` on every subsequent
step and re-stream forever, **never entering the eviction population at
all**. That is the mechanical reason a reorder cannot close the gap.

**4. Judge policy on `byte_hit_rate`.** The count-based rate once moved
57.09% → 81.31% while the gap to the streaming floor **widened** 1.78× →
2.30×.

Also records **#893** (MRU measured better than the shipped LRU:
page-ins 3,008 → 2,160, `byte_hit_rate` 70.16% → 79.15%, byte-identical)
as **open and explicitly not acted on** — one model, one budget, and
favourable, which is the shape of the results this project has had to
retract twice.

Docs only.

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

Refs #893. Documents the forced-managed Windows MRU/LRU sweep; no
runtime default or code changed.

| model | budget | LRU byte hit / H2D per token | MRU byte hit / H2D per
token | H2D delta |
|---|---:|---:|---:|---:|
| Qwen2.5 14B Q4 | 3 GB | 0.00% / 7.871 GB | 22.60% / 6.092 GB |
**-22.6%** |
| Qwen2.5 14B Q4 | 6 GB | 49.45% / 3.979 GB | 66.39% / 2.646 GB |
**-33.5%** |
| Qwen2 0.5B Q4 | 250 MB | 33.30% / 185.35 MB | 35.34% / 179.69 MB |
**-3.1%** |
| Qwen2 0.5B Q4 | 275 MB | 94.02% / 16.62 MB | 96.06% / 10.96 MB |
**-34.1%** |

## Verdict

MRU is directionally positive in all four pressured comparisons, so the
advantage survives a second, materially different geometry (24 layers /
hidden 896 versus 48 / 5120). Its magnitude is strongly budget-specific,
however, and the second model normally fits and required forced offload.
**Keep LRU as default.**

MRU operates within unchanged `StableResident` admission. It complements
scan resistance, but both attack the same cyclic-scan pathology, so
their wins must not be added as independent effects.

Eviction cannot admit a tensor refused by `!can_fit(bytes)`. Given
#886/#837's attribution of ~90% of the recoverable 1.158 GB/step gap to
bypass, the direct victim-order ceiling is about **0.116 GB/step (10%)**
of that gap, even when cache ratios improve substantially.

## Correctness and safety

- Token IDs byte-identical in every arm (14B matches the #893
reference).
- `nvidia-smi --query-compute-apps` empty immediately before every run;
none contended.
- Managed path explicitly enabled; CUDA graph captures > 0 and fallbacks
= 0.
- Oversubscribed bytes = 0; peak committed physical bytes below managed
limit.
- `ref_underflows`, `byte_underflows`, and `unaccounted_committed_bytes`
all 0.

## Validation

- `cargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings`
- `git diff --check`
- No package source changed, so no package `cargo fmt` target applies.

Full protocol, tables, bounds, and falsification criteria are in
`docs/research/mru-eviction-sweep.md`.

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

Several confident performance claims on this project have been
retracted, **including two by me on the same day**. Every one failed the
same way: a number was interpreted before the conditions that produced
it were established. The corrections were each worth more than the
original claims, but they cost real time, and they are avoidable.

There are skills for test discipline and error recovery, but nothing
about making a *measurement* trustworthy — which is where the actual
damage has been.

## What it records

Eight failure modes, each with the incident behind it, because the
abstract rule is forgettable and the incident is not:

1. **A knob you set produced the number you then interpreted.** #834
concluded the engine commits KV eagerly from `committed_len ==
capacity`, when the harness had set `min_bucket = capacity` and
`kv_capacity_bucket` is
`len.next_power_of_two().max(min_bucket).min(hard_max)`. The knob forced
the observation; at the default bucket the engine commits on demand —
the opposite conclusion.
2. **The instrument measured a real thing that was not the thing
named.** `h2d_enqueue_copy_ms` reported 1.7% and retired a line of work;
it bracketed an async enqueue returning, not a transfer completing.
Properly measured: 18.8%.
3. **Arithmetic that cannot close.** `total_weight_bytes` was 2.00× too
large (#853), exposed because measured traffic sat *below* its own
theoretical floor.
4. **A rate over a population with wildly different member costs.**
`hit_rate` rose 57.09% → 81.31% while the gap to the floor *widened*
1.78× → 2.30× (#869).
5. **Contention you did not know was there.** I filed a flaky-gate issue
and retracted it — two contending loops were mine (#851). The strict
re-run was 8/8.
6. **Wall-clock on a box that pages your own memory** — 3.9–28 tok/s
across identical configurations (#863).
7. **A proxy standing in for the real access pattern** — 11.41 GB/s
sequential proxy vs ~5.6 GB/s real strided GEMV (#877 → #880).
8. **A precondition observed rather than established** — a test that
hoped the allocator would reuse an address, green on glibc and vacuous
on Windows (#906).

Each carries the check that catches it, and the corollary I want most:
**build a control that could falsify you.** #891 measured `1/N`
amortization *and* showed it identical at `past_len` 0/512/2048, ruling
out a KV-pressure artifact — a result that survives a control it could
have failed is worth far more than one that merely looks good.

## Also recorded

**The reporting rules that made the good results good** — state the
conditions with the number; report a ceiling, not just a gain (#891's
`N_max ≈ 19 @ 2048 ctx`, #901's ~10% bound on eviction-order tuning,
both of which stopped someone spending a week for a tenth of the prize);
treat a truthful negative as first-class; and say when a favourable
result rests on one measurement — #901 measured MRU as better and still
recommended keeping LRU, which is the harder and more useful call.

**The correctness constraints that outrank performance**, led by
byte-identical token IDs — the check that caught a residency policy
silently collapsing generation 16 → 3 tokens with no error at all
(#886).

Every issue and PR number cited was verified to exist before committing,
which felt like the minimum for a document about not asserting things
you have not checked.

Docs only.

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

## Summary

Implements the zero-copy hybrid weight-residency mode for the CUDA EP
(#864),
behind a **default-OFF** `ONNX_GENAI_ZERO_COPY_HYBRID` knob, and reports
a
**measured negative result**: on this box's RTX 4060 Laptop under WDDM
the
hybrid **cannot beat the OS**, with the mechanism nailed down.

Do **not** merge, do **not** close #864 — this is a report-back for
review.

## What it does

Keep the size-blind `StableResident` hot set copied into VRAM; instead
of
transiently streaming the cold remainder each decode step, bind it in
place from
a `cuMemHostRegister(READ_ONLY | DEVICEMAP)` host mapping.

- The bypass decision is intercepted **before any eviction**, so a
retained hot
page is only ever one that fit without displacing another and no large
weight
ever occupies an evict/re-admit stable slot — the exact pattern that
silently
  corrupted decode in #886.
- The **whole mmap is registered once** per `mapping_id` so every
weight's
device pointer is contiguous over its full length (a per-weight
registration
is only contiguous within itself → reads off the end →
`ILLEGAL_ADDRESS`).

## The finding

- A **single** host-mapped zero-copy read is **bit-identical** (verified
correct
  at 1/8/16/32 cold weights per step — confirms #877/#880 for one op).
- **Aggregate** distinct host-mapped read traffic above **~0.44–0.65
GB/step
silently corrupts** decode: 32 cold weights (~0.44 GB) correct, 48
(~0.65 GB)
collapsed generation **16 → 3 tokens** — the #886 signature, but from
**stale
host-mapped reads past a system-memory-aperture ceiling**, not eviction.
An
A/B (defer exactly as zero-copy would, but do the real copy) is
byte-identical,
proving the deferral/admission flow is correct and the **host-mapped
READ** is
  the fault.
- `cuMemHostRegister` of the full 16.65 GB mapping **only succeeds with
READ_ONLY** (DEVICEMAP-only → `CUDA_ERROR_OUT_OF_MEMORY`), so we can't
drop it.
CPU pre-faulting didn't help; device pointers are 256-byte aligned (not
  alignment).

To guarantee the hard **byte-identical** gate, the shipped opt-in path
applies a
conservative **256 MiB** per-residency zero-copy budget
(`ZERO_COPY_SAFE_BUDGET_BYTES`, well under the observed-safe ceiling)
and copies
the rest. Kept as reviewable instrumentation for other hardware (larger
host
apertures / resizable BAR may not hit the ceiling).

## Measurements (`qwen14b-zp`, `--tokens 16 --steady`, graph ON)

| Arm | tok/s | htod/token | zero_copy/token | byte_hit_rate | tokens |
captures/fallbacks |

|-----|-------|-----------|-----------------|---------------|--------|--------------------|
| **WDDM** (`LEGACY_ALLOCATOR=1`) | **7.37** (5.61–8.32, n=3) | 0
(driver) | 0 | n/a | ✅ ref | 4/0 |
| Managed streaming | 0.14 | 2.35 GB | 0 | 70.2% | ✅ ref | 2/0 |
| Hybrid (safe 256 MiB budget) | 0.04 | 1.71 GB | 0.26 GB | 77.5% | ✅
ref | 2/0 |
| Hybrid (full cold set) | — | — | ~0.87 GB | — | ❌ 16→3 | — |

Reference tokens (all ✅ rows byte-identical):
`[96347, 3375, 724, 11, 358, 2776, 14589, 311, 6723, 429, 498, 3003,
2581, 6617, 315, 752]`

**Conclusion:** WDDM keeps ~7.7 GB resident and moves only ~0.6 GB/step
through
its own paging; our managed budget caps at ~6.1 GB and zero-copy can
only safely
cover ~0.44 GB/step. Both levers are worse than the OS on this hardware,
so the
hybrid does not beat WDDM here. A truthful negative — it settles the
line of
work for consumer-WDDM boxes while leaving the mechanism (a
host-mapped-read
aperture ceiling) documented for datacenter hardware.

## Constraints verified

- Token IDs **byte-identical** to the shipped path (all working arms).
- `captures>0`, `fallbacks==0` (graph capture compatible with
host-mapped
  pointers, confirming #880 in the real integration).
- `oversubscribed_bytes==0`; `ref_underflows`/`byte_underflows`/
  `unaccounted_committed_bytes` all `0`.
- No assert in `Drop`; registrations torn down best-effort.
- **MANDATORY** `mobius_seqmajor_growth_parity_native_cuda` passed
**solo**
  (2/2, GPU verified clear, no `ILLEGAL_ADDRESS`).

## Validation

`cargo fmt` (per-package) ✅ · `clippy -p onnx-runtime-ep-cuda --features
cuda` ✅
· `clippy --locked --all-targets -p onnx-genai-engine --features
native-backend` ✅
· `test -p onnx-runtime-session` ✅ · `test -p onnx-runtime-ep-cuda`
(366) ✅ ·
mobius parity (solo) ✅

Closes: none — leaving #864 open per request.

---------

Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
justinchuby pushed a commit that referenced this pull request Aug 14, 2026
Adds two env-gated static hot-set pin knobs to the CUDA weight residency
cache (threshold+budget, and an explicit key allow-list) to test the one
retention shape byte-aware admission never exercised: pin large tensors at
first touch and never evict or re-admit them. Both are off by default; the
shipped size-blind path stays byte-identical.

Measured on qwen14b-zp (managed WDDM streaming), solo, byte-identity gated:

- Threshold pin (33 already-resident larges, 1.09 GiB): byte-identical PASS
  but htod_bytes_per_token unchanged (2.351 vs 2.349 GB/step) — it pins the
  tensors LRU keeps anyway, so streaming does not move.
- Targeted pin of exactly the 21 chronic bypassers (1.048 GB to the byte):
  decode CORRUPTS to 3 tokens [96347, 3375, 724], the identical #886 signature.

Reconciliation: the budget is saturated, so pinning a tensor that currently
bypasses necessarily evicts-and-re-admits an equal byte-mass of large stable
-slot residents every step (reuse is uniformly 1x/step). The pin does not
avoid evict-and-re-admit; it relocates it onto the victims. This CONFIRMS
#892's localisation rather than refuting it, and closes item 3 as a
definitive negative: no static pin is both safe and useful on a saturated
budget. Also softens the reuse-vs-retention wording per review (#864).

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

Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
justinchuby pushed a commit that referenced this pull request Aug 14, 2026
The reviewer's control refuted the conservation/churn explanation: pinning
67 tensors / 2.371 GB (htod +11.8%, more churn than the 21-key arm) stays
byte-identical, so the corruption is selection-specific, not churn-mass-driven.

Bisecting the 21 chronic-bypasser keys (via the explicit PIN_KEYS allow-list)
narrows the trigger to ONE tensor: key 919, 389,283,840 B = 152064 vocab x
5120 hidden x 0.5 = the int4-quantised lm_head / vocab-projection weight.
Pinning only 919 reproduces the exact #886 3-token collapse; pinning its int4
scale tensor (920) alone is safe; pinning a 35 MiB block weight (287) alone is
safe.

Reconciliation with the safe threshold arm: the key trace shows key 919 is
never pinned by the size+arrival rule even at 2.4 GB budget (retained=0,
bypass=16) because the lm_head is the last large tensor touched each step, so
the budget is spent before it is reached. The threshold rule is safe because
it structurally cannot pin the poisonous tensor; the bypasser rule corrupts
because its allow-list contains it.

Mechanism narrowed but not root-caused: CUDA graph capture is ruled out
(pinning 919 with ONNX_GENAI_CUDA_GRAPH=0 still corrupts). The first three
tokens match the reference before collapse, pointing at retained-page
staleness rather than a first-read address error. Stated as a leading
hypothesis, not a proven cause.

Rewrites the Result 4 reconciliation and conclusion to match the evidence:
#886 is isolated to a single tensor, not closed as a conservation wall.

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

Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
justinchuby added a commit that referenced this pull request Aug 14, 2026
…y 919) (#944)

## #837 item 3 — and the isolation of #886 to a single tensor

Closes #837 (item 3). **Do not merge without your review.**

### Headline update from round 2

Your control killed my conservation/churn explanation, and following the
trigger
you predicted led exactly where you hoped: **the #886 corruption is one
tensor.**

- Over-pinning **67 tensors / 2.371 GB** (threshold 30 MB, budget 2.4
GB) forces
*more* churn than the 21-key arm (`htod` 2.349 → 2.626 GB/step, +11.8%;
`page_ins_per_token` 321 → 375) and stays **byte-identical**.
Mass-forced
evict-and-re-admit does not corrupt. The trigger is *which* tensor, not
churn.
- Bisecting the 21 bypasser keys via the `PIN_KEYS` allow-list:

  | Pinned keys | Count | Result |
  |---|---:|---|
  | 919,920,287,247,912,914,855,874,232,304 | 10 | FAIL (3 tokens) |
  | 919,920,287,247,912 | 5 | FAIL |
  | 919,920 | 2 | FAIL |
  | **919** | **1** | **FAIL** — `[96347, 3375, 724]` |
  | 920 (its int4 scales) | 1 | PASS (16 IDs) |
  | 287 (a 35 MiB block weight) | 1 | PASS (16 IDs) |

**Pinning only key 919 corrupts.** `919 = 389,283,840 B = 152 064
(vocab) ×
5 120 (hidden) × 0.5` — the **int4-quantised lm_head / vocab-projection
weight**,
the one tensor that directly produces the logits deciding token
identity. Its
scale tensor (920) alone is safe, so it is the weight matrix itself, not
the
quant metadata. This turns `MEMORY_ARCHITECTURE.md` §3.5's "unisolated
value-corruption" into a single-tensor bug.

### Reconciliation — why threshold-pin is safe and bypasser-pin is not

Reproducing your 2.4 GB threshold arm **with the key trace on** shows
key 919
`retained_page_ins=0 bypass_page_ins=16` — the lm_head is **never
pinned**, even
at 2.4 GB / 67 tensors. It is the *last* large tensor touched each step
(logits
run after every block), so the size+arrival budget is spent before it is
reached.
The threshold rule is safe because it **structurally cannot pin the
poisonous
tensor**; the explicit rule corrupts because its allow-list **contains**
it. The
difference between the two selection rules *is* the finding — exactly as
you
asked me to state it.

### Mechanism: narrowed, and a hypothesis already refuted by a control

The stable-slot machinery (#716) exists for CUDA-graph pointer baking,
so the
obvious cause is a graph hazard when the lm_head is promoted from bypass
(throwaway VA, never baked) into the stable-slot population. **Ruled
out:**
pinning 919 with `ONNX_GENAI_CUDA_GRAPH=0` (`captures=0 replays=0`)
still
corrupts. The first three tokens match the reference before the
collapse, so the
retained lm_head reads correctly for the first steps and then goes wrong
—
consistent with **retained-page staleness** (a pinned page is filled
once and
served as a hit thereafter, never re-filled), not a first-read address
error.
Stated as a leading hypothesis, **not** a proven cause. Honest state:
isolated to
one tensor and one transition (retain-across-steps vs
stream-fresh-each-step);
precise mechanism not yet root-caused.

This also means #892's "evict-and-re-admit" localisation is at best
incomplete:
here the page is pinned and **never re-admitted**, yet it still
corrupts.

### Round-1 evidence (unchanged, verified)

- 867/867 keys `reads_per_step = 1.000` — no high-reuse tensor.
- Gap = 23 single-read large tensors, `bypassed_page_in_bytes/step =
1.048 GB`
  to the byte (~90% of the 1.158 GB recoverable gap).
- Fresh #886 reproduction on `29d1515a`; softened reuse-vs-retention
wording per
  your note (#864).
- Arm 4a (threshold 1.2 GB, 33 tensors): byte-identical PASS, `htod`
unchanged —
  you reproduced this to the byte.

### On item 3 itself

The practical answer is unchanged: the gap is only safely recoverable by
the
funded **#864 zero-copy hybrid** (never evicts a retained page for a
cold weight;
~8× on Linux #925/#936; WDDM-blocked by the aperture ceiling, not
admission), so
no managed-path admission policy should ship. The value here is the
isolation,
not a policy.

### Gates (run locally; no source changed since they last passed)

- `cargo fmt -p onnx-runtime-ep-cuda -p onnx-genai-engine -- --check` ✅
- `cargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings`
✅
- `cargo test -p onnx-runtime-ep-cuda --features cuda --lib` — **374
passed** ✅
- `cargo test -p onnx-genai-engine --features native-backend,cuda --lib
native_decode` — **96 passed** ✅
- mobius parity SOLO (`--ignored --test-threads=1`) — **2 passed** ✅

All bisection runs were env-only (the `PIN_KEYS`/threshold knobs, off by
default);
the shipped size-blind path is untouched and byte-identical.

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

---------

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

docs(memory): retract the static-hot-set safety claim — pinning is not a
safety property

MEMORY_ARCHITECTURE.md §3.5 stated that a static hot set (retain once,
never
evict, never re-admit) "provably does not exercise the corrupting path",
and
the memory-design risk table recommended exactly that as the mitigation.
#944 falsified it: pinning a single weight, never evicted and never
re-admitted, corrupts decode with the identical #886 signature -- three
correct tokens, then generation stops.

What replaced the claim is a much better lead than the claim was. The
trigger is one specific tensor: key 919, 389,283,840 B = 152064 x 5120 x
0.5,
the int4-quantized lm_head / vocabulary projection. Its own int4 scales
(adjacent key, same layer, ~1/8 the size) pinned alone are safe; so is a
35 MiB block weight; so is a 2.371 GB set chosen by size threshold,
which
forces strictly more evict-and-re-admit (htod +11.8%, page_ins_per_token
321 -> 375) and stays byte-identical. Churn mass is not the trigger,
tensor
selection is. The threshold rule is safe only because it structurally
never
reaches 919, the last large tensor touched each step. CUDA graph is
ruled
out.

That lm_head is the tensor also explains the shape of the failure, which
no
previous hypothesis did: it is the only weight that computes token
identity,
so everything upstream can be exactly right and the tokens still wrong,
and
a stale logits projection selecting EOS gives "three correct tokens then
stop" rather than degenerating output.

Consequences recorded where they land. #892's evict-and-re-admit
localization is incomplete -- it may be a corrupting path, it is not the
only one. #912's hybrid safety argument invoked the static-hot-set
claim;
its measurements stand, that particular justification does not. The
leading
hypothesis, retained-page staleness, is stated as a hypothesis with the
four
experiments that would settle it filed as #945 -- two of which need no
CUDA
toolkit, which is what has blocked this since #886 was opened.

I reproduced the 919 and 920 arms myself before writing any of this.

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

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

Large-model decode is streaming-bound: 0.24 tok/s on qwen14b, with 57k VMM map/unmap per 16 tokens and ~4.9 GB/s effective H2D

2 participants