Repository navigation
feat(rocke): fix and tune attention sink decode - #9391
Conversation
✅ 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. |
765dc19 to
4da77dc
Compare
Pre-commit check failed⛔ pre-commit failed Please run locally:
This repo uses |
74b171c to
388ea06
Compare
388ea06 to
68d6774
Compare
There was a problem hiding this comment.
Ideally, this
if problem.all_decode:
return 1change should be also done on the cpp counterpart that is present at platform/cpp/instances/common/attention_unified_selectors.cpp so that both CPP and PY translators translate down to identical IR.
(I am not sure if we are strictly enforcing that policy just yet but given that its only a two line change on python side, we can do it quickly on cpp as well).
Rest of the PR LGTM.
687ccea to
c10fa4c
Compare
There was a problem hiding this comment.
Pull request overview
This PR fixes correctness and performance issues in rocKE’s attention-sink decode path (notably for gfx942 seqlen_q=1) and extends the decode benchmarking harness to cover attention sinks and sliding-window (SWA) shapes.
Changes:
- Fixes a gfx942 bf16 SWA decode crash by guarding the 2D fp8-loader chunk-count assert behind
KV_FP8. - Tunes gfx942 D64 decode warp selection to prefer
num_warps=1for decode-only (seqlen_q==1/all_decode), preserving prefill behavior. - Adds attention-sink + sliding-window coverage to the decode benchmarks and introduces gpt-oss sink shape JSONs; adds a regression test to prevent the prior crash.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| dnn-providers/hip-kernel-provider/rocke/platform/cpp/instances/common/attention_unified_selectors.cpp | Adjusts gfx942 D64 decode warp selection (Sq=1 -> nw1). |
| dnn-providers/hip-kernel-provider/rocke/library/tests/test_attention_builds.py | Adds regression test ensuring bf16 SWA decode no longer trips the fp8-loader assert. |
| dnn-providers/hip-kernel-provider/rocke/library/kernels/gfx942/attention_tiled_2d.py | Guards fp8 chunk-count assert / chunk computation behind KV_FP8. |
| dnn-providers/hip-kernel-provider/rocke/library/kernels/common/attention_unified.py | Tunes gfx942 2D warp selection to return nw1 for decode-only problems. |
| dnn-providers/hip-kernel-provider/rocke/library/benchmarks/gfx950/attention/decode/gpt_oss_sink_shapes.json | Adds gpt-oss sink + SWA decode benchmark shapes for gfx950. |
| dnn-providers/hip-kernel-provider/rocke/library/benchmarks/gfx950/attention/decode/benchmark_decode_live.py | Extends gfx950 decode benchmark to plumb sinks + sliding_window into the DSL path and shapes loader. |
| dnn-providers/hip-kernel-provider/rocke/library/benchmarks/gfx942/attention/decode/gpt_oss_sink_shapes.json | Adds gpt-oss sink + SWA decode benchmark shapes for gfx942. |
| dnn-providers/hip-kernel-provider/rocke/library/benchmarks/gfx942/attention/decode/benchmark_decode_live.py | Extends gfx942 decode benchmark to plumb sinks + sliding_window into the DSL path and shapes loader; skips Triton when sinks are active. |
Comments suppressed due to low confidence (2)
dnn-providers/hip-kernel-provider/rocke/library/benchmarks/gfx942/attention/decode/benchmark_decode_live.py:228
- sliding_window is now a first-class shape field, but the AITER Triton baseline still hard-codes window_size=(-1, -1). For any shape with sliding_window>0 and use_sinks=False, Triton will benchmark the wrong math (full-causal) and the DSL-vs-Triton comparison becomes invalid.
except ImportError:
dnn-providers/hip-kernel-provider/rocke/library/benchmarks/gfx950/attention/decode/benchmark_decode_live.py:215
- sliding_window is now loaded and passed to the DSL path, but the AITER Triton baseline still hard-codes window_size=(-1, -1). For any shape with sliding_window>0 and use_sinks=False, Triton will benchmark the wrong math (full-causal) and the DSL-vs-Triton comparison becomes invalid.
except ImportError:
return None
0530cd6 to
ed72ae8
Compare
aosewski
left a comment
There was a problem hiding this comment.
Read through this end to end. The fp8 guard is right and the C++ mirror matches (all_decode is defined as max_seqlen_q == 1 in attention_unified_entry.cpp#L125, so the two selectors agree). Four comments inline; these six are on lines outside the diff.
1. --dtype / --batch silently drop the new shape fields (gfx950)
benchmark_decode_live.py#L658-L686 rebuilds DecodeShape field by field, and neither override forwards use_sinks or sliding_window. So this:
python -m benchmarks.gfx950.attention.decode.benchmark_decode_live \
--shapes .../gpt_oss_sink_shapes.json --batch 64
benchmarks plain full-causal attention under rows still labelled full_sink_* / swa_sink_*. A batch sweep is the most likely way these shapes get run.
shapes = [dataclasses.replace(s, dtype=args.dtype) for s in shapes]
shapes = [dataclasses.replace(s, batch=args.batch) for s in shapes]immune to the next field someone adds. (gfx942 is safe only because it has neither flag.)
2. AOTriton isn't skipped for sinks or windows
gfx942#L403 / gfx950#L389 — use_bias covers softcap, alibi and qq_bias but not the two new fields, so all 8 shapes in gpt_oss_sink_shapes.json get timed against scaled_dot_product_attention(is_causal=False): no sink logit, no window. That number becomes best_speedup_vs_aoTriton and lands in the printed geomean.
use_bias = (
alibi_slopes is not None
or data["qq_bias"] is not None
or softcap
or shape.use_sinks
or shape.sliding_window > 0
)3. FlyDSL has no gate at all
gfx942#L498-L503 / gfx950#L480-L485 — _flydsl_supported checks head_size and dtype only, and FlyDSL is called with causal=True and no sink or window operand, so flydsl_ms and best_speedup_vs_flydsl are wrong math for these shapes. The prefill sibling already gets this right (benchmark_prefill2d_live.py#L190-L193):
if shape.use_sinks:
return False, "sinks unsupported"
if shape.sliding_window:
return False, "sliding_window unsupported"Between this, #2 and the inline comment on _run_triton, the same two shape fields get three different policies across three backends — one hard skip, one stale predicate, one nothing. A single _backend_supports(shape) helper would keep them honest.
4. window_size is still hardcoded
gfx942#L251 / gfx950#L237 — window_size=(-1, -1), while sliding_window is now a first-class field. Only the sinks flag short-circuits, so a sliding_window>0, use_sinks=False shape (newly expressible because of this PR) times full-causal Triton against a windowed DSL kernel. Copilot flagged this earlier; still open.
5. Same unguarded assert survives in the gfx950 2D builder
gfx950/attention_tiled_2d.py#L2378-L2382 has the identical unconditional fp8_total_chunks % THREADS == 0. With this PR, gfx942 2D, gfx942 3D and gfx950 3D all guard it and gfx950 2D is the last one that doesn't — and this PR adds gfx950 sink-decode shapes that drive that builder. It survives today only because the gfx950 selector happens to land on nw≤2; any retune to nw=4 trips it. Same if KV_FP8: wrap closes it.
6. The correctness result isn't reproducible from this PR
The description reports max_abs <= 1.2e-4 vs an fp32 reference, but neither benchmark computes a reference, compares outputs, or reports an error norm — out is allocated, written and discarded (gfx942#L699-L781). For a change that alters kernel geometry that's the one number worth being able to re-run. Either an opt-in --check that runs a small fp32 sink reference on the first shape, or a pointer to the test that produced those numbers, would do it.
Minor, take or leave: the results record (L783-L817) mirrors every other DecodeShape field as its own JSON key but not the two new ones, so downstream tooling has to reverse-parse signature to tell a sink run from a plain one. And the gfx950 file got the code without the comments the gfx942 twin got (sinks tensor and the skip at L214) — they're maintained as clones, so the next person to touch one will wonder.
ed72ae8 to
7d1b60e
Compare
Guard the fp8 chunk-count assert and fp8_chunks_per_thread with KV_FP8, to match the 3D builder. Add a codegen regression test covering the bf16 SWA-decode config (t=16, hd=64, threads=256).
Decode is memory-bound; num_warps=1 beats the shipped nw4 on gfx942 D64 with identical output. Apply only to decode (all_decode) so prefill keeps nw4. Add sink and sliding-window support to the decode benchmark plus a gpt-oss sink shapes file
Add attention-sink and sliding-window support to the gfx950 decode benchmark plus a gpt-oss sink shapes file, mirroring the setup for gfx942.
- gate the gfx942 D64 decode num_warps=1 selector on `not use_fp8`
in Python + C++ selector.
- guard the gfx950 2D fp8-loader chunk-count assert behind KV_FP8,
matching the gfx942 fix.
- decode benchmark backend gating (gfx942 + gfx950):
* pass sinks + sliding-window through to AITER Triton instead of
skipping sink shapes.
* skip aoTriton and FlyDSL for sink / sliding-window shapes.
* derive window_size from sliding_window instead of hardcoding (-1,-1).
- use dataclasses.replace for --batch/--dtype overrides so use_sinks and
sliding_window are preserved.
- add selector unit test and harden the fp8-loader-assert regression
test.
- results JSON records use_sinks/sliding_window as keys
- DecodeShape signature distinguishes sink and windowed shapes.
7d1b60e to
baab3ab
Compare
…fx942 (#9742) Child of #9739 (the gfx942 `num_sms` device-parameter resolver + split-KV segment bound). ## Why this exists (the accurate reason) `num_sms` in #9739 is a **device-parameter selection**: it is the device CU count, and it sets `target = num_sms*4`, the boundary where a decode shape stops fitting the serial 2D path and should split across the device (3D split-KV). So for any given shape, **whether it should route 2D or 3D is a function of the device parameter** — and it is both **batch-dependent** and **device-dependent**: - `num_2d <= target` → 3D (needs splitting to fill the device), - `num_2d > target` → 2D (batch alone already fills the grid). The existing decode corpus is **batch=1**, which is the hardest/most latency-bound case (least parallelism) and the right stress case for the kernel body — **but it is already routed 3D even at the stale `num_sms=120`** (`num_2d≈8 ≪ 480`). So the corpus is a **no-op for the routing change** and cannot exercise the 2D↔3D transition #9739 actually moves. This is not a "realism gap" — it's that a device-parameter fix can only act in the transition zone, which batch=1 is not in. ## Routing map (llama8b 32/8 D128, MI300X) | batch | num_2d | @sms=120 | @sms=304 | effect of #9739 | |---|---|---|---|---| | 1–48 | ≤480 | 3D | 3D | already 3D — fix no-op on routing | | 56–~120 | 481–1216 | **2D** | **3D** | **FLIP — the win (mis-routed on a 304-CU device)** | | 128+ | >1216 | 2D | 2D | grid already full — no-op | ## What this adds `decode_batch_sweep_shapes.json` (gfx942) — **70 shapes**: `batch ∈ {8,16,32,64,128,256}` + a per-config flip-window batch, across 5 GQA configs (Llama-3-8B/70B, Llama-3.1-405B, Qwen3-30B, Qwen3-235B), D64/D128, kv {2048, 8192}, `seqlen_q=1`. This spans the whole routing spectrum so the resolved parameter is validated end-to-end: shapes that **flip** (the win), shapes **already 3D** (must exercise the segment bound → stay flat), and shapes that **stay 2D** (controls). ## Coverage (verified with the real dispatcher, MI300X = 304 CU, sms 120→304) - **24 flip 2D→3D** — the transition #9739 moves, - **30 already-3D** — exercise the split-KV segment bound (must not over-split → flat), - **16 stay 2D** — controls (must be unaffected). Dual-device sanity check (MI300X 304 + MI300A 228) confirmed the D128 flips win on **both** parts (10–21× decode, 1.1–2.4× q>1), with routing correctly shifting with the device's CU count — i.e. the parameter selection is right per device. ## Caveats (from review) - **Benchmark input (vault P3):** `benchmark_decode_live` uses `block_table = randint` (with replacement) → some fake L2 reuse. The 2D-vs-3D **speedup ratios** are unaffected (same inputs both sides), but absolute HBM BW read from these shapes is understated. Use distinct/disjoint pages for a true working-set BW measurement. - **D64 shapes need #9391 on develop:** the `qwen3_30b_*` (D64) mid-batch decode shapes hit the unguarded 2D fp8-loader assert (`attention_tiled_2d.py:3150`, `fp8_total_chunks % THREADS`) that #9391 guards behind `KV_FP8`. On develop (without #9391) those flip shapes crash in the 2D baseline; the D128 shapes are develop-clean. ## Notes - **gfx942 only**, matching #9739's scope (gfx950/gfx1250 → AICK-1726). Data-only change (a benchmark shapes list); no kernel/dispatch code. Changes are behind the feature flag HIPKERNELPROVIDER_ENABLE_ROCKE which is defaulted to OFF.
…-batch decode (#9739) ## What was happening before (plain language) Attention dispatch has a knob called **`num_sms`** — it tells the dispatcher **how many compute units (CUs) the GPU has**. That number decides two things: 1. **Which kernel to run** — a serial **2D** kernel, or a **3D split‑KV** kernel that splits work across the whole GPU. 2. **How finely** the 3D kernel splits the KV cache. `num_sms` was **hard‑coded to a stale `120`**, but an **MI300X (gfx942) has 304 CUs**. So the dispatcher thought the GPU was less than half its real size, and **medium/large‑batch decode that should have been split across the device stayed stuck in the slow 2D kernel**, leaving most of the GPU idle. Long‑context decode also under‑split. ## What we changed The `num_sms` device resolver, a direct `target_ctas` knob, and a segment-count bound: 1. **Resolve `num_sms` from the real device — only when verified on-box.** For a gfx942 request built on a gfx942 box, read the live CU count (304 MI300X / 228 MI300A) → the reroute win. Everywhere else — non-gfx942 arches, or a gfx942 kernel built off-box / with no visible gfx942 device — keep the legacy **120** (matches develop), so a cross-compile never bakes the wrong device's count into the kernel. *(Per review: the earlier hardcoded `304` off-box fallback was removed — 304 now comes only from the live query — and the `ROCKE_NUM_SMS` env pin was removed; pin reproducibility via an explicit `num_sms` or the `target_ctas` knob.)* 2. **Add a direct `target_ctas` spec knob.** A single device-subscription target (concurrent workgroups) that drives 2D↔3D routing and 3D segmentation. Default `0` → `num_sms * 4` (byte-identical to before); set `>0` to pin the target directly, so tuners / benchmarks / hipDNN can select the fast path **without** a device CU count. Consolidated behind one accessor (`_effective_target_ctas`) so routing and segmentation share one source of truth. 3. **Bound the split‑KV segment count.** Raising `num_sms` alone made the 3D kernel **over‑split** already‑3D shapes; we clamp the segment count to the **pre‑bump baseline** where it over‑splits (gfx942 D128 decode ≤16384, and all q>1), leaving the cases that genuinely benefit (D128 kv≥32768, D64, D256) uncapped. ## How it acts as an optimization - The true CU count lets under‑filled decode grids **reroute 2D → 3D**, filling all 304 CUs → large speedups. - The clamp keeps that win **without** an over‑split penalty on already‑3D shapes. - **Correctness is untouched** — `num_sms` changes only launch geometry, never math. Every result below was fp32‑gated. --- ## Benchmarks — **GPU: AMD Instinct MI300X (gfx942, 304 CUs)**, bf16/fp16, before/after = `num_sms` 120 → 304, fp32‑gated > **Measurement provenance (please read):** all on‑device numbers were measured on the **#9391 (`users/arai/attention_sink_decode`) branch base**, where this work was developed; this PR is now rebased onto **develop**. Practical implications for a reviewer on develop: > - The **Llama / Qwen** shapes are reproducible on develop (from `decode_shapes.json`, `qwen3_30b_a3b_shapes.json`, and the batch sweep in child PR #9742). The 2D "before" baseline on develop lacks #9391's gfx942 2D `num_warps` tune, so the reroute ratios should be **equal‑or‑larger** there. > - The **gpt‑oss sink** rows depend on #9391's sink feature + shapes file, which are **NOT on develop** — they are marked below and are reproducible only on the #9391 base. ### Headline: mid‑batch decode flips — Llama / Qwen (develop‑reproducible; child PR #9742 shapes) These were stuck in the slow 2D kernel before; the resolver reroutes them to 3D: | shape | config | speedup | |---|---|---| | llama8b_dec_b80_kv8192 | 32/8 D128 | **14.56×** | | qwen235b_dec_b100_kv8192 | 64/4 D128 | **13.49×** | | llama8b_dec_b80_kv2048 | 32/8 D128 | **13.49×** | | llama405b_dec_b50_kv8192 | 128/8 D128 | **13.47×** | | llama405b_dec_b50_kv2048 | 128/8 D128 | **11.80×** | | qwen235b_dec_b100_kv2048 | 64/4 D128 | **11.74×** | | llama70b_dec_b67_kv8192 | 64/8 D128 | **11.20×** | | llama70b_dec_b67_kv2048 | 64/8 D128 | **10.25×** | | qwen30b_dec_b133_kv8192 | 32/4 D64 | **2.65×** | | qwen30b_dec_b133_kv2048 | 32/4 D64 | **2.59×** | **Mechanism (profiled on MI300X, `llama8b_dec_b80_kv8192`, distinct block_table):** | num_sms | path | grid | occ/CU | VALUBusy | HBM/launch | achieved BW | GPU time | |---|---|---|---|---|---|---|---| | 120 | 2D | 512 | 0.70 | 0.8% | 2573 MiB | 0.16 TB/s (**3%** of 5.3 peak) | 17357 µs | | 304 | 3D (seg16) | 6400 | 2.73 | 13.2% | 2612 MiB | 2.31 TB/s (**44%** of peak) | 1187 µs | The 14.6x is **pure device-fill, not fewer bytes** (~2.6 GiB moved either way): the 2D grid of 512 leaves the 304 CUs almost idle (occ 0.70, HBM 3% utilized); the reroute to 3D fills the device (grid 6400, occ 2.73, HBM 44%). `seg16` confirms the segment clamp is active (not the naive 128). rocprofv3, one representative mid-batch D128 shape — mechanism measured, not inferred. ### Prefill / spec‑decode flips (q>1, causal‑fp32‑gated; Llama/Qwen, develop‑reproducible) | shape | q | config | speedup | |---|---|---|---| | qwen30b_q4_b67 | 4 | 32/4 D64 | **2.06×** | | llama405b_q2_b33 | 2 | 128/8 D128 | **1.95×** | | qwen235b_q2_b67 | 2 | 64/4 D128 | **1.93×** | | llama70b_q4_b33 | 4 | 64/8 D128 | **1.92×** | | llama8b_q2_b67 | 2 | 32/8 D128 | **1.82×** | | qwen235b_q4_b40 | 4 | 64/4 D128 | **1.70×** | | llama405b_q4_b20 | 4 | 128/8 D128 | **1.68×** | | qwen30b_q2_b100 | 2 | 32/4 D64 | **1.61×** | | llama8b_q4_b50 | 4 | 32/8 D128 | **1.44×** | | llama70b_q2_b50 | 2 | 64/8 D128 | **1.39×** | ### Existing corpus — 38 affected decode shapes (batch 1–2) **Long‑kv / D64 re‑splits (Llama/Qwen, develop‑reproducible):** | shape | speedup | |---|---| | decode_kv32768 (Qwen‑30B D64) | **1.26×** | | decode_kv16384 (Qwen‑30B D64) | **1.21×** | | llama405b_b1_kv32768 (D128) | **1.18×** | | llama405b_fp16_b1_kv32768 | **1.06×** | | decode_kv4096 / kv8192 (D64) | 1.01× / 1.00× | | llama8b/70b/qwen235b_b1_kv32768 (bf16+fp16, ×6) | 0.97–0.99× (noise) | **Neutralized by the clamp (kernel byte‑identical → flat, no regression) — 24 shapes:** Llama‑8B/70B/405B & Qwen‑235B, D128, kv4096/8192/16384 (bf16+fp16), all 0.97–1.08× (noise on identical kernels). **gpt‑oss sink flips — REQUIRES #9391 (not on develop), measured on the #9391 base:** | shape | speedup | |---|---| | full_sink_b64_kv2048 | 1.94× | | full_sink_b64_kv8192 | 1.93× | --- ## Regressions - **None after the fix** across **38 corpus + 28 mid‑batch/prefill = 66 shapes on MI300X** — zero real regressions, zero correctness failures. - The *naive* version (bump `num_sms` **without** the clamp) regressed **21 of 38** shapes, worst **0.50× (2× slower)** — exactly the over‑split the clamp fixes. - The `0.97–0.99×` readings are **measurement noise (±~3%)**: they sit on **byte‑identical (clamped) kernels**, bf16/fp16 twins disagree run‑to‑run, and a 3× re‑run of the suspect shape read 1.010 / 1.010 / 1.013×. ## Tests Adds `tests/dispatch/attention/test_num_sms_resolver.py` — **9 CPU tests**: resolution order (explicit / gfx942 device / MI300A=228 / other-arch=120), routing scales with `num_sms`, segments stay bounded, and `target_ctas` precedence (overrides `num_sms*4`, flips 2D→3D at fixed `num_sms`, threads through `_problem`). Re-verified on **MI300X**: byte-identity GREEN (66 families), installed-golden PASS, and a same-node A/B confirming the `target_ctas` path is perf-neutral vs pre-change (0 routing changes, 0.11% median timing diff, mid-batch win intact); the knob validated on silicon (2D→3D at fixed `num_sms`, output matches the 2D default, 14.1×). ## Scope / notes - Validated on **MI300X** via the Python dispatch path. Auto-resolution is **gfx942-only** (other archs keep 120 — future scope). - **No environment variables**: the `ROCKE_NUM_SMS` pin was removed per review; pin reproducibility via an explicit `num_sms` or the `target_ctas` knob. - The production **C++ C-ABI engine** hardcodes `num_sms=120` and has no `target_ctas` field today (routing decided in Python); mirroring the device resolver + `target_ctas` there is tracked as a companion follow-up (**AICK-1856**). - The split-KV clamp ceiling (`_PRE_BUMP_SMS=120`) is kept hardcoded here; parametrizing it as a spec knob (reviewer request) is tracked as **AICK-1857**. - Rebased onto **develop**; the mid/large‑batch coverage is extended by child PR **#9742**. Changes are behind the feature flag HIPKERNELPROVIDER_ENABLE_ROCKE which is defaulted to OFF.
Motivation
gpt-oss attention-sink decode (seqlen_q=1) on gfx942 had two problems. First, it was broken: bf16 sliding-window (SWA-128) sink decode crashed when a windowed config fell into the fp8-loader code path and failed an assert (fp8 total chunks must be divisible by THREADS), even though gfx942 has no fp8 K/V support. Second, the D64 decode path ran at a suboptimal num_warps, leaving significant decode performance on the table. AOTriton has no attention-sink kernel, so there is no competing flash path for gpt-oss sink attention on gfx942, the goal is rocKE-only correctness plus a competitive roofline. This PR fixes the SWA-sink decode crash and tunes decode performance.
Technical Details
Performance
Both arches: bf16, D64, GQA 64/8, decode Sq=1. Median of 3 runs, DSL kernel, best across the num_sms sweep. Measured via the Python-lowerer path (C++ engine not built on the bench box), so absolute latencies are a floor.
gfx942 (MI300X)
Same-run nw4 to nw1, 500 iterations, run-to-run variance under 0.1%.
full_sink decode is the primary win (about 1.9x to 3.3x). SWA decode already sits at a ~46 us floor and is unchanged except at B128. full_sink B1 routes to the 3D path and is unchanged. The nw4 to nw1 ratio is same-session and device-consistent.
gfx950 (MI350X)
No kernel or selector change was needed: the gfx950 selector already picks num_warps=1 for D64 decode, the same config the gfx942 change introduces, so gfx950 is already at the tuned optimum. Latency, 500 iterations, run-to-run variance under 2%.
SWA decode is flat (~30 us) across batch and KV length, confirming window-skipping. AOTriton has no sink kernel, so there is no comparison column.
Test Plan
Test Result
Related
JIRA ID : AICK-1498
Submission Checklist
Changes are behind the feature flag HIPKERNELPROVIDER_ENABLE_ROCKE which is defaulted to OFF.