Repository navigation
perf(rocke/attention): restore LDS K padding at D=64 in gfx950 attention_dense - #10102
Conversation
…ion_dense The LDS pitch selector disabled both bank-conflict pads at D=64, because one async_buffer_load_lds writes 64 lanes x 2 bf16 = 128 contiguous elements -- exactly one D=128 row, but two D=64 rows, so a per-row pad would place the second row at the wrong offset. Pad between DMA row-GROUPS instead: the group stays contiguous for the DMA while the group pitch restores the bank spread. K LDS becomes [NBUF, BN // ROWS_PER_INSTR, ROWS_PER_INSTR * D + _LDS_PAD_K64] when ROWS_PER_INSTR > 1, and do_qk maps krow to (krow // K_GROUP, (krow % K_GROUP) * D + col) -- exact because nsub*32 is a multiple of K_GROUP. Mirrored in the default and persistent builders. V keeps the unpadded packed pitch and stays bit-identically addressed. D=128 emission is unchanged by construction: every new operation sits behind ROWS_PER_INSTR > 1, and at ROWS_PER_INSTR == 1 each derived value is identical to the prior expression. Verified against every D=128 shape the kernel serves, under both llvm20 and llvm22. _LDS_PAD_K64 is validated at import as a non-negative multiple of 8 elements: smem_load_vN stamps align 16 on the n=8 read unconditionally, so an 8-byte aligned pitch would keep the ds_read_b128 while breaking its alignment contract silently. Also key _DENSE_LAUNCHER_CACHE on (kernel_name(), batch). batch is baked into the kernel via the K/V buffer extents and the persistent work-item count, but is absent from kernel_name(), so two specs differing only in batch collided and the second silently reused the first binary -- wrong output, or an illegal memory access depending on the order. Keying here rather than widening kernel_name() leaves the emitted IR untouched. JIRA ID : AICK-1828 Co-authored-by: Cursor <cursoragent@cursor.com>
✅ All Checks Passed — Ready for Review
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
|
🎉 All checks passed! This PR is ready for review. |
Review feedback on #10102: thread the pad through AttentionDenseSpec and the builder instead of reading an environment variable at import time. - lds_k_group_pad replaces the _LDS_PAD_K64 module global and the ROCKE_DENSE_KPAD64 override; default 8, and 0 reproduces the old unpadded layout for A/B. - The 16-byte-alignment validation moves into __post_init__ alongside every other spec check, so supports_attention_dense() now reports a bad pad the same way it reports any other invalid field instead of raising at import. - The pad joins kernel_name() on the packed path only. It changes the emitted layout, so it has to be part of the kernel identity or two layouts share a symbol name and a launcher-cache entry. The head_size=128 name is unchanged. - The tests construct specs directly rather than monkeypatching a module global, and gain a case asserting the pad is in the kernel identity at head_size=64 and absent at 128. No functional change to the emitted kernel beyond the name: head_size=128 IR stays byte-identical across every cohort shape under both llvm flavors, and the D=64 numeric matrix is unchanged. JIRA ID : AICK-1828
|
The D=64 approach looks solid. Padding between DMA row-groups preserves the packed async-LDS load path while restoring K read bank spreading, and the alignment validation is particularly important for the ds_read_b128 path. A few things I'd like to see for confirmation: Perf data comparing kpad={0,8,16,24,32} to justify the default value of 8. Otherwise the design direction looks correct and maintains D=128 codegen stability while addressing the D=64 banking issue |
|
@yraparti thanks, taking your three in order. |
fp16 is an allowed dtype and takes the same packed group-pad path as bf16, and fp16 at head_size=64 is already a recorded golden case, so bf16 passing did not by itself establish the invariants for it. Thread dtype through _spec and parametrize the five IR-dependent tests over both dtypes; 33 cases become 63. The misaligned-pad test is left alone since __post_init__ validation never reads dtype. Co-authored-by: Cursor <cursoragent@cursor.com>
…ll benchmark The pad was reachable only from a unit test, so the benchmark could select --d 64 but was pinned to the default pad with no way to A/B against 0. Add --lds-k-group-pad and forward it at both spec sites. Also fix the launcher cache key, which keyed on kernel_name() alone. Neither batch nor waves_per_eu is part of the name, and both are baked into the kernel -- batch through the K/V buffer extents, waves_per_eu through the amdgpu-waves-per-eu attribute. This collides today rather than in theory: the varlen sweep runs B=4 and B=6 at the same max_s, which produce an identical kernel_name() but different lowered IR, so on --mode all the B=6 config silently reused the B=4 binary. Record the tuning knobs on each sweep row so the JSON and CSV reports are self-describing rather than relying on kernel_name() to carry them. Co-authored-by: Cursor <cursoragent@cursor.com>
964cb94 to
5d4300e
Compare
…ke-attention-d64-lds-kpad
…m golden develop registered attention_dense into the platform byte-identity gate after this branch was cut: at its base the representative golden held zero attention_dense entries and two llvm flavors, and it now holds 165 entries and adds llvm23. The D=64 IR change in this PR therefore trips a gate it did not previously reach. Re-blesses exactly the three D=64 cases that the pad is designed to change -- fp16_h64_sq512, persist_h64_sq512 and ragged_h64_sq500 -- across all three flavors. Every D=128 attention_dense entry is untouched, which is the gate independently confirming this PR's D=128 byte-identity claim, including on llvm23 which was not previously available to check against. The re-bless was validated before writing: on unpatched develop the harness reproduces the committed golden with zero drift on all three flavors, so the regenerated values come from an environment that matches the one that blessed it. With the patch applied the drift is exactly the nine expected entries, and each resulting hash matches the value CI reported. Note this is the platform golden, which is CI-wired. The separate library/tests/golden/attention_dense_ir_sha256.json fixture is still not CI-wired and is still stale against develop; refreshing it remains out of scope here. Co-authored-by: Cursor <cursoragent@cursor.com>
…nse spec develop's #10102 ("restore LDS K padding at D=64 in gfx950 attention_dense") added `lds_k_group_pad: int = 8` to AttentionDenseSpec, immediately next to this branch's `d64_kpad: bool = False`. The two are the same lever -- pad between DMA row-groups to break the D64 QK bank pattern -- derived independently per arch, and they landed on the SAME 8-element pad from opposite directions: gfx950 from a bank/phase model, gfx942 from a measured ~2x ON/OFF ablation. Kept both fields. They stay separate because the consumers differ (a bool switch on a fixed gfx942 layout vs a parameterized gfx950 pitch) and collapsing them would re-bless both arches' goldens. Unifying them once the gfx942 layout is parameterized too is noted in the field comment as the follow-up. Two consequences had to be handled, both caught by the gfx942 golden lane: 1. The shared kernel_name() now emits a `kpad{N}` token whenever `128 // head_size > 1`, so every gfx942 D64 symbol picked up `kpad8` -- from a field this builder reads exactly zero times. It is not removable without touching gfx950's name (and its golden), so gfx942's own suffix is renamed `_kpad` -> `_d64kpad`. The tags are now spelled differently on purpose: a substring test for one can no longer match the other, and a gfx942 symbol cannot be misread as describing the gfx950 pitch. p0_kernel_name carries a warning saying so. 2. gfx942 golden re-blessed: 7 D64 cases x 2 flavors. Verified name-only, not codegen -- all 8 D128 cases are unchanged (matching upstream's `128 // head_size > 1` guard), the byte deltas are exactly the symbol-length change, and building the same spec with lds_k_group_pad 8 vs 24 gives a byte-identical gfx942 body modulo the symbol. Test assertions moved from `"_kpad" in name` to `name.endswith("_d64kpad")`, matching the anchoring convention this file already uses for `_wpe`/`_b`. 572 library tests + 138 dispatch tests pass; the 2 remaining failures in test_attention_builds.py are pre-existing on develop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dth a spec field (#10183) **Branch:** `users/dpeabody-amd/aick-1829-k-slice-hd` off `develop` @ `106da66db1`, one commit. --- ## Motivation The gfx942 sliced-K ring stages K as `[ring_depth, T, K_SLICE_HD]` slices, so the slice width sets both `k_groups = head_size // width` and the K_lds row stride. `ring_depth` became a spec field earlier; the width was still a builder-local constant, hardcoded in two places that had to agree — the emitter and the ahead-of-time LDS-budget gate. Editing one and not the other silently desynchronises the admission estimate from what the builder emits. The width is worth expressing because the row stride is what the QK read's LDS bank spread follows. The read has 32 lanes take 32 consecutive K rows at a fixed column, so lane `i` lands on bank `(stride_dwords * i) % 32`. On gfx942's 32 banks of 4 B the number of distinct banks touched is `32 // gcd(stride_dwords, 32)`, and the conflict degree is 32 lanes over that many banks: width 32 is a 16-dword stride reaching 2 banks, width 16 an 8-dword stride reaching 4. Narrowing trades conflict degree against `k_groups`, and so against the per-tile barrier and partial-wait count, which makes it a tuning axis rather than a constant. **This PR only makes the width expressible.** It selects the shipped 32 for every problem, so no kernel changes. Selecting a narrower width is separate work — see below for why it cannot be bundled here. ## Technical Details - **New `k_slice_hd` spec field**, default 32, documented with the bank-stride mechanism and with the slot-reuse arithmetic that makes it safe at any legal width: with the plain modulo map, `K_SLICE_SLOTS == ring_depth` and the prefetch distance is `ring_depth - 1`, so the DMA target `(kg + ring_depth - 1) % ring_depth` is identically `(kg - 1) % ring_depth` — always the slot `slice(kg-1)` was read from, for every depth and every `k_groups`. The existing drain-on-reuse fence therefore already covers any width. - **`__post_init__` validation**, active only when the ring is on. The width must divide `head_size`, or the last slice reads past the K_lds row. It must be a multiple of the QK k-step, which is also why 8 is the floor rather than merely the narrowest useful value — `k_steps_per_group` floors to zero below it and the slice would emit no MFMA. And it must leave at least two slices: a single group skips both the prologue's second issue and the in-loop prefetch, yet still allocates `ring_depth` slots and touches only one, which is dead LDS. - **Two checks on the ring's partial-`vmcnt` fence count.** That count is not just a loop bound: it is the operand `s_waitcnt(vmcnt=...)` uses to leave exactly one slice's VMEM stream in flight. Partial waits are FIFO-count fences, so it must count whole slice prefetches (mirroring the check the full-tile `kv_calls_per_tile` already has), and it must fit the 6-bit `vmcnt` field. Neither fires for any configuration the *selector* reaches — the three shipped ring geometries all give a count of 4 — but `__post_init__` does accept specs that trip them, because the count depends on `num_warps` through `KV_HALVES_PER_CALL` and the validator cannot see it. They are therefore `ValueError` rather than `assert`: an `assert` is stripped under `python -O`, which is precisely when the mis-fenced kernel would otherwise be emitted. - **`kernel_name()` tag `ks{width}`**, emitted only when the ring is on and the width is not 32, so every shipped ring kernel keeps its symbol name and its cached HSACO. - **Launcher cache-key entry**, beside the `ring_depth` entry and for the same reason: two widths are different schedules with different LDS footprints and must not share a cached launcher. This is the failure mode #10102 hit where `_DENSE_LAUNCHER_CACHE` was keyed on a name that omitted `batch`. - **`_select_gfx942_flash_k_slice_hd`**, returning 32, with a `HIPDNN_GFX942_K_SLICE_HD` diagnostic override. Out-of-range or malformed values fall back to 32. The selector mirrors the validator's shape rules — the width must divide `head_size` and must leave at least two slices — rather than only the first of them. That matters because the feasibility gate answers `supported` before the spec is built, so a rule enforced only in `__post_init__` surfaces as a late `ValueError` on the dispatch path instead of a clean fall back to the default. The third rule, a multiple of the QK k-step, holds for every member of `_GFX942_K_SLICE_HD_CHOICES` and cannot fail. The guard is `str.isdecimal()` rather than `isdigit()` deliberately: `isdigit()` accepts characters such as superscripts that `int()` then rejects, which would turn a typo'd env var into an exception on the dispatch path. - **The LDS-budget gate now takes `ring_depth` and the width** instead of pinning three slots of 32. The `ring_depth` half is a follow-up to the depth work, which left the gate sizing depth-2 as three slots. Note the direction, because it is the opposite of the reassuring one: replacing the pinned 3 with `ring_depth` *lowers* the depth-2 estimate, so the gate admits more rather than less. Swept over the gate's legal parameter space, 640 tuples move from reject to admit. None of them is reachable — every one is at a tile size outside {64, 128}, and `__post_init__` pins ring `tile_size_eff` to exactly those two, so no ring spec can exist there. The new estimate was also checked never to under-count what the builder actually allocates. - **Both parameters are passed on both gfx942 routes into that gate** — the general path and the bf16-wide early return — and on neither of the other arches, since only gfx942's `supports_tiled_2d` declares them. Passing a kwarg that only one arch accepts is the failure mode #10126 fixed for a different flag; this change deliberately does not add a second instance of it. ## Why no width is selected here The C++ twin of this builder hardcodes `K_SLICE_HD` (and `K_SLICE_SLOTS`) in `platform/cpp/instances/gfx942/attention_tiled_2d_ctx_prologue.cpp`. A selector returning anything other than 32 would make the two engines emit different `K_lds` extents for ring specs, and the byte-identity gate would not catch it, because the gfx942 tiled-2D parity corpus enumerates no ring configuration. Mirroring the width into the C++ engine therefore belongs with the change that selects one, not with the change that makes it expressible. Worth flagging while in the area: `K_SLICE_SLOTS` is hardcoded to 3 on that side while `ring_depth` is already a spec field, so depth-2 specs diverge between the engines today for the same reason and with the same invisibility. That is pre-existing and not addressed here. ## Risk Assessment - **No behaviour change, verified rather than argued.** Emitted LLVM IR was hashed before and after across a 16-case gfx942 cohort (bf16 and fp16, D128 and D64, Hq/Hkv 32/8, 16/16 and 128/8, two sequence lengths) at both `llvm20` and `llvm22`. All 32 hashes are unchanged, as are all LDS footprints and all kernel names. The only difference is the new field appearing on the spec. - **The byte-identity parity corpus is unchanged**, checked directly rather than assumed: the Python emitter output was hashed for all 7 configs of `gfx942_attention_tiled_2d_emit` and none moved. These kernels *are* in the corpus; none of its configs is a ring config. - **No C++ change rides here, and that is a deliberate exception to `platform/AGENTS.md`.** The rule at `AGENTS.md:88` requires a spec/instance change to be made in both engines in the same change, and its trigger is the class of change rather than behavioural neutrality. Nothing here changes emitted output, so the two engines cannot disagree today — but the C++ twin does not carry the new field, and the mirror is the first commit of the stacked follow-up (`refactor: carry the sliced-K ring geometry in the C++ attention spec`), which lands with the change that selects a non-default width. Flagging it rather than arguing the rule away. - **Every new operation is gated.** The validator and both fence checks only run when the ring is active, and the name tag only appears away from the default. Validation and checks emit no IR. - **Inert off the ring**, by construction and by test: a non-ring spec lowers to identical IR at width 32 and width 16. - **Rebased on top of #10126, which has now landed.** That PR closed a pre-existing gap where the shared feasibility caller passed a kwarg only one arch declared, and renamed it `use_d256_gfx942_fast` → `use_d256_fast` with matching signatures across the arch gates. This branch uses the new name. The two new ring parameters are still gfx942-only, because only gfx942's `supports_tiled_2d` declares them, so the `if arch == "gfx942"` gating at the call site remains necessary — and a test pins that they never reach a non-gfx942 gate. Worth noting for reviewers that this gating is now observable through the real dispatch path: before #10126 the tiled-2D gate raised `TypeError` on every arch except gfx942, so the test had to assert through kwarg capture rather than by provoking the failure. - **The width is not reachable in production.** Only `HIPDNN_GFX942_K_SLICE_HD` can move it, and that knob carries the engine-divergence caveat above, which is why the default path does not use it. ## ASIC Coverage gfx942 only. The sliced-K ring is gated on the gfx942 transposed-x8 cfvst path and the selector checks `_resolve_attention_arch() == "gfx942"`. gfx950 has its own `kv_ring_depth` in a different kernel and is untouched. Everything here is CPU-verifiable; no device required. ## Test Plan - [x] New `library/tests/test_attn_k_slice_hd.py`, CPU-only, 70 tests: selection returns the shipped width for D128 and D64, env-override handling including values that are numeric but not decimal, validator rejections for all three rules, `kernel_name` gating, cache-key separation, inertness off the ring, the exact LDS delta at both ring depths and both tile sizes, admission unchanged for every legal ring configuration, and the arch-gating of the two new feasibility parameters in both directions. - [x] IR-hash A/B over the 16-case gfx942 cohort, pristine `develop` vs this branch, at `llvm20` and `llvm22`. - [x] Parity-corpus hash check over all 7 `gfx942_attention_tiled_2d_emit` configs. - [x] `library/tests` attention selection, comparing the failure set against unpatched `develop` line by line. - [x] Relative-path contract guard. - [x] `black --check` (pinned 25.12.0); `ruff check` without `--fix` (per `platform/AGENTS.md`, the IR builder is side-effecting so F841 autofix silently changes kernels). - [x] `pre-commit run --from-ref origin/develop --to-ref HEAD`. - [ ] On-GPU numeric lanes — **not applicable**: this change cannot alter emitted code, and IR identity is the stronger statement. Numeric gates belong with the change that selects a width. ## Test Result - **IR identity: PASS.** 16 cases x 2 llvm flavors, all 32 sha256 values identical to pristine `develop`, with LDS footprints and kernel names identical. - **Parity corpus: unchanged**, all 7 configs. - **New test: 70/70 pass.** Includes the exact LDS arithmetic — halving the width frees precisely `ring_depth * T * 16 * 2` bytes and nothing else — at both ring depths and both tile sizes. - **No new suite failures**: the attention selection reports the same pre-existing failing set as unpatched `develop` (`diff` of the sorted lists is empty). - **Relative-path guard: PASS.** **`black --check` clean.** **`pre-commit` clean** over the PR range. **`ruff check`: no findings on the changed source files**; the new test reports one `I001` matching its sibling attention tests, which is the tree's import convention — the repo declares no ruff config, so ruff classes `kernels` as third-party, and there is no ruff hook in `.pre-commit-config.yaml`. ## Follow-up Selecting a narrower width per head size, with the C++ mirror, is tracked under the same ticket. ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests. Changes are behind the feature flag HIPKERNELPROVIDER_ENABLE_ROCKE which is defaulted to OFF. JIRA ID : AICK-1829
… on gfx942 develop's #10102 added `lds_k_group_pad: int = 8` to AttentionDenseSpec for the gfx950 D64 K-LDS pad. That is the same lever this branch added as `d64_kpad: bool = False`, not merely a similar one: gfx950 LDROW = K_GROUP * D + spec.lds_k_group_pad gfx942 K_GROUP_STRIDE = ROWS_PER_INSTR * D + _P0_LDS_PAD (_P0_LDS_PAD == 8) Same formula, same group definition, same LDS shape, same constant, and equivalent gates (`128 // head_size > 1` vs `head_size == 64` over the supported {64,128}). `d64_kpad` was a strictly less expressive duplicate: pad=0 is d64_kpad=False, pad=8 is True. Keeping both was unjustified, so gfx942 now reads the shared field and `d64_kpad` is gone. What this buys beyond deleting a field: * The PR no longer touches kernels/gfx950/attention_dense.py AT ALL -- that 12-line field was the only reason it did. The do-not-touch-gfx950 rule now holds by construction rather than by review. * The `_kpad` vs `kpad{N}` tag collision disappears with the field. The shared kernel_name() already emits kpad{N} on the packed path, so p0_kernel_name adds no tag of its own -- one fact, one spelling. (Before this, a gfx942 D64 symbol said `kpad8` while emitting an UNPADDED layout whenever d64_kpad was False: the name was actively wrong.) * gfx942 inherits the parameterization for free. The A/B is now `lds_k_group_pad=0` rather than a module-level probe constant, so `_P0_D64_KPAD` and the `_p0_d64_kpad()` policy function are both deleted. * gfx950's `% 8 != 0` validator now covers gfx942 too. Shipped codegen is UNCHANGED, verified rather than argued: dispatch used to set d64_kpad=True (stride 2*64+8=136) and now inherits pad=8 (stride 136). Lowering the four dispatch specs before and after and hashing the IR with the symbol name normalised out gives identical bodies on all four -- fp16/bf16 x D64/D128: b7fa1ed9 / 7258cee1 / 1f37baee / 0992a24d. Only the symbol changed, losing the now-redundant suffix. Golden re-blessed. The `default_d64_*` cases now pass `lds_k_group_pad=0` explicitly: they were the unpadded-layout coverage, and with the shared field defaulting the pad ON nothing else would pin it. Their bodies are unchanged -- only the name moves kpad8 -> kpad0, which also makes it honest. 573 library + 138 dispatch tests pass; the 2 test_attention_builds.py failures are pre-existing on develop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… launcher cache key (#10754) JIRA ID : AICK-1894 ## Motivation `waves_per_eu` is baked into the emitted kernel binary via the `amdgpu-waves-per-eu` LLVM function attribute, which drives AMDGPU register-file sizing. However, it was absent from the `_DENSE_LAUNCHER_CACHE` key in `attention_dense.py`, which was keyed only on `(kernel_name(), batch)`. Two specs differing only in `waves_per_eu` would collide, causing the second to silently reuse the first binary. Any in-process sweep over `waves_per_eu` would re-measure the wrong kernel. Additionally, `__post_init__` validated every other performance knob but allowed any integer `waves_per_eu` to flow straight to codegen. Follow-up to #10102. ## Technical Details **`rocke/library/kernels/gfx950/attention_dense.py`** - Widened the launcher cache key from `(kernel_name(), batch)` to `(kernel_name(), batch, waves_per_eu)`. This is host-side only: emitted IR, symbol names, and all golden IR hashes in `attention_dense_ir_sha256.json` are untouched. - Added a `__post_init__` bounds check: `waves_per_eu` must be in `[1, 8]` (hardware maximum waves/SIMD on CDNA). Values ≥ 3 are accepted but flagged in the error message as a known performance trap (caps VGPRs at ≤170, forces spills, ~−20% throughput on this kernel). **`rocke/library/tests/test_attention_builds.py`** Added `TestAttentionDenseWavesPerEu` with three tests: - `test_waves_per_eu_validation_rejects_out_of_range` — asserts `__post_init__` rejects `{0, -1, 9, 100}` and accepts `{1, 2, 8}`. - `test_waves_per_eu_ir_attribute` — lowers two specs (`wpe=1`, `wpe=2`) and asserts `"amdgpu-waves-per-eu"="N,N"` appears correctly in each. No comgr needed. - `test_waves_per_eu_cache_key_isolation` — asserts the cache keys for `wpe=1` and `wpe=2` are distinct and that `waves_per_eu` is the distinguishing third element. No comgr needed. - `test_waves_per_eu_cache_isolation_binaries` — compiles both specs through comgr and asserts the resulting HSACOs have distinct SHA-256 hashes. Skips when comgr is unavailable. ## Test Plan Run the new test class (no GPU required): ```bash cd rocke/platform python -m pytest ../library/tests/test_attention_builds.py::TestAttentionDenseWavesPerEu -v ## Test Result - `test_waves_per_eu_validation_rejects_out_of_range` — PASSED (11 subtests) - `test_waves_per_eu_ir_attribute` — PASSED - `test_waves_per_eu_cache_key_isolation` — PASSED - `test_waves_per_eu_cache_isolation_binaries` — PASSED (comgr available on test machine; `wpe=1` and `wpe=2` produced distinct HSACOs) ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
JIRA ID : AICK-1895 ## Motivation `lds_k_group_pad` was added to `AttentionDenseSpec` in PR #10102 with a fixed default of 8. That PR committed to a follow-up sweep over the full pad space to confirm the default and record whether any shape benefits from a different value. This PR closes that follow-up by documenting the sweep methodology, results, and decision in-repo, and making the knob visible in the runbook's knob catalog. ## Technical Details Three documentation edits: **`library/builders/gfx950/attention/prefill/README.md`** - Fixed stale line: "block_n and waves_per_eu are the only performance knobs" — there are now three (`block_n`, `waves_per_eu`, `lds_k_group_pad`). - Added `## Tuning — lds_k_group_pad` section documenting: - Sweep axes: pad ∈ {0, 8, 16, 24, 32} × bn ∈ {64, 128} × 3 GQA ratios × causal/swa/varlen/persistent × S ∈ {2048, 4096, 8192} (840 configs, all correctness-passed). - The whole-wave bank-conflict model explaining why pad=8, 16, and 24 tie at the 4-lanes-per-bank floor, and what it would take to separate them. - Decision: default stays at 8. pad=24's plurality is sub-1% and noise-grade without an ABBA repeat; pad=16 has an unacceptable regression on a bn=128 sliding-window shape that makes it unsafe as a global or shape-conditioned default. - The pad=16 regression recorded as a named constraint for future work. - The multiple-of-8 invariant (`smem_load_vN` stamps align 16 unconditionally; `__post_init__` enforces it). **`platform/dsl_docs/optimization/optimization_runbook.md` §12.1.F** - Added `lds_k_group_pad` knob-catalog row, sibling to the existing `lds_k_pad` and `lds_layout` rows. Points to the gfx950 README for the full decision record. Makes the knob visible to the Step 0 exhaustive lever sweep. **`library/builders/gfx942/attention/prefill/README.md`** - Added one row to the "Lever record → Adopted" table noting that gfx942 reuses the shared `AttentionDenseSpec.lds_k_group_pad` field and inherits default 8, with a cross-reference to the gfx950 sweep. Per `platform/AGENTS.md` §Compliance, absolute TFLOPS figures and per-lever deltas are kept on the protected results page only; the in-repo docs contain methodology, axes, decision, and qualitative direction. ## Test Plan - Documentation-only change; no kernel IR or dispatch logic modified. - Verified the stale knob-count sentence (L26-28 of the gfx950 README) is corrected to reflect three knobs. - Confirmed the new runbook row is correctly placed in §12.1.F alongside sibling LDS-layout knobs. - Confirmed the gfx942 lever table row accurately describes the inheritance relationship and points to the canonical decision record. ## Test Result No functional changes. All existing parity and byte-identity gates are unaffected. ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
Motivation
attention_denseselects its LDS pitch from the DMA row-packing factor:At D=64 both pads collapse to zero, so neither LDS bank-conflict lever is active
on that head size. The module docstring already recorded this as known — "its
reads take some LDS bank conflict (V-pad is a future D=64 perf lever)" — because
D=64 was added for coverage under the constraint that it cost the D=128 schedule
nothing.
Why it matters, in bank terms. On gfx950 bank =
(byte_addr / 4) % 64over 64banks. A wave64
ds_read_b128moves 1024 B while LDS delivers 64 banks x 4 B =256 B/cycle, so 4 lanes per bank is the conflict-free floor, not merely
"better". Aggregating all 64 lanes of one QK read (
n=8bf16 = 4 dwords each,krow = nsub*32 + lane%32,col = ks*16 + (lane//32)*8):[BN, 128][BN, 136][BN, 64][BN/2, 128+8]So D=64 today sits far closer to the unpadded D=128 case than to the shipped one.
Technical Details
A per-row pad is not possible at D=64: one
async_buffer_load_ldsmoves 64 lanesx 2 bf16 = 128 contiguous elements, which is exactly one D=128 row but two D=64
rows, so a padded row pitch would place the second row at the wrong offset. The
pad therefore goes between DMA row-groups — the group stays contiguous for the
DMA, while the group pitch restores the bank spread.
lds_k_group_padspec field (default 8; 0 reproduces the old unpaddedlayout for A/B).
__post_init__rejects a non-multiple-of-8 pad, becausesmem_load_vNstampsalign 16on then=8read unconditionally, so an8-byte-aligned pitch would keep the
ds_read_b128while breaking itsalignment contract silently. It also joins
kernel_name()on the packed path— it changes the emitted layout, so it has to be part of the kernel identity
or two layouts share a symbol name and a launcher-cache entry. The
head_size=128name is unchanged.[NBUF, BN // ROWS_PER_INSTR, ROWS_PER_INSTR * D + spec.lds_k_group_pad]when
ROWS_PER_INSTR > 1; unchanged[NBUF, BN, D + PAD]otherwise._async_loadstrides its destination by group rather than by row. The sixthparameter is renamed
ldrow_bytes->group_bytes; the two are equal whenROWS_PER_INSTR == 1. V passes the plain unpaddedROWS_PER_INSTR * Dpitch,so its layout is bit-identically addressed (
V_GROUP_BYTES * grpreduces to theprevious
VROW * 2 * row0).do_qkmapskrowto(krow // K_GROUP, (krow % K_GROUP) * D + col). This isexact because
nsub*32is a multiple ofK_GROUP, so the group/sub-row splitcommutes with the
nsuboffset. Both terms are hoisted above the KV loop.group split divides evenly so a future head size fails loudly.
all three reach the 4-deep floor. A 16-lane phase (16 x 4 dwords = one full
64-bank sweep) does separate them: pad 8 gives 16 distinct start banks, pad 16
repeats each twice. A sweep over {0, 8, 16, 24, 32} confirms 8 and 16 are
indistinguishable in practice while 0 and 32 are clearly worse (32 falls back to
32 banks at 8 deep). 8 is therefore chosen as the model-optimal and cheaper of
the two equivalents.
library/tests/test_attention_dense_d64_lds.py(CPU-only, 63 cases): thepad is inert at D=128 (identical IR and identical LDS, both grid variants),
live at D=64, grows LDS by exactly one pad per K row-group across
block_n32/64/128 on both builders, and a misaligned pad is rejected. Every invariant
runs on both allowed dtypes: fp16 takes the same packed group-pad path as
bf16, and fp16 at D=64 is already a recorded golden case, so bf16 alone would
not establish it.
_DENSE_LAUNCHER_CACHEis keyed onkernel_name(), which omitsbatch— butbatchis baked into the kernel(buffer extents, persistent work-item count). Two specs differing only in batch
collided, so the second silently reused the first binary: B=1 then B=2 produced
wrong output with only
1/Bof the tensor written, and B=2 then B=1 raisedhipError(700) illegal memory access. The key is now(kernel_name(), batch);keying here rather than widening
kernel_name()leaves the emitted IRuntouched. Pre-existing, reproduces on unpatched develop.
(
library/benchmarks/gfx950/attention/prefill/benchmark_dense_prefill_live.py)as
--lds-k-group-pad, forwarded at both spec sites, so the pad can be sweptdirectly rather than being reachable only from a unit test. Each sweep row
now records the tuning knobs it was measured at, so the JSON/CSV report is
self-describing instead of relying on
kernel_name()to carry them.private
_LAUNCHER_CACHEkeyed onkernel_name()alone, so it collided onbatchandwaves_per_eu. That one fires on the shipped default runrather than only in principle: the varlen sweep drives
[2048] * 4(B=4) and[256, 512, 768, 1024, 1536, 2048](B=6) at the samemax_s, which producean identical
kernel_name()but different lowered IR, so on--mode alltheB=6 config silently reused the B=4 binary. The key is now
(kernel_name(), batch, waves_per_eu). Pre-existing, reproduces on unpatcheddevelop.
V is deliberately left alone. The same 128-element DMA granularity blocks a
per-row V pad, and a group-padded V would need a pad-aware
pv32_v_load_paired— SSA-emitting
platform/helper code, which pulls in the C++ mirror and abyte-identity re-bless.
ROCKE_DENSE_VPADis currently inert at D=64 (VROWis64 at both 0 and 32), so conflict-free transposed-V remains an untapped D=64
lever, tracked as follow-up rather than bundled here.
Risk Assessment
D=128 codegen does not change, by construction. Every new operation sits
behind
ROWS_PER_INSTR > 1, and the builder is side-effecting, so a guardedbranch emits nothing. At
ROWS_PER_INSTR == 1the derived values(
K_GROUP=1, K_ROWS_LDS=BN, LDROW=D+PAD, K_GROUP_BYTES=LDROW*2, V_GROUP_BYTES=1*VROW*2) are each identical to the prior expression. Verifiedempirically: every D=128 shape the kernel serves in the tracked cohort lowers
to identical LLVM IR before and after, under both
llvm20andllvm22;only the D=64 cases move.
Numerically neutral at D=64. A 19-variant matrix (causal / non-causal,
bf16 / fp16, MHA and GQA 4/8/16, persistent and default grids,
block_n32/64/128, sliding window, ragged, lazy-rescale off,
waves_per_eu=1) givesmax_absagainst an fp32 oracle identical to the pre-change baseline to everydigit.
varlenis not in that matrix: it forbidspersistentand shares thedefault builder's K path verbatim, so it is covered structurally rather than
numerically.
Static resources at D=64,
block_n=64(llvm-readelfon the HSACO, nolaunch — throttle-independent):
Zero spill throughout. The persistent builder's register count is unchanged
(the group/sub-row terms are loop-invariant and hoisted); the default builder
takes +4 VGPR with no occupancy change. LDS grows 1 KB per workgroup against
160 KB/CU, and the kernel is VGPR-limited rather than LDS-limited at D=64, so
the extra kilobyte does not move
waves/CU.Python-only; no C++ mirror required. This is a
library/kernels/layoutchange built from existing
IRBuilderprimitives — no new IR op, noplatform/helper or emitter touched, so the C++ lowerer needs no change.The perf(rocke/attention): recover fp16 gfx942 D128 prefill via depth-2 sliced-K ring #9662 / feat(rocke/attention): enable gfx942 bf16 D128 prefill ring for fp16 parity #9198 precedent that these attention kernels are not registered
in the platform byte-identity gate no longer holds: develop registered them
after this branch was cut. At this PR's base the representative golden held
zero
attention_denseentries across two llvm flavors; it now holds 165across three, having gained
llvm23. The D=64 cases are re-blessed hereaccordingly (see Test Result); the C++ lowerer still needs no change.
Blast radius is bounded to an opt-in path.
dispatch/attention.pymatchesthe
attention_densecandidate only on an explicitalgorithm="attention_dense", never underauto.No interaction with D=256.
__post_init__admits only head sizes 64 and128; D=256 is a different kernel family.
ASIC Coverage
gfx950 only.
attention_denseis gfx950-only by construction(
supports_attention_denserejects every other arch), and the change is furthergated behind
ROWS_PER_INSTR > 1, i.e. head size 64. Validated on a physicalgfx950 node.
Test Plan
cohort, pristine vs patched, for
llvm20andllvm22.patched, compared digit-for-digit.
ordering drift, plus a higher-rep 8-vs-16 tiebreak.
block_n{32, 64, 128} xwaves_per_eu{1, 2, 3} xpersistent {on, off}.
SQ_LDS_BANK_CONFLICT,SQ_LDS_IDX_ACTIVE) at theshipped vs padded layout.
llvm-readelf) at D=64 for both builders, per pad.bound same-session A/B repeatability.
test_attention_dense_d64_lds.py, on bf16 and fp16;black --check;ruff check(no--fix—the emitter-code autofix hazard
platform/AGENTS.mddescribes applies equallyto library kernels, and this file has pre-existing
F841s whose assignmentshave op-emission side effects).
Test Result
flavors; only the D=64 cases changed; no build errors.
max_absidentical to the pre-change baseline.whole-wave model predicts a 4x reduction and the measured drop is larger; the
counters are whole-dispatch and cover both the K
ds_read_b128and thestill-unpadded transposed-V reads, so the residual conflict is consistent with
V — the lever tracked as follow-up.
monotonically with sequence length. Quantitative results are in the protected
internal page (AICK-1828).
is inert, is a few tenths of a percent. Absolute throughput on this part swings
far more across sessions, so only same-session ratios were used.
bn=64, waves_per_eu=2, persistentstays optimal.block_n=128is now feasible at D=64 (it isVGPR-blocked at D=128) but does not pay for its doubled LDS.
and fp16; the 3 spec-validation cases stay bf16, since that path never reads
the dtype.
black --checkclean.ruff checkreports the samepre-existing findings as an unpatched build and none from this change.
script is GPU-only, so neither CI nor this environment can run a sweep. Checked
by importing it against a stubbed torch:
--lds-k-group-padis present in--help, and both the success and failure record paths carry the knob fields.The varlen cache collision was confirmed directly rather than inferred, by
building the B=4 and B=6 specs and comparing them: identical
kernel_name(),different lowered-IR SHA.
CI-wired
platform/tests/golden/rocke_representative_ir_sha256.jsonnowcovers
attention_dense, so the intended D=64 IR change trips it. Exactlythree cases move --
fp16_h64_sq512,persist_h64_sq512,ragged_h64_sq500-- on each ofllvm20,llvm22andllvm23, for 27changed fields and nothing else. To confirm the blessing environment was
trustworthy rather than merely convenient, the harness was first run against
unpatched develop, where it reproduced the committed golden with zero drift
on all three flavors; each re-blessed hash then matched the value CI
reported independently.
attention_denseentry in that gate is unchanged, on allthree flavors. That is an independent confirmation of the D=128 byte-identity
claim above, and it extends it to
llvm23, which was not available when theoriginal A/B was run.
test_attention_dense_golden.py::test_attention_dense_cpp_python_byte_identityrequires a built
rocke_engine, which was unavailable in this environment. Nonew IR op is introduced, so no C++ change is expected; a reviewer with the
engine built should confirm.
Notes: pre-existing, not introduced here
library/tests/golden/attention_dense_ir_sha256.jsondoes not match a pristinebuild of
developfor either llvm flavor (0 of 18 cases), which is consistentwith the fixture being deliberately not CI-wired — its own docstring says so, and
drift is therefore unpoliced. This PR does not re-bless it: doing so would
mix an unrelated whole-fixture refresh into a D=64 change, and would land on an
already-red baseline. The byte-identity evidence above is the pristine-vs-patched
diff rather than the committed fixture. Refreshing the fixture (and deciding
whether to CI-wire it) is better handled on its own.
Submission Checklist
Changes are behind the feature flag HIPKERNELPROVIDER_ENABLE_ROCKE which is defaulted to OFF.
JIRA ID : AICK-1828