Repository navigation
#837 item 3: the #886 corruption is one tensor — the int4 lm_head (key 919) - #944
Conversation
…ead population (#837) Add an off-by-default per-key weight-paging trace (ONNX_GENAI_WEIGHT_PAGING_KEY_TRACE) that attributes every residency-cache lookup to its weight key -- len, hits, retained/bypass page-ins -- so the bypassed population can be characterised by size AND reads-per-step. Pure process-local accounting under a Mutex, taken only when enabled; the shipped path is untouched. profile_native dumps it after the existing weight_offload_* lines. Measured on qwen14b-zp (managed streaming, default budget, solo): - Every one of 867 weight keys is read exactly once per decode step (reads_per_step=1.000). No high-reuse tensor exists, so reuse-based reservation has nothing to rank on. - The 1.048 GB/step bypass traffic (44.63% of streamed bytes, ~90% of the recoverable gap) is 23 large single-read tensors (1x371 MiB, 1x46 MiB, 17x33.75 MiB, 4x4.22 MiB thrashing) that do not fit the residual headroom. - Baseline stays byte-identical to the reference 16 token IDs. - Byte-aware admission (the only lever that keeps those tensors resident) still corrupts decode on 29d1515: 3 tokens, byte-identity fails (#886 reproduced). Conclusion: no safe managed-path admission change recovers item 3; the gap is only safely recoverable via the #864 zero-copy hybrid (Windows-blocked by WDDM, not by admission). Documented in docs/research/bypassed-weight-population-837.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #944 +/- ##
==========================================
- Coverage 78.44% 77.81% -0.64%
==========================================
Files 354 354
Lines 148153 148153
Branches 148153 148153
==========================================
- Hits 116226 115287 -939
- Misses 27429 28372 +943
+ Partials 4498 4494 -4
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
🔴 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
|
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
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
|
Validated. Merging — and this is the most valuable thing to come off this line. Reproduced, including the controlSolo, managed streaming forced,
The scales control is what makes this conclusive. Same layer, same quantization group, adjacent key, ~1/8 the size — and it is safe. So it is the weight matrix itself, not "something about that layer" and not "something about large tensors". Why this matters more than the item it closes
And the tensor is the one that would produce exactly this symptom: lm_head is the vocabulary projection — the only weight that computes token identity. Everything upstream can be perfectly correct and you would still get wrong tokens. That also explains the shape of the failure: three correct tokens then generation stops rather than degenerating into garbage, which is what a stale logits projection selecting EOS looks like. Two corrections this forces, both worth more than the closure
Ruling out CUDA graph by re-running with Gatesfmt clean · exact CI CUDA clippy gate exit 0 · All knobs default off and the shipped path is unchanged, so the instrumentation merges on its own merits regardless of where the mechanism hunt lands. What I am doing nextFiling the mechanism as its own issue rather than leaving it in #837's tail: why does a pinned, never-evicted, never-re-admitted page of the int4 lm_head weight go stale? That is now a bounded question with a one-tensor reproduction, which is a completely different proposition from where |
…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
…tion is cross-generational (#950) Closes #837 residency work / narrows #945. **Do not merge without your review.** ## Headline The #944/#945 leading hypothesis — **retained-page staleness** — is **falsified** on qwen14b-zp hardware. Key 919's retained device copy is byte-perfect and stable; the corruption is **cross-generational**, not a page that goes stale within a run. ## What I added (all env-gated, off on the shipped path) In crates/onnx-runtime-ep-cuda/src/weight_paging.rs: - **ONNX_GENAI_WEIGHT_PIN_CHECKSUM=<keys>** (experiment 2/3): hashes a pinned page per 2 MiB granule on admission and re-hashes it on every decode step — a pure device→host read-back + FNV-1a, **no CUDA toolkit needed**. Reports the first step/granule that diverges. - **ONNX_GENAI_WEIGHT_PIN_REFILL_EVERY=<N>** (experiment 1): re-copies the original host source into the *same* resident VA every N hits, never evicting or re-admitting. A **null value re-fills nothing and reproduces the baseline exactly** (measurement-discipline #1). Both are pure additions to the residency lookup/admission path; the size-blind shipped path is untouched. ## Findings (all runs solo, vidia-smi verified empty before each, --tokens 16 --steady) | Experiment | Result | Kills | |---|---|---| | **Checksum** pin 919 | **186/186 granules byte-identical to admission on every step** of a run that still collapses to 3 tokens (per-step full-device sync did **not** mask it) | retained device copy going stale; incomplete/one-shot fill; and — since nothing diverges — the within-tensor bisect (exp 3) is subsumed | | **Force re-fill** every step | still collapses to 3 tokens | the fault is not in the retained page's content | | **--warmups 0** pin 919 | **full byte-identical 16-token reference stream**; --warmups 0 --runs 2 → run 1 = 16 tokens, run 2 = 3 | "the page goes stale as the run proceeds" — the collapse is the **second** generation | | **Mass control**: pin eleven 35 MiB non-projection tensors = **exactly 389,283,840 B** (= 919) across 2 gens | byte-identical (pinned_keys=11 pinned_bytes=389283840 confirmed) | churn/retained-mass; consistent with #944's 2.4 GB arm | 919 is a unique ~8× size outlier (next-largest tensor is 35 MiB), so a *single* ~371 MiB non-projection control cannot exist; the equal-mass multi-tensor control fills that gap and stays safe. ## What the evidence supports now The trigger is **retaining the lm_head projection (919) across generations**, while 919's own bytes stay correct. So the mechanism is a **cross-generation interaction of the retained stable-slot VA** (#716 stable-slot machinery engages only for retained keys) with per-generation setup (re-prefill / KV reallocation under dynamic KV/weight lending) — **not** the page content. Remaining candidates: (a) 919's stable-slot reservation aliases a gen-2 KV/activation buffer (919 stays MATCH because a read-only weight is never written); (b) a consumer-side stream/pointer assumption that holds when 919 streams fresh but breaks once it is a persistent slot. ## Could not do Pinpoint the exact corrupted gen-2 buffer / the precise line — that needs checksumming KV/activation buffers or the arena granule map across the generation boundary; compute-sanitizer is unavailable on this box. Narrowed-but-unsolved, as expected. ## Gates (all local, this branch) - 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** ✅ (one gqa_decode_bf16 numeric test flaked under parallel GPU contention on the 8 GB laptop GPU; **374/374 pass** re-run with --test-threads=1, measurement-discipline #5) - cargo test -p onnx-genai-engine --features native-backend,cuda --lib native_decode — **96 passed** ✅ - mobius parity SOLO (--ignored --test-threads=1) — **2 passed** ✅ - **Byte-identity, all new knobs off** (new binary), qwen14b-zp, --tokens 16 --steady: `[96347, 3375, 724, 11, 358, 2776, 14589, 311, 6723, 429, 498, 3003, 2581, 6617, 315, 752]` ✅ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624 --------- Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
…e page cache, PCIe wall, what to build (#1333) ## MoE expert-offload architecture — synthesis of three converged workstreams Composes the mechanism, policy, and batching findings into one authoritative document, `docs/memory/MOE_OFFLOAD_ARCHITECTURE.md`, in the honesty register of `MEMORY_MANAGEMENT_MODEL_DESIGN.md` (measured vs inferred separated; hardware/model/regime on every figure; per-agent attribution so any claim traces to its evidence). ### The architectural conclusion of the week **Three regimes** decide whether a routing-aware cache is worth building (measured on this box — RTX 4060/WDDM, 63.8 GB DDR5-6400, gemma-3-27b, routing-skewed pattern under swept memory pressure): | regime | condition | OS captures | headroom | |---|---|---|---| | 1 | cache ≥ full bank | ~100% | ≈0 | | 2 | hot-set < cache < bank | ~88% (LRU exploits skew for free) | small | | 3 | cache ≲ hot-set | **33% hot-warm** | **~3× pin win** | - **Host/PCIe crossover:** warm host reads (~10.5 GB/s) exceed every host→device path (zero-copy 5.9, staged 10.9 GB/s), so in regimes 1–2 the host tier is not the bottleneck — **PCIe is**. Only regime-3 eviction-miss traffic drags host throughput below PCIe. - **CUDA UM is empirically closed on WDDM** (~0.6 GB/s, `concurrent=0`, `cuMemAdvise_v2`/`cuMemPrefetchAsync_v2` return `INVALID_DEVICE`) — recorded as a hard negative so nobody re-proposes it. - **Layout constraint gates the whole storage family:** only canonical MatMulNBits int4 / `BlockQuantizedMoE` are file-backed-usable; Marlin and MLAS-prepacked are excluded. - **DirectStorage/GDS** are PCIe-bound Regime-B backstops (bank > host RAM), not breakthroughs; the D3D12→CUDA interop link is flagged inferred-not-measured. ### Composed with the other workstreams (cited, not re-derived) - **Policy** (MoE agent, #1321/#1322/#1326/#1331): concentration milder than 80/20 (25% → 45.6%); static hot-pin within 17–27% of Belady; PCIe floor **1.63 GB/token → ~8 tok/s** (25% VRAM, batch 1, x8); no knee; **dynamic prefetch foreclosed** by a ≈0 intra-layer window. - **Batching**: **−61% bytes/token** (W=2→8) is the largest lever, but **blocked** behind a ~5.4× M=1→2 step overhead. ### What to build / not build 1. Routing-aware **static** hot-pin — **regime 3 only** (near-worthless in 1–2; dynamic prefetch foreclosed). 2. Target (hundreds-of-GB MoE) sits in regime 3 → worth building, but **cannot be validated on this 8 GB laptop**; the doc states what would (Linux datacentre GPU + real MoE). 3. **Quantisation** = best return per complexity (linear PCIe relief, no policy), at a stated accuracy cost. 4. DirectStorage/GDS = Regime-B backstop only. 5. Never re-propose CUDA UM on WDDM. Cross-links the result back into `MEMORY_MANAGEMENT_MODEL_DESIGN.md` (which said MoE "should be measured before any residency policy is designed"); its dense conclusions (`reads_per_step=1.000` #944, ~30× OS win #864/#874) kept intact and reframed as regime 1 with no reuse. Docs-only; nothing under `docs/` is consumed by a build script (verified). Measured on RTX 4060 / driver 591.55 / CUDA 13.1 / WDDM. Closes #1266. Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
#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 (
htod2.349 → 2.626 GB/step, +11.8%;page_ins_per_token321 → 375) and stays byte-identical. Mass-forcedevict-and-re-admit does not corrupt. The trigger is which tensor, not churn.
Bisecting the 21 bypasser keys via the
PIN_KEYSallow-list:[96347, 3375, 724]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 "unisolatedvalue-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, evenat 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) stillcorrupts. 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)
reads_per_step = 1.000— no high-reuse tensor.bypassed_page_in_bytes/step = 1.048 GBto the byte (~90% of the 1.158 GB recoverable gap).
29d1515a; softened reuse-vs-retention wording peryour note (Managed offload is 30x SLOWER than WDDM demand paging on Windows — and it is the default: the copy-to-VRAM buys nothing for a single-touch access pattern #864).
htodunchanged —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 ✅--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