Skip to content

perf(ck_tile/fmha): [gfx1250 opt 3] 16-row short-query tile for the gfx1250 qr_tdm forward - #13469

Closed
Yanxing-Shi wants to merge 10 commits into
developfrom
users/yanxishi/ck/gfx1250-fmha-fwd-short-query-tile
Closed

Yanxing-Shi wants to merge 10 commits into
developfrom
users/yanxishi/ck/gfx1250-fmha-fwd-short-query-tile

Conversation

@Yanxing-Shi

@Yanxing-Shi Yanxing-Shi commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds a 16-row short-query tile for the gfx1250 qr_tdm forward (d=128, fp16/bf16), dispatched first for max_seqlen_q <= 16.

At max_seqlen_q <= 16 the 64-row tile idles at least 3/4 of its M rows: its four warps sit on M, and three of them compute nothing but padding. The new tile has 16 rows and spends its four warps on N instead (1x4x1 for both gemms), so every warp works on the real query rows and owns a quarter of the K/V columns. It is only paired with qr_tdm (a codegen rule): qr's gemm_1 slices P along M and cannot form a slice narrower than one warp tile.

Warps on N route S through LDS, a path no shipped qr_tdm tile exercised, and it needs three fixes:

  • S register layout. MakeSRegTileDistribution built S's layout from the QK gemm's warp config, but S is consumed by gemm_1 as its A operand. The layouts differ, so P's columns reached gemm_1 permuted within each row: the row sum and LSE stay right while O is off by ~1%. It now returns the P distribution, which embeds the PV warp gemm's A encoding.
  • S LDS slot. On the double K/V LDS buffer path S aliased K0, which the K prefetch two iterations ahead overwrites. S now gets its own slot after V1 when it is in LDS at all (kSBytes > 0), and the pipeline takes its address from the arena layout.
  • Progressive K. The progressive K reload walks four N-subtiles per warp and needs the gemm_0 warps all on M, so it is enabled only for kNWarp == 1.

The LDS padding selection also accepts the 16-row, warps-on-N tile (it otherwise ran with unpadded K/V rows). Tiles with the warps on M generate the same code as before.

Resources of the new kernel: 152 VGPRs, no scratch, 76032 B of LDS (the d=128 padded arena plus 4352 B for S), i.e. 4 workgroups per CU like the 64-row tile. Cost: the d=128 fp16/bf16 qr_tdm instance count goes from 601 to 901. The new instances carry the kvlp_plk traits of the d=128 pipelines, but progressive K is off inside them.

Performance

gfx1250, bf16, d=128, lse=0. Baseline is develop at 46d0941 (2026-10-13); median of 3 interleaved runs, each after >= 1 s of warmup (steady-state clocks), on an idle host. "causal" is the bottom-right mask (-mask=b); bshd.

Large batch, small seqlen

batch nhead s_q s_k mask TFLOPS develop TFLOPS this PR speedup
1280 2 8 532 - 94.1 110.0 1.17x
1280 2 532 532 - 1070.1 1069.1 1.00x
1280 2 8 232 - 73.3 87.7 1.20x
1280 2 232 232 - 670.4 671.3 1.00x
1792 1 32 200 - 251.1 249.8 0.99x
1280 2 8 532 causal 94.6 109.1 1.15x
1280 2 532 532 causal 568.0 568.0 1.00x
1280 2 8 232 causal 72.2 85.7 1.19x
1280 2 232 232 causal 314.1 314.9 1.00x
1792 1 32 200 causal 227.8 228.1 1.00x

The s_q=8 shapes take the new tile. 32/200 (s_q > 16) keeps its tile; on this develop, d=128 problems with max_seqlen_q >= 128 take the qr_tdm_sched pipeline (#13063), so 532/532 and 232/232 do not run the qr_tdm kernels at all. Both are unchanged.

Other shapes

batch nhead s_q s_k mask TFLOPS develop TFLOPS this PR speedup
512 8 1 1024 - 15.3 18.0 1.17x
512 8 1 1024 causal 15.1 17.9 1.19x
256 16 4 2048 - 65.0 75.6 1.16x
256 16 4 2048 causal 64.2 75.2 1.17x
1280 2 16 532 - 183.3 209.9 1.15x
1280 2 16 532 causal 181.3 204.5 1.13x
64 32 8 4096 - 131.4 152.5 1.16x
64 32 8 4096 causal 130.3 152.5 1.17x
2048 4 12 300 - 135.5 165.8 1.22x
2048 4 12 300 causal 133.6 162.2 1.21x

Every shape with s_q <= 16 gains 1.13x-1.22x, with or without the causal mask.

Numerics

-v=1 passes 52/52 bf16/fp16 d=128 cases, all on qr_tdm: batch/group mode, padded M/N, causal top-left/bottom-right, sliding window, elementwise bias, alibi, GQA, LSE, the 427x3 uneven grid, bshd, and batch-mode q/kv_eff_lens; and 384/384 short-query cases (bf16/fp16, batch/group, s_q = 1, 8, 12, 16 x s_k = 64, 232, 517, with no mask, causal top-left/bottom-right, sliding window, LSE, alibi, elementwise bias and MQA), 222 of them on the new tile. test_qr_tdm_lds_padding now also checks the padding selection and the LDS arena of the 16-row tile; those checks fail on develop.

QrTdmShortQuery in test_fmha_fwd.cpp now runs 6 cases instead of one (s_q = 1, 8, 16; seqlen_k on and off a tile multiple; no mask, both causal masks and a sliding window; batch and group; LSE on) and checks the 16-row tile in batch mode.

The PR also adds test_ck_tile_fmha_reference to the test_ck_tile_fmha build target. CI builds that target and runs every test_ck_tile_fmha* test when a PR touches example/ck_tile/01_fmha, and reported the reference test as "Not Run" on the non-gfx125 targets.

Base

Based on develop at 46d0941 ("fix(rocke): normalize gfx11 processors to generic catalog (#13531)"), and measured against that commit.

JIRA ID : AICK-2262

@Yanxing-Shi
Yanxing-Shi requested review from a team as code owners October 11, 2026 09:09
@github-actions github-actions Bot added project: composablekernel ck: attention-moe Used to tag composablekernel PRs that require approval from Attention/MOE review team. labels Oct 11, 2026
@therock-pr-bot

therock-pr-bot Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR 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.

🙋 Wish to Override Policy?

@therock-pr-bot

Copy link
Copy Markdown

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

At max_seqlen_q <= 16 the 64-row tile idles at least 3/4 of its M rows.
Add a d=128 fp16/bf16 tile of 16 rows that spends its four warps on N
(1x4x1 for both gemms), dispatched first for max_seqlen_q <= 16 and only
paired with qr_tdm: qr's gemm_1 slices P along M and cannot form a slice
narrower than one warp tile.

Warps on N route S through LDS, a path no shipped qr_tdm tile exercised,
and it needs three fixes:
- MakeSRegTileDistribution built S's layout from the QK gemm's warp
  config, but S is consumed by gemm_1 as its A operand. The layouts
  differ, so P's columns reached gemm_1 permuted within each row: the row
  sum and LSE stay right while O is off by ~1%. Use the P distribution,
  which embeds the PV warp gemm's A encoding.
- On the double K/V LDS buffer path S aliased K0, which the K prefetch
  two iterations ahead overwrites. Give S its own slot after V1 when it is
  in LDS at all (kSBytes > 0); tiles with the warps on M are unchanged.
- The progressive K reload walks four N-subtiles per warp and needs the
  gemm_0 warps all on M, so it is only enabled for kNWarp == 1.

The LDS padding selection also accepts the 16-row, warps-on-N tile, which
otherwise ran with unpadded K/V rows. test_qr_tdm_lds_padding checks the
padding selection and the arena (S at 71680, 76032 bytes) for that tile.

gfx1250, bf16, d=128, bshd, b=1280 h=2, TFLOPS develop -> this:
  s_q=8 s_k=532   96.6 -> 111.5 (1.15x), causal 95.9 -> 110.3 (1.15x)
  s_q=8 s_k=232   73.7 ->  87.0 (1.18x), causal 73.3 ->  85.3 (1.16x)
Shapes with s_q > 16 keep their tiles and are unchanged.
With the gemm_0 warps on N, S has its own slot in the double K/V buffer
arena, but the policy's GetSmemSize still returned the K/V buffers only.
The kernel sizes its LDS from the arena layout, so nothing was under-
allocated; keep the policy's estimate consistent with it anyway.
Run s_q=8 against s_k=509 with LSE and assert the b16x64 qr_tdm instance is
selected. A mis-distributed S on its way through LDS permutes P within a row,
which keeps the row sums (and LSE) right while O is wrong, so this case needs
the O comparison rather than LSE alone.
…tQueryProblem

-Wshadow (an error in the CK build) flags the function-local alias in the
LDS arena checks, which reused the name of the file-scope short-query
problem.
…SmemSize

The hand-summed region sizes dropped the 256-byte region alignment (and the
double-buffer S slot is placed after V1), so they disagreed with the arena the
pipeline actually allocates. The arena layout owns the offsets; use it.
…e Gfx125 rules

Its codegen tests address it as get_rules()[-1].
… tile

max_seqlen_q <= 16 takes the 16-row tile, not the bm0=64 one.
@Yanxing-Shi
Yanxing-Shi force-pushed the users/yanxishi/ck/gfx1250-fmha-fwd-short-query-tile branch from 636f499 to 71530c7 Compare October 13, 2026 08:25
@Yanxing-Shi Yanxing-Shi changed the title perf(ck_tile/fmha): [gfx1250 opt 5] 16-row short-query tile for the gfx1250 qr_tdm forward perf(ck_tile/fmha): [gfx1250 opt 3] 16-row short-query tile for the gfx1250 qr_tdm forward Oct 13, 2026
…ths and masks

QrTdmShortQuery ran one batch-mode s_q=8 dense case with LSE off, while its
comment asked for LSE on. It now runs six representative cases with LSE on:
s_q = 1, 8 and 16, seqlen_k on and off a tile multiple, no mask, both causal
masks and a sliding window, batch and group mode, and checks the tile in batch
mode. MHA keeps pack-GQA from folding the heads into seqlen_q.

Also: ruff-format the rule list, put the progressive-K comment back on
kStagedKPairs, and word the S distribution comment for the code as it is.
…om the problem's

The pipeline's kProgressiveDsLoadK (the problem's flag, off for tiles with the
warps on N) shadowed Problem::kProgressiveDsLoadK, which the static_assert
above and the policy still read; call it kUseProgressiveDsLoadK. No functional
change.
…le_fmha target

When a PR touches example/ck_tile/01_fmha, smart_build_ci.sh builds the
test_ck_tile_fmha umbrella and runs every ctest matching test_ck_tile_fmha.
That includes test_ck_tile_fmha_reference, which the umbrella did not build, so
CI reported it as "Not Run" on gfx1201 and the other non-gfx125 targets.
@poyenc

poyenc commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Location: MakePForGemm1()

This repack unconditionally transposes p_compute, correct only when warps sit on M. Today's one warps-on-N tile happens to have MIterPerWarp==1, making it a no-op by coincidence. No current tile breaks this, but a future warps-on-N tile with MIterPerWarp>1 would get silently reordered. Add a static_assert mirroring the one already on the quantized branch above (lines 476-478): kNWarp == 1 || kPMI == 1.

) -> bool:
# The 16-row tile spends its warps on N, which only qr_tdm handles:
# qr's gemm_1 slices P along M and cannot form a slice narrower than
# one warp tile, so pairing the tile with qr fails to compile.

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.

This check keys on the tile's F_bm0 dimension rather than the geometry invariant it's actually protecting. The comment above explains the real constraint: qr's gemm_1 slices P along M and can't form a slice narrower than one warp tile, which only matters when gemm_1's warps are split along N instead of M. That's exactly what F_rn1 encodes (the tile dataclass defines it as "number of warps for gemm1 along head dim v"). Today's single F_bm0==16 tile happens to be the only N-split tile, so the check works, but it only works by coincidence: a future N-split tile at a different F_bm0 would skip this guard and still fail to compile with qr, while an unrelated M-split tile that happens to use F_bm0==16 would be needlessly barred from qr.

Check the actual invariant instead:

if kernel_ctx.tile.F_rn1 <= 1:
    return True
return kernel_ctx.pipeline.tag == "qr_tdm"

return True
return kernel_ctx.pipeline.tag == "qr_tdm"

# The sched rule stays last (its tests address it as get_rules()[-1]).

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.

This comment documents that check_qr_tdm_sched_codegen_compatibility must stay last in the list because two tests reach it positionally through get_rules()[-1]. Since get_rules()'s output is only ever consumed as a conjunction over all rules, list order has no effect on which kernels get generated — the ordering constraint exists purely to satisfy those two tests' indexing, and this comment is the production code accommodating that rather than the tests being fixed. The next engineer adding a compatibility rule has to remember to append before this one, with only a comment enforcing it; a reorder or an unrelated append placed after it will silently break both tests again, the same way this PR's own first commit did when it introduced check_short_query_tile.

Have those two tests select the rule by name or function identity instead of position, e.g. next(r for r in get_rules() if r.__name__ == "check_qr_tdm_sched_codegen_compatibility"), then this comment and the ordering constraint on rules.extend(...) here can both be removed.

@poyenc

poyenc commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Location: QrTdmLdsArenaLayout

This comment documents LDS offsets only for the pre-existing warps-on-M d128 layout (double buffer ending at 71680, single buffer ending at 22784). It doesn't mention the new warps-on-N short-query tile this PR adds, where the double-buffer layout now places S after V1 and ends at 76032 instead of 71680, per the arena-offset static_asserts in test_qr_tdm_lds_padding.cpp. The numbers aren't wrong, just incomplete — a future reader could take 71680 as the maximum arena size for this configuration and be surprised by the larger one.

Add the warps-on-N offsets to this comment, or replace the concrete numbers with a pointer to the compile-time layout tests that now cover both cases.

@Yanxing-Shi

Copy link
Copy Markdown
Contributor Author

Closing: the 16-row tile now lives only on the new qr_tdm_pingpong pipeline in #13484, and qr_tdm stays as on develop. The review points carried over: the MakePForGemm1 static_assert is in the qr_tdm_pingpong body, the tile/pipeline pairing is a tile owner field instead of a bm0 check, and the arena layouts are pinned by test_qr_tdm_lds_padding.cpp.

asleepzzz pushed a commit that referenced this pull request Oct 30, 2026
…_pingpong (#13484)

## What

A 16-row short-query tile for `qr_tdm_pingpong` (#13471), taken when
`max_seqlen_q <= 16` on the grids `qr_tdm_pingpong` takes (`max_seqlen_q
< 640 && batch * nhead_q >= 2 * num_cus`; d=128 fp16/bf16, gfx1250). The
64-row tile idles 3/4 of its M rows on such queries; this one spends its
four warps on N instead (gemm_0 and gemm_1 warps 1x4x1). This folds in
#13469, which put the same tile on `qr_tdm`; `qr_tdm` stays as on
`develop`.

With the warps on N:

- each warp holds a column slice of S, so S goes through an LDS region
between the K buffers and V (57600 B arena) and comes back in P's
distribution, which gemm_1's A operand takes. An encoding built from the
QK gemm's warp layout would permute P's columns within a row: the row
sums (and LSE) stay right while each key meets the wrong value row;
- the row sum of P cannot run on the matrix unit (one warp tile of
columns no longer spans a row), so `l` is the fp32 sum, which the LSE
shares;
- `MakePForGemm1`'s in-thread (MIter, KIter) reorder is only valid with
the warps on M or one MIter per warp, now a `static_assert` at the call.

The tile is owned by `qr_tdm_pingpong` (`F_pipeline_tag`) and listed
before the 64-row tile, with `max_seqlen_q <= 16` added to the shared
grid condition; on smaller grids short queries stay on `qr_tdm` as on
`develop`. No other rule changes.

## Performance

gfx1250, bf16, d=128, `lse=0`, bshd; median of 3 interleaved runs, each
after >= 1 s of warmup, on an idle host. "causal" is the bottom-right
mask (`-mask=b`). Baseline is this PR's parent, #13471 on `develop`
9951b48.

| batch | nhead | s_q | s_k | mask | TFLOPS develop | TFLOPS base |
TFLOPS this PR | vs base | vs develop |

|------:|------:|----:|-----:|--------|---------------:|------------:|---------------:|--------:|-----------:|
| 1280 | 2 | 8 | 532 | - | 95.0 | 127.6 | 136.8 | **1.07x** | **1.44x**
|
| 1280 | 2 | 8 | 232 | - | 73.0 | 112.5 | 120.2 | **1.07x** | **1.65x**
|
| 1280 | 2 | 16 | 532 | - | 186.3 | 250.9 | 269.0 | **1.07x** |
**1.44x** |
| 2048 | 4 | 12 | 300 | - | 136.0 | 186.8 | 199.9 | **1.07x** |
**1.47x** |
| 512 | 8 | 1 | 1024 | - | 15.4 | 17.5 | 17.9 | 1.02x | **1.16x** |
| 256 | 16 | 4 | 2048 | - | 65.2 | 71.0 | 73.3 | 1.03x | **1.12x** |
| 64 | 32 | 8 | 4096 | - | 131.7 | 133.7 | 155.4 | **1.16x** | **1.18x**
|
| 1280 | 2 | 8 | 532 | causal | 94.5 | 123.4 | 135.5 | **1.10x** |
**1.43x** |
| 1280 | 2 | 8 | 232 | causal | 72.3 | 108.5 | 118.0 | **1.09x** |
**1.63x** |
| 1280 | 2 | 16 | 532 | causal | 182.3 | 240.7 | 263.5 | **1.09x** |
**1.45x** |
| 2048 | 4 | 12 | 300 | causal | 133.4 | 175.5 | 195.2 | **1.11x** |
**1.46x** |
| 512 | 8 | 1 | 1024 | causal | 15.4 | 16.6 | 17.9 | **1.08x** |
**1.16x** |
| 256 | 16 | 4 | 2048 | causal | 64.6 | 67.7 | 73.3 | **1.08x** |
**1.13x** |
| 64 | 32 | 8 | 4096 | causal | 130.6 | 138.8 | 155.3 | **1.12x** |
**1.19x** |

With `lse=1` (instances built without expert scheduling, as in #13471)
the 16-row tile measures 1.15x/1.19x over the base on 1280 x 2 x 8 rows
(s_k 232/532) and 1.42x/1.42x with the causal mask.

Only queries of at most 16 rows take the tile; every other shape runs
the same kernels as the base (the other five-workload rows: 532/532,
232/232 and 32/200 are within 1%).

## Tests

- `test_fmha_fwd.cpp`: `QrTdmShortQuery`, 6 cases (s_q 1/8/16, seqlen_k
a tile multiple and a padded tail, causal and windowed masks, group
mode), each sized to reach the grid threshold on the device it runs on
and checking that the 16-row kernel ran. They check O, which the column
permutation above would break while leaving the LSE right.
- `test_qr_tdm_lds_padding.cpp`: the 16-row tile's arena (S region at
34800, Q/V at 39168, 57600 B) and that it takes the fp32 row sum.
- `test_fmha_fwd_codegen.py`: on a big grid `max_seqlen_q <= 16`
dispatches the 16-row tile and 17 rows the 64-row one (including the
1792 x 1 x 32-row workload); on a small grid 8 rows stay on `qr_tdm`.

## Base

Built on #13471 (on `develop` at 9951b48); it will be rebased onto
`develop` once #13471 lands.

## Series

The gfx1250 FMHA forward optimization series; every PR is based on
`develop` and benchmarked against it (bf16, d=128). #12426 and #13468
were folded into #13471, #13469 into this PR.

| # | PR | Change | Status |

|-------|--------|----------------------------------------------------------------|--------|
| opt 1 | #12474 | Batch-mode early exit and XCD-aware block remap |
[![](https://img.shields.io/github/pulls/detail/state/ROCm/rocm-libraries/12474)](https://github.com/ROCm/rocm-libraries/pull/12474)
|
| opt 2 | #13466 | NT_HT cache hint on the K/V TDM loads |
[![](https://img.shields.io/github/pulls/detail/state/ROCm/rocm-libraries/13466)](https://github.com/ROCm/rocm-libraries/pull/13466)
|
| opt 3 | #13471 | `qr_tdm_pingpong` pipeline, coalesced O store and its
dispatch |
[![](https://img.shields.io/github/pulls/detail/state/ROCm/rocm-libraries/13471)](https://github.com/ROCm/rocm-libraries/pull/13471)
|
| opt 4 | #13484 | 16-row short-query tile on `qr_tdm_pingpong` (this
PR) |
[![](https://img.shields.io/github/pulls/detail/state/ROCm/rocm-libraries/13484)](https://github.com/ROCm/rocm-libraries/pull/13484)
|

JIRA ID : AICK-2262
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ck: attention-moe Used to tag composablekernel PRs that require approval from Attention/MOE review team. organization: ROCm project: composablekernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants