ROCm / ROCm/aiter

[Perf] pa_decode_sparse: BLOCK_K=64 is suboptimal above the CU count — up to +43 % on gfx950 with a load-dependent choice

Open
#5,293 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
565
Forks
585
Avg merge
3d 4h
Merged PRs (30d)
366

Description

pa_decode_sparse hardcodes BLOCK_K = 64 (attention/pa_decode_sparse.py, gfx950 gluon driver). On MI355X this is only the right choice while the launch fits on the CUs — above that, BLOCK_K = 32 is faster by up to 43 %, with identical numerics.

Measurements

MI355X (gfx950), 64 heads, head_dim 512 (448 NoPE + 64 RoPE), packed fp8 cache, top-k 2048 per query, has_invalid=False. Times are the minimum of 5 rounds × 15 iterations.

queries K=64 K=32 better delta
1–16 62–64 µs 62–65 µs K=64 0.4–1.2 %
24 78.4 72.8 K=32 7.6 %
28 / 30 / 32 73–77 90–96 K=64 19.2 %
34 / 36 122–131 103–116 K=32 12.9–18.6 %
48 121.7 110.3 K=32 11.1 %
60 / 64 131–134 165 K=64 19.0–19.9 %
68 / 72 204–207 170–193 K=32 6.0–21.6 %
96 218.9 193.1 K=32 13.0 %
128 264.5 186.9 K=32 41.5 %
192 403.2 354.2 K=32 13.8 %
256 552.1 399.5 K=32 38.2 %

Max abs deviation against a torch reference is 0.000061 for both variants — this is purely a scheduling effect, not a numerics tradeoff.

Why it is not monotonic in num_queries

The switch tracks the number of CTAs, not the query count:

ctas = num_queries * num_splits * heads_blocks

num_splits comes from _decode_num_splits, so e.g. 32 queries can launch fewer CTAs than 34. Sorting the same data by ctas makes it monotonic, and the threshold lands exactly on the CU count (get_num_sms() = 256 here):

BLOCK_K = 32 if ctas > get_num_sms() else 64

This predicts 21 of 21 measured load points correctly. Intuition: while every CTA gets its own CU, the larger block (4 warps, 64 KB LDS) does more work per CU; once multiple waves are needed, the smaller block (2 warps, 32 KB) packs better and hides the gather latency.

Suggested change

In the gfx950 driver, replace the constant with the rule above. Wired in, it stays within ±0.6 % of the per-point optimum from 24 queries upward:

queries vs fixed K=64 vs per-point optimum
24 +7.7 % +0.5 %
48 +10.9 % +0.1 %
96 +12.8 % +0.2 %
128 +43.2 % +0.5 %
192 +13.4 % +0.1 %
256 +39.0 % −0.0 %

Note that num_splits must be computed before BLOCK_K (it is currently derived after), and get_num_sms() should be hoisted to module scope — at 62 µs kernel time a per-call lookup is measurable in microbenchmarks, though not in production where the decode runs inside a CUDA graph.

Things that did not help (same setup)

  • GSPT (bytes per thread in the gather layout): 16 is optimal; 8 costs 14 %, 32 costs 45 % at K=64.
  • waves_per_eu 1 / 2 / 4: −4 % / −4 % / −69 %.
  • Cache modifier .cs instead of .cg: does not compile.
  • Forcing kv_splits: the automatic choice is already optimal at 64+ queries; more splits cost up to 45 %.

Happy to open a PR if the rule looks right to you — and glad to re-measure on other shapes (different head counts, top-k, or a bf16 cache) if that would help.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in attention/pa_decode_sparse.py and inspect the gfx950 gluon driver, especially _decode_num_splits and the current BLOCK_K selection. Verify the CTA-based choice using get_num_sms() across the measured query sizes, then compare timings and numerical output against the reported torch reference. Done means the load-dependent selection reproduces the reported performance without changing numerics.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
performance
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.