Skip to content

perf(rocke): build one K/V resource per page on the gfx942 2D i64 path - #14349

Merged
AnaghaRaoAMD merged 5 commits into
developfrom
users/dlangbe/rocke-gfx942-2d-i64-kv-pages
Oct 27, 2026
Merged

AnaghaRaoAMD merged 5 commits into
developfrom
users/dlangbe/rocke-gfx942-2d-i64-kv-pages

Conversation

@dlangbe

@dlangbe dlangbe commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

JIRA ID : AICK-2717

Motivation

#12446 made paged K/V caches past the i32 buffer range (> 0x7FFF0000 bytes) use i64 K/V addressing, which fixed silent zero reads of the cache's top 64 KiB. On gfx942 the tiled 2D i64 path turned out much slower than the i32 path. For bf16 D128 decode on exactly-2-GiB caches (B=256, page size 16; the decode shapes of Llama-3-8B with Skv=4096 and GQA 32/8, Llama-3-70B with Skv=4096 and GQA 64/8, and Qwen3-235B-A22B with Skv=8192 and GQA 64/4), develop's i64 path takes 1.41x to 1.43x as long as its i32 path (measured as described under Performance).

Cause, from the gfx942 ISA: with i64 addressing the K/V loader built a buffer resource for every copy (192 per kernel for these shapes). Each copy reloaded its page id from the block table, waited for it (v_readfirstlane needs the value), and rebuilt the resource. Because vmcnt drains in order, each of those waits also drained the K/V copies issued before it, so the K/V stream ran with little memory-level parallelism. A 64-token tile spans only 4 pages.

Technical Details

Changes

  • kernels/gfx942/attention_tiled_2d.py: when i64 addressing is on, a tile holds whole pages (T % BS == 0), and no copy crosses a page ((BS * HD) % KV_HALVES_PER_CALL == 0), the K and V loaders load the tile's page ids once (_tile_pages: one masked block-table load and one readfirstlane per page) and build one resource per page. Each copy takes its page's resource and an offset within the page (_page_voff). Other cases, the fast paged descriptor and the i32 path are unchanged.
  • C++ twin (attention_tiled_2d_kv_descriptors_loads.cpp): the same loader, with builder calls in Python's evaluation order. The comments that said i64 never reaches the non-fast loader are corrected.
  • _enable_i64_kv_addr docstring: drops a stale measured-cost claim.

Behaviour change

  • gfx942 tiled 2D with i64 K/V addressing: same results, fewer page-table loads and resources (for the decode spec: 12 page loads and 12 extra resources per kernel instead of 192 and 193).
  • i32 paths, gfx950 and all other kernels: emitted IR unchanged (no existing golden hash moves).
  • Also closes a C++-only gap: the C++ twin's non-fast K/V loaders had no i64 path and would have emitted i32 addressing for these specs; no emit config covered it before cfg7.

Coverage

  • Parity (gfx942_attention_tiled_2d_emit.{py,c}), byte-identical at llvm20/22/23. Develop's C++ loader drifts on exactly cfg7-10:
    • cfg7: the selector's spec for the i64 decode shape (1 warp, 4 pages per tile).
    • cfg8: i64, 4 warps, 2 pages per tile.
    • cfg9: i64, 1 page per tile.
    • cfg10: cfg7 with K copied by global_load_lds.
  • Golden: new representative-IR case attention/gfx942/2d_bf16_d128_b16_decode_i64 (additive diff only).
  • tests/test_attention_gfx942_kv_pages.py:
    • CPU structural check, over 1 warp with 4 pages per tile, 4 warps with 2 pages, and 1 page per tile: the i64 kernel issues one page-table load and one resource per page of each tile load, and the same number of copies as i32. All three fail on the per-copy loader. It builds the spec directly and checks counts only; the numeric cases cover addressing.
    • gfx942 numeric, against an fp32 reference with atol=2e-3, rtol=2**-8 (the relative term covers bf16 rounding of large outputs on short sequences):
      • i64 decode, B=8, Skv=1024, GQA 32/8 and 64/4 (num_kv_blocks overstated to force i64). Fails if the in-page offset drops the kv-head term.
      • Ragged lengths 17 to 1000 with a block table 63 pages wide, so the last 64-token tile reads past every row; the table's tail holds an unmapped page id. Removing the block-table bound faults the test.
      • A cache just over 2 GiB per tensor with the sequences' pages straddling 2^31 bytes (skips under about 5 GiB free). Sign-extending the page base, or falling back to the old whole-cache i32 loader, fails it while the small-cache cases still pass.
    • Enrolled in the installed library GPU CTest list and docs/gpu-attention-test-coverage.md. The gfx942 CI lane has no torch, so the numeric cases skip there and only the structural check runs.

Not in this PR

  • The K-sliced-ring slice loader still builds a buffer resource per copy under i64 addressing. It is reachable for caches between 0x7FFF0000 and 2^31 bytes, where its conflict-free V loader is still in range; the C++ twin rejects the ring.
  • Page ids are bounded by index (block_table_max_idx) but not checked against the cache's block count before they form a 64-bit base, the same as the i32 path and the fast descriptor. Bounding them needs the block count as a kernel argument, which is an ABI change.
  • Each K or V tile load still waits once on its page-table loads before readfirstlane; the per-copy waits are gone.
  • gfx950 tiled 2D has the same per-copy pattern (2 pages per tile, 8 copies). Follow-up, needs gfx950 hardware to measure.
  • The i32 path also loads a page id per copy; moving it to per-page resources changes IR for every gfx942 2D paged spec and is a separate PR.

Performance

Relative results only. gfx942, ROCm 10.0.0 wheels (torch 2.13.0+rocm10.0.0, LLVM 23 from the ROCm SDK, the attention suite's default toolchain), one idle GPU. Each arm: 5 warmup launches through run_unified_attention_torch, then the median of 5 batches of 50 launches timed with HIP events. Correctness gate in the same run: max_abs against an fp32 PyTorch reference on sampled sequences, tolerance 1e-3; every arm passed with matching max_abs (2.8e-4 to 3.4e-4). Page tables were tested contiguous and random; the ratios below hold for both.

Decode on an exactly-2-GiB cache (B=256, D128, bf16, page size 16), branch on the i64 path:

Shape develop i64 vs develop i32 branch i64 vs develop i64 branch i64 vs develop i32
Skv 4096, GQA 32/8 1.43x as long 2.15x faster 1.50x faster
Skv 4096, GQA 64/8 1.42x as long 2.11x faster 1.49x faster
Skv 8192, GQA 64/4 1.41x as long 2.11x faster 1.49x faster

The third column is a near-equivalent comparison: the i32 arm uses a cache two pages smaller, the largest i32 can address, with the same kernel otherwise.

Other shapes on the i64 path (forced by overstating num_kv_blocks, median of 5 batches of 20 launches, same correctness gate), branch vs develop: prefill B=4 S=1024 GQA 32/8 1.21x faster; prefill B=1 S=4096 GQA 64/8 1.30x faster; decode B=64 Skv=8192 GQA 64/8 2.07x faster. These are diagnostic configurations, not shipped cache sizes.

Test Plan

  • ROCKE_LLVM_FLAVOR=llvm23 python rocke/platform/tools/check_byte_identity.py: GREEN.
  • python rocke/tools/run_checks.py --steps gate,parity (llvm20 + llvm22): GREEN.
  • Mutation: without the C++ change, gfx942_attention_tiled_2d cfg7 drifts.
  • rocke_ir_parity_harness.py --check: OK before the new case; after re-bless the golden diff only adds the new case and count bumps.
  • tests/test_attention_gfx942_kv_pages.py on gfx942 with ROCm 10.0.0 wheels (LLVM 23) and torch 2.10.0+rocm7.0 (LLVM 20): 3 passed. Structural test fails on the old loader; numeric test fails with a broken in-page offset.
  • library/tests on CPU, one pytest per file with the GPU hidden (backward attention excluded): 169 files pass; the 6 failing files (test_strided_kv_decode, test_strided_kv_lds, test_kda_hostpack, test_conv_abi, test_conv_dgrad_patch, and the gfx950 use_additive_bias case in test_selector_parity) fail the same way on develop.
  • platform/tests/test_rocke_ci_static.py: 5 passed.
  • gfx942 GPU performance and correctness as in Performance above.

Submission Checklist

@dlangbe
dlangbe requested review from a team as code owners October 24, 2026 00:19
@github-actions github-actions Bot added documentation project: hip-kernel-provider rocke-lib: attention Touches rocKE library attention/FMHA kernels rocke: platform Touches rocKE platform labels Oct 24, 2026
@therock-pr-bot

therock-pr-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ✅ Pass —
🔎 pre-commit ✅ Pass —
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —
🤖 therock-pr-bot ✅ Pass —

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@therock-pr-bot

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

@cgmillette

Copy link
Copy Markdown
Contributor

Automated review: Model - qwen3.6:35b-a3b-coding-mtp-q4_K_M, effort: max

PR #14349 — one K/V resource per page on the gfx942 2D i64 path — 🟢 First-round: no closer look needed beyond AnaghaRaoAMD's two open threads — no other kernel's IR moves, both engines agree on the new loader, and taking out either half of the change makes its own check fail

Prior item Status
AnaghaRaoAMD (10-24) attention_tiled_2d.py:2688: both conditions of PAGE_RSRC_I64 always hold, so assert them instead 🟡 OPEN, no reply yet. Our check agrees for every spec the predicate admits. supports() allows head size 64 / 128 / 256, block size 16 / 32 / 64 and at most 8 warps, which gives at most 1,024 halves per call against a page of at least 1,024 halves, all powers of two (read). The assert would also turn a hand-built spec outside that envelope into a clear error rather than the unhandled i64 case in C++
AnaghaRaoAMD (10-24) test_attention_gfx942_kv_pages.py:159: atol=1e-2 is too loose to catch a partial addressing bug 🟡 OPEN, no reply yet. We have no gfx942 here to measure it, and CI skips this numeric test (below)
Bots / CI 🟢 Green at head 9c32c354: TheRock Multi-Arch, ASAN, the hipDNN superbuild, pre-commit and clang-tidy. In the gfx94X and gfx110X shards, the new structural test passes; both numeric cases skip because the lanes have no torch, as the description says (read)
Our checks 🟢 No other kernel's IR moves: the differential sweep over every anchored family (1,256 configs, llvm20) is identical to develop's (ran). Byte identity is GREEN for all 87 families at llvm22, and for gfx942_attention_tiled_2d at llvm20 and llvm23 too (ran). The native build and 30 ctests pass (ran). The structural test passes with both engines in strict mode (ran). Negative controls (ran): with develop's Python loader, the structural test fails (192 == 4 * 3). With develop's C++ twin, byte identity for gfx942_attention_tiled_2d goes RED on one of its 8 configs. The library CPU tests that import the 2D kernel show no new failure against develop (558 passed) (ran). The test is in the installed GPU CTest list (read). rule_scan is clean (ran)
Merge 🟢 GitHub reports a conflict because develop moved, but only the representative golden conflicts, and the change is additive. Regenerating it on the merge gives develop's entries plus the one new 2d_bf16_d128_b16_decode_i64 case, with no drift, and the golden gate passes at llvm20 / 22 / 23 (ran)

No net-new findings.

Utility: 🟢 Positive — the i64 K/V path on gfx942 2D no longer rebuilds a buffer resource and reloads a page id for every copy. It changes nothing on the i32 path or for any other kernel.


Legend: 🔴 blocking · 🟠 should-fix · 🟡 minor nit · 🟢 info/good · ⚪ retracted

First-round review: a local qwen3.6 model made the independent first pass. The orchestrator (Claude Opus 5.5) verified it. The checks were a regenerated golden for the merge and the native build and ctests. They also included byte identity for every family, a differential sweep of every anchored family against develop, and the new test with both engines in strict mode. Negative controls with develop's Python loader and develop's C++ twin, and the library CPU tests against develop, were run as well. Finally, they covered the CI logs at head. The orchestrator owns this merge-worthiness call. It flags whether a closer look is needed; it is not an approval or a rejection.

@amd-khushbu

Copy link
Copy Markdown
Contributor

Review checklist pass (rocKE PR-review checklist, two independent passes)

Ran this PR through the team's rocKE PR-review checklist (byte-identity/two-engine parity, coverage/fallback behavior, DRY/blast-radius, dispatcher/spec integration, structure/hygiene, merge-readiness, verification honesty, paged/indirection safety, reusable design principles), then did a second independent pass focused on the addressing math and test coverage. No blockers in either pass. The core change is sound: addressing math is correct (page selection, in-page byte offsets, i64 base arithmetic all check out against the pre-existing stride/guard logic), the C++ twin's emit order matches Python exactly (verified op-by-op), and this is correctly scoped as an emitter-only fix (no selection/routing logic touched).

Worth calling out explicitly: this also fixes a latent C++-only correctness gap

Before this PR, the C++ twin's non-fast paged-KV branch had no i64-addressing code path at all — it always built the plain i32 voffset regardless of I64_KV_ADDR (the removed comment said as much: "I64_KV_ADDR is false in gfx942's buildable space, so the plain i32 voffset is used"). Python's equivalent branch, by contrast, already correctly dispatched to offset_i64_split for i64 specs before this PR. So if the C++-built kernel were ever exercised for a cache large enough to need i64 addressing on this non-fast path (exactly the shape the new cfg7 parity case adds, which didn't exist before this PR), it would have silently used i32 addressing — reintroducing the silent-zero-reads bug #12446 fixed, but only in the C++ engine. "Same results" undersells this a bit; might be worth a line in the PR description saying this also closes a latent C++-only gap (reachability of that exact shape via the shipped C++ engine prior to this PR wasn't established either way, for what it's worth).

MEDIUM

  • The K-sliced-ring loader (use_k_sliced_ring, attention_tiled_2d.py:2839-2848) still rebuilds one buffer resource per copy via offset_i64_split under i64 addressing — the same per-copy pattern this PR fixes elsewhere. Nothing in __post_init__ makes use_k_sliced_ring and use_i64_kv_addr mutually exclusive. The PR's "Not in this PR" list names the FP8 loaders, but those are actually unreachable on gfx942 (rejected explicitly) — the K-sliced-ring loader is the sibling that's actually reachable and isn't named.
  • The block-table OOB guard and a ragged (non-tile-aligned) last tile are never exercised — the structural and numeric fixtures both use a seqlen that's an exact multiple of tile_size. Deleting the cmp_lt mask, or an off-by-one in first + page, would still pass both tests. (confirmed independently in both review passes)
  • No committed test actually reads memory past the 2 GiB boundary — the GPU numeric test's cache is ~17 MB, with num_kv_blocks overstated only to flip the i64 flag. Every real offset in the test stays under 2^31, so a wrong multiply width or an incorrectly-sized resource bound would still pass.
  • Coverage is num_warps=1 / 4-pages-per-tile / decode-only. Production also reaches other i64 geometries via _select_2d_num_warps (prefill gets 2 or 4 warps — the PR's own prefill perf numbers ran on this), plus N_BLOCKS_PER_TILE==1 and USE_GLOBAL_LOAD_LDS_K with i64 — none of these have a parity config, golden case, or numeric test, so C++/Python parity for these geometries is unverified.
  • The structural test (test_i64_copies_share_one_resource_per_page) is a pure intrinsic-count check; it would not catch a bug that preserves counts but uses the wrong page/offset (e.g. a swapped page index or bad stride in _page_voff). It's also the only new test that runs in CI (the numeric test is GPU-gated and skips on the gfx942 CI lane, which has no torch).
  • The new GPU test was added to the shared rocke_library_gpu_pytest CTest entry (~30 files, one TIMEOUT 1800) without re-timing the whole entry on hardware after the addition.
  • No VGPR/scratch check (llvm-readelf --notes or rocprofv3) was run despite the change now holding up to 4 live buffer resources per tile instead of 1 — a plausible register-pressure increase that a speedup alone doesn't rule out.
  • The root-cause explanation (vmcnt in-order draining serializing the K/V stream) is derived from reading ISA/instruction-ordering semantics rather than a dynamic hardware counter.

LOW

  • attention_tiled_2d.py:337-339 (the use_i64_kv_addr field's own docstring) still says "it costs a per-call make_buffer_rsrc" — the PR updated the sibling _enable_i64_kv_addr docstring in attention_unified.py but missed this one.
  • The block-table physical id is index-guarded (block_table_max_idx) but not value-range-checked against the real num_kv_blocks before folding into the 64-bit pointer base. A padded/garbage table entry inside the valid range could point the resource base at unmapped memory. This is identical to the pattern in the code it replaces (the removed offset_i64_split call, and _fast_paged_kv_voff's i64 branch) — not new or worsened by this PR, but cheap to harden now since page ids are loaded once per page instead of once per copy.
  • The two engines fail differently on an out-of-range page count: C++ has a new ROCKE__MAX_TILE_PAGES = 16 cap, Python has none. Both reviews independently computed the real ceiling of n_blocks_per_tile under supports_tiled_2d's LDS gate and got exactly 16 — provably unreachable today, but worth mirroring the cap on the Python side for clarity.
  • A vmcnt drain still happens once per tile load (the page-id load + readfirstlane at the head of each K/V tile load) — the serialization this PR targets is reduced, not eliminated. Possible follow-up, not a defect.
  • The n_pages > ROCKE__MAX_TILE_PAGES C++ rejection doesn't follow the codebase's NOT_YET_IMPLEMENTED: convention used elsewhere for implementation-shortcut rejections.
  • test_i64_copies_share_one_resource_per_page enters via dataclasses.replace(_DECODE, use_i64_kv_addr=True), bypassing the real selector. Defensible as a structural check (the companion numeric test independently confirms reachability through the selector), but the docstring doesn't say so.
  • The "12 loads/resources instead of 192/193" comparison isn't reproducible by any committed test, since the old per-copy loader was deleted. Inherent to any refactor that removes the old code, not unique to this PR.
  • The C++-built kernel for this path is never executed on a GPU in this PR's own tests, only byte-identity-diffed (as IR text) against Python's build, which is the one validated on silicon. Matches the repo's existing two-engine test strategy, not unique to this PR.

Open questions (not findings)

  • Which develop commit/worktree was the "develop i64 vs develop i32" baseline measured against?
  • Is there a live production C++ dispatcher (outside this diff) that actually routes real inference traffic through this exact path, or is C++ validated only via byte-identity today?
  • Was the (i64 addressing + non-fast descriptor) combination ever actually reachable via the shipped C++ engine prior to this PR, or was it purely latent/untested until now?

@cgmillette

Copy link
Copy Markdown
Contributor

Automated review: Model - qwen3.6:35b-a3b-coding-mtp-q4_K_M, effort: max

PR #14349 — one K/V resource per page on the gfx942 2D i64 path — 🟢 Follow-up: no closer look needed — both of AnaghaRaoAMD's threads are fixed, the refactor leaves every parity config's IR unchanged, and the per-page resources cost 5 SGPRs while saving 22 VGPRs; one 🟡 doc line

Prior item Status
AnaghaRaoAMD (10-24) thread 1: assert the two invariants instead of branching on them 🟢 FIXED in 9670fa88. The gate is now I64_KV_ADDR and not FAST_PAGED_KV_DESC, and the unreachable i64 fallback is gone from both engines' K and V loaders. A hand-built spec outside the envelope now gets a ValueError; the C++ twin raises a build error with the same message. At the old head, head size 96 with 8 warps and i64 addressing still built through the removed path; now it is refused with the new message (ran). All 8 gfx942 tiled-2D parity configs emit identical IR at the old head and on the merge, at llvm20 / 22 / 23 (ran)
AnaghaRaoAMD (10-24) thread 2: atol=1e-2 is too loose 🟢 FIXED: the tolerance is now atol=2e-3 (read). The author measured 6.4e-4 to 8.9e-4 on gfx942; we have no gfx942 to repeat it
amd-khushbu (10-26) checklist pass: no blockers; MEDIUM items on coverage and on register pressure 🟡 OPEN; no reply yet. The coverage items: the ragged last tile and the OOB mask are not exercised, no test reads past 2 GiB, several i64 geometries are untested, and the K-sliced-ring loader still builds a resource per copy. Register pressure, measured (ran): the i64 decode kernel (cfg7), compiled for gfx942 with ROCm clang 24, uses 160 VGPRs and 63 SGPRs with no scratch or spills. Develop uses 182 and 58, so the per-page resources save 22 VGPRs and cost 5 SGPRs. The i32 kernel is unchanged (182 / 56)
Bots / CI 🟡 TheRock Multi-Arch has been queued since 16:01 UTC. ASAN, the hipDNN superbuild (which ran before today's develop break), pre-commit, clang-tidy and Component CI are green (read)
Our checks 🟢 The merge with develop is clean (22 behind). The native build and 32 ctests pass (ran). Byte identity is GREEN for all 85 families at llvm22 and for gfx942_attention_tiled_2d at llvm20 / 23 (ran). The golden gate passes ×3, with its new i64 decode case unchanged (ran). The structural test passes with both engines in strict mode (ran). rule_scan is clean (ran)
# Sev file:line Finding
N1 🟡 library/kernels/gfx942/attention_tiled_2d.py:337 The use_i64_kv_addr field comment still says i64 addressing "costs a per-call make_buffer_rsrc". The sibling _enable_i64_kv_addr docstring now describes one resource per page → update the field comment to match. (Worker's catch, verified by reading.)

Utility: 🟢 Positive — the i64 K/V path on gfx942 2D builds one resource per page instead of one per copy. With fewer VGPRs it is cheaper, and the unreachable fallback is now a clear error in both engines.


Legend: 🔴 blocking · 🟠 should-fix · 🟡 minor nit · 🟢 info/good · ⚪ retracted

Follow-up review: a local qwen3.6 model made the independent first pass. The orchestrator (Claude Opus 5.5) verified it. The checks were the IR of every parity config at the old head and on the merge, at three flavors, and out-of-envelope specs at both heads. They also included register and scratch use from gfx942 code objects against develop, the native build and ctests, byte identity, the golden gate, the structural test with both engines and the CI state. The orchestrator owns this merge-worthiness call. It flags whether a closer look is needed; it is not an approval or a rejection.

@dlangbe

dlangbe commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

@amd-khushbu
Thanks for the thorough pass. Changes in the latest push, then the rest.

Fixed

  • Field comment at attention_tiled_2d.py:337 now says one resource per page (also the bot's N1).
  • C++ page cap: removed rather than mirrored. The page pointer/resource arrays are arena-allocated per tile, so both engines accept the same specs and there is no NOT_YET_IMPLEMENTED limit left.
  • Ragged tiles and the block-table guard: new test_i64_ragged_lengths_and_block_table_tail (lengths 17..1000, block table 63 pages wide so the last 64-token tile reads past every row). The table is a view whose tail holds 0x7FFFFFFF, so reading it unmasked faults: removing the cmp_lt bound aborts the test with a GPU memory fault.
  • Real >2 GiB addresses: new test_i64_decode_reads_past_2gib uses a cache of 2 GiB + 4 MiB per tensor with the sequences' pages straddling 2^31 bytes (skips under ~5 GiB free). Sign-extending the page base, or falling back to the old whole-cache i32 loader, fails it while the small-cache tests still pass. (An i32 multiply that is zero-extended is still correct below 4 GiB, so that particular mutation is not a bug at this size.)
  • Other geometries: emit configs cfg8 (4 warps, 2 pages/tile), cfg9 (1 page/tile) and cfg10 (i64 + use_global_load_lds_k), byte-identical at llvm20/22/23; develop's C++ drifts on exactly cfg7-10. The structural test now runs over the 1-warp/4-page, 4-warp/2-page and 1-page geometries, and fails on the per-copy loader for all three.
  • Structural test docstring says it builds the spec directly and only counts; the numeric tests cover addressing through the selector.
  • Tolerance is now atol=2e-3, rtol=2**-8: the 17-token sequence has |o| up to 1.8, where the bf16 rounding of the output alone is 3.7e-3. Misaddressing errors I measured are 0.09-0.2.

Measured, no change

  • Registers (gfx942, ROCm 10, LLVM 23): i64 decode spec 159 VGPR / 63 SGPR vs develop's 180 / 58; 4-warp i64 239 / 96 vs 237 / 95; no spills or scratch; i32 unchanged.
  • CTest: the file's 7 tests take about 11 s and pass inside the full library GPU list. On my gfx942 node the full list does not complete on either develop or this branch: the process dies in test_mla_prefill_gfx942_numeric.py::test_mla_prefill_numeric_dispatched[square_h64], which also happens with that file alone on develop. That is unrelated and I'll raise it separately.

Follow-ups, not in this PR (added to the description)

  • The K-sliced-ring slice loader still builds a resource per copy under i64 (reachable between 0x7FFF0000 and 2^31 bytes; the C++ twin rejects the ring).
  • Page-id value range: not checked against num_kv_blocks, same as the i32 path and the fast descriptor. Bounding it needs the block count as a kernarg, which is an ABI change.
  • One page-table load and readfirstlane per tile load remain; the per-copy waits are gone.
  • Root cause is from the ISA (per-copy s_waitcnt vmcnt before each readfirstlane) plus the measured speedup; I did not capture hardware counters.

Open questions

  • Baseline: develop at 7a1c04c9ccdc for both develop columns.
  • C++ at run time: the gfx942 tiled 2D kernel is built in Python and lowered from its serialized IR; the provider has no native gfx942 tiled 2D pack, so the C++ builder is only checked through byte identity.
  • Reachability before this PR: the non-fast i64 combination was reachable in Python (the selector sets i64 for caches past the buffer range), so the C++ builder's missing i64 path was a parity gap with no config covering it, not a live runtime bug. Agreed it's worth saying, and the description now does.

dlangbe and others added 5 commits October 27, 2026 00:53
With i64 K/V addressing, the gfx942 tiled 2D loader built a buffer
resource for every K/V copy: each copy reloaded its page id, waited on
that load and rebuilt the resource. Because vmcnt drains in order, those
waits also drained the copies already in flight, which serialized the
K/V stream for caches past the i32 buffer range.

Load a tile's page ids once and build one resource per page, so each
copy needs only its offset within the page. This applies when a tile
holds whole pages and no copy crosses a page. The C++ twin gets the same
loader, a gfx942 tiled 2D parity config pins the selector's i64 decode
spec, and a representative-IR golden case is added; no existing hash
changes and i32 paths emit unchanged IR.

Co-Authored-By: Claude <noreply@anthropic.com>
Add test_attention_gfx942_kv_pages.py to the installed library GPU test
list so CI runs it, and record its numeric cases in the GPU attention
coverage map. The gfx942 CI lane has no torch, so the numeric test skips
there and only the CPU structural check runs.

Co-Authored-By: Claude <noreply@anthropic.com>
…gfx942 2D

The spec requires tile_size to be a multiple of block_size, and
supports_tiled_2d bounds a copy to at most one page, so every i64 K/V
copy on the non-fast loader takes the per-page path. Gate per-page
resources on I64_KV_ADDR alone, raise if a hand-built spec ever breaks
those invariants, and remove the unreachable offset_i64_split branches
from the K and V fallbacks, which are now i32-only. The C++ twin reports
the same error. No emitted IR changes.

Tighten the i64 decode test to atol=2e-3, below the output's spread.
The C++ twin held a tile's page pointers and resources in fixed 16-entry
arrays and refused larger tiles, a limit the Python builder does not
have. Allocate them from the builder arena per tile so both engines
accept the same specs. Also update the use_i64_kv_addr field comment,
which still described one resource per copy. No emitted IR changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Add gfx942 numeric cases for ragged sequence lengths whose last tile
reads past the block table (the table's tail holds an unmapped page id),
and for a cache just over 2 GiB whose pages straddle 2^31 bytes. Run the
structural check over 1-warp/4-page, 4-warp/2-page and 1-page tiles.
Add tiled 2D emit configs for those i64 geometries and for K copied by
global_load_lds. The numeric bound adds a relative term for the bf16
rounding of large outputs on short sequences.

Co-Authored-By: Claude <noreply@anthropic.com>
@AnaghaRaoAMD
AnaghaRaoAMD force-pushed the users/dlangbe/rocke-gfx942-2d-i64-kv-pages branch from 36fc391 to 866daeb Compare October 27, 2026 04:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Low-level GPU addressing changes need human confirmation of the reported numerical and Python/C++ parity results, especially because numerical tests skip in the stated CI lane.

No open issues 🎉

What changed in this PR

Optimizes rocKE’s gfx942 tiled 2D attention for large paged K/V caches by reusing one buffer resource per page instead of rebuilding it per copy, following the i64 addressing fix in #12446.

Changes:

  • Implements per-page i64 K/V loading in Python and C++, including previously missing C++ i64 handling.
  • Adds parity configurations, additive IR goldens, and structural and GPU numerical tests.
  • Registers the tests with CTest and updates addressing and coverage documentation.

Paths below are relative to dnn-providers/hip-kernel-provider/rocke/.

File Description
platform/​tests/​instances/​rocke_ir_parity_harness.py Adds a representative i64 decode case.
platform/​tests/​golden/​rocke_representative_ir_sha256.json Adds hashes for the new case across three LLVM versions.
platform/​cpp/​instances/​gfx942/​attention_tiled_2d_kv_descriptors_loads.cpp Implements matching C++ per-page resources and offsets.
platform/​CMakeLists.txt Registers the new test file with installed GPU tests.
library/​tests/​test_attention_gfx942_kv_pages.py Tests resource counts, ragged sequences, and large-cache addressing.
library/​tests/​parity/​gfx942_attention_tiled_2d_emit.py Adds four Python i64 parity configurations.
library/​tests/​parity/​gfx942_attention_tiled_2d_emit.c Adds corresponding C parity configurations.
library/​kernels/​gfx942/​attention_tiled_2d.py Reuses page resources in the i64 K/V loaders.
library/​kernels/​common/​attention_unified.py Updates the addressing docstring and removes a stale performance claim.
docs/​gpu-attention-test-coverage.md Documents the new numerical cases and CI coverage limitations.

Review effort: Balanced

@CongMa13 CongMa13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found. I agree with the following existing comments:

  • Make the whole-page i64 assumptions explicit and reject unsupported specs — AnaghaRaoAMD — #14349 (comment)
  • Tighten the numeric test tolerance — AnaghaRaoAMD — #14349 (comment)
  • Cover ragged table tails and real large addresses, and add coverage for alternate i64 geometries — amd-khushbu — #14349 (comment)
  • Call out that GPU numeric tests skip in the torchless CI lane — copilot-pull-request-reviewer[bot] — #14349 (review)
  • Update the use_i64_kv_addr field comment to describe per-page resources — cgmillette — #14349 (comment)
  • Track the K-sliced-ring i64 per-copy resource path as follow-up work — amd-khushbu — #14349 (comment)

@cgmillette

Copy link
Copy Markdown
Contributor

Automated review: Model - qwen3.6:35b-a3b-coding-mtp-q4_K_M, effort: max

PR #14349 — one K/V resource per page on the gfx942 2D i64 path — 🟢 Follow-up 2: no closer look needed — every open item is fixed or listed under "Not in this PR". The new geometries are byte-identical ×3, the structural test fails on develop's per-copy loader in all three geometries, and no geometry spills. One 🟡 description line

Prior item Status
Ours N1 🟡 (10-26): the use_i64_kv_addr field comment still described a per-call resource 🟢 FIXED in c72e80b2 (read)
amd-khushbu (10-26) MEDIUM: the ragged last tile and the block-table guard are never exercised 🟢 FIXED: new test_i64_ragged_lengths_and_block_table_tail (read). We checked its arithmetic. The table is 63 pages wide, so the last 64-token tile of a 1,000-token row reads page index 63. The guard spans the whole table (num_seqs × bt_stride = 504 entries), so it masks the last row's read at index 504, where the tail holds 0x7FFFFFFF. Rows 0–6 over-read the next row's valid ids, which the token mask discards (read). GPU-only; we have no gfx942 to run it
amd-khushbu MEDIUM: no test reads past 2 GiB 🟢 FIXED: new test_i64_decode_reads_past_2gib (read). Pages are 32 KiB and each tensor has 65,664 of them (2 GiB + 4 MiB). The used pages, 65,408–65,663, straddle page 65,536, which starts at 2^31 bytes. num_kv_blocks is the true count, so the selector picks i64 by itself (read). GPU-only
amd-khushbu MEDIUM: other i64 geometries have no parity config 🟢 FIXED: cfg8 (4 warps, 2 pages per tile), cfg9 (1 page) and cfg10 (use_global_load_lds_k). gfx942_attention_tiled_2d byte identity is GREEN for all 11 configs at llvm20 / 22 / 23 (ran). Given the same specs, develop's engine emits identical IR for cfg0–6 and different IR for exactly cfg7–10, at all three flavors (ran, Python engine)
amd-khushbu MEDIUM: the structural test counts only, for one geometry 🟢 FIXED: it now covers three geometries and passes on the merge. Run against develop's per-copy loader, it fails in all three (192, 48 and 192 page loads where 12, 6 and 3 are expected) (ran, negative control). It still checks counts only; the GPU tests cover addressing
amd-khushbu MEDIUM: register pressure unmeasured 🟢 Measured (ran; ROCm clang 24, gfx942, -O3). i64 VGPR / SGPR, merge vs develop:
• cfg7: 160 / 63 vs 182 / 58
• cfg8: 241 / 100 vs 238 / 99
• cfg9: 205 / 57 vs 204 / 57
• cfg10: 176 / 63 vs 254 / 79
No config uses scratch or spills, and the i32 kernels are unchanged. The author's LLVM 23 figures point the same way
amd-khushbu MEDIUM + LOW, the rest 🟢 Each is addressed (read):
• Recorded under "Not in this PR": the K-sliced ring still builds a resource per copy; the page-id value range is unchecked, and checking it needs an ABI change; one page-table wait per tile load remains; the root cause comes from ISA reading.
• C++ page cap removed: pages are now arena-allocated per tile. The native build is clean (ran).
• Docstring: now says the structural test builds the spec directly.
• CTest timing: the author measured the file at about 11 s within the GPU list. They report that the full list dies in test_mla_prefill_gfx942_numeric.py on develop as well; that is a develop issue, which they will raise
AnaghaRaoAMD (04:55) and amd-khushbu (06:19) APPROVED at 866daebb; Copilot (05:15) "needs a closer look" because the numerics skip in CI; CongMa13 (15:06) COMMENTED at 866daebb: "no new issues", agreeing with the existing items 🟢 Approvals are at head, and every item CongMa13 lists is addressed above (read). Copilot's point is confirmed in the next row
Bots / CI 🟡 All 46 checks pass at head; none fail or are pending (read). The gfx94X lane does collect the file through the GPU CTest list. Its 3 structural cases pass, but the 4 numeric cases skip with No module named 'torch' (ran, CI logs). So far only the author's node has run the numerics, the same torch gap seen on other PRs
Our checks 🟢
• The merge with develop is clean (20 behind) (ran).
• Commits 1–3 changed only in rebase context (range-diff).
• cfg0–7 emit identical IR at the old head and on the merge at llvm20 / 22 / 23, so commit 4 changes no IR (ran).
• Out-of-envelope specs still raise the same ValueError (ran).
• The native build and 33 ctests pass; rocke_arch_domain_reproducibility skips under clang 24 as usual (ran).
• Byte identity is GREEN for every gfx942 family at llvm22 (ran).
• The golden gate passes ×3 (ran).
• rule_scan is clean (ran)
# Sev file:line Finding
N1 🟡 PR description, Test Plan The kv-pages line still says "3 passed", a count from before the four new cases. The file now has 7 cases (3 structural, 4 numeric), and the author's 10-27 reply says all 7 pass → refresh the line

Utility: 🟢 Positive — the i64 K/V path on gfx942 2D builds one resource per page. It needs fewer VGPRs on the decode and global_load_lds variants, both engines agree on all 11 configs, and the new GPU tests cover the ragged block-table tail and real addresses past 2 GiB.


Legend: 🔴 blocking · 🟠 should-fix · 🟡 minor nit · 🟢 info/good · ⚪ retracted

Follow-up review: a local qwen3.6 model made the independent first pass. The orchestrator (Claude Opus 5.5) verified it. The checks were:

  • a range-diff against our last review;
  • IR of every parity config at the old head, on the merge and on develop, at three flavors;
  • the structural test on the merge and against develop's loader;
  • register and scratch use from gfx942 code objects against develop;
  • the native build and ctests, byte identity, the golden gate, and the CI logs at head.

The orchestrator owns this merge-worthiness call. It flags whether a closer look is needed; it is not an approval or a rejection.

@CongMa13 CongMa13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new P0 or P1 issues were identified during this review. The changes are approved.

@AnaghaRaoAMD
AnaghaRaoAMD merged commit 5472e43 into develop Oct 27, 2026
97 checks passed
@AnaghaRaoAMD
AnaghaRaoAMD deleted the users/dlangbe/rocke-gfx942-2d-i64-kv-pages branch October 27, 2026 15:15
jakpiase added a commit that referenced this pull request Oct 27, 2026
…ke/gfx1250-dgrad-depthwise

Regenerate the IR golden after merging develop (#14349). This also
refreshes stale kernel_name/bytes metadata for
conv_direct_depthwise_tiled/gfx1250/dgrad_fp16_k51x5_w32 (missing the
_p25x2 padding suffix); its IR sha256 is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@assistant-librarian

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

adickin-amd pushed a commit that referenced this pull request Oct 28, 2026
Fixes #14474

# Summary: For human reviewers

The golden file `llvm_gfx_all.json` is stale. The checks
`rocke_differential_golden_ll` and `rocke_differential_golden_canonical`
fail on `develop`.

This PR records the golden again on `f37a7056c79`. The PR changes only
`llvm_gfx_all.json`.

## What changed

- **`target_intrinsics`**: #13723 changed the IR. The golden changes for
llvm20, llvm22, llvm23 and canonical.
- **`moe_fused_mega_fp8`**: #13976 changed the IR. The golden changes
for llvm20, llvm22 and llvm23.
- **`gfx942_attention_tiled_2d`**: The golden also changes for this
family. The issue does not list it. The cause is not confirmed.

## Review request

1. Confirm the authors of #13723 and #13976 meant the IR changes.
2. Confirm the author of #14349 meant the `gfx942_attention_tiled_2d`
change. The probable source is #14349.

## Verification

- Before the change, the golden check failed on `develop` for llvm20 and
llvm22.
- After the change, `run_diff.py --mode ll --python-only --check-golden
--flavor llvm20 --only target_intrinsics,moe_fused_mega_fp8` prints
`GOLDEN OK`.
- The full check for each flavor, the canonical check and the
byte-identity gate did not run. CI runs them.

# Summary: For AI reviewers

- The diff changes one JSON file. No emitter code changes.
- The command `run_diff.py --record-golden` wrote the file with
`--python-only`.
- Check the changed hashes only against the three families above.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
amd-khushbu added a commit that referenced this pull request Oct 30, 2026
… sliced-K ring, phase split and HBM-direct K/V to C++

JIRA ID : AICK-2712

## Summary

This is PR 5 of a stack of 7 that brings the rocKE C++ unified-attention engine to parity with Python (see the stack below). It sits on PR 4.

The gfx942 fp16/bf16 flash prefill specs Python selects use the transposed-32x32 VALU stack (mask-limit, invariant hoist) and the sliced-K ring. The C++ builder rejected both, after PR 1 made it stop silently ignoring them. This PR ports them, plus the causal mask phase split and HBM-direct K/V, so C++ builds the production gfx942 flash specs byte-identically.

## Changes

All paths are under `dnn-providers/hip-kernel-provider/rocke/platform/cpp` unless noted.

- **Mask-limit and invariant hoist.**
  - The per-lane query-row invariants (and the ALiBi slope) are hoisted before the KV loop.
  - The mask-limit threshold compare honours `skip_mask`.
  - The ALiBi slope is picked as Python picks it; plain transposed + ALiBi previously failed to build.
  - The first parity configs to combine the production flash stack exposed other gaps on this path, fixed here: sinks seeding under transposed scalar state, the K single-buffer and split-cfvst branches, and C argument-order drift at four call sites.
- **Sliced-K ring (depth 2 or 3).**
  - The transposed QK streams K in `k_slice_hd`-wide slices through `ring_depth` LDS slots, with `ring_depth - 1` slices in flight behind a partial `vmcnt` fence.
  - When a prefetch targets the slot the previous slice was read from, the slot is drained before reuse. The depth-3 ring uses the CK LdsSeq slot maps.
  - Slice geometries Python rejects are rejected the same way.
- **Causal mask phase split, V/K from HBM.** `use_causal_mask_phase_split`, `use_v_hbm_direct` and `use_k_hbm_direct` are added to the shared spec, defaulted off; gfx950 ignores them. Their name tokens (`cmps`, `vhbm`, `khbm`) match Python.
- **Gate:** the unified tiled-2D gate no longer rejects the gfx942 flash mask-limit and sliced-K ring cohorts.
- **Stopgap: 64-bit KV offsets with the sliced-K ring.**
  - Develop (#14349) ported the gfx942 2D per-page i64 K/V loads. The ring's K slice loader still builds one i32 offset, so the C++ IR would differ from Python's and overflow at run time.
  - Until that is ported, the builder rejects `use_i64_kv_addr` with an active ring (NOTIMPL), and the 2D gate rejects those problems as "not ported to C++ yet". The gap is recorded in `known_gaps.md` §9.
  - A sweep of every i64 problem Python's 2D gate admits, run at the top of the stack: 810 specs, of which 558 are identical and the 252 ring specs are rejected.

Co-Authored-By: Claude <noreply@anthropic.com>
AnaghaRaoAMD added a commit that referenced this pull request Oct 30, 2026
…orward attention on gfx942/gfx950 (#14367)

ISSUE ID : AICK-2772

## Summary

Forward-attention GPU-time optimizations on gfx942 and gfx950 for both
rocKE KV layouts: the unified split-KV kernels (paged KV cache, plus
non-paged strided-KV decode, which runs on the same 3D kernel) and the
dense kernels (contiguous, non-paged Q/K/V). Measured against the
251-shape SDPA forward suite (decode, causal prefill, chunked prefill,
sliding window with sinks, FP8 KV, head_size 192). Several changes build
on #14216, #12714, #14276 and #14349; all four have landed on develop
and this branch merges develop, so none of them appear in the diff. The
results table below was measured at `afbf60ed85c`, before the latest
develop merge; a refresh against the new develop is in progress. Python
and C++ engines change together; byte-identity is green at every LLVM
flavor.

## Optimizations

**Decode (unified split-KV)**
- Split-KV for GQA ratios that do not divide the 16-row q-block (e.g. 5,
6, 12), with padding-row store masking in the 3D kernel.
- Route decode to the split-KV kernel when the 2D body is
occupancy-bound (gfx942 and gfx950).
- Head-size-256 decode split sizing (at least two pages per segment;
about two workgroups per CU on gfx942), and a decode segment cap that
counts one q-block per sequence.
- LLC-aware K/V cache policy: stream K/V only when the per-call K+V read
exceeds the last-level cache.

**Dense prefill**
- gfx942: 128-row query tile when the 256-row grid under-fills the
device; transposed-V path with a NaN-free RNE pack for bf16 head_size
128.
- gfx950 persistent body: softmax VALU trim (folded scale FMA,
pair-reduced row sums) and K/V DMA overlapped with compute through LDS
alias scopes (head_size 128).
- Bottom-right causal on gfx942 dense, and a KV-segment split for dense
prefill on both arches with a wave-filling segment count, so chunked
prefill (Sq < Skv) runs on the dense kernels.
- gfx942 sliding window: narrow-window geometry, a band-phase split, and
a wave-uniform skip of fully masked tiles; the 128-row under-fill rule
also covers head_size 64 and sinks.
- gfx950 sliding window and sinks: the single-batch 32x32 combo admits
windows and sinks at head_size 64/128, and dense windows get a 128-row
default tile when the 256-row grid is not already full.
- gfx950 persistent wide-DMA body: 128-row tile when the 256-row work
count leaves the device under-filled, and default dispatch routes
under-filled plain-causal head_size 128 to it.
- gfx950 head_size 256 unified prefill: query blocks run longest-first,
so the costly causal tail no longer finishes last.

**Coverage and dispatch**
- head_size 192 in the unified tiled kernels on both arches:
single-batch combo on gfx950; on gfx942, prefill on the lean 4-warp body
and decode with Q staged through the V LDS slot (3 workgroups per CU
instead of 2).
- FP8-KV prefill on the gfx950 single-batch 32x32 combo; FP8 prefill
without a 2D kernel now takes the split-KV path instead of the scalar
fallback.
- Dense auto-selection for contiguous-layout prefill in
`dispatch_attention`; contiguous-layout decode (Sq=1) now routes to
strided split-KV decode, and the strided-decode support check follows
the kernel gate, so GQA ratios 5/6/12 and head_size 192 dispatch too.

**Fixes found along the way**
- FP8 K/V scales are now forwarded through the explicit tuning binding.
- The gfx950 3D page-id K/V loader handles partial-row copies (head_size
192).
- The 3D K/V LDS swizzle is rejected when the chunks per row are not a
power of two.
- The dense KV-segment count is capped so the fp32 workspace index stays
in int32 range.
- The gfx942 lean 4-warp body (head_size 256 and 192) bounds every K/V,
block-table and Q read to the sequence; odd-length sequences previously
read past the cache.
- Sink requests on the transposed path keep scalar softmax state, so
sinks land on the right rows.
- `DenseSpace.defaults()` fingerprints only the knobs a shape does not
record, so a tuning ID replays across chunk shapes.

## Results

rocKE against itself, GPU-only, geomean speedup of the faster of the
unified and dense kernel per shape (higher is better). Each cell gives
geomean / median speedup and the number of shapes faster / slower by
more than 2%. All arms passed the on-silicon numeric check (max |error|
<= 0.05 against an fp64 reference) in the measured runs.

| Arch | Config | develop -> cherry-picks (#14276, #14349) | develop ->
this PR | cherry-picks -> this PR | Newly covered shapes |
|---|---|---|---|---|---|
| gfx950 | tuned per-shape configs | +0.1% / +0.0% (faster 8, slower 6
of 240) | **+7.0%** / +2.5% (faster 128, slower 1 of 240) | **+6.9%** /
+2.7% (faster 130, slower 2 of 240) | 9 |
| gfx950 | default dispatch | +0.0% / +0.0% (faster 6, slower 7 of 240)
| **+34.0%** / +7.7% (faster 160, slower 1 of 240) | **+33.9%** / +7.2%
(faster 165, slower 1 of 240) | 9 |
| gfx942 | tuned per-shape configs | +0.5% / -0.2% (faster 5, slower 15
of 238) | **+57.5%** / +19.1% (faster 170, slower 2 of 238) | **+56.6%**
/ +19.7% (faster 175, slower 1 of 238) | 11 |
| gfx942 | default dispatch | +0.6% / +0.0% (faster 16, slower 25 of
238) | **+57.0%** / +18.3% (faster 170, slower 6 of 238) | **+56.1%** /
+18.8% (faster 169, slower 3 of 238) | 9 |

By workload, cherry-picks -> this PR (gfx950 / gfx942, default dispatch
unless noted):
- decode: +63.3% / +148.5% (tuned configs: +0.6% / +149.4%, see caveats)
- causal prefill d128/d256: +7.5% / +19.5% (tuned configs: +10.4% /
+19.5%)
- prefill d64: +5.1% / -0.1% (tuned configs: +4.9% / +0.9%)
- chunked prefill: +50.2% / +4.4%; with the dense kernel serving Sq <
Skv on gfx942, +225.6% (2.2x to 6.1x on 9 shapes, measured separately)
- sliding window and sinks: +49.4% / +21.6% (tuned configs: +3.5% /
+24.0%)
- FP8 KV prefill: +513% on gfx950 (2 shapes); on gfx942, 2 shapes newly
covered under tuned configs
- head_size 192: newly covered (9 shapes per arch: 4 prefill, 5 decode)
- non-paged (strided-KV) decode, measured separately on all decode
shapes: +7.7% (gfx950) / +12.4% (gfx942) geomean, plus 20 shapes the
cherry-pick base rejects (GQA ratios 5/6/12 and head_size 192)

**Method.** Each kernel call is captured 64 times in a HIP graph; 1 warm
replay and 4 timed replays, upper median per shape. Three trees: develop
(`98737a5c5f4`, the develop this PR merged), cherry-picks (develop plus
#14276 and #14349, `8617b561496`), and this PR (`afbf60ed85c`). Per arch
and config, all three trees ran one after another in the same job, on
the same node; both gfx950 jobs ran on one node and both gfx942 jobs on
another, one sample per cell. On gfx942 the four FP8 shapes are excluded
from the default-dispatch rows, because develop and the cherry-picks run
them on the scalar fallback. Software: torch 2.13 / ROCm 10.0 wheels,
LLVM 23 comgr, `ROCKE_LLVM_FLAVOR=llvm23`. "Tuned per-shape configs"
pins each shape's kernel configuration from a tuning run that predates
this work; "default dispatch" calls `dispatch_attention` with no pinned
configuration.

**Caveats.** These results hold for the measured configurations only.
Because the pinned per-shape configurations predate this work, they
still select the earlier decode and chunked-prefill kernels on gfx950
(decode +0.6% tuned against +63.3% default); the default-dispatch rows
show those gains, and re-tuning the configurations would expose them. On
gfx942, the dense path for chunked prefill is reached only when the
caller routes Sq < Skv to dense; the +225.6% figure uses such a caller.

## Verification

- Byte-identity gate (Python vs C++ engine LLVM IR) GREEN at llvm20,
llvm22 and llvm23, all families, with new emitter configs for the
GQA-ratio-5 and head_size 192 3D paths. `test_rocke_ci_static.py` 5/5.
- Python vs C++ lowerer parity (`test_attention_ir_cpp_parity.py`, with
the `rocke_engine` extension built): every `attention_dense` and
`attention_d256` case byte-identical at all three flavors.
- CPU pytest after the develop merge: `library/tests -k "attention and
not bwd"` 1961 passed, 3 failed; `library/tests/dispatch` 1593 passed, 2
failed. Two of the failures were `test_unified_lse_dispatch.py` config
keys that moved with the K/V cache-policy change; they are re-pinned in
the last commit, and that file now passes 31/31. The remaining failure
(`TestMaskScalar::test_scalar_matches_reference[mixed-no_mask]`) also
fails on develop.
- Off-suite GPU parity stress on both arches (non-power-of-two GQA
ratios with multi-block prefill, bottom-right offsets with KV segments,
sliding-window boundaries, fully masked rows, large logits, FP8,
head_size 192; at least 3 seeds): 0 failures, 0 NaN/Inf; positive
controls with a deliberately shifted mask fail as expected.
- Known pre-existing issue, unchanged by this PR: a comgr codegen
failure for gfx950 head_size 256 with block_size 64.

---------

Co-authored-by: Adam Osewski <Adam.Osewski@amd.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: dlangbe <David.Langbehn@amd.com>
Co-authored-by: Anton Gorenko <anton@streamhpc.com>
dlangbe added a commit that referenced this pull request Nov 3, 2026
…ime (#14530)

JIRA ID : AICK-2676

The gfx942 tiled attention kernels (2D, 4-warp GQA, 3D split-KV and
reduce) now read the head counts and the sliding-window size as kernargs
and derive the `cu_q` search trip count from `num_seqs`. One compiled
binary therefore serves every head pair at a given GQA ratio, every
window size and every batch. This is the kernel-side readiness hipDNN
AOT packaging needs: a packaged catalog ships one binary per kernel
symbol and cannot enumerate head counts, window sizes or batch sizes.

Follow-up to #13372 and #13889, which removed raw shape fields from the
tiled and scalar cache keys. Builds on #14216 and #14349, merged from
develop. The gfx950 counterpart (AICK-2675) reuses the plumbing added
here.

## Risk Assessment

Risk 4. Every gfx942 tiled kernel changes its argument list and emitted
IR, the tiled cache keys change, and the paged K/V descriptor gains
runtime strides in both engines. That is a performance-critical path.
Mitigations: byte identity is GREEN at llvm20, llvm22 and llvm23 for
every family; gfx950 and gfx1250 tiled IR is unchanged (golden); and the
gfx942 numerics and an A/B against develop were run on hardware (see
Testing).

## ASIC Coverage

- **gfx942:** all kernel changes. Verified manually on gfx942: numeric
tests, the existing gfx942 GPU suites, and a develop-vs-branch A/B.
Superbuild and TheRock CI do not run these GPU numerics on gfx942.
- **gfx950 / gfx1250:** host-only changes (signature helpers, cache
keys, launch-cache keys). Their tiled spec classes declare no runtime
kernargs, so their IR and goldens do not move. PR CI is sufficient.

## Testing Summary

- **Byte identity:** full `check_byte_identity.py` GREEN at llvm20,
llvm22 and llvm23. The gfx942 2D, 2D stress, 3D and reduce emit configs
cover windowed, i64 and sinks specs.
- **Goldens:** the representative-IR golden moves only for the gfx942
tiled cases (2D, 3D, reduce and both 4-warp kernels); the strided-KV
library golden only for its gfx942 configs. Every gfx950 and gfx1250
entry is unchanged.
- **Cache keys (CPU):**
- shapes that differ only in head pair (same GQA ratio), window size or
batch bucket share one key and lower to byte-identical IR at every
flavor, on 2D, 4-warp D256, the 4-warp D128 fold and 3D;
  - the invariant sweep holds;
  - gfx950 and gfx1250 still split on the batch bucket.
- **Launch signature (CPU):** every production builder route and three
gfx942 tuning candidates are checked: the kernel's params equal its
launch signature by name and type.
- **One binary, many shapes (gfx942 GPU):**
`test_attention_gfx942_runtime_shape_numeric.py` runs pairs of shapes
that share a key back to back through `run_unified_attention_torch`. The
second launch must compile nothing, and both outputs must match an fp32
paged reference (max_abs ≤ 4e-2; bf16/fp16 inputs, randn) with unused
block-table slots on a poison block.
- Pairs cover 2D default, windowed 2D, D256 4-warp, the D128 window
fold, 3D decode and two-token 3D. One pair changes only the head counts
at identical token counts.
- Checked against mutants: baking the window back in fails the windowed
pair (max_abs 0.88); baking the kv head count into the K/V stride faults
on the device.
- **Existing GPU suites (gfx942):** 69 attention, MLA and strided-KV
library test files. Results match develop; the remaining failures fail
identically on develop (MLA comgr environment errors, strided-KV and
selector-parity cases).

**A/B, develop vs this branch (gfx942, 304 CUs)**
- **Workload:** 116 problems through `run_unified_attention_torch` with
production routing (`backend="auto"`): 98 on the 2D path, 18 on 3D.
Cohorts cover d64/d128/d256, bf16/fp16 and fp8 K/V, GQA ratios 1, 4 and
8, sinks, sliding window, the 4-warp D256 and fold paths, and a 64/16
head pair.
- **Method:** HIP-event mean over 50 launches after 10 warmup, median
over 3 rounds. Arm order alternates per round, and each arm runs in a
fresh process each round.
- **Toolchain:** torch 2.13.0+rocm10.0.0, LLVM 23 from the ROCm 10 SDK
wheels, Python lowering.
- **Baseline:** develop at `f37a7056c791`.
- **Numeric gate:** every row of both arms is within 4e-2 of an fp32
reference; worst max_abs is 1.24e-2 in both arms.
- **Results (branch / develop):**

| Measure | Result |
|---|---|
| Compiled binaries per process | 18 vs 54 |
| Total first-launch time | 0.37× |
| Steady state, geometric mean over 116 problems | 0.994× (range
0.87–1.06×) |
| Per cohort | within 0.98–1.01×, except the two below |
| d64 GQA-8 block-32 **sinks** (2 problems, 2D) | 1.05–1.06× |
| d64 GQA-8 **fp8 K/V** decode (4 problems, 3D) | 1.01–1.03× |

- **Sinks caveat, isolated:**
- Timing each commit puts the whole sinks cost in the head-count commit,
and only on the narrow 2D kernel.
- A timing-only variant with the K/V token stride restored to a constant
removes it; restoring the query head count changes nothing.
- The per-copy K/V offset now multiplies the token index by the runtime
stride inside the KV loop, where the constant stride used to compile to
a shift.
- Accepted here as a known cost. Hoisting the in-page offsets out of the
loop belongs with the planned per-page i32 K/V loader rework.

**AOT dry run:** a draft engine manifest pinned to one gfx942 tuning
candidate (`transposed_x8`, 4 warps, tile 4× block) expands with
`registry_enumerator.py --plan-space`: 24 cells (6 head configurations ×
bf16/fp16 × block 16/32), production sweep, 20 varying knobs. Result:
- 324 instances (within the default per-arch budget of 1000);
- 162 distinct kernels on this branch against 324 on develop, because
head pairs at one GQA ratio now build the same kernel.

## Testing Checklist

- [x] Byte identity - `ROCKE_LLVM_FLAVOR=llvm{20,22,23} python
tools/check_byte_identity.py` - Status: Passed
- [x] Representative-IR golden - `python
tests/instances/rocke_ir_parity_harness.py --check
tests/golden/rocke_representative_ir_sha256.json` - Status: Passed
- [x] Library host tests (attention, MLA, strided KV, dispatch,
layering), GPU hidden - `python -m pytest <file>` per file - Status:
Passed (3 files fail identically on develop)
- [x] Platform dynamic-descriptor tests - `python -m pytest
tests/core/test_transforms_dynamic.py` - Status: Passed
- [x] gfx942 GPU: new runtime-shape numeric test - `python -m pytest
tests/test_attention_gfx942_runtime_shape_numeric.py` - Status: Passed
(14/14)
- [x] gfx942 GPU: existing attention/MLA/strided-KV library suites (69
files) - Status: Passed, matching develop
- [x] gfx942 GPU: develop-vs-branch A/B, 116 problems, 3 rounds -
Status: Passed (numeric gate), caveats above

## Technical Changes

- **Runtime-kernarg plumbing.** Tiled spec classes declare
`RUNTIME_KERNARG_FIELDS`. `_tiled_2d_signature` / `_tiled_3d_signatures`
append those fields to the launch signature, and
`_runtime_kernarg_values` packs them. Every gfx942 launch site uses the
arch-aware signature. The 4-warp GQA kernels now declare
`qq_bias_stride_0`, which the shared 2D signature already passed.
- **Search trip count.** `binary_search_seq_idx_trip_count` computes
`bit_length(num_seqs)` from the `num_seqs` kernarg. It's exact below
2^24 sequences; past that it adds at most one no-op iteration. Specs
declare `RUNTIME_SEARCH_ITERS` and the keys drop the batch bucket. The
existing helper and its other callers emit the same IR.
- **Sliding window.** The size is a kernarg. Whether a window applies
still selects structure, so the name token is `sw` and the keys carry
`window > 0`.
- **Head counts.** `num_query_heads` and `num_kv_heads` are kernargs.
The Q/O, segment-output and paged K/V strides are runtime values through
`DynamicTensorDescriptor`, which now takes mixed int/Value strides plus
runtime strides in `offset_i64_split` (both engines) and `offset_i64`
(Python). Names carry `g<Hq/Hkv>`, and the keys carry the GQA ratio.
Every selector that reads absolute head counts feeds an output that's
already keyed.
- **Launch caches.** `_shape_key` (2D grid, 3D grids and workspace) now
carries both head counts, since one binary now serves several head
pairs.
- **i64 name token.** The gfx942 2D name carries `i64` when
`use_i64_kv_addr` is set; i32 and i64 builds used to share a symbol.
- **C++ twins.** The 2D, 3D and reduce twins mirror all of the above.
The gfx942 2D C++ entry also builds the gfx950 fastKV register-P twin,
so its runtime changes are gated to gfx942.
- **CI enrollment.** `test_attention_tiled_launch_signature.py` joins
the installed attention CPU cohort;
`test_attention_gfx942_runtime_shape_numeric.py` joins the library GPU
cohort.

**ABI note:** in this repo, the gfx942 tiled kernels are launched only
through `run_unified_attention_torch`, the dispatch tuning candidates
and the harnesses updated here. Anything that launches them by name must
append `num_query_heads`, `num_kv_heads` and `sliding_window` (2D and
3D) or `num_query_heads` (reduce).

**Notes for the packaging work:**
- The gfx942 fast paged-KV descriptor path (`use_fast_paged_kv_desc`,
reachable only through the `fastkv` tuning knob) still hard-codes 64/8
heads, pinned by its spec validator. A packaged catalog must serve it
only at 64/8 or leave that knob out.
- gfx942 routes windowed decode to 2D, so production never takes the
windowed 3D path; byte identity covers it.
- `AttentionTuningSpec.cache_key()` remains field-complete, so it still
splits on the runtime fields. That over-keys but is never wrong.

## Submission Checklist

- [ ] Look over the contributing guidelines at
https://github.com/ROCm/TheRock/blob/main/GOVERNANCE.md#pull-requests.

---------

Co-authored-by: Claude <noreply@anthropic.com>
dpeabody-amd added a commit that referenced this pull request Nov 3, 2026
…ed attention

Adds UnifiedAttention2DTiledSpec.runtime_layout. With it, Q/O/K/V strides,
the query head count, the sliding window and dense-or-ragged Q are kernel
arguments; the page tables and seq lens are read on device; a grid-stride
loop covers every q block; page-table and K/V reads stop at seq_len; rows
with no visible key write zeros, and so do the query rows a sequence owns
past seq_lens_q (padding), as the hipDNN contract requires.

K pages through block_tables_ptr and V through the appended
block_tables_v_ptr, which shares block_table_stride and may be the same
pointer, so cuDNN-style graphs with separate K and V page tables are served.

K/V use one 64-bit buffer resource per page, built once per tile load from
one page-table read: a runtime_layout tile never spans more than one page.
On gfx942 this is the per-page mechanism of the i64 path (#14349), which the
runtime_layout loaders sit beside. runtime_layout=False emits unchanged IR.

Additive bias (gfx950), the optional LSE output (emit_lse) and the gfx950
longest-first query-block order (use_qblock_reverse) are not combinable with
runtime_layout and are rejected by the runtime-layout checks; the gfx942
4-warp GQA builder refuses a runtime_layout spec.

Dispatch never sets runtime_layout: it is a semantic field (no sweep varies
it), and while it is off it stays out of the tuning spec's cache key and
config defaults, so every spec_hash, config_key and stored pin is unchanged.

The CPU checks of the two runtime_layout test files run in the new
rocke_library_attention_runtime_layout_pytest CTest entry (-m "not gpu");
their GPU numerics run in rocke_library_gpu_pytest. The scenarios include
sequences with 0 < seq_len < q_len, whose leading rows see no key.
dpeabody-amd added a commit that referenced this pull request Nov 4, 2026
…ed attention

Adds UnifiedAttention2DTiledSpec.runtime_layout. With it, Q/O/K/V strides,
the query head count, the sliding window and dense-or-ragged Q are kernel
arguments; the page tables and seq lens are read on device; a grid-stride
loop covers every q block; page-table and K/V reads stop at seq_len; rows
with no visible key write zeros, and so do the query rows a sequence owns
past seq_lens_q (padding), as the hipDNN contract requires.

K pages through block_tables_ptr and V through the appended
block_tables_v_ptr, which shares block_table_stride and may be the same
pointer, so cuDNN-style graphs with separate K and V page tables are served.

K/V use one 64-bit buffer resource per page, built once per tile load from
one page-table read: a runtime_layout tile never spans more than one page.
On gfx942 this is the per-page mechanism of the i64 path (#14349), which the
runtime_layout loaders sit beside. runtime_layout=False emits unchanged IR.

Additive bias (gfx950), the optional LSE output (emit_lse) and the gfx950
longest-first query-block order (use_qblock_reverse) are not combinable with
runtime_layout and are rejected by the runtime-layout checks; the gfx942
4-warp GQA builder refuses a runtime_layout spec.

Dispatch never sets runtime_layout: it is a semantic field (no sweep varies
it), and while it is off it stays out of the tuning spec's cache key and
config defaults, so every spec_hash, config_key and stored pin is unchanged.

The CPU checks of the two runtime_layout test files run in the new
rocke_library_attention_runtime_layout_pytest CTest entry (-m "not gpu");
their GPU numerics run in rocke_library_gpu_pytest. The scenarios include
sequences with 0 < seq_len < q_len, whose leading rows see no key.
dpeabody-amd added a commit that referenced this pull request Nov 5, 2026
…ed attention

Adds UnifiedAttention2DTiledSpec.runtime_layout on top of the runtime shape
kernargs the tiled 2D bodies already take (num_query_heads, num_kv_heads,
sliding_window, and the cu_q search trip count derived from num_seqs). With
it, the Q/O/K/V strides and a dense-or-ragged Q are kernel arguments too;
the page tables and seq lens are read on device; a grid-stride loop covers
every q block; page-table and K/V reads stop at seq_len; rows with no
visible key write zeros, and so do the query rows a sequence owns past
seq_lens_q (padding), as the hipDNN contract requires. The mode appends 15
kernel arguments after the runtime shape kernargs. Its window mask is always
on, with the sliding_window kernarg as its size, and its ragged q-block
search uses the shared runtime trip count (no iteration for a dense Q).

K pages through block_tables_ptr and V through the appended
block_tables_v_ptr, which shares block_table_stride and may be the same
pointer, so cuDNN-style graphs with separate K and V page tables are served.

K/V use one 64-bit buffer resource per page, built once per tile load from
one page-table read: a runtime_layout tile never spans more than one page.
On gfx942 this is the per-page mechanism of the i64 path (#14349), which the
runtime_layout loaders sit beside. runtime_layout=False emits unchanged IR.

Additive bias (gfx950), the optional LSE output (emit_lse) and the gfx950
longest-first query-block order (use_qblock_reverse) are not combinable with
runtime_layout and are rejected by the runtime-layout checks; the gfx942
4-warp GQA builder refuses a runtime_layout spec.
The runtime-layout checks take their head-count rule and text from the
shared check_head_counts (rocke_attention_check_head_counts in C++).

The rocKE C++ twin mirrors the mode op for op. rocke_attention_tiled_2d_spec_t
gains runtime_layout, so ROCKE_ABI_VERSION and BINARY_ABI move to 10.

Dispatch never sets runtime_layout: it is a semantic field (no sweep varies
it), and while it is off it stays out of the tuning spec's cache key and
config defaults, so every spec_hash, config_key and stored pin is unchanged.

The CPU checks of the two runtime_layout test files run in the new
rocke_library_attention_runtime_layout_pytest CTest entry (-m "not gpu");
their GPU numerics run in rocke_library_gpu_pytest. The scenarios include
sequences with 0 < seq_len < q_len, whose leading rows see no key.
dpeabody-amd added a commit that referenced this pull request Nov 5, 2026
…ed attention

Adds UnifiedAttention2DTiledSpec.runtime_layout on top of the runtime shape
kernargs the tiled 2D bodies already take (num_query_heads, num_kv_heads,
sliding_window, and the cu_q search trip count derived from num_seqs). With
it, the Q/O/K/V strides and a dense-or-ragged Q are kernel arguments too;
the page tables and seq lens are read on device; a grid-stride loop covers
every q block; page-table and K/V reads stop at seq_len; rows with no
visible key write zeros, and so do the query rows a sequence owns past
seq_lens_q (padding), as the hipDNN contract requires. The mode appends 15
kernel arguments after the runtime shape kernargs. Its window mask is always
on, with the sliding_window kernarg as its size, and its ragged q-block
search uses the shared runtime trip count (no iteration for a dense Q).

K pages through block_tables_ptr and V through the appended
block_tables_v_ptr, which shares block_table_stride and may be the same
pointer, so cuDNN-style graphs with separate K and V page tables are served.

K/V use one 64-bit buffer resource per page, built once per tile load from
one page-table read: a runtime_layout tile never spans more than one page.
On gfx942 this is the per-page mechanism of the i64 path (#14349), which the
runtime_layout loaders sit beside. runtime_layout=False emits unchanged IR.

Additive bias (gfx950), the optional LSE output (emit_lse) and the gfx950
longest-first query-block order (use_qblock_reverse) are not combinable with
runtime_layout and are rejected by the runtime-layout checks; the gfx942
4-warp GQA builder refuses a runtime_layout spec.
The runtime-layout checks take their head-count rule and text from the
shared check_head_counts (rocke_attention_check_head_counts in C++).

The rocKE C++ twin mirrors the mode op for op. rocke_attention_tiled_2d_spec_t
gains runtime_layout, so ROCKE_ABI_VERSION and BINARY_ABI move to 10.

Dispatch never sets runtime_layout: it is a semantic field (no sweep varies
it), and while it is off it stays out of the tuning spec's cache key and
config defaults, so every spec_hash, config_key and stored pin is unchanged.

The CPU checks of the two runtime_layout test files run in the new
rocke_library_attention_runtime_layout_pytest CTest entry (-m "not gpu");
their GPU numerics run in rocke_library_gpu_pytest. The scenarios include
sequences with 0 < seq_len < q_len, whose leading rows see no key.
cgmillette pushed a commit that referenced this pull request Nov 6, 2026
JIRA ID : AICK-2606

Partially addresses #14651 (the `TestKvAddrLimit` classification failure
on develop).

## Motivation

#12446 added a launch-time guard that rejects a paged KV cache larger
than the compiled kernel can address. Some gfx942 K/V loaders form an
i32 byte offset from the cache base even when the spec is built with
`use_i64_kv_addr=True`. The guard caps those specs through
`_I32_ONLY_KV_LOADERS`.

The conflict-free V store (cfvst) loader is the capped loader that
matters in production. It is the default V loader for gfx942 bf16 D64
and fp16 D64/D128 prefill (no sliding window). A sweep of the gfx942
default-route specs one block past 2^31 bytes found that cfvst is the
only capped loader any shipped i64 spec selects.

Its V offsets wrap past 2^31 bytes. Before #12446 that read wrong data
or faulted the GPU; since #12446 the guard raises `ValueError` before
launch. Dispatch has no fallback route, so a deployment whose per-layer
K cache is over 2^31 bytes fails every prefill on these configurations.

This PR gives the cfvst loader an i64 path that costs no more than the
i32 path, and removes the cap. That lifts the cap for every shipped
gfx942 configuration.

## Technical Details

**cfvst V loader** (`library/kernels/gfx942/attention_tiled_2d.py`,
`_cfvst_load_v_regs`)
- Each cfvst load call covers a fixed range of tokens: 16 at D64 and 8
at D128 with 4 warps. On every geometry the launcher and the tuning
space build, that range lies inside one page, and which page is known at
build time.
- Under `I64_KV_ADDR` the loader loads the tile's page ids once, builds
one 64-bit page pointer per page, and indexes each V load with an i32
offset within the page. The per-lane select and divide stay i32, as on
the i32 path. The pointers come from `_tile_page_ptrs`, split out of
#14349's `_tile_pages` with the same op order.
- The in-page byte offset, the kv head's byte offset and the
`PAGE_RSRC_I64` rule come from `_kv_page_off`, `_kv_head_page_b` and the
shared predicate, which the per-page K/V loaders use too, so a change to
the paged K/V layout reaches cfvst as well. They use the runtime token
stride from #15024.
- A geometry whose load calls would span pages raises `ValueError` at
build time. This follows the existing rule for K/V copies that straddle
pages. No such geometry can launch: the launcher accepts block sizes 16,
32 and 64, and the tuning space offers 1, 2 or 4 warps.
- It covers every load form: the vector `global_load_vN` path and the
`HIPDNN_GFX942_CFV_STORE_SEPOFF` / `_SCALAR_LOAD` diagnostic paths.
- The i32 cfvst loader emits the same IR as before.
- An earlier commit in this PR used full per-lane i64 offsets
(`offset_i64`). That was correct, but at fp16 D128, which already sits
at the VGPR limit, it added register spills and made the i64 kernel
noticeably slower than the i32 one. The page-pointer version replaces
it.

**Running-max carry on the transposed softmax path**
(`attention_tiled_2d.py`, the loop yield)
- After merging develop, the fp16 D128 i64 cfvst kernel was 1.3x slower
than its i32 form. A bisect over the merged develop commits found
#14542, which carries the raw running max. On the transposed path the
raw max then stayed live through the whole body beside the clamped
exponent reference. This kernel already sits at the VGPR ceiling, so the
one extra value made the compiler route the per-tile compares through
VCC one at a time.
- The carry is now rebuilt at the yield as `new_l == 0 ? -inf : m_new`.
The raw max is -inf exactly while the row has seen no key, which is when
l is 0 (a seen key adds P = 1 at the max), so the carried value is the
same and #14542's behaviour is kept. Testing l rather than the max
compare matters: LLVM folds a rebuild from the compare back into the raw
max.
- This changes the IR of every gfx942 tiled 2D kernel on the transposed
path, i32 included. The non-transposed paths and gfx950 are unchanged.

**Scalar `global_load` lowering** (`core/lower_llvm.py`,
`cpp/core/lower_llvm/mem.cpp`)
- `global_load_typed` always wrote its GEP index as `i32`, so an i64
element index gave invalid IR. It now uses the index operand's own type,
as `global_load_vN` already does. This was found while writing the first
i64 loader. The final cfvst loader no longer needs it, but the lowering
was wrong for any i64 index. Every existing caller passes an i32 index,
so no existing kernel changes.

**C++ twin**
- The page-pointer cfvst loader and the same build error are mirrored in
`attention_tiled_2d_conflict_free_v.cpp`.
`rocke_gfx942_attn2d_tile_page_ptrs`, `rocke_gfx942_attn2d_kv_page_off`,
`rocke_gfx942_attn2d_kv_head_page_b` and
`rocke_gfx942_attn2d_page_rsrc_i64` are shared with the per-page K/V
loaders.
- The rebuilt running-max carry is mirrored in
`attention_tiled_2d_kv_body_qk_softmax.cpp` and
`attention_tiled_2d_kv_body_pv.cpp`, only when that body builds for
gfx942. gfx950 fastkv_regp shares the C++ body but its Python
(`gfx950/attention_tiled_2d.py`) still carries the raw max, so it keeps
that carry.
- No emit config covered cfvst before, and the twin did not match
Python. It is now byte-identical:
- It computes the per-tile values once and reuses the cached
`max_seq_prefix_len`.
- It places the split load and store where the transposed softmax issues
them.
- It fixes C argument evaluation order (unspecified, right-to-left in
practice) in `v_t_slot`, the transposed PV token base, and the cfvst
block guard and prezero loop.

**Lift the cap**
- `use_conflict_free_v_store` is removed from `_I32_ONLY_KV_LOADERS` and
classified as i64-safe in
`test_every_tiled_flag_is_classified_for_kv_addressing`.
- `test_gfx942_cfvst_meta_has_no_limit`,
`test_limit_memo_is_keyed_per_kernel` and
`TestGfx942RouteGuard.test_cfvst_route_launches_past_2gib` now expect no
limit. The route test launches at 65535, 65536, 65537 and 69632 blocks
and checks the spec is i64 cfvst.
- `use_additive_bias` (#13120) is also classified
(`_NO_KV_ADDRESS_FLAGS`). The classification test already failed on
develop without it.

**Byte-identity coverage**
- `library/tests/parity/gfx942_attention_tiled_2d_emit.{py,c}`: cfg18 is
i32 cfvst and cfg19 is i64 cfvst, both on the bf16 D64 GQA 32/8 prefill
geometry. The shipped spec also sets the sliced-K ring, direct-Q-global
and the mask-limit stack. The C twin does not port those yet, so the
configs leave them off. They still cover the new V loader code.
- `llvm_gfx_all.json`: cfg18/19 blessed at llvm20, llvm22, llvm23,
llvm24 and in canonical IR. The running-max change re-blesses the 18
transposed-path configs of `gfx942_attention_tiled_2d` and
`gfx942_attention_tiled_2d_stress`; no other family moves.
- Representative-IR golden: only
`attention/gfx942/2d_lse_transposed32_bf16_d128` moves, from the
running-max change.

**Tests and docs**
- `platform/tests/core/test_global_load_index_width.{py,cpp}` check the
GEP index width; the C++ one is a host CTest.
- `library/tests/test_gfx942_cfvst_i64_kv_addr.py` (CPU):
- every V load of the shipped i64 cfvst spec indexes a page pointer with
an i32 offset, including both diagnostic forms;
  - the i32 spec keeps i32 offsets from the cache base;
  - a page-spanning geometry is refused;
  - every cfvst geometry in the gfx942 tuning space builds with i64.
- `library/tests/test_gfx942_cfvst_i64_kv_addr_numeric.py` (gfx942 GPU
lane): a CPU routing anchor per shipped shape, plus an on-GPU check for
bf16 D64, fp16 D64 and fp16 D128. Each runs
`run_unified_attention_torch` on a 2.125 GiB GQA 32/8 causal cache with
every block-table entry above 2^31 bytes, against an fp32 reference. It
gates on `get_device_arch() == "gfx942"`.
- `library/tests/test_attention_unified_masked_tile_ir.py` accepts the
rebuilt carry in its exact form and still rejects a clamped carry.
- CI lanes: `test_gfx942_cfvst_i64_kv_addr.py` runs in
`rocke_library_attention_cpu_pytest`, and the numeric file in the
library GPU lane. `rocke_global_load_index_width` is registered in
`tests/cmake/CoreTests.cmake` and listed in the quick and ffm-quick
tiers of `ROCKE_ENGINE_test_categories_external.yaml`.
- `builders/gfx950/attention/README.md`: describes the cfvst
page-pointer addressing; the paged-cache K/V limit list no longer names
cfvst.

**Out of scope (follow-ups)**
- The other `_I32_ONLY_KV_LOADERS` entries: cfv, vhbm and khbm on
gfx942, and fp8-PV on gfx942 and gfx950. No shipped spec selects them
under i64.
- Under i64 the sliced-K ring K loader still builds a buffer resource
per copy, the pattern #14349 replaced in the other K/V loaders.
- `global_store_typed` still writes its GEP index as `i32` in both
engines, so an i64 store index would give invalid IR. No caller passes
one.
- Running `test_selector_parity.py` in a CI lane (the rest of #14651).
- Replacing the hardcoded `0x7FFF0000` K/V `num_records` with one named
limit (changes IR and goldens).
- Exposing the cache-size limit to `supports_native_unified_attention*`.
- Porting the sliced-K ring, direct-Q-global and the mask-limit stack to
the gfx942 C twin, so an emit config can match a full shipped spec.

## Test Plan

CPU:
- Byte-identity gate at every flavor:
`ROCKE_LLVM_FLAVOR=llvm20|llvm22|llvm23|llvm24 python
tools/check_byte_identity.py`
- Differential golden, as CI runs it:
`run_diff.py --mode ll --python-only --check-golden --flavor
llvm20,llvm22,llvm23,llvm24`
  `run_diff.py --mode ir --canonical --python-only --check-golden`
- Representative-IR golden: `rocke_ir_parity_harness.py --check
tests/golden/rocke_representative_ir_sha256 --flavor <flavor>` at each
flavor.
- `test_selector_parity.py`, `test_gfx942_cfvst_i64_kv_addr.py`,
`test_attention_unified_masked_tile_ir.py`,
`test_attention_gfx942_kv_pages.py`, `test_global_load_index_width.py`
and the attention spec/selector suites, with the GPU hidden.
- `rocke_global_load_index_width` CTest.

gfx942 GPU (torch 2.13.0+rocm10.0.0 wheels, Python lowering):
- `test_gfx942_cfvst_i64_kv_addr_numeric.py`.
- #14542's `test_attention_unified_2d_masked_tile_numeric.py` and
`test_attention_unified_scalar_masked_tile_numeric.py` (fully masked
first tile, NaN rows), plus
`test_attention_tiled_runtime_shape_numeric.py` and
`test_gfx942_4wgqa_bounds_numeric.py`.
- `builders/gfx942/attention/prefill/parity_unified_attention.py
--scenario default`.
- Kernel resources (VGPR and SGPR counts, spills) from the code object
notes of the shipped cfvst kernels, i32 and i64.
- Bisect of the fp16 D128 i64 slowdown: each develop commit merged into
the pre-merge head, compiled, and its SGPR count read; the boundary then
timed.
- Performance on the shipped prefill shapes, i32 vs i64 on this branch.
The workload is 2 sequences of 4096 tokens, causal, GQA 32/8, 32-token
pages.
- Both arms run the same small cache. The i64 arm only claims a larger
block count, which selects the i64 spec.
- Each arm is first checked against an fp32 reference (declared gate
max_abs ≤ 4e-2).
- Time is the median of 50 launches after 10 warm-up launches, measured
with HIP events, over two repeats.

## Test Result

- Byte-identity (`check_byte_identity.py`, every family): GREEN at
llvm20, llvm22, llvm23 and llvm24, including cfg18/19.
- Differential golden: GOLDEN OK at every flavor and in canonical IR.
Representative-IR golden: OK at all four flavors.
- CPU tests pass. The new CPU tests fail on develop and pass here.
- gfx942 numeric, at this head: all three shipped shapes pass on caches
past 2^31 bytes (bf16 gate 4e-2, fp16 gate 2e-2). #14542's masked-tile
tests pass, including the NaN rows on the transposed path.
- gfx942 parity (default group): 22 PASS, 6 SKIP, 0 FAIL. The skips are
D256 shapes over the LDS budget.
- Kernel resources: the fp16 D128 i64 kernel uses 252 VGPRs and 101
SGPRs with no spills; before the running-max change it used 254 and 49.
- Performance, at these measured configurations, as time ratios. Every
arm passed the numeric gate (max_abs 7.9e-3 bf16, 1.1e-3 fp16).

| shape | branch i64 / branch i32 | fp16 D128 i64, running-max change /
without it |
  |---|---|---|
  | bf16 D64 | 1.01–1.02 | |
  | fp16 D64 | 0.99–1.00 | |
  | fp16 D128 | 0.95 | 0.72 |

The running-max change leaves the i32 kernels' times unchanged within
run-to-run noise.

## Submission Checklist

- [ ] Look over the contributing guidelines at
https://github.com/ROCm/TheRock/blob/main/GOVERNANCE.md#pull-requests.

---------

Co-authored-by: Claude <noreply@anthropic.com>
dpeabody-amd added a commit that referenced this pull request Nov 6, 2026
…ed attention

Adds UnifiedAttention2DTiledSpec.runtime_layout on top of the runtime shape
kernargs the tiled 2D bodies already take (num_query_heads, num_kv_heads,
sliding_window, and the cu_q search trip count derived from num_seqs). With
it, the Q/O/K/V strides and a dense-or-ragged Q are kernel arguments too;
the page tables and seq lens are read on device; a grid-stride loop covers
every q block; page-table and K/V reads stop at seq_len; rows with no
visible key write zeros, and so do the query rows a sequence owns past
seq_lens_q (padding), as the hipDNN contract requires. The mode appends 15
kernel arguments after the runtime shape kernargs. Its window mask is always
on, with the sliding_window kernarg as its size, and its ragged q-block
search uses the shared runtime trip count (no iteration for a dense Q).

K pages through block_tables_ptr and V through the appended
block_tables_v_ptr, which shares block_table_stride and may be the same
pointer, so cuDNN-style graphs with separate K and V page tables are served.

K/V use one 64-bit buffer resource per page, built once per tile load from
one page-table read: a runtime_layout tile never spans more than one page.
On gfx942 this is the per-page mechanism of the i64 path (#14349), which the
runtime_layout loaders sit beside. runtime_layout=False emits unchanged IR.

Additive bias (gfx950), the optional LSE output (emit_lse) and the gfx950
longest-first query-block order (use_qblock_reverse) are not combinable with
runtime_layout and are rejected by the runtime-layout checks; the gfx942
4-warp GQA builder refuses a runtime_layout spec.
The runtime-layout checks take their head-count rule and text from the
shared check_head_counts (rocke_attention_check_head_counts in C++).

The rocKE C++ twin mirrors the mode op for op. rocke_attention_tiled_2d_spec_t
gains runtime_layout, so ROCKE_ABI_VERSION and BINARY_ABI move to 10.

Dispatch never sets runtime_layout: it is a semantic field (no sweep varies
it), and while it is off it stays out of the tuning spec's cache key and
config defaults, so every spec_hash, config_key and stored pin is unchanged.

The CPU checks of the two runtime_layout test files run in the new
rocke_library_attention_runtime_layout_pytest CTest entry (-m "not gpu");
their GPU numerics run in rocke_library_gpu_pytest. The scenarios include
sequences with 0 < seq_len < q_len, whose leading rows see no key.
dpeabody-amd added a commit that referenced this pull request Nov 7, 2026
…ed attention

Adds UnifiedAttention2DTiledSpec.runtime_layout on top of the runtime shape
kernargs the tiled 2D bodies already take (num_query_heads, num_kv_heads,
sliding_window, and the cu_q search trip count derived from num_seqs). With
it, the Q/O/K/V strides and a dense-or-ragged Q are kernel arguments too;
the page tables and seq lens are read on device; a grid-stride loop covers
every q block; page-table and K/V reads stop at seq_len; rows with no
visible key write zeros, and so do the query rows a sequence owns past
seq_lens_q (padding), as the hipDNN contract requires. The mode appends 15
kernel arguments after the runtime shape kernargs. Its window mask is always
on, with the sliding_window kernarg as its size, and its ragged q-block
search uses the shared runtime trip count (no iteration for a dense Q).

K pages through block_tables_ptr and V through the appended
block_tables_v_ptr, which shares block_table_stride and may be the same
pointer, so cuDNN-style graphs with separate K and V page tables are served.

K/V use one 64-bit buffer resource per page, built once per tile load from
one page-table read: a runtime_layout tile never spans more than one page.
On gfx942 this is the per-page mechanism of the i64 path (#14349), which the
runtime_layout loaders sit beside. runtime_layout=False emits unchanged IR.

Additive bias (gfx950), the optional LSE output (emit_lse) and the gfx950
longest-first query-block order (use_qblock_reverse) are not combinable with
runtime_layout and are rejected by the runtime-layout checks; the gfx942
4-warp GQA builder refuses a runtime_layout spec.
The runtime-layout checks take their head-count rule and text from the
shared check_head_counts (rocke_attention_check_head_counts in C++).

The rocKE C++ twin mirrors the mode op for op. rocke_attention_tiled_2d_spec_t
gains runtime_layout, so ROCKE_ABI_VERSION and BINARY_ABI move to 10.

Dispatch never sets runtime_layout: it is a semantic field (no sweep varies
it), and while it is off it stays out of the tuning spec's cache key and
config defaults, so every spec_hash, config_key and stored pin is unchanged.

The CPU checks of the two runtime_layout test files run in the new
rocke_library_attention_runtime_layout_pytest CTest entry (-m "not gpu");
their GPU numerics run in rocke_library_gpu_pytest. The scenarios include
sequences with 0 < seq_len < q_len, whose leading rows see no key.
dpeabody-amd added a commit that referenced this pull request Nov 7, 2026
…ed attention


Adds UnifiedAttention2DTiledSpec.runtime_layout on top of the runtime shape
kernargs the tiled 2D bodies already take (num_query_heads, num_kv_heads,
sliding_window, and the cu_q search trip count derived from num_seqs). With
it, the Q/O/K/V strides and a dense-or-ragged Q are kernel arguments too;
the page tables and seq lens are read on device; a grid-stride loop covers
every q block; page-table and K/V reads stop at seq_len; rows with no
visible key write zeros, and so do the query rows a sequence owns past
seq_lens_q (padding), as the hipDNN contract requires. The mode appends 15
kernel arguments after the runtime shape kernargs. Its window mask is always
on, with the sliding_window kernarg as its size, and its ragged q-block
search uses the shared runtime trip count (no iteration for a dense Q).

K pages through block_tables_ptr and V through the appended
block_tables_v_ptr, which shares block_table_stride and may be the same
pointer, so cuDNN-style graphs with separate K and V page tables are served.

K/V use one 64-bit buffer resource per page, built once per tile load from
one page-table read: a runtime_layout tile never spans more than one page.
On gfx942 this is the per-page mechanism of the i64 path (#14349), which the
runtime_layout loaders sit beside. runtime_layout=False emits unchanged IR.

Additive bias (gfx950), the optional LSE output (emit_lse) and the gfx950
longest-first query-block order (use_qblock_reverse) are not combinable with
runtime_layout and are rejected by the runtime-layout checks; the gfx942
4-warp GQA builder refuses a runtime_layout spec. On gfx942 the support
gate and both builders (Python and C++) charge runtime_layout_lds_bytes
against ArchTarget lds_capacity_bytes and refuse a spec over it.
The runtime-layout checks take their head-count rule and text from the
shared check_head_counts (rocke_attention_check_head_counts in C++).

The rocKE C++ twin mirrors the mode op for op. rocke_attention_tiled_2d_spec_t
gains runtime_layout, so ROCKE_ABI_VERSION and BINARY_ABI move to 13.

Dispatch never sets runtime_layout: it is a semantic field (no sweep varies
it), and while it is off it stays out of the tuning spec's cache key and
config defaults, so every spec_hash, config_key and stored pin is unchanged.

The CPU checks of the two runtime_layout test files run in the new
rocke_library_attention_runtime_layout_pytest CTest entry (-m "not gpu");
their GPU numerics run in rocke_library_gpu_pytest. The scenarios include
sequences with 0 < seq_len < q_len, whose leading rows see no key.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants