Skip to content

feat(rocke): gfx942 D256 bf16 prefill 4-warp GQA fast path - #9310

Merged
AviralGoelAMD merged 36 commits into
developfrom
users/avirgoel/ck/d256-gfx942-4wgqa
Jul 29, 2026
Merged

AviralGoelAMD merged 36 commits into
developfrom
users/avirgoel/ck/d256-gfx942-4wgqa

Conversation

@AviralGoelAMD

@AviralGoelAMD AviralGoelAMD commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor

JIRA ID : AICK-1495

Summary

Adds a fast attention path for D256 (head_size 256) bf16 causal-prefill on gfx942. gfx942 had no fast route for this shape before, so it fell back to a slow path. Stacked on #9233 (users/avirgoel/ck/d256-gfx950-opt).

What changed

  • New D256 gfx942 kernel (build_gfx942_4warp_gqa), a faster 4-warp GQA design.
  • Dispatch now routes the D256 gfx942 bf16 prefill shape to it; every other shape is unchanged.
  • Removed the old, now-unused D256 builder it replaces.
  • Added a routing test that runs on CPU (no GPU needed).

How the speedup works

The original D256 path was stuck at ~1 workgroup per CU, bottlenecked on shared-memory traffic/bank conflicts with the matrix cores nearly idle. The new kernel keeps the hardware busy through:

  • Wider tile per workgroup (4-warp, BLOCK_M=128). 128 query tokens are handled by 4 wave64 warps at once, so the per-launch prelude is amortized and the matrix (MFMA) units stay fed. This geometry was previously unreachable on the default builder: it Python-unrolls the head_dim=256 body, and num_warps>1 blew up the DSL→LLVM lowering pass (the multi-minute "lowerer hang"). The new kernel is runtime-rolled (scf_for), so it compiles in seconds at 4 warps — which is what makes the occupancy win buildable.
  • K and Q read directly from HBM into the MFMA operands — no K_lds/Q_lds staging (K/Q are L2-resident across the tile). Removes an LDS round-trip.
  • V loaded via wide (16-byte / dwordx4) vectorized global reads into registers, then stored wide to V_lds. This bypasses the CDNA3 async global→LDS DMA's 1-dword/lane cap (which otherwise issues many narrow loads), cutting VMEM traffic. V_lds remains the transpose medium — only the load path is widened.
  • LDS-free softmax reduction via ds_bpermute (register cross-lane butterfly), so the online-softmax row-max / row-sum need no shared memory. V_lds is the kernel's only LDS consumer.
  • Cheaper softmax math — the scale is folded into the log2 domain so the exponential is a single exp2; the accumulator rescale uses a fused fma; the final normalization uses a fast reciprocal.

Orientation note: this kernel computes scores in the transposed-QK orientation (S^T = K·Qᵀ), the same as the previous path — it does not change the QK orientation, and V / output still use the transpose (V_lds + an O^T epilogue). The wins above are the load path, softmax cost, and occupancy — not transpose elimination.

Performance

Improvement over the original (pre-optimization) D256 gfx942 kernel — wall-clock of our own kernel, then vs now:

Shape Before any optimization Now Speedup
Sq4096 22.0 ms ≈ 1.6 ms ~14x
Sq8192 87.5 ms ≈ 5.7 ms ~15x

The pre-optimization baseline (~24-27x behind AOTriton, shared-memory-conflict-bound at 1 workgroup/CU) was measured on MI300X; today's numbers are on MI300A, which has fewer CUs, so the true speedup is if anything larger.

Vs AITER unified_attention today (gfx942 MI300A, same-run, D256 bf16 prefill, GQA 16/2), measured as OURS / AITER:

Shape New kernel
Sq4096 ≈ 0.98x AITER (parity)
Sq8192 ≈ 0.94x AITER (~6% faster)
  • Ratio is the headline (device-robust); confirmed with the shipped dispatch_perf_vs_aiter.py + a 3x-repeat, and consistent with the earlier MI300X run (0.97x / 0.94x).
  • Output verified against the fp32 reference and AITER within bf16 tolerance.

TODOs / measured follow-ups (per @yraparti review)

  1. Perf vs FlyDSL v0.2.4 (gfx942): N/A — FlyDSL v0.2.4 does not support head_dim=256 for paged-KV attention. flydsl_flash_attn_func raises NotImplementedError: native paged KV supports head_dim=64 or 128, got 256 (kernels/attention/flash_attn_interface.py:424-425 @ tag v0.2.4; the paged-light path is gated the same at line 472). The D256 prefill cohort is paged → FlyDSL can't run it on this arch, so there is no head-to-head number.

  2. Raw MFMA utilization + 8-wave scope (rocprofv3, MI300X):

    • MfmaUtil ≈ 8-10% (Sq4096 8.3%, Sq8192 10.1%).
    • Not pipe-throughput-bound: VALUBusy ~9%, LdsUtil ~9%, SALUBusy ~0.3%, MemUnitStalled ~0.1% — every execution pipe idles ~90% and memory is not stalled. (VALUUtilization=100% is per-instruction lane efficiency, not pipe-busy.) The kernel is latency-bound at low occupancy (OccupancyPercent ~11-16%, ~3.5-5 waves/CU), register-capped at 128 arch VGPR + 128 AGPR = 256 reg/wave.
    • 8-wave: limited/no scope. A bigger tile (BLOCK_M=256) does not raise waves/CU — the per-lane accumulator is invariant to wave count, and an 8-wave CTA needs 8 co-resident waves at the same 256-reg footprint, so occupancy stays capped (and it risks not launching). The lever to lift MFMA utilization is reducing register pressure to raise occupancy (e.g. HD-split the accumulator), not more waves per CTA. → follow-up ticket.

AviralGoelAMD and others added 10 commits July 7, 2026 05:04
The register-PV 2D-tiled prefill path stages only K/V tiles in LDS; at
head_dim=256 the default double-buffered K tile is 204800 B, over gfx950's
163840 B cap, so the kernel fails to compile ("local memory (204800) exceeds
limit (163840)"). Add _resolve_lds_budget, hooked on _tiled_spec_from_problem's
main return: it computes the exact register-PV LDS footprint and, when over the
arch cap, deterministically shrinks the geometry (single-buffer K, then T=64)
until it fits -- a strict no-op otherwise, so all currently-compiling configs
are byte-identical. Scoped to gfx950 register-PV; mechanism reads the cap
dynamically.

GPU-verified on MI350X (bf16, GQA 16/2): D256 prefill compiles (139264 <=
163840), runs, and matches the fp32 paged reference (max_abs 1.5625e-02) at
Sq 4096 and 8192.
… dedup footprint model

The Python D256 gfx950 register-PV resolver had no C++ twin: once attention is
wired through the shipped provider, its spec-assembly path would select the
overflowing K-double geometry and fail comgr CODEGEN. Mirror the selection-layer
resolver in the C++ selectors so the provider shrinks the geometry the same way
(single-buffer K, then T=64) before build.

- rocke_unified_attn_tiled_2d_lds_bytes: single source of truth for the tiled-2D
  LDS footprint, shared by the gfx942 admission gate and the register-PV budget
  resolver in both engines -- removes the duplicated per-buffer arithmetic that
  could drift.
- rocke_unified_attn_resolve_lds_budget: byte-faithful C twin of
  _resolve_lds_budget; strict no-op for fitting / non-register-PV / non-gfx950.
- Python: _tiled_2d_lds_bytes now backs both _lds_bytes_regpv and the gfx942 gate
  (byte-identical numbers); the shrink levers narrow their except to ValueError
  and thread the reject reason into the resolver diagnostic.
- Tests: footprint single-source, lever reason-threading, infeasible-raises.
…d256-prefill

# Conflicts:
#	dnn-providers/hip-kernel-provider/rocke/library/kernels/common/attention_unified.py
…-bytes

The shared _tiled_2d_lds_bytes footprint model counted fp8 K/V cache tiles at
2 B/elem: _kv_storage_dtype() emits "fp8e4m3" but _kv_lds_elem_bytes() (Py) and
attn_kv_lds_elem_bytes() (C++) only matched "fp8"/"bf8"/"e4m3"/"e5m2". Add the
full dtype strings so fp8-cache specs are modeled at 1 B/elem in both engines.
bf16/fp16 (incl. D256) unaffected; byte-identity green.
…interleave fast path

Route the gfx950 D256 bf16 prefill cohort to the 32x32 transposed fast path plus
the FA3-style softmax<->MFMA interleave (mode2, groups=4). Gates the three
launch-critical geometry selectors (num_warps / tile_size / block_m_per_warp) so
the builder spec, _tiled_cache_key and _get_2d_launch_meta all agree; the codegen
flags (mfma_32x32, transposed stack, register_pv off, K single-buffer, interleave)
are applied by the spec override in _tiled_spec_from_problem. Narrow +
arch/feature-guarded: every other shape is byte-identical; the kernel codegen the
flags trigger already ships on the base.

Validated on MI355X (direct-launch sweep, fp32-ref max_abs=1.5625e-02):
+9.1% @ Sq4096 (1.29x torch-flash) and +10.1% @ Sq8192 over the 32x32 base.
… lever sweep

The live preset bench (_variant_flags menu) can't express default-off levers
(use_softmax_mfma_interleave / use_k_single_buffer / use_register_pv), so running
it does not satisfy Step 0. Point to the raw-flag sweep (probe_config_sweep.py /
_select_2d_* monkey-patch) and the autotuner, with the D256 interleave miss as
the concrete failure.
…che key

The D256 gfx950 fast route applies a codegen override in _tiled_spec_from_problem
(32x32 transposed stack + softmax<->MFMA interleave), but _tiled_cache_key
recomputed those flags from the ungated _enable_* helpers, so the key recorded
e.g. use_transposed_qk_32x32=False / use_register_pv=True and omitted the
interleave flags entirely -- the key no longer described the built kernel.

Add the _d256_gfx950_fast(problem) cohort bit to the key. It flips exactly when
the override applies, so key === built kernel again, without duplicating the
override's ~15 flags. Keying on the predicate (not the flags) deliberately keeps
the override a Layer-2 hand-pinned exception ABOVE the autotuner-regenerable
_enable_* helpers, rather than folding it into tables a future sweep->gate is
meant to overwrite.
Break the row-aliased LDS bank conflict on the D256 gfx950 bf16 prefill
QK
K read by padding K_lds between the 2-row async-DMA slabs. The async DMA
writes one WAVE_BYTES (2-row) contiguous slab per wave/call, so padding
BETWEEN slabs keeps the write contiguous (DMA-safe) while shifting each
slab's banks; the QK read remaps (row,col) -> (row/2, (row%2)*HD + col)
so
    write and read stay consistent (numerics preserved).

    Gated behind use_kq_lds_pad / kq_lds_pad_halves (default off -> byte
identical; byte-identity gate GREEN). Wired pad=16 into the D256 route
in
_tiled_spec_from_problem. Measured on MI355X (direct-launch, fp32-ref
    max_abs=1.5625e-2):

SQ_LDS_BANK_CONFLICT rate 897 -> 660 per 1000 (-4.5x conflict cycles)
Sq8192 ~1592 -> ~1195 us (~25% faster); vs torch-flash 0.86x -> 1.14x
      Sq4096 vs torch-flash 1.27x -> ~1.6-2.0x

Occupancy-neutral: LDS 65 KB <= 80 KB budget keeps 2 WG/CU; VGPR
unchanged.
    Python-engine only for now (the C++ twin NOTIMPL-rejects the host
    use_q_direct_reg / use_softmax_mfma_interleave path). Also adds the
parity harness lanes used to develop this: direct-launch D256 lane,
occupancy
probe, torch-flash comparison, interleave toggle, and pad/xor-swizzle
probes.

    ISSUE ID : AICK-1495


## Submission Checklist

- [ ] Look over the contributing guidelines at
https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
…256-gfx950-opt

# Conflicts:
#	dnn-providers/hip-kernel-provider/rocke/library/kernels/common/attention_unified.py
#	dnn-providers/hip-kernel-provider/rocke/library/tests/test_lds_budget_resolver.py
#	dnn-providers/hip-kernel-provider/rocke/platform/cpp/instances/common/attention_unified_selectors.cpp

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.

Pull request overview

Adds a dedicated fast-path implementation and routing for gfx942 D256 bf16 causal-prefill (GQA) so this cohort no longer falls back to the slow tiled builder on gfx942.

Changes:

  • Introduces a new gfx942 D256 4-warp GQA kernel builder and related spec/support plumbing.
  • Updates unified-attention dispatch to route the targeted D256 gfx942 bf16 prefill cohort to the new builder and launch geometry.
  • Adds a CPU-only routing/contract unit test to lock in the dispatch gate and builder surface.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
dnn-providers/hip-kernel-provider/rocke/library/tests/dispatch/attention/test_d256_gfx942_fast_routing.py Adds CPU unit tests to validate the new gfx942 D256 fast-path gate and builder contract.
dnn-providers/hip-kernel-provider/rocke/library/kernels/gfx942/attention_tiled_2d.py Adds fast-path-related flags/plumbing and introduces the new build_gfx942_4warp_gqa kernel builder.
dnn-providers/hip-kernel-provider/rocke/library/kernels/common/attention_unified.py Adds _d256_gfx942_fast predicate and dispatch/launch-meta routing to the new gfx942 D256 fast kernel.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Move the D256 gfx950 bf16-prefill codegen constellation from a
post-build replace() override in kernels.common _tiled_spec_from_problem
into the builder (builders.common.attention_spec_builder), per review:
the winning spec is created in the builder, not baked into the dispatch
layer. The _d256_gfx950_fast cache-key discriminator is retained so the
key stays faithful to the built kernel. The predicate is called via the
module (_kau._d256_gfx950_fast) so tests patching
attention_unified._d256_gfx950_fast still steer the builder branch.

Verified byte-identical: built spec + _tiled_cache_key unchanged for the
D256 fast path and every fallback/non-D256 shape; fast vs feature-gated
fallback keys stay distinct; byte-identity gate GREEN (10/10 attention
families, Python==C++); attention routing + lds-budget unit tests pass.

## Motivation

<!-- Explain the purpose of this PR and the goals it aims to achieve.
-->

## Technical Details

<!-- Explain the changes along with any relevant GitHub links. -->

## Test Plan

<!-- Explain any relevant testing done to verify this PR. -->

## Test Result

<!-- Briefly summarize test outcomes. -->

## Submission Checklist

- [ ] Look over the contributing guidelines at
https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
…es (#9343)

JIRA ID : AICK-1495

## What

Hooks the D256 gfx950 bf16 prefill fast path (`_d256_gfx950_fast`: 32x32
transposed + FA3-style softmax<->MFMA interleave) into the two
repeatable harnesses, per review request to wire new kernels into the
dispatcher **and** the benchmark harnesses. The dispatcher hookup
already shipped in #9233; this closes the harness side.

- **Parity harness**
(`builders/gfx950/attention/prefill/parity_unified_attention.py`): adds
`prefill_d256_gqa8_b16_sq4096` / `prefill_d256_gqa8_b16_sq8192` to
`default_scenarios()` (Qwen3-Next-80B-A3B cohort: GQA 16/2, head_size
256, bf16 - the two measured points). The pre-existing only
head_size==256 scenario was decode-class and does not trip the prefill
fast-path gate.
- **Benchmark shapes**
(`benchmarks/gfx950/attention/prefill/d256_gfx950_prefill_shapes.json`):
committed aiter-trace-schema shape set (Sq 2048/4096/8192) for the
shape-driven `benchmark_prefill2d_live.py` sweep. No committed
head_size==256 prefill shapes existed before.

## Verification (gfx950 / MI350X, kreb rocm7.13, real GPU run)

- **Parity: PASS.** Both scenarios route to the D256 fast path
(`dispatch_path=2d`, kernel
`rocke_uattn2d_tiled_d256_b16_t64_h16kv2_bf16_w2_mw32_mfma32_stqk_..._mphsplit_smxil2g4_qdreg`)
and match the fp32 `ref_paged_attn` reference at `max_abs = 1.5625e-2`
(= the documented parity value; within bf16 `atol=1e-1`). Identical
`max_abs` across the auto / 2d / 3d paths.
- **vs AOTriton (the ticket baseline): CK wins.** `--torch-flash` (torch
SDPA FLASH = AOTriton on ROCm): Sq4096 = **1.59x**, Sq8192 = **1.03x**
(ck-auto 431 / 1429 us vs torch-flash 685 / 1470 us). Consistent with
"beats AOTriton at every ticket shape".
- **Benchmark sweep: runs all 3 shapes, `correct=3 incorrect=0`,** and
the `prod` variant is confirmed to dispatch the D256 fast kernel
(`dispatch_path=2d`, `_d256_gfx950_fast=True`, same kernel name as
above).
- Note on the sweep's speedup column: `benchmark_prefill2d_live.py` on
this branch compares against **AITER-Triton**
(`aiter.ops.triton.unified_attention`), not AOTriton, so its "speedup vs
Triton" is a different comparison than the AOTriton number above. It
also times CK via the provider path (`run_unified_attention_torch`)
while timing the Triton lane as a raw kernel call; the same CK kernel
measures ~431 us direct (parity) vs ~1221 us via the provider path in
the sweep - a measurement-path difference (mechanism not yet isolated).
The AOTriton comparison above (direct GPU-time) is the representative
metric. (An AOTriton baseline lane for this harness lives on
`attn-bench-framework-toggles` @ f8f05cc, not in this line.)

Stacked on the #9233 branch (`users/avirgoel/ck/d256-gfx950-opt`).
Blank line after the import block flagged by black --check in CI pre-commit.
- attention_tiled_2d.py: rewrite the `use_kq_lds_pad` field comment. It
  described the experimental measurement-only pad (intentionally-wrong
  numerics); production wires the DMA-safe slab-granularity pad (numerics
  preserved, enabled by the D256 fast route). Comment now matches behavior.
- parity_unified_attention.py: the `pr9233_d256_*_bs64` scenarios set
  num_kv_heads=16 (MHA) but the PR cohort is GQA 16/2. Set num_kv_heads=2
  so they match the reported cohort (num_queries_per_kv=8).
- benchmark_prefill2d_live.py: `CkVariantBench.build()` compiled every
  variant via LLVM-direct, so --compile-backend only affected the prod lane.
  Route build() through the backend (self.compile_backend or
  _select_2d_compile_backend); the flag now applies to all lanes and the
  variant lanes match production compile by default.
@AviralGoelAMD AviralGoelAMD self-assigned this Jul 13, 2026
@AviralGoelAMD

Copy link
Copy Markdown
Contributor Author

I have added our shapes to benchmark scripts.

…tcher (#9405)

JIRA ID : AICK-1495

This PR adds an optional **`AttentionSpec.tiled_overrides`** field to
the attention dispatcher (`library/dispatch/attention.py`). That is an
**intentional extension of the dispatcher's deliberately-coarse parity
identity** — the module docstring states the CTA geometry/knobs are
*"intentionally OUT of the parity identity"* (which is `(path,
head_size, block_size)`).

## Commit 2 — knob handoff (MVP)
- `_d256_gfx950_spec_overrides()` = single source of truth for the
32×32-transposed + softmax↔MFMA-interleave (mode2/g4) + slab-pad
constellation. Both the builder override (`attention_spec_builder`) and
the dispatch candidate consume it → dispatched spec and built kernel
cannot drift. **Byte-identical** to the previous inline `.replace(...)`.
- `_tiled_spec_from_problem(problem, *, overrides=None)` applies
dispatcher-supplied overrides on top of the heuristic base and **still
runs `_resolve_lds_budget` in all cases**. The no-arg (production) path
is unchanged.
- `AttentionSpec.tiled_overrides` carries the pinned knobs (see the
reviewer-attention note above).

## Honest ROI / scope
**Behavior-neutral today.** Production `run_unified_attention_torch`
still **bypasses the dispatcher** (it uses `select_path` +
`_tiled_spec_from_problem`), and for the D256 cohort the dispatcher's
overrides are **idempotent** with the builder's existing `_impl`
override. So nothing routes or runs differently today. The value is
**de-risking the eventual production reroute**: it proves the
"dispatcher declares knobs → orchestrator accepts" path so the later
flip (route production through dispatch + delete the `_impl` override)
is a small, safe change.

## Verification
- 13/13 gfx950 dispatch tests (8 routing + 5 knob-handoff: SSOT
constellation, dispatched spec carries the overrides, generic candidate
carries none, orchestrator accepts overrides [SSOT reproduces the spec],
invalid override rejected by the spec `__post_init__`); existing
dispatch suite 12/12 (no regression).


<img width="721" height="834" alt="rocKE_dsl"
src="https://github.com/user-attachments/assets/ab62e8a6-50d2-4c9e-b393-e2c6d8d8d5ab"
/>
…lders/common; PARITY_* env->CLI

Addresses two #9233 review comments on the D256 gfx950 parity harness.

Comment (hoist): _run_torch_flash and _probe_print_occupancy are not
D256/gfx950-specific. Move the reusable cores to builders/common so the
gfx942/decode harnesses can consume one implementation instead of drifting
into near-identical copies:
- builders/common/torch_flash_reference.py: run_torch_flash(...), decoupled
  from the harness-local Scenario/data (plain tensors/params; timing injected).
- builders/common/occupancy_probe.py: print_occupancy(hsaco, num_warps, arch=)
  with an arch arg (was hardcoded gfx950).
gfx950 keeps a thin _run_torch_flash adapter and calls print_occupancy directly.
gfx942 is left untouched -- it imports these if/when it needs them.

Comment (PARITY_* levers): the experimental env flags are all read in one
main(), so they become real argparse flags (discoverable in --help, typed):
  PARITY_NO_INTERLEAVE -> --no-interleave
  PARITY_KQ_SWIZZLE    -> --kq-swizzle
  PARITY_KQ_PAD/_W     -> --kq-pad HALVES
  PARITY_PROBE_OCC     -> --probe-occupancy
The --no-interleave lever still patches the production _tiled_spec_from_problem
(the auto/production-dispatch lane builds the spec internally, so a local
_replace can't reach it), but the global mutation is now restored in a main()
try/finally wrapper -- no leak to later in-process callers.

GPU-verified on gfx950 (MI355X), prefill_d256_gqa8_b16_sq4096:
- baseline unchanged (ck-2d 394 us, max_abs 1.56e-2);
- --probe-occupancy prints the hoisted occupancy line;
- --no-interleave drops the interleave (kernel name loses smxil2g4; 443 us
  32x32 base);
- --torch-flash uses the hoisted run_torch_flash (torch-flash 626 us,
  backend=flash; ck-auto vs torch-flash 1.58x).
…harness-local ProbeSpec

Addresses the #9233 review: use_kq_xor_swizzle produces INTENTIONALLY WRONG
numerics (a read-only bank-conflict probe) but lived on the production
UnifiedAttention2DTiledSpec guarded only by a comment -- one typo from silently
corrupt output. Move it off the production spec so it can never be set there by
accident:

- drop the field from UnifiedAttention2DTiledSpec; the kernel emit now reads it
  via getattr(spec, "use_kq_xor_swizzle", False) -- False for every production
  spec, so production/normal paths are byte-identical.
- the parity harness owns a frozen _ProbeTiledSpec(UnifiedAttention2DTiledSpec)
  subclass; --kq-swizzle builds it via _with_kq_xor_swizzle(spec).

Verified (CPU): constructing a production spec with use_kq_xor_swizzle=True now
raises TypeError; the ProbeSpec carries the flag, survives dataclasses.replace
(kq-pad-after-swizzle path), and kernel_name() works.
…ymmetry)

The K_lds slab pad's async-DMA write always pads when KQ_LDS_PAD is set, but the
read only remaps for the non-fp8 K path -- an fp8+pad combo would pad the write
and NOT remap the read, addressing different slots -> silent numeric corruption.
The pad is a bf16 bank-conflict optimization and fp8+pad is not a supported combo
(the D256 gfx950 cohort that uses the pad is bf16-only), so disable the pad under
fp8 in the geometry guard, keeping write and read consistent. Byte-identical for
the supported bf16 D256 path.

Addresses the #9233 review (K_lds pad fp8 write/read asymmetry).
…s/tile)

Closes the block_size coverage gap flagged in #9233 review: the D256
gfx950 cohort predicate has no block_size constraint and the fast route
forces T=64, so bs=32 (2 blocks/tile) took the fast path while only
bs=16 (4/tile) was validated. GPU-verified on gfx950/MI355X: bs=32
builds the fast spec (b32_t64...) and is correct (max_abs=0.01562, bf16
band) -> bs=32 stays enabled, gap closed by coverage rather than by
narrowing the predicate.
Closes the #9233 review gap: no golden covered the D256 fast spec
(use_kq_lds_pad + use_softmax_mfma_interleave). The slab pad's async-DMA
write and padded read remap must agree byte-for-byte or numerics
silently corrupt, but only the spec dataclass was asserted -- an
addressing drift would pass every CPU test and fail only on GPU.

Adds an "attention" IR-parity case (attention/gfx950/d256_pad_interleave)
that builds the production fast spec and pins its lowered LLVM IR
(sha256, both llvm20/llvm22 flavors) in rocke_representative_ir_sha256.
_d256_gfx950_fast gates on the memoized runtime device arch via
_resolve_attention_arch (not the per-case arch, which only feeds the
lowerer), and the spec builder holds a bound import of it; build_attention
patches both bindings wholesale to gfx950 so the fast spec is selected
deterministically on any bless/CI host, and raises if the fast spec /
pad+interleave is not selected (never silently pins the fallback).
Bless is add-only: no existing case sha changed.
Copilot review (#9471): if every query_len is <= 0, seqs stays empty and
the seqs[0] backend probe IndexErrors into the generic "no working torch
sdpa backend" NotImplementedError -- a misleading failure mode. Raise an
explicit ValueError naming the query_lens so the failure is deterministic
and actionable.
Self-review (rocke-review.md sec.4 / one-way dependency): the D256 IR
golden added earlier lived in the platform IR-parity harness
(platform/tests/instances/rocke_ir_parity_harness.py) and imported the
library packages kernels/builders -- a platform-test-imports-library
violation. Precedent: library/tests/test_ci_static_attention.py was moved
out of platform/tests for exactly this reason ("references the library
builders package and therefore belongs in the library test layer").

Relocate it:
- Revert the platform harness + rocke_representative_ir_sha256.json to
  platform-pure (byte-identical to before; the harness only builds
  rocke.instances kernels again).
- Add library/tests/test_d256_attention_ir_golden.py + its golden. Same
  pinning (both _resolve_attention_arch bindings -> gfx950, raises if the
  fast spec / pad+interleave is not selected) and same lowered IR sha
  (llvm20/llvm22), but importing only rocke.core.lower_llvm from platform
  (library -> platform is the allowed direction). CPU-only, no GPU/comgr.
@yraparti

Copy link
Copy Markdown
Contributor

-Thanks for adding fast path to the d256 gqa. Can you just post the following metrics in TODOs?

  • What is the performance compared to flydsl v_0.2.4 kernel on this architecture?
  • What is the raw mfma utilization, and is there a scope to push this to 8-wave design?

Resolve dispatch/attention.py: keep BOTH D256 candidates — ours
_make_gfx950_d256_candidate (prefill/2D) and develop's #9462
_make_d256_decode_candidate (decode/3D). Disjoint cohorts (2D vs 3D
select_path), so both register and never collide at selection.
Resolve attention_unified.py: keep BOTH function sets — ours (_d256_gfx950_cohort/
_fast/_spec_overrides, D256 prefill) and develop's #9403 _enable_softmax_mfma_interleave
(d128 nw=4 interleave gate). Independent, disjoint cohorts; both defs + the d128
call site preserved. GPU-verified D256 perf unchanged; both gates fire scoped.
…esc is valid; drop redundant HLPV parity variant

The d64/b32/h64kv8 bf16 single-seq long-prefill combo enables use_fast_paged_kv_desc,
but _select_2d_tile_size picked T=128 for it -> the spec validator (fast_paged_kv_desc
requires T=64) raised, so production crashed on these shapes on gfx950. num_warps was
already 4; tile_size was the incompatibility.

- _select_2d_tile_size: force T=64 for the bf16 h64kv8 no-SW single-batch d64 family
  so its fast_paged_kv_desc enablement is valid. Measured ~1.3-1.4x over the T=128
  no-fast_paged path on MI355X; other d64 single-batch shapes keep the wider T=128.
- attention_spec_builder: gate use_fast_paged_kv_desc on _select_2d_tile_size==64
  (self-consistency -- never set the flag with an incompatible tile).
- parity_unified_attention: the 2d lane now builds the production spec directly;
  removed the hand-tuned HLPV variant (use_hlpv_variant + the r4_hlpv_parity spec),
  now redundant because production emits the same T=64 + fast_paged kernel.

Verified on MI355X (gfx950): the 4 family shapes build + run correct (bf16) with
the T=64 + fast_paged kernel and match the former HLPV ceiling.
Add a fast dispatch path for D256 (head_size=256) bf16 causal-prefill
paged attention on gfx942 (CDNA3 / MI300X). gfx942 had no fast D256 route
on the default builder: the wide-flash gate excludes head_size==256 and the
narrow 16x16x16 fallback overflows LDS at a competitive tile.

Kernel (kernels/gfx942/attention_tiled_2d.py::build_gfx942_4warp_gqa):
BLOCK_M=128 / 4-wave64 CTA natural-QK (S = Q @ K^T) attention on the
32x32x8 bf16 MFMA atom. Reads paged K/V direct HBM->register (i32
element offset, i64-overflow fallback via the default builder for caches
> 2 GiB), varlen token-major Q via TensorDescriptor.naive +
in-kernel binary_search_seq_idx on cu_seqlens_q, bottom-right causal
(chunked-prefill / decode), GQA head->kv_head, softmax scale*log2e fold,
LDS 5-stage swizzle reduction, conflict-free V.

Dispatch (kernels/common/attention_unified.py): _d256_gfx942_fast gates
the cohort (bf16, head_size=256, prefill, block_size in {16,32}, no
fp8/sliding-window/softcap/sinks/alibi/qq_bias, cache <= 2 GiB); it sets
the launch-critical geometry selectors so the spec, _tiled_cache_key and
_get_2d_launch_meta agree, selects the builder in _get_2d_launcher, and
keys the cache so its HSACO never shares a slot with the default builder.
Every other shape is byte-identical.

supports_tiled_2d exposes use_d256_gfx942_fast, which bypasses the generic
staged-tile LDS-budget model (the fast-path builder manages its own LDS).

Test: tests/dispatch/attention/test_d256_gfx942_fast_routing.py covers the
gate accept/reject matrix (arch, dtype, head_size, block_size, decode,
feature flags) and the builder contract, arch-pinned so it runs host-side
without a gfx942 GPU.
The _d256_gfx942_fast docstring cited the removed std-QK research kernel's
dense TF/s (115.4/124.9). Replace with the shipped 4-warp GQA builder's
same-run measurement vs AITER unified_attention (D256 bf16 causal prefill,
GQA 16/2, block_size=16): ~0.98x @ Sq4096 (parity), ~0.94x @ Sq8192 (OURS
~6% faster) on MI300A, matching the MI300X prototype run (0.97x/0.94x).
Output verified against the fp32 reference and AITER within bf16 tol.
…nch)

The _d256_gfx942_fast docstring claimed spec/_tiled_cache_key/_get_2d_launch_meta
all agree on num_warps=1/block_m_per_warp=32. They do not: those values are the
cohort discriminator (distinct HSACO cache slot, byte-identity for other shapes),
while build_gfx942_4warp_gqa is a 4-wave64/BLOCK_M=128 kernel that
_get_2d_launch_meta deliberately launches with block=(256,1,1)/BLOCK_M=128.
Reword so a '4-warp' builder isn't documented as '1-warp ... all agree'.
- attention_tiled_2d.py: drop unused `mfma_k_loop` import.
- supports_tiled_2d: validate the d256_gfx942_fast fast-path preconditions
  (bf16 / head_size==256) before returning supported, instead of trusting the
  caller's flag; fix the comment (K is direct HBM->reg, V is staged via V_lds).
- 4-warp kernel header: correct the K/V + addressing claim -- K direct
  HBM->register, V staged through V_lds, i32 paged addressing with the
  `_d256_gfx942_fast` gate excluding > 2 GiB caches that need i64.
- _select_2d_num_warps / _select_2d_block_m_per_warp: note the d256_gfx942_fast
  returns are cache-key discriminators only; the dedicated 4-warp kernel
  launches block=(256,1,1) / BLOCK_M=128 via _get_2d_launch_meta.
Adds d256_gfx942_prefill_shapes.json (GQA 16/2, head_size 256, bf16,
Sq4096/8192) so the dispatch-driven benchmark_prefill2d_live.py exercises the
4-warp kernel through backend="auto" against AITER unified_attention (paged,
2D-forced) -- apples-to-apples paged-vs-paged.

Fixes the stale final_shapes_check.py note that claimed no tiled d256 path on
gfx942; build_gfx942_4warp_gqa now routes via _d256_gfx942_fast.

Measured (MI300X, D256 bf16 causal prefill GQA 16/2, paged, vs AITER
unified_attention 2D): OURS/AITER ~0.97 @SQ4096, ~0.98 @sq8192; pure-fp32-ref
max_abs 0.05/0.08 (within bf16 tolerance).
@AviralGoelAMD
AviralGoelAMD force-pushed the users/avirgoel/ck/d256-gfx942-4wgqa branch from 8b1b96a to 9c4fcef Compare July 21, 2026 21:20
… library golden

Extends the D256 IR golden with build_gfx942_4warp_gqa (the byte-sensitive V
wide-load -> V_lds transpose path), mirroring the gfx950 fast-spec case. CPU-only
(pure-python lowerer, both llvm20/llvm22 flavors); guards the codegen/addressing
drift the routing test can't catch. Kernel numerics independently verified on
MI300X: bf16 worst-element max_abs is seed-variance 0.02-0.16, consistent with
the documented 0.05/0.08.
@AviralGoelAMD

Copy link
Copy Markdown
Contributor Author

-Thanks for adding fast path to the d256 gqa. Can you just post the following metrics in TODOs?

  • What is the performance compared to flydsl v_0.2.4 kernel on this architecture?
  • What is the raw mfma utilization, and is there a scope to push this to 8-wave design?
  1. FlyDSL does not support HDIM 256 https://github.com/ROCm/FlyDSL/blob/5e90bf7ec36df5509c62827722c45d7f85c8fbe0/kernels/attention/flash_attn_interface.py#L425
  2. Mfma Util for Sq 4096 8.2 % and Sq 8192 10.0%.
  3. Maybe not, because if we double the waves, it will not reduce VALU instructions which are currently the bottleneck.

Base automatically changed from users/avirgoel/ck/d256-gfx950-opt to develop July 22, 2026 13:47
@therock-pr-bot

therock-pr-bot Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
🌿 Branch Name ✅ Pass —
📝 PR Title/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.

Resolve 3 D256 conflicts (develop already carries the gfx950 D256 work):
- attention_unified.py: union - keep both _d256_gfx950_fast (develop) and
  _d256_gfx942_fast (ours) selectors at all 4 sites; develop's other changes
  auto-merged untouched.
- golden json + test: take ours (clean superset - develop's gfx950 case is
  byte-identical; ours adds the gfx942 4-warp case).

Verified CPU-side: golden IR sha256 matches for both gfx942+gfx950 cohorts;
gfx942 routing (12), gfx950 routing (13), lds-budget (9) all pass.
@therock-pr-bot

Copy link
Copy Markdown

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

@AviralGoelAMD
AviralGoelAMD merged commit d2150b5 into develop Jul 29, 2026
35 checks passed
@AviralGoelAMD
AviralGoelAMD deleted the users/avirgoel/ck/d256-gfx942-4wgqa branch July 29, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants