Skip to content

Replace KvLayout enum with a KV-cache stride descriptor (#783) - #792

Merged
justinchuby merged 2 commits into
mainfrom
squad/783-kv-stride-descriptor
Aug 11, 2026
Merged

justinchuby merged 2 commits into
mainfrom
squad/783-kv-stride-descriptor

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

Replace KvLayout enum with a KV-cache stride descriptor

Refs #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

cuMemMap maps 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:

  1. Token-major collapses the per-(layer, side) buffer boundary both existing variants preserve, so it is not expressible as another enum axis-permutation.
  2. Layout preference is per-EP / per-platform, not global. ORT GQA is BNSH-only on every dispatch path (group_query_attention_helper.h hardcodes Q_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).
  3. We JIT with NVRTC, so strides are known at module-build time and each layout compiles to a specialized variant — MODULE_KEY versioning 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_capacity a runtime picks), so the descriptor stores each axis stride symbolically — as the unordered set of runtime dims it multiplies. Empty factor list = unit stride.

axis head-major BNSH seq-major BSNH
batch kv_heads·seq·head_dim seq·kv_heads·head_dim
head seq·head_dim head_dim
seq head_dim kv_heads·head_dim
head_dim 1 1

head_dim is unit (contiguous) for every honored layout — the fp16 read vectorizes it as half2, the fused write addresses it as dst + d.

  • Metadata (onnx-genai-metadata): kv_layout: Option<KvCacheLayout>, an untagged enum of a readable Named shorthand (serde aliases head_major_bnsh / seq_major_bsnh) or an Explicit KvStrideDescriptor. 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.
  • CUDA EP (onnx-runtime-ep-cuda): KvCacheStrides generates the CUDA #define prelude (GQA_KV_BASE/GQA_KV_STRIDE for the read, GQA_KV_DST for the write) that specializes each kernel per descriptor. Static per-layout module-cache keys (decode v8, prep v3); head-major reuses the shared prep module via an #ifndef GQA_KV_DST baked 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_enum tests, so the migration cannot silently change a layout). The kv_layout node attribute (0=BNSH, 1=BSNH) still selects them on the wire via KvCacheStrides::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)

KvStrideDescriptor carries reservation_offset_elements and reservation_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 by require_converted_path_support (as is any non-unit head_dim or 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 -- --check
  • cargo 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 warnings
  • metadata: 33 fixtures + schema-sync + 5 descriptor tests
  • ep-cuda: 6 kv_stride unit tests (named constructors reproduce legacy strides; reservation-view and non-unit-head_dim descriptors rejected)
  • GPU: 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-threaded

Note on an unrelated fix

Added the missing memory_strategy_plan field to a native-engine test fixture in runtime.rs — a pre-existing merge artifact between #752 (added the field) and #758 (added a sibling fixture without it) that broke the onnx-genai-engine all-targets clippy gate on main. Independent of this change; included so the required gate passes.

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

codecov Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.87234% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.02%. Comparing base (7246e63) to head (268911c).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
crates/onnx-genai-metadata/src/schema/model_io.rs 97.87% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            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     
Flag Coverage Δ
cli-ort-linux 83.50% <ø> (ø)
cli-ort-windows 83.09% <ø> (ø)
mlas 81.29% <ø> (+0.36%) ⬆️
offline 79.86% <97.87%> (+0.64%) ⬆️

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

Files with missing lines Coverage Δ
crates/onnx-genai-metadata/src/schema/model_io.rs 96.29% <97.87%> (+40.04%) ⬆️

... and 8 files with indirect coverage changes

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

@justinchuby

Copy link
Copy Markdown
Owner Author

Coordinator pre-merge validation on the merged branch (Windows, RTX box), after
merging current main (no conflicts):

  • cargo fmt -p onnx-runtime-ep-cuda -p onnx-genai-metadata -p onnx-genai-engine -- --check clean
  • cargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings clean (exact CI gate, no --all-targets)
  • cargo test -p onnx-runtime-ep-cuda --lib kv_stride: 6 passed
  • cargo test -p onnx-genai-metadata: 33 fixtures + schema-sync passed
  • gqa_seqmajor_parity_gpu: passed on device
  • group_query_attention_gpu head-major regression: 25 passed, 0 failed

I also independently checked the "pre-existing broken gate" this PR reports
fixing forward: cargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -- -D warnings is clean on current main, so that
breakage has already been resolved upstream. The fix-forward here is harmless
either way, but it is no longer load-bearing.

Why this is the right shape

Three unit tests are what make this safe rather than merely working:

  • named_layouts_are_honored — the named constructors reproduce exactly the
    strides the old enum variants implied, so replacing the enum cannot silently
    change a layout
    . That was my main risk with this change.
  • reservation_view_is_rejected and non_unit_head_dim_is_rejected — the hard
    gate holds in descriptor form. Anything the converted path cannot honor
    errors rather than mis-indexing, which is the property that lets seq-major
    exist in the tree without being able to half-work.
  • decode_prelude_matches_layout_formulae / prep_prelude_matches_layout_formulae
    — the generated NVRTC preludes are pinned to the layout formulae, so a stride
    bug shows up as a failing test rather than as wrong logits.

And the GPU parity test still asserts what matters, including the independent
CPU oracle
— without it, a shared indexing error across both layouts would
still produce "byte-identical" agreement and pass.

Why a descriptor rather than a third enum variant

Recorded so this is not re-litigated:

  1. Token-major collapses the per-(layer, side) buffer boundary that both
    existing variants preserve, so it is not expressible as another axis
    permutation — the descriptor's reservation-view fields are what will carry it.
  2. Layout preference is per-EP and per-platform, not global. ORT GQA is
    BNSH-only on every dispatch path; Intel reportedly prefers BNHS, which is
    coherent because Level Zero exposes ~64 KiB pages where the layout penalty is
    ~32x smaller than our measured 2 MiB granule. An enum would need a variant per
    (platform, preference) combination.
  3. We JIT with NVRTC, so strides are compile-time constants per module —
    generality costs nothing at runtime. That is a real advantage of a JIT backend
    over an AOT one, and this PR uses it: static per-layout module keys (decode v8,
    prep v3), no runtime branch, and head-major reuses the shared prep module via
    #ifndef GQA_KV_DST.

Default behavior is unchanged: absent metadata still means head-major, and
seq-major stays hard-gated to the one converted fp16 path.

Notably, the descriptor expressed everything cleanly — no ModelIoSpec
limitation surfaced, which was the main thing that could have forced a redesign.

Merging.

@justinchuby
justinchuby merged commit 26f2657 into main Aug 11, 2026
1 check passed
@justinchuby
justinchuby deleted the squad/783-kv-stride-descriptor branch August 11, 2026 19:57
@github-actions

Copy link
Copy Markdown

⚠️ Benchmark Change Detected

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

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

Status Scenario Base PR Change
⚠️ gather/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: ⚠️ ≥ 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: { 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)

justinchuby added a commit that referenced this pull request Aug 11, 2026
## 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
justinchuby added a commit that referenced this pull request Aug 12, 2026
## 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>
justinchuby added a commit that referenced this pull request Aug 12, 2026
…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>
justinchuby added a commit that referenced this pull request Aug 12, 2026
…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>
justinchuby added a commit that referenced this pull request Aug 12, 2026
## What

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

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

## Most misleading claim found

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

## Changes

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

## Method

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

## Note on #762

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

## Validation

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

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

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

2 participants