Skip to content

fix(rocke): carry the raw running max in unified attention - #14542

Merged
AviralGoelAMD merged 30 commits into
developfrom
users/avirgoel/rocke/tiled-2d-running-max-carry
Oct 31, 2026
Merged

AviralGoelAMD merged 30 commits into
developfrom
users/avirgoel/rocke/tiled-2d-running-max-carry

Conversation

@AviralGoelAMD

@AviralGoelAMD AviralGoelAMD commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

ISSUE ID : AICK-2829

Also fixes the empty-segment and keyless-row cases of AICK-2686.

What was wrong

Online softmax keeps a running max m per query row. When a row's first KV tile was fully masked (sliding window, or a leading split-KV segment), the kernels replaced m = -inf with 0 and carried that 0 to later tiles. Real scores were then exponentiated against 0 instead of the row's max. If every visible score is very negative, P rounds to 0 and the row's output is wrong (0 or NaN) even though the row sees keys.

The output guard had a second problem. Rows with no key were detected with l > 0, which also turned a NaN row into 0 and hid it.

This blocks LSE output on the 2D path (AICK-2685).

What changed

Kernel Builder (Python + C++ twin) Bug Fix
Tiled 2D, gfx942 + gfx950 build_unified_attention_2d_tiled Clamped max carried; l > 0 guard hid NaN Carry the raw max; guard l == 0
Scalar 2D build_unified_attention_2d Clamped max carried; default mask had no keyless guard (NaN when Sq > Skv) Carry the raw max; l == 0 guard under every mask
Scalar 3D segment build_unified_attention_3d Clamped max carried; the default mask ignored a sliding window (older than this PR) Carry the raw max with a safe exponent reference; apply the window under every mask. The C++ twin changes too, and now emits the same IR as Python (binary sequence-index search, Python's operand order)
Scalar split-KV reduce build_unified_attention_reduce Empty segments merged; zero total gave NaN One exponent reference: the overall max, or 0 when every segment is empty. An empty segment (max = -inf) gets weight 0; zero total writes O = 0
  • New GPU tests: test_attention_unified_2d_masked_tile_numeric.py and test_attention_unified_scalar_masked_tile_numeric.py, both in the CTest GPU list.
  • Goldens re-blessed for the tiled 2D and scalar attention families only. The byte-identity gate now also covers the scalar reduce (configs 6-7) and the scalar 3D segment kernel (configs 8-10; config 10 has a sliding window).

Why it works

The loop carries the raw max, which stays -inf until the row sees a key. The -inf → 0 substitute is used only as the exponent reference for P and the rescale factor. At the first tile with real keys the reference becomes that tile's max, so the largest P is 1 and cannot underflow. The tiled 3D kernels already use this update (#14276). The l == 0 guard marks only truly keyless rows, so NaN still reaches the output.

How we validated

Develop 56ad847e204 vs this head, one GPU allocation per arch, Python backend, LLVM 23.

Gate gfx942 gfx950
New tests (fp32 reference) 65 fail → 65 pass 69 fail → 69 pass
Mutant (old l > 0 tiled guard) fails exactly the 8 NaN cases same
Broad attention selection only the new cases change same
Byte identity, full gate (llvm20/22/23/24) GREEN GREEN

After review: the scalar numeric and IR tests pass on gfx942 and gfx950. The new bottom-right windowed scalar 3D case fails before the window fix.

Performance

Cold memory, D128, GQA 32/8, KV page 16, Sq = Skv = 8192, 5 alternating rounds. Time relative to develop (lower is faster):

Shape gfx942 gfx950
causal fp16 0.99× 1.00×
causal bf16 0.99× 1.00×
sliding window 4096, bf16 0.99× 0.99×

No scratch on any timed kernel. The gfx942 sliding-window shape was timed with the 4-warp route forced off, because production sends it to the 4-warp kernel, which this PR does not change.

Notes

The empty-segment guards compared the segment sum and the final
denominator with an ordered greater-than, which is false for NaN, so a
NaN sum was dropped as an empty segment and a NaN denominator gave O = 0.
Treat only an exact zero as empty so NaN propagates as before.
Run every epilogue branch at page sizes 16 and 64, using the 1x-block
gfx942 variants at page size 64 where the 2x-block ones exceed LDS,
assert each geometry still gives a row a fully masked leading tile, and
correct the fp16/bf16 P underflow thresholds in the docstring.
…erage

Scalar 3D: write NaN into one segment sum of a row that sees keys between
the segment and reduce launches; that row's O must be NaN and every other
row must match the reference. An ogt-style empty-segment guard would skip
the NaN segment and leave the row finite.

Tiled 2D: run the pinned epilogue branches at head sizes 64, 128 and 256
(256 skipped, with the canonicalizer's reason, where the variant exceeds
the LDS budget), and add one auto-route D64 bf16 case per arch whose
production kernel is the multi-warp default builder.
Build the scalar split-3D reduce in the representative IR golden
(scalar_reduce_{fp16,bf16}_d128_seg8) and in the attention_unified C/Python
parity emitter pair (configs 6-7), so check_byte_identity compares the
reduce between the C++ and Python engines. Goldens gain only the new keys.

Also fix a gfx950 comment that named m_new as the state handed to the PV
bucket; it is m_carry, the raw running max.
…ry mask

The scalar 2D epilogue guarded acc * rcp(l) only for non-default masks, on
the claim that the default mask never yields a row without keys. Bottom-right
causal with q_len > kv_len does: its leading rows see no key, l is 0 and O
was NaN. The C++ twin had no guard at all.

Guard every mask, and test l == 0 (fcmp oeq) instead of l > 0 (fcmp ogt) so
a NaN sum still reaches O instead of being stored as a keyless 0. Mirror the
same ops in the C++ engine and re-bless the representative and differential
goldens for the scalar 2D cases.

Tests: a default-mask q_len > kv_len case whose keyless rows must be 0, and a
NaN query-row probe that must give NaN O under the default and top-left masks.
The tiled 2D epilogue on gfx942 and gfx950 stored 0 for any row whose
softmax sum failed 'l > 0'. A NaN sum fails that test too, so a corrupt
row came out as a finite 0. Store 0 only for an exact-zero sum
(fcmp oeq), the rule the scalar 2D epilogue and the scalar reduce use.
Rows with a finite nonzero sum are unchanged. Python builders and C++
twins change in step; the fastKV register-P variant inherits the change.

Also correct the scalar 2D and scalar 3D comments about keyless rows,
and re-bless the representative and differential goldens of the tiled
2D families.
Add a NaN-query-row case per tiled 2D epilogue branch, state what each
mask case of the scalar 2D NaN test pins, and drop the unused segm_max
argument of the scalar 3D workspace hook.
@github-actions github-actions Bot added project: hip-kernel-provider rocke-lib: attention Touches rocKE library attention/FMHA kernels rocke: platform Touches rocKE platform labels Oct 28, 2026
@therock-pr-bot

therock-pr-bot Bot commented Oct 28, 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

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

Copy link
Copy Markdown

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

@AviralGoelAMD
AviralGoelAMD marked this pull request as ready for review October 28, 2026 14:09
@AviralGoelAMD
AviralGoelAMD requested review from a team as code owners October 28, 2026 14:09
@AviralGoelAMD
AviralGoelAMD requested a balanced review from Copilot October 28, 2026 14:12
@AviralGoelAMD AviralGoelAMD self-assigned this Oct 28, 2026

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

…arry

Resolve the differential golden conflict with #14501 by regeneration:
take develop's file, then re-record the attention_tiled_2d and
attention_unified families (ll llvm20/22/23 + ir canonical, python-only).
Other families keep develop's entries.
@brockhargreaves-amd
brockhargreaves-amd requested a balanced review from Copilot October 28, 2026 14:41
…arry

Resolve the representative IR golden conflict with #14357 (fp64 GEMM on
gfx942) by regeneration: take develop's file, then re-record it with
rocke_ir_parity_harness.py --write on the merged tree. Every entry equals
develop's or this branch's hash. The differential golden and the parity
harness merged cleanly, and check-golden is OK at every flavor.
AviralGoelAMD added a commit that referenced this pull request Oct 29, 2026
…nified-2d-lse

Bring in #14542's block_table_stride fix for the scalar 3D test launch and
its merge of develop (#14357, fp64 GEMM on gfx942). Resolve the
representative IR golden conflict by regeneration: take #14542's file,
then re-record it with rocke_ir_parity_harness.py --write on the merged
tree. Every entry equals one side's hash apart from the summary counters.
…heck

The attention_unified emitter pair built only the scalar 2D kernel and the
reduce, so nothing compared the Python and C++ scalar 3D segment kernel. Add
configs 8-9: the split-3D segment kernel (8 segments) of two default-mask
problems, built by build_unified_attention_3d and
rocke_build_unified_attention_3d_scalar_new.

The two engines did not produce the same IR for it. Reconcile the C++ builder
with Python:
- search the sequence index with rocke_binary_search_seq_idx (per token,
  32 iterations), as _emit_find_seq_idx_scan does, instead of a linear scan;
- emit nested builder calls in Python's left-to-right order (QK extracts,
  the segment end, and the accumulator update), because C leaves argument
  evaluation order unspecified.

Record the new configs in the differential golden.
The segment kernels now store the raw running max, so an empty segment
already has max = -inf and exp2(max - overall) is 0 whenever the overall max
is finite. The per-segment "sum == 0" select only mattered when every segment
is empty (overall = -inf), where exp2(-inf - -inf) is NaN.

Clamp the exponent reference once instead, the same way the segment kernels
do: ref = select(overall > -inf, overall, 0), and factor = exp2(max - ref).
Results are unchanged (an all-empty row gets factor 0, den 0 and O = 0; a NaN
sum still gives a NaN den), with one fcmp and one select less per segment,
and no reliance on an exact-zero sum. Both engines emit the same ops.

The IR test now pins one reference outside the loops, read by the combine
pass, and no compare or select inside the loops. Re-bless the scalar reduce
representative entries and the differential reduce configs (6-7).
The default mask is bottom-right causal with no right bound, and it can have a
sliding window. The scalar 3D segment kernel applied only the causal bound
under that mask, so a bottom-right windowed problem attended to every key
before the diagonal, outside its window. Both engines had the same gap.

Apply the window under every mask: Python drops the window=False special case
(and the now-unused parameter of _scalar_band_ok), and the C++ segment loop
emits the same left-bound check after the causal one when sliding_window > 0.
The kernel name now carries the "sw" flag for any windowed problem, so the
fixed kernel does not share a name with the old one.

Tests: the scalar 3D numeric test gains the bottom-right windowed case (rows
past the first window were wrong before this change), and the IR test checks
that a window compare masks the score for every windowed scalar 2D and 3D
problem. The byte-identity emitters gain a windowed segment config (10).
AviralGoelAMD added a commit that referenced this pull request Oct 29, 2026
…nified-2d-lse

Bring in #14542's review fixes: scalar 3D segment configs in the byte-identity gate (with the C++ 3D seq-index search and op order matched to Python), one exponent reference in the scalar reduce combine pass, and the sliding window applied under every mask in the scalar 3D body. The merge is clean.
@cgmillette

Copy link
Copy Markdown
Contributor

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

PR #14542 — carry the raw running max in unified attention (AviralGoelAMD, AICK-2829) — 🟠 First-round: not merge-ready — rebase needed — both goldens conflict with #13889's scalar re-bless; the numerics fix checks out

Prior item Status
Copilot (10-28) ×2: default-mask scalar 3D still NaN for a fully masked trailing segment; Rule 10, differential golden not re-blessed 🟢 FIXED 1d1beb90 / ADDRESSED
• NaN segment: every mask now uses select(new_m_raw > -inf, new_m_raw, 0) as the exponent reference, and the loop still yields the raw max (attention_unified.py:6826, :6832). The C++ twin follows the same op order (attention_unified_3d_reduce.cpp:172–191). In our CPU emulation, a default-mask row whose trailing segment is fully masked comes out NaN with develop's body and matches the fp64 reference at head (ran: probe). The new default-mask test has only been run by the author: no CI has run at 1d1beb90.
• Golden: at head the differential golden holds attention_unified cfgs 0–7 (including reduce cfgs 6–7) at canonical and llvm20/22/23, plus the re-blessed tiled-2D families (ran: parsed). The GOLDEN OK run is the author's (read).
• Both threads are resolved
arai713 (10-28) attention_unified.py:6930: the reduce's empty-segment select is dead after the raw-max carry; drop it from both engines 🟢 FIXED ff25dfac — the max pass now loads only segm_max (attention_unified.py:6923–6926; C++ attention_unified_3d_reduce.cpp:363). The combine-pass skip (:6939–6940) and the den == 0 guard (:6953) stay, and they are needed: when every segment is empty, exp2(-inf - -inf) is NaN (ran: probe). The author replied "fixed"; the thread is still unresolved
CongMa13 (10-29): three threads unresolved or unanswered 🟢 Mostly — the author has answered all three. Both Copilot threads are resolved; arai713's is still open (read)
arai713 (10-29) nit: remove product names from the PR description 🟢 FIXED — the body now names device IDs only (ran: body scan). 🟡 The author's 10-29 reply on Copilot's 3D thread still names a product, an internal site and internal job IDs → edit that reply too (read)
arai713 (10-29) APPROVED at 1d1beb90 🟢 Current — given at head, after both fixes. The rebase and golden regeneration below will leave it on an older commit, where it still counts: develop keeps approvals across pushes (read)
Bots / CI 🟡
• The policy bot, labeler and base-freshness checks pass at head.
• No CI at head: no pull_request run exists at 1d1beb90, which was pushed on 10-29 at 07:24Z (15:40Z is the approval). GitHub reports the PR CONFLICTING, so TheRock Multi-Arch, the hipDNN superbuild, pre-commit and clang-tidy cannot start until the rebase. ff25dfac has no CI either.
• Last full CI: at the develop merge 86fc67b0 (10-28), TheRock Multi-Arch and the hipDNN superbuild passed, including hipkernelprovider shards 1/2 and 2/2 on Linux gfx94X and Windows gfx110X. On all four shards both new test files skip at import ("could not import 'torch'"), so CI has never run the PR's numerics (N1). No gfx950 test job ran (read: CI logs)
Our checks 🟠
• Merge: git merge-tree with develop bec4b98a (53 behind, 19 ahead) conflicts only in the goldens. The representative golden has 24 hunks: 7 scalar 2D/3D cases × 3 flavors, plus the 3 ok_cases counters. The differential golden has 4: attention_unified 0–5 and attention_unified_stress 0–31, at canonical and llvm20/22/23. The cause is #13889 (landed 10-29 15:23Z), which re-blessed the same scalar entries: it adds a block_table_stride kernarg and drops q{total_q} from the scalar kernel names. Develop changed no tiled-2D entry in either golden since the base (ran).
• Re-bless scope (ran: lowered on the auto-merged code at llvm20): the PR's tiled-2D entries carry over; 3 sampled cases match its golden. All 9 scalar cases differ from both sides. That includes the PR's 2 new scalar_reduce_* cases: git does not flag them, but their names lose q64. Differential cfgs 6–7 go stale the same way (read). → After the rebase, regenerate rather than hand-merge:
1. rocke_ir_parity_harness.py --write (llvm20/22/23, plus develop's llvm24 → llvm23 alias).
2. Re-record attention_unified (8 cfgs) and attention_unified_stress over develop's differential file, at ll llvm20/22/23 and canonical, as was done at 86fc67b0.
3. Run --check and byte identity.
• Semantic merge: #13889 touches no max or guard code in either engine, and its stride changes auto-merge beside the PR's (read). Its CPU contract test test_attention_scalar_cache_key.py passes 27/27 on the merged code (ran). #14150 sends bf16 D128 sliding-window single-batch prefill to the gfx950 transposed-32x32 combo. On the merged code that route yields the raw max and tests l == 0, so it inherits the fix (ran: IR). #14524 (open) adds the old clamped carry to the C++ scalar kernels; the body already says whichever lands second must port (read).
• Numerics (ran: CPU emulation of the kernels' op order):
– Take a row whose first tile is fully masked and whose visible scores are −200 natural (fp32 and bf16 P) or −20 natural (fp16 P). Develop gives O = 0 or NaN; for fp16 it gives a silent 0 even though l > 0. Head matches the fp64 reference.
– Alpha must come from the raw carried max: a clamped one gives inf, then NaN. It does on both arches (gfx942/attention_tiled_2d.py:4870, gfx950/attention_tiled_2d.py:4001).
– Split-KV: develop gives NaN for an empty leading segment at −200 and for a keyless row; head gives the reference and 0.
– NaN rows now reach O, where develop stored 0, and a NaN segment sum still yields NaN.
• IR (ran): in gfx942 parity cfg 2 (bf16, SW 4096) and the gfx950 fp16 SW combo route, head yields fmax(m, tile_max), the clamped value feeds only P and alpha, and the epilogue tests oeq. Develop yields the select and tests ogt. The IR is otherwise the same length. With sinks, m starts at the sink logit, so the clamp never fires (read).
• Rule 9 (ran: grep): no other builder on the merged tree carries a clamped max. Dense, the FMHA warp body and the gfx942 4-warp kernels use finite sentinels; gfx1151 WMMA decode and tiled 3D already carry the raw max. The body defers the l > 0 guards left in other families to AICK-2831.
• Other: the C++ twins match Python op for op (read). Byte identity was not run (no native build here). Rules 1–3 clean (0/0/0); no product names in the body or commit messages (ran)
# Sev file:line Finding
N1 🟡 library/tests/test_attention_unified_2d_masked_tile_numeric.py:33, test_attention_unified_scalar_masked_tile_numeric.py:33, :52 Problem: both new files importorskip("torch"), and CI has no torch. At 86fc67b0 they skipped on all four hipkernelprovider shards.
Consequence: the fix's numerics have only been run by the author. CI protects the fix only through golden hashes, which a later re-bless can silently overwrite.
Evidence: the property can be checked on the host. The main loop must yield the raw fmax, and the epilogue must test oeq (ran: IR trace).
Fix: port both files to the torch-free numpy device-buffer shim that #13889 added in test_attention_scalar_block_table_numeric.py. The gfx942 cases would then run on CI's gfx94X shards, and the arch-neutral scalar cases would no longer need the gfx942/gfx950 gate (:52). Also add a CPU IR check per tiled-2D epilogue branch and per scalar kernel

Utility: 🟢 Positive — fixes masked-first-tile output collapse and hidden NaN rows; GPU numerics author-run.


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

First-round review: a local qwen3.6 model made the independent first pass, and the orchestrator (Claude Opus 5.5) verified it. The PR conflicts with develop, so there is no merged tree yet. The build, byte identity, full golden checks and test runs wait for the rebase. The orchestrator simulated the merge with git merge-tree and attributed every golden change to the PR, develop or both. It lowered the scalar cases and three tiled-2D representative cases on the auto-merged code at llvm20, and ran #13889's scalar contract test there. It emulated the online-softmax update, the epilogue guard and the split-KV reduce on CPU in fp32, fp16 and bf16. It traced the loop-carried max in the emitted IR on head, develop and the merge, and swept the merged tree for other clamped-max carries. It also read the four hipkernelprovider CI logs of the last full CI run. The orchestrator owns this merge-worthiness call. This review flags whether a closer look is needed; it is not an approval or a rejection.

@cgmillette

Copy link
Copy Markdown
Contributor

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

PR #14542 — carry the raw running max in unified attention (AviralGoelAMD, AICK-2829) — 🟡 Follow-up: no closer look needed — every earlier item is fixed at head and the numerics now run in CI; arai713's approval predates the codegen changes, and CI at head is still building

Correction: the automated comment just above (20:36Z) is an outdated first-round draft that was posted by mistake. Please disregard it; this comment is the current follow-up review at 44974cb7.

Prior item Status
Copilot (10-28) ×2, arai713 (10-28, 10-29), CongMa13 (10-29): round-1 items (3D NaN segment, differential golden, dead reduce select, product names in the body, unanswered threads) 🟢 Still fixed — the 3D body still carries the raw max and uses the safe exponent reference (attention_unified.py:6814, :6822); the reduce's max pass reads only segm_max; the body names device IDs only; their threads stay resolved (ran: body scan, GraphQL)
arai713 (10-29) APPROVED at 1d1beb90 🟡 Stale — predates 39bbccad (C++ 3D codegen), 0f329d8b (reduce) and 44974cb7 (window behaviour). GitHub reports REVIEW_REQUIRED
Ours (10-29) N1: the new numeric tests import torch, so CI skips them 🟢 FIXED 35d9846b — both files now use rocke.runtime device buffers and a numpy FP32 reference; the scalar file runs on any arch in known_arches(). The new CPU IR test test_attention_unified_masked_tile_ir.py (every tiled 2D epilogue branch, scalar 2D/3D, reduce) is in the attention CPU lane and passes 47/47 on the merge (ran). In the Multi-Arch run at 35d9846b, both gfx94X hipkernelprovider shards passed 49/49 lanes before the next push cancelled the run; on gfx942 the 2D file gave 47 passed and 10 skipped (D256 over the LDS budget), and the scalar file 20 passed (read: CI logs)
Ours (10-29): the author's reply on Copilot's 3D thread named a product, an internal site and job IDs 🟢 FIXED — edited at 16:45Z; none of the author's replies names a product, link, host or job ID now (ran: scan)
Ours (10-29) Our checks: "the C++ twins match Python op for op (read)" ⚪ RETRACT for the scalar 3D segment twin: it searched the sequence index linearly and emitted some operands in a different order, so it was not byte-identical (see AnaghaRaoAMD's :189 thread). The PR's own edits did match
Copilot (10-29) …scalar_masked_tile_numeric.py:234: the 3D segment launch omits block_table_stride 🟢 FIXED a58c9266 — the launch passes the stride, and the launcher asserts that the signature names the kernel's params in order (:208). On the merge, the segment and reduce kernels' params equal the test's signatures (ran: CPU probe). Thread resolved
AnaghaRaoAMD (10-29) attention_unified_3d_reduce.cpp:189: no Python↔C++ byte identity for the 3D segment kernel; the body said its C++ needs "no change" 🟢 FIXED 39bbccad — emitter configs 8–10 build the segment kernel in both engines (fp16/bf16, D64–256, sinks; cfg 10 has a window). To match Python, the C++ 3D path now calls rocke_binary_search_seq_idx (attention_unified_prologue.cpp:236; the tiled kernels already use it) and fixes the operand order at three sites. Byte identity is GREEN for cfgs 0–10 at llvm20/22/23, and ctest 33/33 and the library C++/Python parity pass (ran); llvm24 is author-reported. Only tests build the C++ scalar 3D kernel (read). Body updated. Thread unresolved; the author replied
AnaghaRaoAMD (10-29) attention_unified.py:6936: replace the reduce's per-segment select with one clamped exponent reference 🟢 FIXED 0f329d8b in both engines (attention_unified.py:6920, attention_unified_3d_reduce.cpp:428); reduce cfgs 6–7 byte-identical (ran). An fp32 emulation of the old and new combine passes agrees on 8 edge cases (every segment empty, NaN sum, NaN max, very negative max) and 20,000 random rows (ran: probe). Only the 2 scalar reduce cases ×3 flavors and differential cfgs 6–7 are re-blessed (ran: golden OK). 🟡 The docstring of test_scalar_3d_reduce_propagates_nan_segment_sum (…scalar_masked_tile_numeric.py:442) still describes the removed per-segment fcmp oeq. Thread unresolved; the author replied
AnaghaRaoAMD (10-29) attention_unified.py:6814: the default-mask 3D body ignores the sliding window (older than this PR) 🟢 FIXED 44974cb7 — the 3D body calls _scalar_band_ok as the 2D body does (:6803), so every mask applies the window. The C++ twin adds the same left-bound compare (attention_unified_3d_reduce.cpp:172–179); its C problem has no mask fields, so the default mask is all it builds. Both engines add sw to the kernel name. Windowed cfg 10 is byte-identical and blessed at every flavor, and no other pinned config moved (ran). A mutant that drops the window under the default mask fails the new IR check [3d-bottom_right_window] (ran: probe). No dispatcher route reaches the scalar 3D kernels (read). The new windowed GPU case is author-run only so far. Thread unresolved; the author replied
Bots / CI 🟡
• Merge state: head 44974cb7 (pushed 19:22Z) is 3 behind develop 2304e8d0 and 26 ahead; GitHub reports MERGEABLE, blocked on the required review.
• CI at head (20:31Z): pre-commit, clang-tidy, Component CI, both hipDNN superbuilds and the policy bot pass. TheRock Multi-Arch is still building (math-libs for gfx94X, gfx950 and gfx125X queued, gfx110X building), so no hipkernelprovider test shard has run at head. Nothing has failed; the one cancelled check is a superseded duplicate policy-bot run.
• At 35d9846b: that run's one red job, hipDNN integration tests on gfx94X, is a GPU hang inside hipDNN's own random-fill test, not this PR (read: CI logs)
Our checks 🟢
• Range: no rebase; cc6de09d is an ancestor of head. The develop merge 7fe29bb7 hand-resolves only the representative golden's arch counters, correctly (develop's gfx942 +2, the PR's gfx950 +2) (ran: merge-tree).
• Merge d03e5076 (head + develop 2304e8d0, 0 conflicts): native build and ctest 33/33; byte identity GREEN for the 8 touched families at llvm20/22/23; representative golden PASS ×3; differential golden OK for ll and canonical IR, where the five commits only add attention_unified cfgs 8–10 and re-bless the reduce; ROCm clang 24 -O3 compiles of 90 configs with 0 failures, 0 new rejections and 0 spills; the PR's tests on CPU: 47 passed, 82 GPU skips; Rules 1–3 clean (ran).
• Land order: open #14524 now conflicts with this PR in 4 files (the emitter pair, the helper header and attention_unified_2d.cpp); the body already says whichever lands second ports (ran: merge-tree)

Net-new: none.

Utility: 🟢 Positive — fixes masked-tile collapse, hidden NaN rows and the 3D window; numerics CI-run.


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 with an interdiff of the five new commits against cc6de09d, a check of the develop merge's resolution, and, on a merge with today's develop, the native build and ctest, byte identity of the touched families, both golden gates, ROCm clang 24 compiles and the PR's CPU tests. It also ran CPU probes of the reduce rewrite, the 3D launch signature and a window mutant, made a scratch merge with #14524, read the gfx94X CI logs of the run at 35d9846b, and checked the CI state at head. It owns this merge-worthiness call. This review flags whether a closer look is needed; it is not an approval or a rejection.

@cgmillette cgmillette 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.

LGTM

…arry

Resolve the representative-IR golden conflict with the per-family shard
split: drop the single-file golden develop deleted, then re-bless only the
attention and attention_d256 shards. The re-bless changes the same 84
flavor/case hashes this branch changed in the old file; other shards keep
develop's content.
…arry

Resolve the golden conflicts with develop's attention changes by
regeneration: take develop's files, then re-record this branch's six
attention families in the differential golden (ll llvm20/22/23 + ir
canonical, python-only) and re-bless the attention and attention_d256
representative-IR shards. The re-bless touches the same 84 flavor/case
entries as before the merge; other families keep develop's entries.
@AviralGoelAMD
AviralGoelAMD merged commit 809c17d into develop Oct 31, 2026
162 of 166 checks passed
@AviralGoelAMD
AviralGoelAMD deleted the users/avirgoel/rocke/tiled-2d-running-max-carry branch October 31, 2026 01:21
@assistant-librarian

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

cgmillette added a commit that referenced this pull request Nov 4, 2026
…r engines (#14242)

## Summary
Packages rocKE's gfx950 unified attention as two hipDNN kernel-ingestor
engines:
`hipkernel:Gfx950AttentionUnified2D` (prefill and the decode shapes 3D
does not cover) and
`hipkernel:Gfx950AttentionUnified3D` (split-KV decode, a segment and a
reduce launch per kernel
through #14158's multi-launch `sources`). Both ship AOT, so an
SDPA-forward graph on gfx950 can
be served by unified attention with no JIT.

Both catalogs are reduced to what a correctness-checked MI355X sweep
measured as needed:
Unified2D ships 222 of its 792-kernel full space and Unified3D 123 of
348, with no graph losing
coverage or more than the measured noise (the final reduced-vs-full
sweep is below).

JIRA ID : AICK-2542
JIRA ID : AICK-2682

## Risk Assessment
Risk 2 of 5. The PR adds two engines and their descriptors. No existing
engine's code, catalog or
ranking changes: an unpinned SdpaFwd graph still ranks
`hipkernel:Gfx950AttentionDense` first
(device test `DenseStillRanksAheadWhenUnpinned`). The shared pieces it
touches:
- generator tooling: `registry_enumerator` full-space and multi-launch
sections, the
regeneration gates, provenance stamping, and two fixes in
`variant_reduce`. Only
`provenance.generated_from` is opt-in. The expanded registry output now
always writes
per-kernel `provenance.rocke_spec_id` / `rocke_tuning_id`, including
Dense's 840 catalog kernels
(its manifest pins a `spec_id`). The compact `variants:` selections drop
it, so no shipped
descriptor changes, but any other consumer of the expanded output sees
the new field;
- one rocKE builder, `build_unified_attention_3d_tiled_module`;
- the ingestor save census's list of dispatch symbols.

These are covered by their own tests and by the existing dense
regeneration gates, which still
pass. 3D is the first production user of `launchNames()` (#14158).

## ASIC Coverage
gfx950 only: both engines ship only for gfx950, and other architectures'
shards carry neither
engine. PR CI has no gfx950 device lane (`hkp-probe-gfx950` packs and
asserts but launches
nothing), so the device evidence comes from MI355X (and, before the
rebase, MI350X) runs.

## Testing Summary
- Host: unit and census tests for both packs, the arch-neutral geometry,
and the generator suite
under `hipdnn_strict_skips`, including regeneration gates g1–g7 for all
three gfx950 manifests;
static descriptor gates on the installed shards (variant sets, coverage
rungs 1–2, validator,
  launch surface).
- Device, MI355X (gfx950), on the shipped (reduced) catalogs at this
head's code:
- engine-pinned SdpaFwd quick and standard suites against the CPU and
GPU references, both
    engines plus a Dense regression run;
- the knobs suite: every shipped kernel class forced and checked against
the CPU reference,
    plus declines, precedence and a NaN-prefilled workspace;
- a timing comparison of every engine that serves each corpus graph, and
PyTorch;
- RUNBOOK stage 7's final sweep: the full catalog against the reduced
one in one session.
- How the catalogs were reduced: a 3-round dnn-benchmark sweep with
correctness on, over 188
  prefill graphs (2D) and 88 decode graphs (3D), then `variant_reduce`.

## Packaging and performance statistics
All numbers below come from tool output (packaging logs, device suite
runs, dnn-benchmark sweeps).

**Packaging** (gfx950 shard, shipped catalogs; pack time is the build's
own `hkp_pack` command
per engine, median of 3, relative to Dense's on the same job)

| Engine | Kernels (full space) | AOT budget | Code objects, zstd | KDP
| Pack time vs Dense, build default workers | Serial time per kernel vs
Dense |
|---|---:|---:|---:|---:|---:|---:|
| Unified2D | 222 (792) | 1000 | 1.55 MiB | 0.29 MB | 0.11× | 1.58× |
| Unified3D | 123 (348), 246 launches | 400 | 0.64 MiB | 0.26 MB | 0.41×
| 1.06× |
| Dense (catalog unchanged, for scale) | 840 (52 distinct binaries since
#14059) | 1000 | 6.16 MiB | 1.26 MB | 1 | 1 |

- The gfx950 kpack is 8.49 MiB at this head; the two engines are 2.19
MiB, 26% of it. Since #14059
(runtime head counts for Dense), Dense's 840 catalog kernels compile to
52 distinct binaries, but the
kpack still stores one frame per kernel, so Dense grew from 5.79 to 6.16
MiB. Collapsing those entries
is #14059's stated follow-up. The pack-time ratios and the full-space
figure (14.45 MiB) were measured
  against Dense before #14059.
- Provider load (`hipdnn_list_engines`, median of 5) is 1.24× its time
without these
  descriptors.

**Correctness and coverage** (MI355X, shipped catalogs)

| Suite | Unified2D | Unified3D | Dense |
|---|---|---|---|
| SdpaFwd quick + standard, CPU and GPU reference (served / declined /
failed) | 43 / 6 / 0 | 4 / 45 / 0 | 45 / 4 / 0 |
| Knobs suite (forced kernel classes, declines, precedence) | 30 / 30
(both engines) | | 62 / 62 |
| Corpus graphs served and validated against PyTorch | 188 / 188 | 82 /
88 | |

Every decline is intentional: Unified2D leaves decode to Unified3D and
declines Hkv > Hq;
Unified3D serves decode only, and its 6 corpus declines are batch-256
decode graphs whose K/V is
over the 32-bit addressing cap (ASM SDPA serves those).

**Kernel time** (MI355X, autotuned, median of 3 rounds; ratio = Unified
/ other, below 1 = Unified
faster; every engine's round-to-round drift ≤ 0.4%). The Unified2D rows
were re-measured at this
head, on the kernels develop's tiled-2D changes recompiled (see the
checklist).

| | Graphs both serve | Geomean ratio | Time-weighted (other / Unified)
| Unified faster on |
|---|---:|---:|---:|---:|
| Unified3D vs ASM SDPA (decode) | 60 | 0.200 | 8.17 | 60 / 60 |
| Unified3D vs PyTorch | 82 | 0.062 | 34.2 | 82 / 82 |
| Unified2D vs Dense (prefill) | 179 | 2.37 | 0.38 | 0 / 179 |
| Unified2D vs PyTorch | 188 | 1.38 | 0.55 | 39 / 188 |

- Unified3D serves 22 decode graphs no existing hipDNN engine serves.
- Unified2D is slower than Dense wherever Dense serves, and Dense keeps
ranking first there; it
adds coverage Dense lacks (the 9 bottom-right causal graphs in these
corpora).

**Variant reduction**

| Engine | Kernels | Chosen eps | Measured drift | Final sweep, reduced
/ full kernel time (geomean, rounds 1–3) |
|---|---|---:|---:|---|
| Unified2D | 792 → 222 | 0.02 | 0.62% | 1.000, 0.999, 1.000 (u2d_attn);
1.000, 0.999, 1.000 (u2d_rocke) |
| Unified3D | 348 → 123 | 0.03 | 1.08% | 1.004, 1.002, 0.999 (u3d_attn);
1.002, 1.007, 1.004 (u3d_rocke) |

Both arms of the final sweep served the same graphs and passed
correctness on every corpus.

## Testing Checklist
- [x] Unit (host, no GPU) - `HIP_VISIBLE_DEVICES=-1
hip_kernel_provider_tests` - 1454 passed, including
`TestKernelIngestorEngine.EveryHandlerReportsSupportsSavingExactlyWhenItSavesAndRestores`.
Our build host also fails 11 Dense signature/dispatch tests: it has a
`/nonexistent` directory, which those tests assume is absent. They pass
in CI - Status: Passed
- [x] Census - `ctest -R 'hkp-census|rocke_unified'` - 36/36 (the census
inventories are the reduced catalogs') - Status: Passed
- [x] Generator suite (strict skips) - `pytest -p hipdnn_strict_skips
IngestorGenerator/tests` - 1646 passed, 0 skipped - Status: Passed
- [x] Regeneration gates on the reduced selections -
`tests/test_registry_regeneration.py` (g1–g7) 39 passed; AOT budget
tests 12 passed - Status: Passed
- [x] rocKE attention CPU tests and packaging guards - 2D/3D mask_type,
3D module, decode split, KV cache policy, spec-predicate parity: 145
passed, plus 6 develop KV-cache tests that need the compiled
`rocke_engine`; `test_hkp_pack_producer_guards.py` and
`test_gfx950_attention_unified_3d_bundle_coverage.py`: 26 passed -
Status: Passed
- [x] Golden IR, before the reduction - `ctest -R rocke_golden_static` -
the reduction changes no rocKE code - Status: Passed
- [x] Launch surface - `launch_surface.py
configs/gfx950_attention_unified_{2d,3d}.engine.yaml --check` - Status:
Passed
- [x] Variant sets - `verify_variant_sets.py --mode structural` and
`--mode full --arch gfx950 --manifest …` - 2D 222/222 distinct binaries,
3D 123/123 - Status: Passed
- [x] Coverage gate rungs 1–2 - `coverage_gate.py --mode full
--expect-engine …` - Status: Passed
- [x] Validator - `hipdnn_validate_descriptors <shard> --expect-engine …
--json` - success, no warnings - Status: Passed
- [x] Packaging probe - gfx942 configure with
`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON`, `ctest -R
'^hkp-probe-gfx950'` - 12/12; `hkp-probe-gfx950` packs 51 UKDs of the
current descriptors in a gfx942-only build - Status: Passed
- [x] Device suites, MI355X - engine-pinned SdpaFwd quick + standard
with GPU reference
(`hip_kernel_provider_gfx950_attention_*_gpu_ref_integration_tests`) and
CPU reference (`*-external-integration_{quick,standard}_suite`) - 0
failures: Unified2D 43 served / 6 declined, Unified3D 4 / 45, Dense 45 /
4 - Status: Passed
- [x] Device knobs suite, MI355X -
`hip_kernel_provider_integration_tests
--gtest_filter='*Gfx950AttentionUnified*'` - 30/30; Dense knobs 62/62 -
Status: Passed
- [x] Fault re-check, MI355X - the whole `u2d_rocke` corpus in one
autotuned process, twice and then with serialized launches - 93/93 each,
0 errors - Status: Passed
- [x] Reduction sweep + `variant_reduce.py` - 3 rounds, correctness on;
2D `SWEEP_DONE`, 3D `SWEEP_DONE`; 2D 792 → 222, 3D 348 → 123 - Status:
Passed
- [x] Final sweep, reduced against full (RUNBOOK §7) - same coverage,
correctness passed on both arms, ratios above - Status: Passed
- [x] Unified3D catalog regenerated for #14367's LLC-aware K/V cache
policy - all 123 kernels now `kv_cache_policy: "all"` (was `"stream"`),
same kernel classes; MI355X re-run: suites and knobs unchanged, 246/246
decode rows correct, kernel time against the `"stream"` catalog geomean
0.998, time-weighted 0.999 - Status: Passed
- [x] Device evidence at this head (rebased onto develop 2026-11-03,
after the review-round refactors) - Dense's and Unified3D's code objects
are byte-identical to the previous device runs; develop's #13652
recompiled 201 of the 222 Unified2D code objects (same kernels and ids),
and the Unified2D/3D matchers moved onto a shared header, so both
engines' device runs were repeated at this head: suites Unified2D 43 / 6
/ 0, Unified3D 4 / 45 / 0, Dense 45 / 4 / 0 (passed / skipped / failed),
knobs 30/30 and 62/62; compare 188/188 Unified2D graphs (564/564 rows)
and 82 Unified3D graphs (246/246 rows) correct against PyTorch in all 3
rounds. Kernel time vs the previous runs: Unified2D geomean 0.985 (none
of 188 graphs >2% slower), Unified3D 1.003 (ASM_SDPA control 1.006). A
final rebase picked up #14059, which recompiled Dense's 840 code objects
only (Unified2D/3D byte-identical to the run above); the suites and
knobs were re-run at this head with the same results,
`DenseStillRanksAheadWhenUnpinned` included - Status: Passed
- [x] DVC - both reduced KDPs on the `ingestor` remote; `dvc status -c`
in sync - Status: Passed
- [ ] PR CI - GitHub PR checks - Status: Pending

## Technical Changes
- **Unified2D pack** (`Gfx950AttentionUnified2DNative.cpp`): matches
BSHD SDPA forward on the
identity paged-KV layout and builds block_tables, seq_lens and
query_start_len in the
  workspace.
- It declines: batch > 511 (the baked 9-iteration binary search), Skv
not a multiple of 16,
i32 KV over 0x7FFF0000, any window, and the decode cells Unified3D
covers (Sq = 1 with the
    GQA group dividing 16).
- Engine knobs: tile_size, num_warps, block_m_per_warp, use_mfma_32x32
and
    use_transposed_qk_32x32.
- **Unified3D pack** (`Gfx950AttentionUnified3DNative.cpp`): decode
only.
- `launchNames()` is `{segment, reduce}`, and both definitions are
resolved through
    `launchKernelDefinition` with a signature check before any load.
- The segment launch writes `[token, Hq, seg, D]` scratch and the reduce
always runs, so the
    workspace needs no zeroing.
  - Knob: num_segments. No batch limit.
- **Arch-neutral geometry** (`UnifiedAttentionGeometry.hpp`): the launch
geometry and workspace
layouts take a `UnifiedAttentionArchTraits`, and the gfx950 headers are
thin wrappers, so a
  gfx942 pack (AICK-2544) only supplies traits.
- **rocKE**: `build_unified_attention_3d_tiled_module` packs the segment
and its derived reduce
(`reduce_spec_for`) as one `KernelModule`. The module predicate gates
both halves. The 3D
  pack compiles develop's running-max fix for split-KV (#14276).
- **Generator**:
- `enumerate.also` sections may use `space: full` and carry manifest
`launches`. The
enumerator refuses a registry that pairs a reduce when the manifest
declares no launches,
    and keeps one default per cell.
- The gates are launch-aware: g1 keys on launches, g3 uses `--subset`
for full spaces, g4
compares launch sets, and g6 walks every launch and checks module roles.
- g2 and g3 run per engine with the shipped ids reused (#14109),
comparing every descriptor
    byte for byte.
- `generate.py --stamp-provenance` stamps `provenance.generated_from`
(config, manifest,
generator and rocKE commits). It is opt-in, so a default regeneration
stays byte-identical.
  - `variant_reduce` fixes:
- it reads the format-2 winner lines the runtime has written since
#13028;
- it keys a graph on its content with `Graph.id` (minted fresh at each
finalization)
      zeroed, so sweep rounds match.
- **Catalogs** (the manifests enumerate the full spaces; the selections
are `variant_reduce`'s):
- Unified2D: 222 kernels over 198 cells (33 head configs × bf16/fp16 × 3
masks): the narrow
nw4/mw16/t64 geometry in every cell, transposed 32x32 nw4/mw32/t64 in 21
and narrow
nw2/mw16/t32 in 3. The wide 32x32 geometry won no graph and is not
shipped.
- Unified3D: 123 kernels: num_segments 16 in all 116 decode cells, 32 or
64 in 7 bf16
bottom-right D128 variants. The K/V cache policy follows #14367's
LLC-aware rule, which
    picks `"all"` for every shipped decode cell.
- **Descriptors**: both are generated with develop's generator
(post-#13028 UED/UHD, UKD
`sources` for 3D), every kernel id reused from the full catalog. A
regeneration with the shipped
ids reused reproduces them byte for byte. The KDPs live in DVC
(`ingestor` remote).
- **Shared code** (one home per fact):
- `packs/UnifiedAttentionGraphMatch.hpp`: the graph-match helpers both
unified packs use
(operand/BSHD checks, SDPA node, mask classification, dtype, metadata
readers); the
    tests' graph builder is `UnifiedAttentionTestGraphs.hpp`.
- rocKE `kernels/common/attention_mask_type.py` (mask ordinals and band
-> kind, used by
both tiled specs and re-exported by dispatch) and
`attention_split_kv_reduce.py` (the
segment -> reduce derivation the dispatcher and the module export
share).
- CMake registers Dense, Unified2D and Unified3D by one loop per lane
(external
    integration, gpu_ref, census).
- **Tests**:
- The device knobs suite
(`IntegrationGpuGfx950AttentionUnifiedKnobs.cpp`) adds a harness
`prepareWorkspace` hook. It forces the default geometry/segment count
across the head
configurations and each other shipped class on a cell that carries it,
and
    `CoversEveryCatalogKernelClass` holds those tables to the catalog.
- The engine-pinned `gpu_ref` targets and external-integration lanes
cover both engines.
  - The ingestor save census names all seven pack dispatch symbols.
- The Unified2D/3D integration TOMLs are reserved with the gfx950 census
shard
(`hkp_reserve_census_shard(... FILES ...)`, as #14573 requires of every
shard entry).

**Constraints and follow-ups**
- #13372 and #13889 are both in develop; this branch no longer carries a
copy of #13889.
- Heads are catalog cells (baked), window = 0, 2D batch ≤ 511, i32 KV ≤
0x7FFF0000. D256 is
  deferred for both engines. The kernarg work is tracked in AICK-2675.
- There is no 2D/3D `select_path` crossover replay: the split is the
static decode rule above.
- Runtimes older than UKD 1.1 skip the 3D descriptors. Strided-KV 3D is
a follow-up.
- Follow-up: execution-plan save/restore (#13315) for both handlers.
Today they report
  `supportsSaving() == false`, as the RDNA handler does.
- One earlier MI355X sweep hit a single GPU fault in Unified2D on
`u2d_rocke/rocke_sdpa_035`
(bf16, D 128, Hq 64 / Hkv 8, causal, S 512). It has not reproduced
since: not in the forced
geometries on that cell, 15 single-graph runs, 4 full-corpus runs on
MI350X, or the MI355X
  full-corpus re-check above.
- Unified2D kernel time moved with develop's tiled-2D changes (#14542,
#14616), not with this
PR: at this head it is 1.035× (geomean) its pre-rebase time on the same
188 graphs, up to
1.36× on long causal bf16 D128 prefill (S 8192), while Dense in the same
runs is flat (0.997).
Dense still ranks first wherever it serves; the table above is at this
head.
- Measurement note: the pinned dnn-benchmarking (v1 result schema, which
`sweep.py` reads) has
no bottom-right causal PyTorch reference. The sweeps used upstream
dnn-benchmarking #84's
reference, with the 9 bottom-right corpus graphs written as
`causal_mask: false,
right_bound: 0, diagonal_alignment: BOTTOM_RIGHT` (the same band under
hipDNN's
`resolveDiagonalBand`): #84 reads `causal_mask: true` as top-left
regardless of the alignment.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Christopher Millette <63608002+cgmillette@users.noreply.github.com>
dlangbe added a commit that referenced this pull request Nov 5, 2026
… live set

Carrying the raw running max (#14542) kept it live through the whole
transposed softmax body alongside the clamped exponent reference. At
fp16 D128 with i64 K/V addressing that one extra value pushed the
kernel to the VGPR ceiling, and the compiler then serialized the
per-tile compares through VCC.

Rebuild the carry at the yield as new_l == 0 ? -inf : m_new instead.
The raw max is -inf exactly while the row has seen no key, which is
when l is 0, so the carried value is unchanged. Testing l rather than
the max compare keeps LLVM from folding the raw max back in. The IR
test accepts the rebuilt carry and still rejects a clamped one.

Co-Authored-By: Claude <noreply@anthropic.com>
cycheng pushed a commit that referenced this pull request Nov 6, 2026
…r engines (#14242)

## Summary
Packages rocKE's gfx950 unified attention as two hipDNN kernel-ingestor
engines:
`hipkernel:Gfx950AttentionUnified2D` (prefill and the decode shapes 3D
does not cover) and
`hipkernel:Gfx950AttentionUnified3D` (split-KV decode, a segment and a
reduce launch per kernel
through #14158's multi-launch `sources`). Both ship AOT, so an
SDPA-forward graph on gfx950 can
be served by unified attention with no JIT.

Both catalogs are reduced to what a correctness-checked MI355X sweep
measured as needed:
Unified2D ships 222 of its 792-kernel full space and Unified3D 123 of
348, with no graph losing
coverage or more than the measured noise (the final reduced-vs-full
sweep is below).

JIRA ID : AICK-2542
JIRA ID : AICK-2682

## Risk Assessment
Risk 2 of 5. The PR adds two engines and their descriptors. No existing
engine's code, catalog or
ranking changes: an unpinned SdpaFwd graph still ranks
`hipkernel:Gfx950AttentionDense` first
(device test `DenseStillRanksAheadWhenUnpinned`). The shared pieces it
touches:
- generator tooling: `registry_enumerator` full-space and multi-launch
sections, the
regeneration gates, provenance stamping, and two fixes in
`variant_reduce`. Only
`provenance.generated_from` is opt-in. The expanded registry output now
always writes
per-kernel `provenance.rocke_spec_id` / `rocke_tuning_id`, including
Dense's 840 catalog kernels
(its manifest pins a `spec_id`). The compact `variants:` selections drop
it, so no shipped
descriptor changes, but any other consumer of the expanded output sees
the new field;
- one rocKE builder, `build_unified_attention_3d_tiled_module`;
- the ingestor save census's list of dispatch symbols.

These are covered by their own tests and by the existing dense
regeneration gates, which still
pass. 3D is the first production user of `launchNames()` (#14158).

## ASIC Coverage
gfx950 only: both engines ship only for gfx950, and other architectures'
shards carry neither
engine. PR CI has no gfx950 device lane (`hkp-probe-gfx950` packs and
asserts but launches
nothing), so the device evidence comes from MI355X (and, before the
rebase, MI350X) runs.

## Testing Summary
- Host: unit and census tests for both packs, the arch-neutral geometry,
and the generator suite
under `hipdnn_strict_skips`, including regeneration gates g1–g7 for all
three gfx950 manifests;
static descriptor gates on the installed shards (variant sets, coverage
rungs 1–2, validator,
  launch surface).
- Device, MI355X (gfx950), on the shipped (reduced) catalogs at this
head's code:
- engine-pinned SdpaFwd quick and standard suites against the CPU and
GPU references, both
    engines plus a Dense regression run;
- the knobs suite: every shipped kernel class forced and checked against
the CPU reference,
    plus declines, precedence and a NaN-prefilled workspace;
- a timing comparison of every engine that serves each corpus graph, and
PyTorch;
- RUNBOOK stage 7's final sweep: the full catalog against the reduced
one in one session.
- How the catalogs were reduced: a 3-round dnn-benchmark sweep with
correctness on, over 188
  prefill graphs (2D) and 88 decode graphs (3D), then `variant_reduce`.

## Packaging and performance statistics
All numbers below come from tool output (packaging logs, device suite
runs, dnn-benchmark sweeps).

**Packaging** (gfx950 shard, shipped catalogs; pack time is the build's
own `hkp_pack` command
per engine, median of 3, relative to Dense's on the same job)

| Engine | Kernels (full space) | AOT budget | Code objects, zstd | KDP
| Pack time vs Dense, build default workers | Serial time per kernel vs
Dense |
|---|---:|---:|---:|---:|---:|---:|
| Unified2D | 222 (792) | 1000 | 1.55 MiB | 0.29 MB | 0.11× | 1.58× |
| Unified3D | 123 (348), 246 launches | 400 | 0.64 MiB | 0.26 MB | 0.41×
| 1.06× |
| Dense (catalog unchanged, for scale) | 840 (52 distinct binaries since
#14059) | 1000 | 6.16 MiB | 1.26 MB | 1 | 1 |

- The gfx950 kpack is 8.49 MiB at this head; the two engines are 2.19
MiB, 26% of it. Since #14059
(runtime head counts for Dense), Dense's 840 catalog kernels compile to
52 distinct binaries, but the
kpack still stores one frame per kernel, so Dense grew from 5.79 to 6.16
MiB. Collapsing those entries
is #14059's stated follow-up. The pack-time ratios and the full-space
figure (14.45 MiB) were measured
  against Dense before #14059.
- Provider load (`hipdnn_list_engines`, median of 5) is 1.24× its time
without these
  descriptors.

**Correctness and coverage** (MI355X, shipped catalogs)

| Suite | Unified2D | Unified3D | Dense |
|---|---|---|---|
| SdpaFwd quick + standard, CPU and GPU reference (served / declined /
failed) | 43 / 6 / 0 | 4 / 45 / 0 | 45 / 4 / 0 |
| Knobs suite (forced kernel classes, declines, precedence) | 30 / 30
(both engines) | | 62 / 62 |
| Corpus graphs served and validated against PyTorch | 188 / 188 | 82 /
88 | |

Every decline is intentional: Unified2D leaves decode to Unified3D and
declines Hkv > Hq;
Unified3D serves decode only, and its 6 corpus declines are batch-256
decode graphs whose K/V is
over the 32-bit addressing cap (ASM SDPA serves those).

**Kernel time** (MI355X, autotuned, median of 3 rounds; ratio = Unified
/ other, below 1 = Unified
faster; every engine's round-to-round drift ≤ 0.4%). The Unified2D rows
were re-measured at this
head, on the kernels develop's tiled-2D changes recompiled (see the
checklist).

| | Graphs both serve | Geomean ratio | Time-weighted (other / Unified)
| Unified faster on |
|---|---:|---:|---:|---:|
| Unified3D vs ASM SDPA (decode) | 60 | 0.200 | 8.17 | 60 / 60 |
| Unified3D vs PyTorch | 82 | 0.062 | 34.2 | 82 / 82 |
| Unified2D vs Dense (prefill) | 179 | 2.37 | 0.38 | 0 / 179 |
| Unified2D vs PyTorch | 188 | 1.38 | 0.55 | 39 / 188 |

- Unified3D serves 22 decode graphs no existing hipDNN engine serves.
- Unified2D is slower than Dense wherever Dense serves, and Dense keeps
ranking first there; it
adds coverage Dense lacks (the 9 bottom-right causal graphs in these
corpora).

**Variant reduction**

| Engine | Kernels | Chosen eps | Measured drift | Final sweep, reduced
/ full kernel time (geomean, rounds 1–3) |
|---|---|---:|---:|---|
| Unified2D | 792 → 222 | 0.02 | 0.62% | 1.000, 0.999, 1.000 (u2d_attn);
1.000, 0.999, 1.000 (u2d_rocke) |
| Unified3D | 348 → 123 | 0.03 | 1.08% | 1.004, 1.002, 0.999 (u3d_attn);
1.002, 1.007, 1.004 (u3d_rocke) |

Both arms of the final sweep served the same graphs and passed
correctness on every corpus.

## Testing Checklist
- [x] Unit (host, no GPU) - `HIP_VISIBLE_DEVICES=-1
hip_kernel_provider_tests` - 1454 passed, including
`TestKernelIngestorEngine.EveryHandlerReportsSupportsSavingExactlyWhenItSavesAndRestores`.
Our build host also fails 11 Dense signature/dispatch tests: it has a
`/nonexistent` directory, which those tests assume is absent. They pass
in CI - Status: Passed
- [x] Census - `ctest -R 'hkp-census|rocke_unified'` - 36/36 (the census
inventories are the reduced catalogs') - Status: Passed
- [x] Generator suite (strict skips) - `pytest -p hipdnn_strict_skips
IngestorGenerator/tests` - 1646 passed, 0 skipped - Status: Passed
- [x] Regeneration gates on the reduced selections -
`tests/test_registry_regeneration.py` (g1–g7) 39 passed; AOT budget
tests 12 passed - Status: Passed
- [x] rocKE attention CPU tests and packaging guards - 2D/3D mask_type,
3D module, decode split, KV cache policy, spec-predicate parity: 145
passed, plus 6 develop KV-cache tests that need the compiled
`rocke_engine`; `test_hkp_pack_producer_guards.py` and
`test_gfx950_attention_unified_3d_bundle_coverage.py`: 26 passed -
Status: Passed
- [x] Golden IR, before the reduction - `ctest -R rocke_golden_static` -
the reduction changes no rocKE code - Status: Passed
- [x] Launch surface - `launch_surface.py
configs/gfx950_attention_unified_{2d,3d}.engine.yaml --check` - Status:
Passed
- [x] Variant sets - `verify_variant_sets.py --mode structural` and
`--mode full --arch gfx950 --manifest …` - 2D 222/222 distinct binaries,
3D 123/123 - Status: Passed
- [x] Coverage gate rungs 1–2 - `coverage_gate.py --mode full
--expect-engine …` - Status: Passed
- [x] Validator - `hipdnn_validate_descriptors <shard> --expect-engine …
--json` - success, no warnings - Status: Passed
- [x] Packaging probe - gfx942 configure with
`HIPKERNELPROVIDER_ENABLE_PACKAGING_PROBES=ON`, `ctest -R
'^hkp-probe-gfx950'` - 12/12; `hkp-probe-gfx950` packs 51 UKDs of the
current descriptors in a gfx942-only build - Status: Passed
- [x] Device suites, MI355X - engine-pinned SdpaFwd quick + standard
with GPU reference
(`hip_kernel_provider_gfx950_attention_*_gpu_ref_integration_tests`) and
CPU reference (`*-external-integration_{quick,standard}_suite`) - 0
failures: Unified2D 43 served / 6 declined, Unified3D 4 / 45, Dense 45 /
4 - Status: Passed
- [x] Device knobs suite, MI355X -
`hip_kernel_provider_integration_tests
--gtest_filter='*Gfx950AttentionUnified*'` - 30/30; Dense knobs 62/62 -
Status: Passed
- [x] Fault re-check, MI355X - the whole `u2d_rocke` corpus in one
autotuned process, twice and then with serialized launches - 93/93 each,
0 errors - Status: Passed
- [x] Reduction sweep + `variant_reduce.py` - 3 rounds, correctness on;
2D `SWEEP_DONE`, 3D `SWEEP_DONE`; 2D 792 → 222, 3D 348 → 123 - Status:
Passed
- [x] Final sweep, reduced against full (RUNBOOK §7) - same coverage,
correctness passed on both arms, ratios above - Status: Passed
- [x] Unified3D catalog regenerated for #14367's LLC-aware K/V cache
policy - all 123 kernels now `kv_cache_policy: "all"` (was `"stream"`),
same kernel classes; MI355X re-run: suites and knobs unchanged, 246/246
decode rows correct, kernel time against the `"stream"` catalog geomean
0.998, time-weighted 0.999 - Status: Passed
- [x] Device evidence at this head (rebased onto develop 2026-11-03,
after the review-round refactors) - Dense's and Unified3D's code objects
are byte-identical to the previous device runs; develop's #13652
recompiled 201 of the 222 Unified2D code objects (same kernels and ids),
and the Unified2D/3D matchers moved onto a shared header, so both
engines' device runs were repeated at this head: suites Unified2D 43 / 6
/ 0, Unified3D 4 / 45 / 0, Dense 45 / 4 / 0 (passed / skipped / failed),
knobs 30/30 and 62/62; compare 188/188 Unified2D graphs (564/564 rows)
and 82 Unified3D graphs (246/246 rows) correct against PyTorch in all 3
rounds. Kernel time vs the previous runs: Unified2D geomean 0.985 (none
of 188 graphs >2% slower), Unified3D 1.003 (ASM_SDPA control 1.006). A
final rebase picked up #14059, which recompiled Dense's 840 code objects
only (Unified2D/3D byte-identical to the run above); the suites and
knobs were re-run at this head with the same results,
`DenseStillRanksAheadWhenUnpinned` included - Status: Passed
- [x] DVC - both reduced KDPs on the `ingestor` remote; `dvc status -c`
in sync - Status: Passed
- [ ] PR CI - GitHub PR checks - Status: Pending

## Technical Changes
- **Unified2D pack** (`Gfx950AttentionUnified2DNative.cpp`): matches
BSHD SDPA forward on the
identity paged-KV layout and builds block_tables, seq_lens and
query_start_len in the
  workspace.
- It declines: batch > 511 (the baked 9-iteration binary search), Skv
not a multiple of 16,
i32 KV over 0x7FFF0000, any window, and the decode cells Unified3D
covers (Sq = 1 with the
    GQA group dividing 16).
- Engine knobs: tile_size, num_warps, block_m_per_warp, use_mfma_32x32
and
    use_transposed_qk_32x32.
- **Unified3D pack** (`Gfx950AttentionUnified3DNative.cpp`): decode
only.
- `launchNames()` is `{segment, reduce}`, and both definitions are
resolved through
    `launchKernelDefinition` with a signature check before any load.
- The segment launch writes `[token, Hq, seg, D]` scratch and the reduce
always runs, so the
    workspace needs no zeroing.
  - Knob: num_segments. No batch limit.
- **Arch-neutral geometry** (`UnifiedAttentionGeometry.hpp`): the launch
geometry and workspace
layouts take a `UnifiedAttentionArchTraits`, and the gfx950 headers are
thin wrappers, so a
  gfx942 pack (AICK-2544) only supplies traits.
- **rocKE**: `build_unified_attention_3d_tiled_module` packs the segment
and its derived reduce
(`reduce_spec_for`) as one `KernelModule`. The module predicate gates
both halves. The 3D
  pack compiles develop's running-max fix for split-KV (#14276).
- **Generator**:
- `enumerate.also` sections may use `space: full` and carry manifest
`launches`. The
enumerator refuses a registry that pairs a reduce when the manifest
declares no launches,
    and keeps one default per cell.
- The gates are launch-aware: g1 keys on launches, g3 uses `--subset`
for full spaces, g4
compares launch sets, and g6 walks every launch and checks module roles.
- g2 and g3 run per engine with the shipped ids reused (#14109),
comparing every descriptor
    byte for byte.
- `generate.py --stamp-provenance` stamps `provenance.generated_from`
(config, manifest,
generator and rocKE commits). It is opt-in, so a default regeneration
stays byte-identical.
  - `variant_reduce` fixes:
- it reads the format-2 winner lines the runtime has written since
#13028;
- it keys a graph on its content with `Graph.id` (minted fresh at each
finalization)
      zeroed, so sweep rounds match.
- **Catalogs** (the manifests enumerate the full spaces; the selections
are `variant_reduce`'s):
- Unified2D: 222 kernels over 198 cells (33 head configs × bf16/fp16 × 3
masks): the narrow
nw4/mw16/t64 geometry in every cell, transposed 32x32 nw4/mw32/t64 in 21
and narrow
nw2/mw16/t32 in 3. The wide 32x32 geometry won no graph and is not
shipped.
- Unified3D: 123 kernels: num_segments 16 in all 116 decode cells, 32 or
64 in 7 bf16
bottom-right D128 variants. The K/V cache policy follows #14367's
LLC-aware rule, which
    picks `"all"` for every shipped decode cell.
- **Descriptors**: both are generated with develop's generator
(post-#13028 UED/UHD, UKD
`sources` for 3D), every kernel id reused from the full catalog. A
regeneration with the shipped
ids reused reproduces them byte for byte. The KDPs live in DVC
(`ingestor` remote).
- **Shared code** (one home per fact):
- `packs/UnifiedAttentionGraphMatch.hpp`: the graph-match helpers both
unified packs use
(operand/BSHD checks, SDPA node, mask classification, dtype, metadata
readers); the
    tests' graph builder is `UnifiedAttentionTestGraphs.hpp`.
- rocKE `kernels/common/attention_mask_type.py` (mask ordinals and band
-> kind, used by
both tiled specs and re-exported by dispatch) and
`attention_split_kv_reduce.py` (the
segment -> reduce derivation the dispatcher and the module export
share).
- CMake registers Dense, Unified2D and Unified3D by one loop per lane
(external
    integration, gpu_ref, census).
- **Tests**:
- The device knobs suite
(`IntegrationGpuGfx950AttentionUnifiedKnobs.cpp`) adds a harness
`prepareWorkspace` hook. It forces the default geometry/segment count
across the head
configurations and each other shipped class on a cell that carries it,
and
    `CoversEveryCatalogKernelClass` holds those tables to the catalog.
- The engine-pinned `gpu_ref` targets and external-integration lanes
cover both engines.
  - The ingestor save census names all seven pack dispatch symbols.
- The Unified2D/3D integration TOMLs are reserved with the gfx950 census
shard
(`hkp_reserve_census_shard(... FILES ...)`, as #14573 requires of every
shard entry).

**Constraints and follow-ups**
- #13372 and #13889 are both in develop; this branch no longer carries a
copy of #13889.
- Heads are catalog cells (baked), window = 0, 2D batch ≤ 511, i32 KV ≤
0x7FFF0000. D256 is
  deferred for both engines. The kernarg work is tracked in AICK-2675.
- There is no 2D/3D `select_path` crossover replay: the split is the
static decode rule above.
- Runtimes older than UKD 1.1 skip the 3D descriptors. Strided-KV 3D is
a follow-up.
- Follow-up: execution-plan save/restore (#13315) for both handlers.
Today they report
  `supportsSaving() == false`, as the RDNA handler does.
- One earlier MI355X sweep hit a single GPU fault in Unified2D on
`u2d_rocke/rocke_sdpa_035`
(bf16, D 128, Hq 64 / Hkv 8, causal, S 512). It has not reproduced
since: not in the forced
geometries on that cell, 15 single-graph runs, 4 full-corpus runs on
MI350X, or the MI355X
  full-corpus re-check above.
- Unified2D kernel time moved with develop's tiled-2D changes (#14542,
#14616), not with this
PR: at this head it is 1.035× (geomean) its pre-rebase time on the same
188 graphs, up to
1.36× on long causal bf16 D128 prefill (S 8192), while Dense in the same
runs is flat (0.997).
Dense still ranks first wherever it serves; the table above is at this
head.
- Measurement note: the pinned dnn-benchmarking (v1 result schema, which
`sweep.py` reads) has
no bottom-right causal PyTorch reference. The sweeps used upstream
dnn-benchmarking #84's
reference, with the 9 bottom-right corpus graphs written as
`causal_mask: false,
right_bound: 0, diagonal_alignment: BOTTOM_RIGHT` (the same band under
hipDNN's
`resolveDiagonalBand`): #84 reads `causal_mask: true` as top-left
regardless of the alignment.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Christopher Millette <63608002+cgmillette@users.noreply.github.com>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

organization: ROCm project: hip-kernel-provider rocke: platform Touches rocKE platform rocke-lib: attention Touches rocKE library attention/FMHA kernels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants