Repository navigation
Replace KvLayout enum with a KV-cache stride descriptor (#783) - #792
Conversation
Replace the two-valued `KvLayout {HeadMajorBnsh, SeqMajorBsnh}` contract on
`ModelIoSpec` with a symbolic stride descriptor, and generalize the two
converted CUDA GQA decode kernels to index from the descriptor via NVRTC
per-layout specialization instead of a runtime `if (kv_layout==0)` branch.
Default behavior (head-major BNSH) is unchanged; seq-major stays hard-gated
to the one converted fp16 decode pair.
Metadata (onnx-genai-metadata):
- Add symbolic `KvStrideDim` / `KvAxisStrides` (per-axis stride as an unordered
product of runtime dims; empty = unit stride) and `KvStrideDescriptor` with
reservation view fields for a future token-major layout.
- Replace `kv_layout: Option<KvLayout>` with `Option<KvCacheLayout>`, an
untagged enum of a readable `Named` shorthand (serde aliases
`head_major_bnsh` / `seq_major_bsnh`) or an `Explicit` descriptor. Absent
still means head-major. Regenerate the committed JSON schema.
CUDA EP (onnx-runtime-ep-cuda):
- Add `kernels::kv_stride::KvCacheStrides`: symbolic strides + CUDA `#define`
prelude codegen, static per-layout module-cache keys, and a hard
`require_converted_path_support` gate that errors on any descriptor the
converted path cannot honor (non-unit head_dim, reservation view, unnamed).
- Generalize the fp16 split-K read kernel and the fused decode-prep write
kernel to index from the descriptor. Bump module keys (decode v8, prep v3).
Head-major reuses the shared prep module via an `#ifndef GQA_KV_DST` default;
only seq-major prepends a specialized prelude under a distinct key.
Tests: named constructors reproduce the exact legacy strides; a
reservation-view / non-unit-head_dim descriptor is rejected with an error
rather than mis-indexed. gqa_seqmajor_parity_gpu and the 25-test head-major
group_query_attention_gpu suite stay green.
Also add the missing `memory_strategy_plan` field to a native-engine test
fixture (a pre-existing merge artifact between #752 and #758 that broke the
`onnx-genai-engine` all-targets clippy gate), unrelated to this change.
Refs #783, #782, #787
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #792 +/- ##
==========================================
+ Coverage 79.41% 80.02% +0.61%
==========================================
Files 331 331
Lines 137690 137782 +92
Branches 137690 137782 +92
==========================================
+ Hits 109346 110266 +920
+ Misses 23996 23162 -834
- Partials 4348 4354 +6
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
Coordinator pre-merge validation on the merged branch (Windows, RTX box), after
I also independently checked the "pre-existing broken gate" this PR reports Why this is the right shapeThree unit tests are what make this safe rather than merely working:
And the GPU parity test still asserts what matters, including the independent Why a descriptor rather than a third enum variantRecorded so this is not re-litigated:
Default behavior is unchanged: absent metadata still means head-major, and Notably, the descriptor expressed everything cleanly — no Merging. |
|
| Status | Scenario | Base | PR | Change |
|---|---|---|---|---|
gather/large_f32_threads=1-internal/131072 |
30.95 µs | 39.40 µs | +27.3% | |
matmul/small_generic_f16_threads=8/1x256x256 |
31.90 µs | 39.95 µs | +25.2% | |
reduce_mean/large_f32_threads=1-internal/262144 |
1.03 ms | 1.29 ms | +25.2% | |
matmul/small_generic_f16_threads=1/1x256x256 |
30.24 µs | 37.13 µs | +22.8% | |
grammar_masking/llguidance_compute_mask/32 |
81.22 µs | 96.05 µs | +18.3% | |
block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 |
1.19 ms | 1.40 ms | +17.9% | |
| ✅ | matmul/medium_generic_f32_threads=8/32x512x512 |
1.54 ms | 1.76 ms | +14.3% |
| ✅ | block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 |
95.57 µs | 108.19 µs | +13.2% |
| ✅ | qwen3_sampling_processors/top_k_partial_selection |
142.39 µs | 158.67 µs | +11.4% |
| ✅ | qwen3_sampling_processors/top_p_fast_after_top_k |
530.94 µs | 590.00 µs | +11.1% |
| ✅ | matmul/small_generic_bf16_threads=1/1x256x256 |
37.88 µs | 40.84 µs | +7.8% |
| ✅ | logit_processing/seven_processor_chain_per_step |
352.46 µs | 377.55 µs | +7.1% |
| ✅ | reduce_mean/medium_f32_threads=1-internal/65536 |
253.29 µs | 269.92 µs | +6.6% |
| ✅ | matmul/medium_generic_bf16_threads=8/32x512x512 |
691.69 µs | 736.78 µs | +6.5% |
| ✅ | matmul/medium_generic_bf16_threads=1/32x512x512 |
566.93 µs | 599.96 µs | +5.8% |
| ✅ | qwen3_sampling_processors/top_k_top_p_fast |
637.63 µs | 674.72 µs | +5.8% |
| ✅ | qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline |
3.44 ms | 3.62 ms | +5.4% |
| ✅ | matmul/medium_generic_f16_threads=1/32x512x512 |
35.71 µs | 37.58 µs | +5.2% |
| ✅ | sampling_latency/greedy_per_token |
3.51 µs | 3.69 µs | +5.2% |
| ✅ | reduce_mean/small_f32_threads=1-internal/4096 |
14.27 µs | 14.82 µs | +3.9% |
| ✅ | matmul/small_generic_f32_threads=8/1x256x256 |
44.74 µs | 46.32 µs | +3.5% |
| ✅ | gather/small_f32_threads=1-internal/4096 |
837.7 ns | 865.3 ns | +3.3% |
| ✅ | matmul/small_generic_bf16_threads=8/1x256x256 |
37.89 µs | 38.92 µs | +2.7% |
| ✅ | matmul/medium_generic_f16_threads=8/32x512x512 |
42.74 µs | 43.77 µs | +2.4% |
| ✅ | matmul/large_generic_f32_threads=8/32x1024x1024 |
6.71 ms | 6.83 ms | +1.9% |
| ✅ | block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 |
80.02 µs | 80.70 µs | +0.8% |
| ✅ | add/small_bf16_threads=1-internal/1024 |
14.30 µs | 14.23 µs | -0.5% |
| ✅ | kv_cache/alloc_dealloc_pages |
40.01 µs | 39.74 µs | -0.7% |
| ✅ | sampling_latency/top_k_per_token |
55.03 µs | 52.52 µs | -4.6% |
| ✅ | qwen3_sampling_processors/top_k_top_p_full_sort_baseline |
6.10 ms | 5.81 ms | -4.8% |
| ✅ | add/medium_f16_threads=1-internal/262144 |
2.76 ms | 2.60 ms | -5.8% |
| ✅ | tokenization/encode_tokens_per_second |
431.06 µs | 400.37 µs | -7.1% |
| ✅ | matmul/large_generic_bf16_threads=1/32x1024x1024 |
2.20 ms | 2.04 ms | -7.3% |
| ✅ | gather/medium_f16_threads=1-internal/32768 |
2.68 µs | 2.48 µs | -7.4% |
| ✅ | gather/medium_bf16_threads=1-internal/32768 |
2.54 µs | 2.34 µs | -7.9% |
| ✅ | matmul/large_generic_f32_threads=1/32x1024x1024 |
10.73 ms | 9.87 ms | -8.0% |
| ✅ | add/small_f16_threads=1-internal/1024 |
16.44 µs | 14.88 µs | -9.5% |
| ✅ | qwen3_sampling_processors/top_k_full_sort_baseline |
2.29 ms | 2.07 ms | -9.7% |
| ✅ | add/large_bf16_threads=1-internal/4194304 |
44.76 ms | 40.36 ms | -9.8% |
| ✅ | sampling_latency/top_p_per_token |
429.40 µs | 386.24 µs | -10.1% |
| ✅ | sampling_latency/min_p_per_token |
232.75 µs | 206.55 µs | -11.3% |
| ✅ | add/medium_f32_threads=1-internal/262144 |
2.78 ms | 2.46 ms | -11.3% |
| ✅ | matmul/large_generic_f16_threads=8/32x1024x1024 |
105.14 µs | 92.57 µs | -12.0% |
| ✅ | matmul/large_generic_f16_threads=1/32x1024x1024 |
102.93 µs | 90.40 µs | -12.2% |
| ✅ | add/large_f16_threads=1-internal/4194304 |
44.40 ms | 38.77 ms | -12.7% |
| ✅ | matmul/medium_generic_f32_threads=1/32x512x512 |
2.90 ms | 2.48 ms | -14.5% |
| ✅ | tokenization/decode_tokens_per_second |
7.52 ms | 6.41 ms | -14.7% |
| 🟢 | gather/medium_f32_threads=1-internal/32768 |
4.86 µs | 4.02 µs | -17.2% |
| 🟢 | gather/large_bf16_threads=1-internal/131072 |
17.79 µs | 14.64 µs | -17.7% |
| 🟢 | add/medium_bf16_threads=1-internal/262144 |
3.75 ms | 3.04 ms | -18.9% |
| 🟢 | block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 |
549.19 µs | 418.61 µs | -23.8% |
| 🟢 | matmul/large_generic_bf16_threads=8/32x1024x1024 |
2.33 ms | 1.77 ms | -24.3% |
| 🟢 | add/large_f32_threads=1-internal/4194304 |
50.19 ms | 37.91 ms | -24.5% |
| 🟢 | gather/small_f16_threads=1-internal/4096 |
662.1 ns | 489.5 ns | -26.1% |
| 🟢 | gather/large_f16_threads=1-internal/131072 |
18.98 µs | 13.80 µs | -27.3% |
| 🟢 | matmul/small_generic_f32_threads=1/1x256x256 |
55.10 µs | 38.65 µs | -29.9% |
| 🟢 | gather/small_bf16_threads=1-internal/4096 |
772.5 ns | 527.6 ns | -31.7% |
| 🟢 | add/small_f32_threads=1-internal/1024 |
339.2 ns | 205.7 ns | -39.4% |
| 🟢 | block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 |
138.17 µs | 72.35 µs | -47.6% |
Visual flags:
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: { 6.53 4.64 6.19 }
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)
## Summary - convert flash/prefill KV reads, prefill cache writes, append, and K-RoPE indexing to the shared `KvCacheStrides` descriptor - keep NVRTC specialization static per layout and bump the affected module keys - centralize converted-path support in `kv_stride.rs`, preserving hard errors for unsupported descriptors/paths - add full prefill + decode seq-major parity coverage with independent CPU prefill and decode oracles, both with CUDA graph capture enabled and disabled This completes the CUDA compute path needed for a full seq-major generation through flash prefill and fp16 split-K decode. Head-major remains the default, and absent metadata still selects head-major. Related: #782, #783, #792, #750, #777. ## Correctness The new GPU test runs an actual prefill followed by decode and verifies: - head-major and seq-major output bytes are identical - each layout independently matches CPU causal-prefill and decode oracles - capture-on and capture-off results agree - capture installs one graph segment I also patched the available native int4 qwen2.5 0.5B model with `kv_layout=1`; head-major and seq-major produced the same 32 token IDs and identical text. The exact requested `qwen2.5-0.5b-int4-onnx-native` directory was not present on this machine, so this check used the available `qwen2.5-0.5b-q4_0-mobius` native model instead. ## Measurements Measurements were taken in the same local session and interleaved. These are committed **physical** bytes, not nominal KV content bytes. | Model | Head-major committed | Seq-major committed | Result | |---|---:|---:|---| | qwen2.5 0.5B native int4 | 100,663,296 B | 100,663,296 B | equal | | qwen14b-zp | 402,653,184 B | 402,653,184 B | equal | For the 0.5B capture run, both layouts reported one installed segment, one initial capture, 29 replays, zero post-install re-captures, and zero fallbacks. This refutes the expected residency improvement at the current engine boundary: native CUDA bindings still allocate bucket-sized packed shapes and commit flat bucket ranges without consuming the KV layout metadata. Therefore seq-major currently changes kernel indexing but not reservation/commit geometry. For the same reason, there was no distinct end-to-end layout-specific boundary-crossing event from which to reproduce the 49.5 ms versus 4.5 ms microbenchmark result; boundary and normal steps use the same current growth geometry. The 14B model required weight offload on this GPU, which disables CUDA graph capture, so graph segment/re-capture measurements are not available for that scale run. Both layouts generated token 96347 (`Polit`). Wall-clock throughput and decode ms/token varied too widely on this box to report as a meaningful layout comparison; deterministic byte, residency, and capture counters are reported above instead. ## Remaining work Seq-major is now usable end to end for the flash-prefill + fp16-decode compute path. The expected physical-memory and growth-boundary benefits still require layout-aware native binding/residency allocation. Non-flash/reference, f32 decode, phase2a, and unfused decode-prep paths remain deliberately rejected for seq-major until converted. Token-major and prefix sharing remain out of scope. ## Validation - `cargo fmt -p onnx-runtime-ep-cuda -- --check` - `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-ep-cuda --features cuda kv_stride --lib` - `cargo test -p onnx-runtime-ep-cuda --features cuda --test gqa_seqmajor_parity_gpu -- --test-threads=1` - `cargo test -p onnx-runtime-ep-cuda --features cuda --test group_query_attention_gpu -- --test-threads=1` (25 passed, one benchmark ignored) --------- Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
## Headline: layout now moves committed physical bytes PR #794 converted flash prefill and decode to the shared `KvCacheStrides` descriptor and then measured that head-major and seq-major commit **identical** physical bytes — `100,663,296 B` on qwen2.5-0.5b and `402,653,184 B` on qwen14b. The cause was on the binding layer: the native CUDA KV binding always committed a single flat bucket range `0..(capacity × kv_heads × head_dim × elem)` regardless of layout, so seq-major changed **kernel indexing only**, not reservation or commit geometry. #787 reached the same conclusion from the allocator side: the read path is free; the cost is on the binding layer. This PR builds the layout-aware commit **geometry**, makes the binding layer **consume** the KV layout descriptor, and **measures** the residency delta directly at the CUDA driver level. ### Measured committed physical bytes (deterministic, `vmm_kv_layout_residency_gpu`) On a fixed full-context stride (the stable-VA regime), at the near-empty floor: | Model | Per-binding head-major | Per-binding seq-major | Ratio | Model-wide head-major | Model-wide seq-major | |---|---:|---:|---:|---:|---:| | qwen2.5-0.5b | 4 MiB (2 granules) | 2 MiB (1 granule) | **2× = kv_heads** | 192 MiB (96 granules) | 96 MiB (48 granules) | | qwen14b | 16 MiB (8 granules) | 2 MiB (1 granule) | **8× = kv_heads** | 1536 MiB (768 granules) | 192 MiB (96 granules) | This is the number #794 measured as *equal*. It moves by the `kv_heads` factor once committed geometry follows the descriptor — the `layers × 2` floor materialising, on real 2 MiB CUDA granules. ### Growth / re-capture The same GPU test grows a seq-major fixed-stride binding **under a captured replay** — committing tail granules at a stable VA and stride (granule `0 → 3`) — and the originally captured graph replays unchanged: **0 re-captures**. This is the stable-stride win #782 could not demonstrate end to end, shown here on hardware. Bucket growth, by contrast, re-strides and forces a re-capture. ### Boundary-crossing step cost Not measured end to end in this PR, and reported as such rather than extrapolated. #778's 49.5 ms vs 4.5 ms microbenchmark needs a seq-major **generation** through the engine to reproduce, which requires the BSNH fixed-stride shape build described below (there is no seq-major model on record to drive it). The driver-level residency measurement is the correctness-independent result this PR stands on. ## What changed - **`crates/onnx-genai-engine/src/native_decode/kv_commit.rs`** (new) — pure, unit-tested `KvCommitLayout` geometry. `live_prefix_ranges` returns the commit ranges a layout implies on a fixed full-context stride: seq-major is one dense run `0..ceil(live_bytes/granule)`; head-major is one live fragment per head stripe. `KvCommitLayout::default() == HeadMajor`, so **absent metadata means head-major, byte-identical to today**. 9 unit tests. - **Engine consumes the descriptor** — `resolve_cuda_kv_layout` maps `ModelIoSpec.kv_layout` (`#792` view-carrying descriptor) to `KvCommitLayout` (env override `ONNX_GENAI_CUDA_KV_LAYOUT` for diagnostics only), threaded from `load.rs` into `DecodeCudaState`, surfaced as `CudaKvDebugStats.kv_layout_seq_major`. Head-major keeps its exact behavior; the growth path (`apply_vmm_growth` / `vmm_growth_requests`, which #716 edits) is **untouched** for a clean merge. - **`crates/onnx-runtime-cuda-memory/tests/vmm_kv_layout_residency_gpu.rs`** (new) — driver-level GPU measurement (2 tests), the headline above. Uses the #740 pool's granule model; commits/reads back real committed bytes. - **`docs/MEMORY_ARCHITECTURE.md`** — floor table + note updated from "not realized today" to the measured driver-level result, plus the precise structural boundary. ## Honest structural boundary (per the task's "stop and report" clause) The layout-aware commit *geometry* is built and measured, and the engine now consumes the layout metadata. What is **not** wired is the seq-major dense-prefix commit on the end-to-end **generation** path, because: - `persistent_state_shapes` hard-codes a **BNSH** physical shape whose axis-2 (seq) grows; there is no **BSNH fixed-stride** physical-shape build to hang the dense-prefix commit on, and - no model on this machine declares seq-major, so a byte-identical seq-major *generation* cannot be validated here. The per-layer `key_cache_*`/`value_cache_*` bindings must survive as views (they do — this PR does not collapse them). The BSNH fixed-stride shape build is the shared prerequisite for token-major and #777 page-level prefix sharing, and is the documented next step. This is reported as information, not a failure. ## Correctness `gqa_seqmajor_parity_gpu::seq_major_full_generation_prefill_and_decode_is_bit_identical` (independent CPU prefill + decode oracles) passes — decode output is byte-identical between head-major and seq-major for a full generation, capture on and off. ## Validation (all green locally; not waiting for CI) - `cargo fmt -p onnx-genai-engine -p onnx-runtime-cuda-memory -- --check` ✓ - `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` ✓ - full `cargo test -p onnx-runtime-session` ✓ - GPU single-threaded: `vmm_kv_layout_residency_gpu`, `vmm_kv_contiguous_tail_gpu`, `vmm_kv_token_major_floor_gpu`, `gqa_seqmajor_parity_gpu` (2), and `group_query_attention_gpu` (25 passed, 1 ignored H200 bench) ✓ - `kv_commit` unit tests (9) + `cuda_kv_layout_resolves_from_metadata_with_env_override` ✓ ## Notes / caveats - The box is heavily contended; wall-clock is unreliable (identical runs have ranged 3.9–28 tok/s). Results above lead with **byte and granule counters**. - **qwen14b** needs weight offload on this 8 GiB GPU, which still disables capture (#716, separate) — so capture counters are unavailable at that scale. The re-capture demonstration is at the driver level and on the small model. Refs #794, #787, #782, #750, #777. **Do not merge; does not close any issue.** --------- Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
…809) ## What this does (#777) PR #803 landed the prefix-sharing primitive (`create_shared_prefix` / `commit_shared_prefix` / `incremental_owned_bytes_for_shared_prefix`) on the production `CudaVmmAllocator`, but **nothing in production called it** — only its definition and GPU tests. An unconsumed contract reads as working infrastructure while proving nothing; that is exactly the shape that left #721 stage 3 stranded. This PR gives the primitive a **first real, deliberately narrow consumer**. ### What is now shared in production - **An allocator-agnostic seam on the `DeviceAllocator` trait** (`onnx-runtime-memory-governor`): `create_shared_prefix`, `incremental_owned_bytes_for_shared_prefix`, `commit_shared_prefix`, plus an opaque `dyn SharedDevicePrefix` handle and `SharedPrefixCommitInfo`. A caller holding only `dyn DeviceAllocator` can now pin a token prefix once and have subsequent sequences map it. Default impls **refuse** (`InvalidRequest`) rather than mis-map, so every existing non-VMM allocator keeps compiling and refuses safely (unsupported requests error rather than mismap, per #745/#750). - **`CudaVmmAllocator` implements the seam** by delegating to the merged inherent methods (#803) and downcasting the opaque handle via `as_any()`. Granules come from the existing #740 authority-scoped pool through `carve()`; no second allocator, no per-sequence physical reservation. - **First consumer:** the real **seq-major** fused fp16 GQA decode kernel (#782/#792/#794) reads shared-prefix VMM KV through the `dyn DeviceAllocator` seam. The prefix is physically contiguous as `layers × 2` ranges (seq-major, the gate #793 requires); token-major (one multi-map/sequence) is measured (#787) but not built here. The API is **explicit** (a caller declares the shared prefix), as #793 recommended for the highest-value case (a shared system prompt), and is **restricted to prefixes that are provably read-only for the sharers' lifetime** — copy-on-write at divergence is out of scope. ### Proof + measured numbers New GPU parity test `crates/onnx-runtime-ep-cuda/tests/gqa_shared_prefix_parity_gpu.rs` drives the real seq-major GQA decode kernel over shared-prefix VMM KV and proves two sequences sharing one pinned prefix produce **byte-identical** output to two independent sequences. Measured (KV_HEADS=8, HEAD_DIM=128, f16, 1024-token prefix + 1024-token private tail per sequence; 2 MiB granule → 1024 tokens/granule): | quantity | value | | --- | --- | | independent (2 sequences) committed **physical** bytes | **16,777,216 B** (8 granules) | | shared (2 sequences) committed **physical** bytes | **12,582,912 B** (6 granules) | | removed by sharing | **4,194,304 B** (2 granules = `(C−1)×(K_prefix+V_prefix)`) | | second-sharer admission cost | **4,194,304 B** — private tails only | | prefix `incremental_owned_bytes_for_shared_prefix` | **0** (charged once) | | output | **byte-identical** for both sequences | Deterministic counters lead; wall-clock is omitted (this box's identical runs range 3.9–28 tok/s and other agents build concurrently — no number is extrapolated). ### Validation (all local; not waiting for CI, fix-forward) - `cargo fmt -p onnx-runtime-memory-governor -p onnx-runtime-cuda-memory -p onnx-runtime-ep-cuda -- --check` — clean (`--all` fails on this box with os error 206, a Windows command-line-length artifact, not formatting). - `cargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -- -D warnings` — clean. - `cargo clippy -p onnx-runtime-cuda-memory --features gpu-tests --all-targets -- -D warnings` — clean. - `cargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings` — clean. - Full `cargo test -p onnx-runtime-session` — pass. - Single-threaded GPU suites — pass: `vmm_commit_shared_prefix_gpu` (4), `vmm_prefix_share_gpu` (2), `vmm_prefix_share_ledger_gpu` (1), `vmm_prefix_share_write_protect_gpu` (1), `vmm_prefix_share_compose_gpu` (1), `gqa_seqmajor_parity_gpu` (2), and the new `gqa_shared_prefix_parity_gpu` (1). Harness respects the known traps: VMM allocator built on the EP's own CUDA context; all device ops route through the single EP stream (#797); pool env set before any allocator is constructed and the process is isolated (#804); no assertions inside any `Drop`; live reservations explicitly deallocated before allocators drop. ### What remains (structural / out of scope) - **Auto engine use is blocked structurally.** The engine generation loop cannot call this seam automatically until `native_decode/cuda.rs` can build a seq-major fixed-stride physical KV shape — `persistent_state_shapes` is hard-coded BNSH and no model declares a seq-major end-to-end physical shape (#794 showed seq-major changed only kernel indexing, not commit geometry). A BNSH/seq-major fixed-stride physical-shape build is the prerequisite. This PR delivers the seam + the seq-major GQA-kernel consumer + proof; the automatic decode-path wiring is the next structural increment. - Hash-based automatic prefix detection (later; explicit path proven first). - Token-major (one multi-map per sequence; measured #787, not built). - Copy-on-write at divergence (#793 measured ~5.5 ms pooled / ~9.8 ms cold, once). Docs updated: `docs/MEMORY_ARCHITECTURE.md` (concurrency-scaled prize section) and `docs/PREFIX_SHARE_INVESTIGATION.md` (smallest next increment) now record the first production consumer, the measured numbers, and the remaining structural gap. Refs #777 #803 #793 #782 #745 #750 > Please do **not** merge and do **not** close #777 from this PR. Edits are > confined to the KV/prefix integration path to merge cleanly with concurrent work. Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
…ajor-only KV consumers (#777 #801 #797 #794 #809) (#812) # Remove the last blocker to seq-major KV being usable in production: gate the fourth place the layout lives **Outcome reached: #3 — "it works only under a condition."** Seq-major decode works end-to-end and is byte-identical to head-major, but only on the **pure decode path where our own GQA kernel is the sole consumer of the device KV buffer**. This PR makes the runtime **refuse rather than mis-map** when that condition does not hold, and pins down — with citations — exactly what remains between "growth byte geometry is seq-major" (#801) and "the `layers × 2` floor realised + prefix sharing available." Refs #777 #797 #794 #801 #809 (and #787 for token-major). Does **not** merge, does **not** close any issue. ## The question this PR answers > What remains between "growth byte geometry is seq-major" and "a real model runs seq-major end to end with the floor realised and prefix sharing available"? A fourth place the layout lives, or can the declared BNSH shape and a seq-major physical layout coexist provided only our own kernels read the buffer? **Both.** The declared BNSH shape and a seq-major physical layout *do* coexist on the native decode path (KV never leaves the device; only our GQA kernel indexes it). But there **is a fourth place the layout lives** — two engine-side *non-kernel* consumers of the device KV buffer that still stride it with hard-coded head-major (BNSH) arithmetic: 1. **`DecodeCudaState::read_present_kv`** (`native_decode/cuda.rs`) — the device present-KV **host mirror** used by prefix/paged read-out and diagnostics. It compacts per head with `capacity_head_stride = physical_shape[2] * head_dim * elem` and a per-head fragment read. 2. **`DecodeCudaState::seed_prefix`** (`native_decode/cuda.rs`) — the **paged prefix-reuse device seed** (#777/#809's device counterpart). It writes each head at `head * capacity_head_stride * elem_size`, the same head-major capacity offset. Under a seq-major (BSNH) physical buffer, heads are **interleaved per token** (`slot * kv_heads * head_dim + h * head_dim + d`), not laid out as `max_len × head_dim` capacity stripes. So both offsets index the wrong bytes: the host mirror would **silently return mis-indexed KV**, and the prefix seed would **corrupt the cache**. Neither had a seq-major guard — a real latent-corruption path. This is the fourth place the layout lives, beyond the three that are already converted: 1. kernel indexing — done (#792/#794); 2. commit geometry — done (#797); 3. growth byte geometry — done (#801); 4. **engine-side non-kernel KV consumers — gated here.** ## What this PR changes `crates/onnx-genai-engine/src/native_decode/cuda.rs`: - `read_present_kv` and `seed_prefix` now **error under `kv_layout.is_seq_major()`** with a precise message stating the condition and why (head-major byte arithmetic vs interleaved seq-major bytes). This matches the kernel-level "unsupported readers/writers fail rather than silently mis-index" gate (`kv_stride.rs::require_converted_path_support`) at the engine level. - **Head-major is untouched and byte-identical** — the gate is a pure `if is_seq_major() { bail! }` before the existing code. Absent metadata ⇒ head-major ⇒ no gate. `docs/MEMORY_ARCHITECTURE.md` (§ "KV layout and residency"): records the fourth layout location and the refuse-not-mismap gate as a bullet in the existing residency-caveat block. ## The structural constraint (#801), re-verified The BNSH binding **metadata** shape must stay for both layouts. The CUDA GQA node validates `past_key`/`past_value` as `[batch, kv_heads, seq, head_dim]` and reads `present_capacity` from **axis 2 regardless of `kv_layout`** (`group_query_attention.rs`), and `DeviceIoBinding` requires matching axis order. `persistent_state_shapes` is therefore *correctly* BNSH for seq-major too; seq-major lives in the byte geometry, not a permuted shape. Confirmed still true after merging the concurrent GQA-workspace change. ## Why the `layers × 2` floor is still not realised through the engine (and what it needs) Measured directly, **before** the merge broke the harness, with the validated #801/#805 setup — stock `qwen2.5-0.5b-q4_0-mobius` (BNSH) vs a copy whose 24 GQA nodes carry `kv_layout=1`, forcing growth with `ONNX_GENAI_KV_MIN_BUCKET=8`, 48 greedy tokens, 3 growth events (`mobius_seqmajor_growth_parity_native_cuda`, this branch @ pre-merge): | Growth path | Capture | Output | Physical committed bytes (head = seq) | d2d KV copy bytes head | d2d KV copy bytes seq | |---|---|---|---:|---:|---:| | in-place VMM (default) | OFF | **byte-identical** | 100,663,296 | 688,576 | **0** | | in-place VMM (default) | ON | **byte-identical** | 100,663,296 | 688,576 | **0** | | legacy realloc | OFF | **byte-identical** | 786,432 | 688,576 | 688,576 | Capture-ON, per-process: head-major and seq-major both `(growth 3, captures 4, replays 39, invalidations 4)`. So on this branch the seq-major decode path is **byte-identical head vs seq, capture ON and OFF, and moves 0 KV bytes on in-place growth** — but **`physical_committed_bytes` is identical between layouts**, i.e. the floor is *not* realised through the engine. Two cited reasons: 1. **`vmm_growth_requests` commits the full bucket** (`(index, 0, full_bucket_bytes)`) for every KV binding, for both layouts, instead of the dense live-prefix ranges `native_decode/kv_commit.rs::live_prefix_ranges` computes (those helpers are unit-tested and mirror the driver GPU test but are `allow(dead_code)` — not wired into the engine). 2. **The `kv_heads×` difference only becomes *measurable* under a large fixed stride.** With bucket growth the bucket ≈ 2× valid, so on 0.5b both layouts land in **1 granule/binding** (48 granules = 96 MiB). The `kv_heads=2` gap appears only when a head stripe crosses a granule boundary, i.e. `capacity × head_dim × elem ≥ granule` (`ONNX_GENAI_CUDA_KV_MAX_LEN=32768` in the driver test `vmm_kv_layout_residency_gpu`): head-major = `layers × 2 × kv_heads` granules, seq-major = `layers × 2` (the measured 192 MiB → 96 MiB, 2× = `kv_heads`; 1536 MiB → 192 MiB, 8× on qwen14b). Realising the measurable floor therefore requires **a fixed full-context stride + dense-prefix commit + per-step commit-ahead**, which eliminates growth-driven bucket recapture. That change lives in the **growth/capture-invalidation decision** — the explicit lane of the concurrent `conditional-invalidation` / `bsnh-fixed-stride` work — and outside this PR's shape/binding-construction lane. It is left as the documented next step (shared with token-major #787 and prefix sharing #777), consistent with the existing doc note: *"wiring the dense-prefix commit into the live path is the documented next step."* ## Prefix sharing (#777/#809) under seq-major It does **not** fall out naturally. #809's device consumer is `seed_prefix`, a head-major byte writer; a shared prefix is only physically contiguous under seq-major, but the seed that would install it cannot yet address the seq-major buffer. This PR makes that explicit (the seed errors under seq-major) rather than letting prefix reuse silently corrupt seq-major KV. Realising it means teaching `seed_prefix` (and `read_present_kv`) the BSNH byte geometry — same dense-prefix work as the floor. ## Validation - `cargo fmt -p onnx-genai-engine -- --check` ✓ (`--all` fails with os error 206 on this box — a command-line-length artifact, not formatting) - `cargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -- -D warnings` ✓ - `cargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings` ✓ (post-merge) - full `cargo test -p onnx-runtime-session` ✓ (post-merge) - `mobius_seqmajor_growth_parity_native_cuda` (byte-identical head vs seq, capture ON/OFF, 0 bytes moved) ✓ **both pre-merge and post-merge** (post-merge re-run includes the #814 governor fix, see below); the gate does not regress it. All four configs identical to the pre-merge numbers above (`physical_committed_bytes` 100,663,296 / 786,432; seq `kv_bytes_moved=0`). ### Cross-agent regression surfaced by `merge origin/main` — now fixed by #814 Merging `origin/main` (which brought the `gqa-bnsh-transpose` GQA transpose-scratch change #810) initially broke the mobius end-to-end test **during prefill workspace prep**, before any decode: ``` Error: prepare native CUDA prefill workspace Caused by: cannot reserve 2097152 bytes of device memory for Workspace { step_scoped: false }: 0 of 2097152 bytes are already leased, leaving 0 ``` #810 reserves a new **session-persistent** BNSH transpose-scratch `Workspace { step_scoped: false }` (`WorkspaceLifetime::SessionPersistent`) during prefill; the `vram-limit dynamic KV/weight lending` strategy did not provision headroom for it (GPU otherwise 100% free — 0 MiB used). This was a governor role-budgeting interaction in the **GQA/memory lane**, independent of this PR's gate (which only adds error paths in `read_present_kv`/`seed_prefix`, never on the prefill workspace path). I attributed it precisely and reported it to the `gqa-bnsh-transpose` sibling rather than fixing out of lane. **It has since been fixed forward in #814** (commit `54f89a03`: "mapped growth now claims unassigned authority capacity before holder reclaim, with rollback/overestimate accounting"). Merging #814 locally on top of this branch, the mobius test **passes all four VMM/realloc × capture configurations, byte-identical head vs seq**, confirming this PR's gate and #814's governor fix coexist cleanly. #814 is not yet on `main`, so this branch is left at its clean two-file state (the fix will arrive via `main`); the post-merge validation above was run against `this branch + #814` locally. ## Files - `crates/onnx-genai-engine/src/native_decode/cuda.rs` — seq-major refuse-not-mismap gate on the two head-major-only KV consumers. - `docs/MEMORY_ARCHITECTURE.md` — fourth-layout-location note. Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
## What Documentation-only sync of five memory/KV design docs to the merged, executable-test-backed facts about VMM commit geometry, KV layout, weight offload, CUDA-graph capture, and plugin export. No code or tests changed. `MEMORY_ARCHITECTURE.md` is treated as the single source for KV geometry and the audit table; the other four link into it rather than restating (per #791). ## Most misleading claim found `MEMORY_ARCHITECTURE.md` — the document explicitly designated the single source of truth — still asserted the engine *"explicitly invalidates the graph on every KV bucket growth for both layouts"* and that *"the engine does not yet use that fact to retain the captured graph,"* with a table showing seq-major at `captures=4, invalidations=4`. That was falsified by **#811**: conditional invalidation has landed, seq-major fixed-stride growth now **keeps** the captured graph (growth-attributable invalidations **3 → 0**, `captures=1/invalidations=1`, matching the no-growth control), verified by `mobius_seqmajor_growth_parity_native_cuda.rs` including a negative oracle. Because this doc is the single source others link to, the stale claim had the widest blast radius. ## Changes - **MEMORY_ARCHITECTURE.md** — correct the #811 conditional-invalidation prose + table (verified against `cuda.rs` `binding_growth_signature`/`record_growth_decision` and the parity test); add the ORT-backend KV-routing + #762 plugin-export note (verified `allocator.rs:232` `for_session_device` selects by device, not implementation); thread #822 (granule queried), #812 (engine-side consumer gate), and a #800/#802 record correcting this doc's earlier fp16 sizing of Phase-2a scores (verified `attention.rs:368` uses `dtype.element_size()`). - **CUDA_GRAPH_CAPTURE.md** — replace the mutual-exclusion result with #796 (stable VA removes it), #798 (managed no-spill default), #811 (conditional invalidation), and the #804/#807 `captures=0` process-artifact correction. - **WEIGHT_OFFLOAD.md** — correct the "mutually exclusive today" claim (#796) and the north-star status for the managed no-spill default (#798). - **KV_INSERTION_DESIGN.md** (most stale, 2026-07-15) — add a synchronization banner linking to `MEMORY_ARCHITECTURE.md`; note the layout line post-dates the memo, prefix sharing is a landed primitive (#777), and block-table paged attention remains a proposal (verified: no `block_table` in any CUDA attention kernel). - **DESIGN.md** — forward-pointer in the already-superseded §39.5 noting layout (seq-major/token-major, #787/#792) is now the primary residency lever. ## Method Every retained performance claim cites the PR that measured it; measured fact / design intent / not-yet-built are distinguished; superseded designs are labelled, not deleted. The "still open" item (engine commits flat buckets, not the dense live-prefix ranges `kv_commit.rs` computes, so the `layers×2` floor is not yet realised through the engine) is described as in-flight, not delivered. ## Note on #762 The task framing had #762 (EP plugin export) as open; `gh pr view 762` shows it **merged** today. It is described accurately per that ground truth: the plugin-EP mechanism landed for the CPU EP (validated end-to-end), while the CUDA plugin shim compiles but is unvalidated on hardware — so plugin-EP-hosted VMM KV remains in-progress design intent, not a shipped capability. ## Validation Documentation only — `git diff --stat` touches exactly the five `.md` files; working tree otherwise clean. **Not merged; no issue closed.** Refs #736, #762, #776, #777, #783, #787, #791, #792, #794, #796, #797, #798, #801, #802, #804, #807, #811, #812, #822 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… baseline (#841) (#849) Follow-up to #841, correcting the single-source doc. #841 measured, end to end on qwen14b with on-demand commit **active**, that head-major and seq-major commit **byte-identical** physical KV. The mechanism is in `native_decode/cuda.rs`: head-major growth sets `new_shape[2] = new_capacity`, so its per-head stride is *the current bucket*, not a fixed full context. It re-packs dense and already sits on the `layers × 2` floor (96 granules) — **0.25× of the 768-granule figure this line of work has quoted since #797**. So the `kv_heads ×` ("8×") saving seq-major was pursued for does not exist on the path the engine actually runs, and no further layout work would have produced it. What this changes in the doc: - The floor table now distinguishes **head-major with a fixed full-context stride** (768 granules — a geometry the engine never instantiates) from **head-major as the engine actually runs it** (96 granules, measured). - The driver-level `vmm_kv_layout_residency_gpu` 8× ratio is still correct *as measured*, but is now explicitly marked as **not transferring to the engine**, since it is measured on that fixed stride. - The `layers × 2 × kv_heads` prose earlier in the section is now qualified with the stride condition it silently assumed. - Seq-major's real, measured advantage is recorded plainly: **growth cost, not residency** — 0 bytes moved and capture retained on growth, against head-major's 100,667,392 bytes moved plus invalidation. That is what the layout chain (#782/#792/#794/#797/#801/#812/#827) bought, and it is worth having; it is simply not a residency saving. Lesson recorded in the doc: **before quoting a floor, confirm the code instantiates the geometry that floor assumes.** The arithmetic was right throughout; the baseline was not. Notably `cuda.rs` has said head-major grows a *packed* bucket since #827 — the fact was in the tree before the measurement caught up to it. Docs only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
Replace
KvLayoutenum with a KV-cache stride descriptorRefs #783, #782, #787. Contract change plus a kernel-indexing generalization, not a layout migration. Head-major BNSH stays the default and the behavior for all existing metadata. Do not merge; do not close any issue.
Why a descriptor, not a third enum variant
cuMemMapmaps whole granule-aligned VA windows onto whole physical granules, so layout controls the VMM residency floor (BNSH:layers x 2 x kv_heads; BSNH seq-major #782:layers x 2; token-major #787: 1 per sequence, a measured 768x reduction). But the next step is not another axis permutation:(layer, side)buffer boundary both existing variants preserve, so it is not expressible as another enum axis-permutation.group_query_attention_helper.hhardcodesQ_K_V_BNSH); Intel reportedly prefers BNHS (coherent, because Level Zero exposes ~64 KiB pages where the layout penalty is ~32x smaller than our 2 MiB granule). An enum would need a variant per(platform, preference).MODULE_KEYversioning already does exactly this. Generality is close to free on a JIT backend.The descriptor
Absolute element strides are a serving-time property (they depend on the
cache_capacitya runtime picks), so the descriptor stores each axis stride symbolically — as the unordered set of runtime dims it multiplies. Empty factor list = unit stride.kv_heads·seq·head_dimseq·kv_heads·head_dimseq·head_dimhead_dimhead_dimkv_heads·head_dim11head_dimis unit (contiguous) for every honored layout — the fp16 read vectorizes it ashalf2, the fused write addresses it asdst + d.onnx-genai-metadata):kv_layout: Option<KvCacheLayout>, an untagged enum of a readableNamedshorthand (serde aliaseshead_major_bnsh/seq_major_bsnh) or anExplicitKvStrideDescriptor. Absent still means head-major exactly as before. Committed JSON schema regenerated; schema-sync test green. A raw stride tuple is unreadable, so the common cases stay nameable and existing string-valued metadata keeps working.onnx-runtime-ep-cuda):KvCacheStridesgenerates the CUDA#defineprelude (GQA_KV_BASE/GQA_KV_STRIDEfor the read,GQA_KV_DSTfor the write) that specializes each kernel per descriptor. Static per-layout module-cache keys (decodev8, prepv3); head-major reuses the shared prep module via an#ifndef GQA_KV_DSTbaked default, so only seq-major prepends a specialized prelude under a distinct key — avoiding double-compiling the always-f32 metadata/reference prep kernels.How both existing layouts map
The two named layouts expand to exactly the strides the old enum variants implied (asserted by
*_strides_match_legacy_enumtests, so the migration cannot silently change a layout). Thekv_layoutnode attribute (0=BNSH,1=BSNH) still selects them on the wire viaKvCacheStrides::from_attribute; the.with_kv_layout(0/1)test entry point is preserved as a thin adapter.How token-major would map (not implemented here)
KvStrideDescriptorcarriesreservation_offset_elementsandreservation_seq_slots: token-major stores every layer's tokens in one reservation and hands each(layer, side)a sub-view whose seq stride is taken over the reservation's total token count, starting at a non-zero offset. That is expressible today but rejected byrequire_converted_path_support(as is any non-unithead_dimor unnamed descriptor) — the hard gate errors rather than mis-indexing.The hard gate is preserved
Head-major is honored by every reader and writer. Any non-head-major layout is gated to the one converted fp16 single-token decode pair; anything the converted path cannot honor errors, never mis-indexes.
Follow-up increment
Make bindings actual views into one reservation (wire up
reservation_*end to end), then convert the remaining BNSH kernels (prefill flash read) so seq-major can be enabled end to end, then token-major and #777 page-level prefix sharing. All explicitly out of scope here.Validation (all local, green)
cargo fmt -p onnx-genai-metadata -p onnx-runtime-ep-cuda -- --checkcargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings(exact CI gate, no--all-targets)cargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -- -D warningskv_strideunit tests (named constructors reproduce legacy strides; reservation-view and non-unit-head_dim descriptors rejected)gqa_seqmajor_parity_gpu(BNSH byte-identical to BSNH, capture on==off, CPU oracle holds) +group_query_attention_gpu(25 head-major regression tests), single-threadedNote on an unrelated fix
Added the missing
memory_strategy_planfield to a native-engine test fixture inruntime.rs— a pre-existing merge artifact between #752 (added the field) and #758 (added a sibling fixture without it) that broke theonnx-genai-engineall-targets clippy gate onmain. Independent of this change; included so the required gate passes.