Repository navigation
[Triton/Gluon] [gfx1250] paged MQA logits: 8-token pages; group32 FP8 GEMM; grouped MoE tile_k fix - #6626
[Triton/Gluon] [gfx1250] paged MQA logits: 8-token pages; group32 FP8 GEMM; grouped MoE tile_k fix#6626yhl-amd wants to merge 3 commits into
Conversation
DeepSeek-V4.1's FP8 index pages hold 8 rows (candidate_block_size=8, model semantics). On gfx1250 the preshuffle path of deepgemm_fp8_paged_mqa_logits TDM-loads one page per iteration and feeds it as the WMMA B tile, which is 16 tokens wide, so pages below 16 were rejected by an assert. A tile is now assembled from 16 // KVBlockSize pages: each page is its own TDM load into adjacent shared-memory slots (`index()` targets; a `slice()` target loses its offset under TDM), and the tile is read back through a per-buffer `reinterpret` view in the page's (8, 16) shuffle layout. Waits scale with the 2 * pages loads a tile issues; page ids are still read one tile ahead. Pages of 16 and up keep one load per tile. gemm_a8w8_blockscale_group32 was gated to gfx950; the AFP8WFP8 Triton kernel it calls is correct on gfx1250, so the gate admits gfx1250 too. Tests: test_paged_mqa_logits_preshuffle now runs on gfx1250 (pages 8-256, ChunkK 128/256, decode and MTP; ChunkK % page != 0 is skipped there, as the gfx1250 compile path requires). The group32 entry-point tests run on gfx1250; the tuned-config test stays gfx950-only (no gfx1250 tuned configs yet). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
One backend per PR: PR title tags & labels: |
There was a problem hiding this comment.
🟡 Changes recommended
An accepted small-chunk configuration permits overlapping writes that can produce incorrect logits.
1 open finding
What changed in this PR
This PR enables gfx1250 support needed by DeepSeek-V4.1-Flash for paged MQA logits and group32 FP8 GEMM.
Changes:
- Assemble 8-token pages into 16-token WMMA tiles.
- Allow group32 FP8 GEMM on gfx1250.
- Enable related tests on gfx1250.
| File | Description |
|---|---|
op_tests/triton_tests/gemm/basic/test_gemm_afp8wfp8.py |
Enables gfx1250 group32 tests. |
op_tests/test_pa_mqa_logits_offset.py |
Enables gfx1250 preshuffle tests. |
aiter/ops/triton/gluon/pa_mqa_logits.py |
Adds multi-page tile assembly. |
aiter/ops/triton/gemm/basic/gemm_a8w8_blockscale_group32.py |
Allows gfx1250 dispatch. |
aiter/ops/triton/attention/pa_mqa_logits.py |
Removes the short-page restriction. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| col = gl.arange(0, TileRows, layout=gl.SliceLayout(0, mfma_layout)) | ||
|
|
||
| n_blocks = tl.cdiv(split_context_length, KVBlockSize) | ||
| # split_context_start is a multiple of ChunkK, hence of TileRows. |
The TDM grouped GEMM walks K in whole tile_k tiles and silently drops a remainder. gemm2's K is inter_dim; DeepSeek-V4.1-Flash at TP4 has 640 per rank, so with the default tile_k=256 every routed expert lost 128 of its 640 K columns: the MoE output came out 10-25% off (norm 3-7% low) and GSM8K 3-shot fell to 0.892 on se3 vs 0.924 for the same code on MI355X. _grouped_a8w4_tdm_moe now halves each stage's tile_k (gemm1 against model_dim, gemm2 against inter_dim) until it divides K into at least two tiles, and raises below 128 (one 4-scale WMMA step) instead of computing a wrong result. The e8m0 scale preshuffle keeps k order for any 128-multiple tile, so weights prepared for 256 are read correctly at 128. Shapes that already divide keep their tile. Tests: test_grouped_inter_dim_not_tile_k_multiple (a4w4 Silu, inter_dim 384/640, the same TDM K loop as a8w4) goes from logits_diff 0.19/0.08 to 0.0000; test_fit_tile_k covers the selection. On gfx1250 (se3) the file goes from 11 failed / 16 passed to 3 failed / 24 passed; the 3 fail before and after (missing AOT cache in run-only mode, a8w4 situv2 reference). End to end, DeepSeek-V4.1-Flash TP4 on se3, GSM8K 3-shot full set: 0.8916 -> 0.9280 (MI355X same code and command: 0.9242). Decode TPOT 8.8 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
azaidy
left a comment
There was a problem hiding this comment.
Please create different PRs for different backends and different kernels.

Summary
DeepSeek-V4.1-Flash does not start on gfx1250 (MI455X) because of two AITER gaps (1, 2). Once those are fixed it runs, but GSM8K is about 3 points below MI355X because the gfx1250 grouped MoE GEMM drops part of K (3). This PR fixes all three. The ATOM side is ROCm/ATOM#2617, which depends on this PR.
1.
deepgemm_fp8_paged_mqa_logits: 8-token pages on gfx1250Why it failed. V4.1's FP8 index plane stores 8 rows per page (
candidate_block_size: 8in the checkpoint config). This is model semantics, so the page size cannot be changed. On gfx1250 the preshuffle path TDM-loads one page per iteration and uses it directly as the WMMA B tile, which is 16 tokens wide. So any page below 16 was rejected:gfx950 already supports 8-token pages (two pages per MFMA tile). This brings gfx1250 to the same contract.
What changed (
aiter/ops/triton/gluon/pa_mqa_logits.py, gfx1250 branch of_gluon_deepgemm_fp8_paged_mqa_logits_preshuffleonly):PagesPerTile = 16 // KVBlockSizepages. Each page is its own TDM load, for both values and scales, into adjacent shared-memory slots.reinterpret(shape=[NUM_BUFFERS, 1, TileRows * HiddenDim]). Each double buffer's pages are therefore one contiguous view, which is reshaped in the page's(8, 16)shuffle layout (shuffle_weight(kv, layout=(min(page, 16), 16))). Scales are read the same way.async_waitcounts scale with the2 * PagesPerTileloads each tile issues: values first, then scales.PagesPerTile == 1, so the loop issues the same loads in the same order as before.KVBlockSize >= 16assert in the wrapper is removed. The compile path's own gfx1250 check (KVBlockSize > 1andChunkK % KVBlockSize == 0) still applies.Two Gluon details shaped this design, in case they matter for other gfx1250 kernels:
async_loadinto asmem.slice(...)view loses the slice's offset: both pages landed in slot 0. Loading intosmem.index(i)is correct.sliceneeds a constexpr start, andslicefollowed byreshapeon this allocation crashed LLVM. Hence thereinterpretview.2.
gemm_a8w8_blockscale_group32: allow gfx1250The entry point asserted
get_gfx_runtime() == "gfx950". The kernel it calls, the shared AFP8WFP8 Triton kernel (tl.dot_scaled), is correct on gfx1250, so the gate now admits gfx1250 as well. V4.1's dense FP8 projections (32x32 / 1x32 E8M0 scales) all go through this entry point.3. Grouped MoE (gfx1250 TDM): fit
tile_kto each stage's KWhy it was wrong. The gfx1250 grouped MoE GEMM (
grouped_gemm_gfx1250_a8w4, which is the default MoE path on gfx1250) walks K in wholetile_ktiles and silently drops any remainder. gemm2's K isinter_dim. DeepSeek-V4.1-Flash at TP4 hasinter_dim = 2560 / 4 = 640per rank, and with the defaulttile_k = 256every routed expert lost 128 of its 640 K columns. Nothing errored and the server answered sensibly, but:What changed (
aiter/ops/flydsl/grouped_moe_gfx1250.py):_grouped_a8w4_tdm_moenow halves each stage'stile_kuntil it divides that stage's K into at least two tiles. gemm1 is checked againstmodel_dim, gemm2 againstinter_dim. Below 128 (one 4-scale WMMA step) it raises instead of computing a wrong result. The e8m0 scale preshuffle keeps k order for any 128-multiple tile, so weights prepared for 256 are read correctly at 128. Shapes that already divide keep their tile, so no other model changes.Results on se3.
test_grouped_inter_dim_not_tile_k_multiple(a4w4 Silu,inter_dim384 and 640; a4w4 runs the same TDM K loop as a8w4) gives logits_diff 0.19 / 0.08 and output norm 0.83 / 0.93 of the fp32 reference before the fix, and 0.0000 / 1.000 after.Accuracy
On gfx1250 (MI455X, se3), with this container: triton
3.8.0.dev20260903+git9551a57b, flydsl0.3.3.dev886.Paged MQA logits.
test_paged_mqa_logits_preshuffleis now enabled on gfx1250: 44 passed, 4 skipped. The skipped cases are page 256 with ChunkK 128, which the gfx1250 compile path does not admit. Coverage:group32 GEMM. The group32 entry-point tests in
test_gemm_afp8wfp8.pynow run on gfx1250, and all 64 pass. They cover panel scales and tails, extreme scale codes, CUDA-graph replay with live inputs, row scales with split-K tails, the public dispatch, raw views, preallocated output, and the invalid contract.test_tuned_variants_use_production_configstays gfx950-only, because no tuned group32 configs exist for gfx1250 yet.End to end. With this PR and the ATOM PR, DeepSeek-V4.1-Flash serves on gfx1250: TP4,
--level 0, BF16 KV, FP8 index.Performance
deepgemm_fp8_paged_mqa_logits(Preshuffle=True)on gfx1250, decode, 64 heads, D=128, mean of 50 calls:* This cell reads 18-31 us depending on what ran earlier in the same process. Repeated isolated runs give 17.8-18.9 us patched and 18.2-18.6 us original.
Test plan
op_tests/test_pa_mqa_logits_offset.pyon gfx1250: 61 passed, 64 skipped. The skips are pre-existing gfx1250 skips plus the 4 above.op_tests/triton_tests/gemm/basic/test_gemm_afp8wfp8.pyon gfx1250: 714 passed, 125 skipped (650 / 189 before this PR).test_gemm_afp8wfp8_preshuffle[...-gluon-...]cases sometimes abort the process on gfx1250. This also happens with the unmodified test file, and those tests are not touched here.blackandruff checkare clean on the changed files.op_tests/flydsl_tests/test_flydsl_grouped_gemm.pyon gfx1250: 24 passed, 3 failed (11 failed, 16 passed before). The 3 fail before and after this change: missing AOT cache in run-only mode, and the a8w4 situv2 reference.🤖 Generated with Claude Code