Skip to content

Govern GQA BNSH transpose workspace - #810

Merged
justinchuby merged 1 commit into
mainfrom
squad/736-gqa-bnsh-transpose
Aug 12, 2026
Merged

justinchuby merged 1 commit into
mainfrom
squad/736-gqa-bnsh-transpose

Conversation

@justinchuby

Copy link
Copy Markdown
Owner

References #736

Result

  • Governs WS_Q_BNSH and WS_OUT_BNSH through the existing GQA workspace_requirement / prepared composite; execute_with_workspace carves the same helper-computed layout and rejects short views deterministically.
  • Finds partial over-reservation: unpacked, non-RoPE Sq == 1 decode now uses input Q and output directly, so both transpose regions charge zero and the whole GQA requirement is NONE when staging/scores are also absent.
  • Unpacked RoPE decode still needs writable Q scratch; packed decode needs Q extraction. Packed Sq == 1 overlays Q extraction/BNSH with packed-Q staging rather than summing mutually exclusive uses. Every Sq > 1 route needs Q and output transpose buffers.
  • Seq-major BSNH affects KV-cache strides only; Q/output consumers remain BNSH, so prefill still needs both transposes.
  • Preserves direct-execute self-owned pooled compatibility fallback. Prepared grants retain the existing typed pre-header 429 capacity-refusal path.

Bytes and lifetime

Derived from dtype.byte_size() and code geometry (not allocator measurements): at B=1, H=32, head_dim=128, Sq=1024, Q+output transpose scratch is 32 MiB f32 or 16 MiB fp16/bf16. At Sq=1, unpacked non-RoPE is 0; unpacked RoPE is Q-only: 16 KiB f32 or 8 KiB fp16/bf16.

The old GqaWorkspace slots grew and remained resident across calls, so this is truthfully WorkspaceLifetime::SessionPersistent. The executor-owned prepared grant supplies success/error/cancellation cleanup and device-authority attribution.

Audit/docs

Updates MEMORY_ARCHITECTURE.md §2.1 and the site audit, and selects default-domain Attention staged K/V as the next evidence-backed slice. Adds a CPU-only source-scan regression preventing unconditional raw transpose-slot reservations.

Local validation

  • cargo fmt -p onnx-runtime-ep-cuda -p onnx-runtime-session -- --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-session (153 core tests plus all integration/doc suites green)
  • GPU, single-threaded: group_query_attention_gpu (25 passed), gqa_seqmajor_parity_gpu (2), attention_gpu (5), allocation_counts_gpu (2)

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

Copy link
Copy Markdown
Owner Author

Coordinator pre-merge validation on the merged branch: cargo fmt -p onnx-runtime-ep-cuda -p onnx-runtime-session -- --check clean; cargo clippy -p onnx-runtime-ep-cuda --features cuda -- -D warnings clean (exact CI gate); full
cargo test -p onnx-runtime-session: 153 passed
; GPU single-threaded —
group_query_attention_gpu 25, gqa_seqmajor_parity_gpu 2,
allocation_counts_gpu 2.

The lead paid off, and the split is the honest kind

I pointed this slice at a specific clue: the source notes that the BSH->BNSH
transpose is identity for Sq == 1, i.e. a no-op on the decode hot path. The
result:

unpacked non-RoPE Sq == 1 decode now charges zero transpose scratch;
prefill remains genuinely dependent on both transpose buffers.

That is exactly the shape this audit keeps finding — bytes charged against the
device authority on a path that never uses them — and it lands on the hot path for
every model we care about. The prefill half being genuinely needed is equally
important to state: it stops the next reader from assuming the whole slot is
removable.

The tally, six slices in

Slice Result
#751 IndexShare over-reservation — staging the 3-output decode path never touches
#795 GQA WS_SCORES over-reservation — f32 reference route only (~128 MiB hot path)
#799 cuBLASLt GEMM over-reservation — 32 MiB was a heuristic ceiling; measured use 0-96 bytes
#802 default-domain Attention scores genuinely needed — no flash path, every dispatch materializes
#806 GQA QKV staging over-reservation — unpacked and fp16/bf16 fused-decode routes charge 0
#810 GQA BNSH transpose over-reservation on decode; genuinely needed on prefill

Five of six. The guidance recorded in #800 — start from use, not from allocation
— has now paid for itself five times, and #802 remains the valuable
counter-example that keeps it from becoming a assumption rather than a method.

Worth noting the pattern within the pattern: three of the five over-reservations
were route-conditional rather than wholly unnecessary (#795 f32-only, #806
packed-input-only, #810 prefill-only). The recurring defect is not "this buffer is
useless" but "this buffer is charged on routes that do not use it" — which is why
the fix is almost always a static signal threaded through the shared sizing helper,
not a deletion.

Node matching went into the centralised is_planned_workspace_node (+3 lines),
not a parallel predicate.

Merging.

@justinchuby
justinchuby merged commit d7db6f2 into main Aug 12, 2026
5 of 14 checks passed
@justinchuby
justinchuby deleted the squad/736-gqa-bnsh-transpose branch August 12, 2026 13:27
@codecov

codecov Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.55%. Comparing base (d0c79ee) to head (dfca5f2).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #810   +/-   ##
=======================================
  Coverage   83.55%   83.55%           
=======================================
  Files          12       12           
  Lines        5192     5192           
  Branches     5192     5192           
=======================================
  Hits         4338     4338           
  Misses        677      677           
  Partials      177      177           
Flag Coverage Δ
cli-ort-linux 83.50% <ø> (ø)
cli-ort-windows 83.09% <ø> (ø)

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

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

@github-actions

Copy link
Copy Markdown

🔴 Benchmark Regression Detected

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

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

Status Scenario Base PR Change
🔴 block_quantized_matmul_cached_dense/mxfp4_cached_dense_repeated_call/1x1024x1024 51.38 µs 138.04 µs +168.7%
🔴 block_quantized_matmul_cached_dense/mxfp4_preexpanded_dense_oncelock_like_proxy/1x1024x1024 69.41 µs 107.08 µs +54.3%
🔴 block_quantized_moe_cached_dense/mxfp4_uncached_expert_dequant_each_call/rows=1,H=256,I=256,E=4,top_k=1 391.13 µs 597.79 µs +52.8%
🔴 block_quantized_matmul_cached_dense/mxfp4_uncached_dequant_each_call/1x1024x1024 825.39 µs 1.25 ms +51.1%
🔴 matmul/large_generic_f32_threads=8/32x1024x1024 4.45 ms 6.57 ms +47.6%
🔴 gather/large_f32_threads=1-internal/131072 31.34 µs 43.05 µs +37.4%
🔴 matmul/large_generic_bf16_threads=8/32x1024x1024 1.68 ms 2.25 ms +34.1%
⚠️ gather/medium_f16_threads=1-internal/32768 2.58 µs 3.30 µs +27.9%
⚠️ tokenization/decode_tokens_per_second 6.11 ms 7.63 ms +24.9%
⚠️ matmul/small_generic_bf16_threads=1/1x256x256 32.94 µs 40.41 µs +22.7%
⚠️ gather/small_bf16_threads=1-internal/4096 569.7 ns 691.4 ns +21.3%
⚠️ tokenization/encode_tokens_per_second 375.59 µs 453.57 µs +20.8%
⚠️ block_quantized_moe_cached_dense/mxfp4_cached_dense_expert_repeated_call/rows=1,H=256,I=256,E=4,top_k=1 77.61 µs 92.55 µs +19.2%
⚠️ sampling_latency/top_k_per_token 52.26 µs 62.22 µs +19.0%
⚠️ gather/large_f16_threads=1-internal/131072 18.01 µs 21.38 µs +18.7%
⚠️ qwen3_sampling_processors/top_k_partial_selection 151.98 µs 179.86 µs +18.3%
⚠️ matmul/small_generic_f16_threads=1/1x256x256 33.98 µs 39.87 µs +17.3%
⚠️ sampling_latency/greedy_per_token 3.31 µs 3.85 µs +16.3%
⚠️ matmul/small_generic_bf16_threads=8/1x256x256 36.59 µs 42.49 µs +16.1%
⚠️ gather/medium_bf16_threads=1-internal/32768 2.71 µs 3.14 µs +16.0%
⚠️ sampling_latency/min_p_per_token 215.96 µs 249.00 µs +15.3%
✅ add/small_f16_threads=1-internal/1024 15.21 µs 17.39 µs +14.4%
✅ add/large_f16_threads=1-internal/4194304 41.62 ms 47.39 ms +13.9%
✅ matmul/large_generic_f16_threads=1/32x1024x1024 85.22 µs 96.84 µs +13.6%
✅ add/medium_bf16_threads=1-internal/262144 2.87 ms 3.22 ms +12.0%
✅ qwen3_sampling_processors/top_p_fast_after_top_k 540.66 µs 597.82 µs +10.6%
✅ matmul/small_generic_f32_threads=8/1x256x256 52.76 µs 57.99 µs +9.9%
✅ matmul/medium_generic_bf16_threads=8/32x512x512 619.27 µs 678.55 µs +9.6%
✅ add/small_bf16_threads=1-internal/1024 13.02 µs 14.25 µs +9.5%
✅ matmul/small_generic_f16_threads=8/1x256x256 37.52 µs 40.57 µs +8.2%
✅ sampling_latency/top_p_per_token 422.87 µs 452.43 µs +7.0%
✅ qwen3_sampling_processors/top_p_full_sort_after_top_k_baseline 3.88 ms 4.14 ms +6.7%
✅ qwen3_sampling_processors/top_k_full_sort_baseline 2.41 ms 2.56 ms +6.0%
✅ gather/small_f32_threads=1-internal/4096 737.5 ns 779.7 ns +5.7%
✅ matmul/medium_generic_f16_threads=8/32x512x512 46.17 µs 48.55 µs +5.2%
✅ matmul/large_generic_f16_threads=8/32x1024x1024 107.79 µs 113.25 µs +5.1%
✅ matmul/large_generic_f32_threads=1/32x1024x1024 11.41 ms 11.75 ms +3.0%
✅ gather/large_bf16_threads=1-internal/131072 15.25 µs 15.62 µs +2.5%
✅ matmul/small_generic_f32_threads=1/1x256x256 48.97 µs 49.03 µs +0.1%
✅ gather/medium_f32_threads=1-internal/32768 4.96 µs 4.96 µs +0.1%
✅ add/medium_f32_threads=1-internal/262144 2.66 ms 2.64 ms -0.8%
✅ matmul/medium_generic_bf16_threads=1/32x512x512 652.74 µs 647.31 µs -0.8%
✅ matmul/large_generic_bf16_threads=1/32x1024x1024 2.31 ms 2.29 ms -0.9%
✅ qwen3_sampling_processors/top_k_top_p_fast 856.99 µs 809.83 µs -5.5%
✅ gather/small_f16_threads=1-internal/4096 618.0 ns 583.4 ns -5.6%
✅ kv_cache/alloc_dealloc_pages 54.01 µs 50.32 µs -6.8%
✅ add/large_bf16_threads=1-internal/4194304 53.46 ms 49.48 ms -7.4%
✅ add/large_f32_threads=1-internal/4194304 47.57 ms 43.99 ms -7.5%
✅ qwen3_sampling_processors/top_k_top_p_full_sort_baseline 7.56 ms 6.72 ms -11.1%
✅ reduce_mean/large_f32_threads=1-internal/262144 1.17 ms 1.04 ms -11.3%
✅ grammar_masking/llguidance_compute_mask/32 100.52 µs 88.82 µs -11.6%
✅ add/small_f32_threads=1-internal/1024 260.6 ns 227.1 ns -12.9%
✅ logit_processing/seven_processor_chain_per_step 433.47 µs 370.08 µs -14.6%
✅ matmul/medium_generic_f32_threads=1/32x512x512 3.22 ms 2.74 ms -14.7%
🟢 reduce_mean/small_f32_threads=1-internal/4096 19.60 µs 16.57 µs -15.5%
🟢 matmul/medium_generic_f16_threads=1/32x512x512 50.39 µs 42.06 µs -16.5%
🟢 matmul/medium_generic_f32_threads=8/32x512x512 2.02 ms 1.66 ms -18.0%
🟢 add/medium_f16_threads=1-internal/262144 3.27 ms 2.66 ms -18.6%
🟢 reduce_mean/medium_f32_threads=1-internal/65536 349.33 µs 256.62 µs -26.5%

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

Host info
CPU: Apple M1 (Virtual)
Cores: 3
OS: Darwin 25.5.0 arm64
Rust: rustc 1.97.1 (8bab26f4f 2026-07-14)
Load avg: { 4.26 3.59 6.25 }
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 12, 2026
Follow-up to #810 / issue #736.

## Regression

The newly governed GQA prefill transpose workspace exposed a
mapped-growth authority gap in the native CUDA VMM path. The executor
prepares session-persistent and step-scoped workspace slots
independently, but both share the canonical arena mapped zone. That zone
starts with a zero-byte allowance. Its first 2 MiB granule could only
grow by transferring capacity from a registered holder; when no such
holder existed, prefill failed with `TierExhausted` despite unassigned
device-authority capacity.

## Fix

- Assign mapped capacity that belongs to no zone before attempting
holder transfer/reclaim.
- Keep committed capacity with the arena zone.
- Return newly assigned capacity when a grant is rolled back or the
allocator maps fewer bytes than planned (including reuse of an existing
granule).
- Preserve typed `TierExhausted` refusal once the authority is genuinely
exhausted.
- Document the mapped-zone growth order.

This preserves the truthful session-persistent GQA transpose lifetime
and the `Sq == 1` route-sensitive `NONE` behavior from #810; it does not
restore self-owned scratch or weaken authority accounting.

## Validation

- `cargo test -p onnx-runtime-memory-governor` (39 + 5 passed)
- memory-governor Clippy with `-D warnings`
- exact CUDA EP Clippy gate
- exact engine all-target Clippy gate
- full `cargo test -p onnx-runtime-session`
- Mobius seq-major growth parity native-CUDA regression (all
VMM/realloc, capture on/off configurations passed)
- GPU suites single-threaded: GQA 25, seq-major parity 2, Attention 5,
allocation counts 2

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