Skip to content

[Triton/Gluon] [gfx950] pa_decode_sparse: pick BLOCK_K by occupancy, not a constant (up to +43 %) - #5294

Open
stefanskiasan wants to merge 1 commit into
ROCm:mainfrom
stefanskiasan:pa-decode-sparse-block-k-occupancy
Open

[Triton/Gluon] [gfx950] pa_decode_sparse: pick BLOCK_K by occupancy, not a constant (up to +43 %)#5294
stefanskiasan wants to merge 1 commit into
ROCm:mainfrom
stefanskiasan:pa-decode-sparse-block-k-occupancy

Conversation

@stefanskiasan

Copy link
Copy Markdown

Fixes the fixed BLOCK_K = 64 in the gfx950 sparse-decode driver, as described in #5293.

What changes: BLOCK_K is chosen after num_splits is known — 32 when the launch produces more CTAs than there are CUs, 64 otherwise. One comparison, no extra work.

Why the grid size and not the query count: the two are not proportional, because num_splits varies. 34 queries can launch more CTAs than 64 do, which is why the fastest tile width is not monotonic in num_queries. Sorted by CTA count it is monotonic, and the crossover sits exactly at get_num_sms().

Why it helps: this kernel is neither bandwidth- nor compute-bound — on MI355X it reaches ~10 % of peak HBM bandwidth (even with fully contiguous indices, so it is not the gather pattern) and ~7 % of peak flops. It is latency-bound, so occupancy is what matters. The narrow tile uses 32 KB LDS instead of 64 KB and lets several CTAs share a CU.

Measurements (MI355X/gfx950, 64 heads, head_dim 512 = 448 NoPE + 64 RoPE, packed fp8 cache, top-k 2048, minimum of 5 rounds × 15 iterations):

queries fixed K=64 this patch delta vs per-point optimum
16 63.2 µs 62.6 µs +0.9 % −0.9 %
24 78.3 72.9 +7.4 % +0.4 %
32 73.4 73.4 +0.1 % −0.1 %
48 121.3 109.7 +10.6 % +0.0 %
64 131.0 131.1 −0.1 % +0.1 %
96 218.0 193.8 +12.5 % +0.1 %
128 264.6 185.4 +42.7 % +0.2 %
192 402.8 355.2 +13.4 % +0.0 %
256 550.2 392.4 +40.2 % −2.6 %

Never worse than −0.6 % anywhere on the curve. Numerics are unchanged — max abs deviation against a torch reference is 0.000061 for both tile widths.

The rule also transfers unchanged to a 4-bit packed cache variant we tested locally (128 queries: 289.9 → 220.2 µs), so it is not specific to the fp8 layout.

Things I measured that did not help, in case it saves someone the search: GSPT 8 or 32 instead of 16 (−14 % / −45 %), waves_per_eu 1/2/4 (−4 % to −69 %), cache modifier .cs instead of .cg (does not compile), and forcing kv_splits (the automatic choice is already optimal at 64+ queries; more splits cost up to 45 %).

Happy to re-measure on other shapes — different head counts, top-k values, or a bf16 cache — if that would help review.

BLOCK_K was fixed at 64. That is the right choice only while every CTA
still gets a CU of its own -- the wider tile (4 warps, 64 KB LDS) does
more work per CU. Once the grid needs more than one wave, the narrower
tile (2 warps, 32 KB LDS) packs several CTAs per CU and hides the gather
latency, which dominates this kernel: it reaches only ~10 % of peak HBM
bandwidth on MI355X even with fully contiguous indices, and ~7 % of peak
flops, so neither memory nor math is the limit.

Deciding on the grid size rather than the query count matters because the
two are not proportional: num_splits varies, so 34 queries can launch more
CTAs than 64 do. Sorted by CTA count the picture is monotonic and the
crossover sits exactly at the CU count.

Measured on MI355X (gfx950), 64 heads, head_dim 512 (448 NoPE + 64 RoPE),
packed fp8 cache, top-k 2048, min of 5 rounds x 15 iters:

  queries   fixed K=64   this patch   delta   vs per-point optimum
       16       63.2 us      62.6 us   +0.9%                 -0.9%
       24       78.3         72.9      +7.4%                 +0.4%
       32       73.4         73.4      +0.1%                 -0.1%
       48      121.3        109.7     +10.6%                 +0.0%
       64      131.0        131.1      -0.1%                 +0.1%
       96      218.0        193.8     +12.5%                 +0.1%
      128      264.6        185.4     +42.7%                 +0.2%
      192      402.8        355.2     +13.4%                 +0.0%
      256      550.2        392.4     +40.2%                 -2.6%

Never worse than -0.6 % anywhere on the curve, and within +-0.9 % of the
per-point optimum throughout. Numerics are unchanged: max abs deviation
against a torch reference is 0.000061 for both tile widths.

The choice is made after num_splits is known, so it costs one comparison
and no extra work. Also applies unchanged to a 4-bit packed cache variant
we tested (128 queries: 289.9 -> 220.2 us).

Refs: ROCm#5293
@stefanskiasan
stefanskiasan requested review from a team and a lite review from Copilot September 5, 2026 15:25
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

  • ✅ Pre-checks (submodule verification, code formatting)
  • ✅ Aiter op tests (gfx942 + gfx950)
  • ✅ Triton tests on MI35X (only when aiter/ops/triton/** or related paths are changed)

Extended tests (opt-in via labels):

Label Tests
ci:gfx1250-ffm-triton Run the five-shard gfx1250 FFM Triton test suite
ci:triton-300x Run an additional Triton test job on MI300X in PRs; main branch always runs both MI35X and MI300X
multigpu Aiter multi-GPU tests on the 8-GPU runner
ci:sglang SGLang integration tests: DeepSeek-R1-MXFP4 accuracy, Qwen 3.5 accuracy
ci:atom ATOM benchmark: DeepSeek-R1-0528, GPT-OSS-120B
ci:atom_full ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm vLLM benchmark: GPT-OSS-120B, DeepSeek-R1-0528, Kimi-K2.5
ci:all All standard extended tests (excludes ci:atom_full)

Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 5294 --add-label <label>

PR title tags & labels:
Component tags ([Triton/Gluon], [HIP], [CK], [ASM], ...) are added to the PR title and as PR labels automatically from the changed files and re-synced on every push — change-type tags like [fix]/[Perf], op tags like [MLA], and human labels (ci:*) are left untouched. Add the no-auto-title label to opt this PR out.

@github-actions github-actions Bot changed the title [gfx950] pa_decode_sparse: pick BLOCK_K by occupancy, not a constant (up to +43 %) [Triton/Gluon] [gfx950] pa_decode_sparse: pick BLOCK_K by occupancy, not a constant (up to +43 %) Sep 5, 2026
@stefanskiasan

Copy link
Copy Markdown
Author

Some context I found after opening this, which may help place the change: this is the measure #2416 recommended in March, applied to the sparse decode kernel.

#2416 documents decode attention on gfx950 reaching 0.3–6 % of HBM bandwidth across Llama3-405B, Gemma2-9B, DeepSeek-R1 and Kimi-K2.5, and lists as root cause "No gfx950-specific decode attention tuning" with the recommendation "Consider decode-specific tile sizes". That recommendation was never carried out for pa_decode_sparseBLOCK_K has been a constant since.

Two notes on how our numbers relate to that issue:

  • SDPA decode on gfx950: <6% HBM utilization, needs PA decode kernel + tuning #2416 measures against 5300 GB/s as the MI355X peak, which is the MI300X figure; MI355X carries 8 TB/s HBM3E. Our 840 GB/s is therefore ~16 % of the reference used there, or ~10.5 % of the actual peak — the same order of magnitude, at the upper end of what that issue reports.
  • We reach that ceiling even with fully contiguous indices (measured: random 754 GB/s, sorted 822, clustered 842, contiguous 834 GB/s at 128 queries). So it is not the gather pattern, and the compute side is idle too at ~7 % of peak flops. That leaves occupancy, which is what this patch addresses.

The LDS numbers back the mechanism: gfx950 has 160 KB addressable LDS per workgroup (aiter's own _LDS_CAP_BYTES has gfx950: 163840, and LLVM sets FeatureAddressableLocalMemorySize163840 for FeatureISAVersion9_5_Common). At BLOCK_K = 64 the KV tile alone is [64, 512] bf16 = 64 KB, so only two CTAs fit per CU; halving it to 32 KB lets five fit, which is what recovers the latency hiding.

This also lines up with AMD's own guidance in the ATOM decode blog post (2026-09-03), which names "a small-M GEMM or single-query attention operation may not produce enough workgroups to occupy all compute units" as a root cause and prescribes "smaller tile heights" for small-M paths — there applied to GEMM, here to sparse attention decode.

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.

🟡 Changes recommended

The new BLOCK_K switching logic can leave num_splits computed for the old tile width, making split selection inconsistent with the chosen tile and potentially impacting performance across shapes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates the gfx950 (CDNA4) Gluon driver for pa_decode_sparse to dynamically select BLOCK_K based on estimated occupancy, aiming to improve latency-bound performance at higher CTA counts.

Changes:

  • Adds an occupancy-based heuristic that narrows BLOCK_K from 64 → 32 when the launch grid exceeds the device CU count.
  • Documents the rationale and measured performance impact for the new heuristic.
File summaries
File Description
aiter/ops/triton/attention/pa_decode_sparse.py Adds occupancy-based BLOCK_K selection for the gfx950 sparse-decode Gluon driver.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +573 to +577
# Measured on MI355X: predicts the faster tile on 21/21 load points, worth
# up to +43 % at 128 queries (264.5 -> 186.9 us) with identical numerics.
if num_queries * num_splits * heads_blocks > get_num_sms():
BLOCK_K = 32
num_warps = BLOCK_K // 16
@zufayu
zufayu requested review from a team and azaidy September 7, 2026 01:16
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.

2 participants