Skip to content

[FlyDSL] [gfx950] Fix 8-wave a8w8 GEMM reading LDS before the last tile lands - #6368

Merged
coderfeli merged 10 commits into
ROCm:mainfrom
yangyuhuiling:xiangch/fix-8wave-gemm-lds-barrier
Oct 26, 2026
Merged

coderfeli merged 10 commits into
ROCm:mainfrom
yangyuhuiling:xiangch/fix-8wave-gemm-lds-barrier

Conversation

@yangyuhuiling

@yangyuhuiling yangyuhuiling commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The 8-wave a8w8 bpreshuffle GEMM can return wrong results: a few wrong rows, no NaN, only on some launches.

The peeled tail reads the last K step's B0 and A0 from LDS before the wait that covers their global→LDS loads:

# step K_ITERS - 2
a0_frag = a_s2r.load(a_cur0)
rocdl.s_barrier()                    # drains nothing
...
b0_frag = b_s2r.load(b_next0, ...)   # reads the final B0

# step K_ITERS - 1
a0_frag = a_s2r.load(a_cur0)         # reads the final A0
wait_barrier(0)                      # too late

Fix

  • Tail: replace that bare s_barrier() with wait_barrier(0), so the final tile has landed before it is read. The last step's existing wait_barrier(0) stays (it covers the final A1).
  • Main loop S0: rocdl.s_waitcnt(lgkmcnt=0) + rocdl.s_barrier() (the same form as 1,Balance the prologue's extra wave_m == 1 barrier so both groups' ep… FlyDSL#1306, no inline asm), so LDS reads finish before the other wave group refills B0. This one is a statically found gap; the tail change alone fixes every failure reproduced here, and it costs nothing measurable.
  • Epilogue: close the prologue's half-wave barrier stagger. if wave_m == 1: rocdl.s_barrier() leaves waves 4-7 one barrier ahead, so the last unconditional s_barrier only paired with their previous one and waves 4-7 waited until waves 0-3 had stored and exited: the two groups' epilogues ran back to back. The final barrier is now issued on wave_m == 0 only, as 1,Balance the prologue's extra wave_m == 1 barrier so both groups' ep… FlyDSL#1306 does for the mxfp8 kernel.

An earlier revision of this PR instead lowered the main loop's end-of-step wait from 2A+B to A+B-1. That only made the final tiles land sooner on long K, charged every K step for it, and does nothing at K=256, where the main loop runs zero times. It has been reverted; the main loop keeps its original count. Thanks to the reviewer who found the K=256 counterexample.

Correctness

MI355X, 8 launches per shape, output NaN-poisoned before each, counting elements off by >5%:

shape tile main A+B-1 this PR
4096×7168×256 256×256 8/8 wrong 8/8 wrong 0/8
3456×7168×4224 128×256 1/8 wrong 0/8 0/8
32768×7168×1792 256×256 8/8 wrong 0/8 0/8
7 more tuned shapes, both tiles 0/8 0/8 0/8

Which shapes fail on main varies with machine load, since it depends on how the waves interleave.

Cost

The synchronisation fix alone (before the epilogue change), same harness for all three, idle-gated:

shape tile main A+B-1 this PR
1472×3584×7168 128×256 78.7 86.0 (+9.3%) 80.2 (+1.9%)
2048×3584×7168 128×256 81.6 86.1 (+5.6%) 82.9 (+1.6%)
3456×7168×4224 128×256 124.6 131.0 (+5.2%) 126.9 (+1.9%)
4096×2176×7168 256×256 104.5 106.5 (+1.9%) 105.3 (+0.8%)
3840×8448×7168 256×256 206.8 210.7 (+1.9%) 208.8 (+1.0%)
32768×7168×1792 256×256 442.9 477.3 (+7.8%) 449.3 (+1.4%)

−0.5% to +2.2% against main across the ten shapes measured, against up to +9.3% for A+B-1.

Net, whole PR against main

MI355X, FlyDSL 0.4.0, the 16 tuned rows below at their tuned tile / waves_per_eu / xcd_swizzle, 7 rounds interleaved per shape, idle-gated, median µs (last column: rounds where this PR was faster):

shape tile wpe/xcd main this PR faster
896×8704×4096 128×256 1/8 31.9 31.9 (-0.1%) 3/7
1024×6288×7168 128×256 4/4 46.8 48.3 (+3.1%) 0/7
1472×3584×7168 128×256 4/4 43.3 44.5 (+2.8%) 0/7
2048×3584×7168 128×256 4/8 49.4 50.2 (+1.6%) 1/7
3456×7168×4224 128×256 2/8 94.7 96.8 (+2.2%) 2/7
1856×7168×768 256×256 1/0 18.1 17.2 (-5.0%) 7/7
2112×7168×1024 256×256 1/0 20.8 20.1 (-3.4%) 7/7
1472×7168×1536 256×256 3/0 23.9 22.5 (-5.7%) 7/7
2048×6144×4096 256×256 2/8 49.1 49.3 (+0.4%) 4/7
3392×7168×1792 256×256 3/4 52.5 49.9 (-5.0%) 6/7
3840×8448×7168 256×256 3/8 169.1 167.7 (-0.9%) 7/7
1920×8448×7168 128×512 1/8 93.2 95.3 (+2.2%) 0/7
8192×2048×2048 256×256 4/4 32.9 31.9 (-3.0%) 6/7
32768×4096×2048 256×256 4/4 234.1 228.7 (-2.3%) 7/7
16384×6144×4096 256×256 3/0 300.9 295.3 (-1.8%) 7/7
32768×6144×12288 256×256 1/4 1586.9 1575.1 (-0.7%) 7/7

Geomean -1.0%. The 256×256 rows gain -0.7% to -5.7% from the epilogue overlap; the long-K 128×256 rows stay +1.6% to +3.1%, where the sync fix's cost is larger than the overlap. The 128×512 row is from a separate 7-round run against main; all three tuned 128×512 rows are +1.2% to +2.2% (that tile is already at 256 VGPRs, and the epilogue branch raises spills from 4 to 11). The same 16 rows plus 4096x7168x256 and 32768x7168x1792 are 0/8 wrong with NaN-poisoned output.

Test

No new test file. op_tests/test_gemm_a8w8.py already runs every tuned flydsl row through gemm_a8w8_bpreshuffle, which is the only way this kernel is reached on gfx950 (untuned shapes fall back to CK). On 32768x7168x1792 its ck bpreshuffle err is 0.05–0.09% before this fix and 0 after. That column is reported, not asserted, so it will not fail CI on its own. K=256 is not reachable through the dispatch; it was run against the kernel directly.

Scope

gfx950 ptpc kernel only. The mxfp8 8-wave kernel from #6407 already has the tail wait_barrier(0).

🤖 Generated with Claude Code

…tep barrier

The barrier that ends each main-loop K step admits `2 * N_LDS_STEPS_A +
N_LDS_STEPS_B` outstanding global->LDS loads. That is more than the step can
have in flight and still be safe: the barrier passes while writes the next
step reads have not landed, and the next step consumes stale LDS.

The result is a few wrong rows, not a NaN -- rel_l2 lands around 1e-3, close
enough to fp8 quantisation noise to go unnoticed. Whether it fires depends on
how the waves interleave, so a shape can be clean on one launch and wrong on
the next, and larger grids hit it more often.

Shapes in the tuned configs are affected. `M=32768 N=7168 K=1792` on the
256x256 tile is wrong on 4 of 4 runs, and `M=3456 N=7168 K=4224` on 128x256
on 1 of 4.

Lowering the count to `N_LDS_STEPS_A + N_LDS_STEPS_B - 1` clears it: both
shapes above are then correct and bit-identical across runs. A sweep of the
128x256 tile (where the two counts are 4 and 2) shows 4 and 3 corrupting and
2 and 0 clean, so this is the loosest count that is safe there.

Waiting on more loads costs throughput, mostly on 128x256 and mostly at small
grids where the K loop is short enough for the barrier to matter:

  tile      grid   before    after
  128x256    168    55.6us   65.0us   +16.9%
  128x256    224    60.6us   67.8us   +11.9%
  128x256    243    64.9us   68.0us    +4.8%
  128x256    252    63.7us   67.9us    +6.6%
  256x256    144    84.7us   87.0us    +2.7%
  256x256    363   167.8us  177.3us    +5.7%
  256x256    495   204.9us  204.7us    -0.1%
  256x256   3584   451.7us  479.0us    +6.0%

Measured on MI355X, min of per-arm medians over two alternating passes.
Recovering this would need a tighter count per tile rather than one formula;
correctness comes first here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@yangyuhuiling
yangyuhuiling requested a review from a team October 10, 2026 08:18
@github-actions

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 (added automatically when gfx942 configs change); main branch always runs both MI35X and MI300X
ci:triton-355 Run the full Triton test suite on MI35X, not only the tests the change affects
ci:multi-gpu 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: DeepSeek-V4-Pro, MiniMax-M3-MXFP4
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 6368 --add-label <label>

One backend per PR:
A PR changes one kernel backend: [Triton/Gluon] (Triton and Gluon count as one), [HIP], [ASM], [CK], [OPUS] or [FlyDSL]. If the title ends up with two backend tags, split the PR -- as stacked pull requests when one part cannot merge without the other.

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 stop the title rewrites; labels stay in sync either way.

xiangch and others added 2 commits October 10, 2026 08:23
The prologue's `if wave_m == 1: rocdl.s_barrier()` is executed by waves 4-7
only. Waves 0-3 skip it and reach the next barrier instead, so the hardware
pairs the two groups one barrier apart and keeps them that way for the rest of
the kernel.

Documented rather than changed: making it unconditional while keeping the old
wait count still corrupts, and with the corrected count both forms are clean,
so it is not what this PR is fixing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit read the divergent `if wave_m == 1: s_barrier()` as a bug
in itself. It is not: it deliberately puts waves 0-3 one phase ahead, which is
the stagger the double-buffered main loop is built around. Making it
unconditional destroys that and fixes nothing.

What is missing is its counterpart. The stagger is never closed, so the two
halves are still a barrier apart when the epilogue starts. gcnasm closes it
before the same epilogue (opus_gemm_a2a_lsa quad-subtile template) and so does
mori's fused copy of this pipeline, with `if wave_m == 0: rocdl.s_barrier()`.

Still a note rather than a change: nothing follows store_c here, so the
imbalance cannot be observed today. It stops being unobservable the moment
anything is appended to the epilogue -- a `wait_barrier(0)` placed after
store_c would rendezvous waves 0-3 with waves 4-7 still one barrier back and
before their stores, rather than meaning what it says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zufayu
zufayu requested a review from yadaish October 12, 2026 01:26
@zufayu
zufayu requested review from coderfeli and xytpai October 15, 2026 06:36
@zufayu zufayu assigned coderfeli and unassigned yadaish Oct 15, 2026
@zufayu
zufayu requested review from solinzby1 and removed request for xytpai October 15, 2026 07:16
Tightening the main loop's end-of-step wait from 2A+B to A+B-1 was the
wrong place for it, for two reasons.

It does not cover every legal K. At K=256, K_ITERS is 2 and the main
loop body runs zero times, so the changed line never executes:
M=4096 N=7168 K=256 tile=256x256 is still wrong 6 runs out of 6.

And it pays per K step for a dependency the tail owns. The peeled steps
pre-read the last K step's B0 (end of step K_ITERS-2) and A0 (top of
step K_ITERS-1), both before that step's wait_barrier(0). Tightening the
main loop only made those tiles land in LDS earlier, which is why it
happened to fix the long-K shapes.

So: restore the main loop's original count and drain the final-tile
producers in the tail, where the pre-reads are. The last step's wait
stays -- it still covers the final A1, which is issued after it.

Also order the WAR on B0 explicitly. The leading group refills that slot
one stage after the barrier the lagging group's S0 pairs with, and LDS
reads retire under LGKM, which this kernel's VM waits say nothing about.
read_done_barrier() is lgkmcnt(0) + s_barrier. This one is a statically
found gap: the tail drain alone already clears every shape here, so it
is not separately demonstrated -- it costs nothing measurable and writes
the dependency down.

On 8x MI355X, 8 runs per shape with NaN-poisoned output, all clean
including the two new K=256/512 entries. At 32768x7168x1792 the main
loop's restored count is worth 6%: 445.2us against 476.3us for A+B-1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@yangyuhuiling

Copy link
Copy Markdown
Contributor Author

Updated: the fix now sits in the tail, and the main loop's original count is restored. Thanks to whoever dug into this — the K=256 counterexample is correct and it changes the diagnosis.

A+B-1 was the wrong fix in the wrong place. Two problems with what this PR originally did:

It does not cover every legal K. At K=256, K_ITERS == 2, so range_constexpr(K_ITERS - 2) runs zero times and the line I changed never executes. Reproduced here: M=4096 N=7168 K=256 tile=256x256 is wrong 6 runs out of 6 both before and after the original patch.

And it charges every K step for a dependency that belongs to the tail. The peeled steps pre-read the final K step's B0 (end of K_ITERS-2) and A0 (top of K_ITERS-1), both before that step's wait_barrier(0). Tightening the main loop only got those tiles into LDS sooner, which is why it appeared to fix the long-K shapes.

What it does now. Restore 2A+B in the main loop, and drain the final-tile producers in the tail where the pre-reads actually are. The last step's wait_barrier(0) stays — it still covers the final A1, which is issued after it.

Separately, read_done_barrier() (lgkmcnt(0) + s_barrier) at the main loop's S0 orders the WAR on B0: the leading group refills that slot one stage after the barrier the lagging group's S0 pairs with, and LDS reads retire under LGKM, which this kernel's VM waits say nothing about. This one is a statically found gap, not a demonstrated one — the tail drain alone already clears every shape I can reproduce, so I cannot attribute any observed failure to it. It costs nothing measurable and writes the dependency down.

Measured, 8x MI355X, ptpc kernel, 8 runs per shape with NaN-poisoned output:

variant 4096x7168x256 32768x7168x1792 time @ 32768
before this PR 6/6 wrong 6/6 wrong 443.9 us (wrong)
A+B-1 (this PR, previously) 6/6 wrong 0/6 476.3 us
tail drain only 0/6 0/6 446.1 us
tail drain + S0 read-drain (now) 0/8 0/8 445.2 us

So the tail fix is both more correct and ~6% faster than A+B-1 at the large shape, because the per-K-step cost is gone.

Full shape list now passing 8/8: (4096,7168,256), (4096,7168,512), (1472,3584,7168), (3392,2176,7168), (3456,7168,4224), (1920,7168,1792), (2624,8448,7168), (32768,7168,1792). The two K=256/512 entries are new in the regression test — they are not tuned shapes, but they are legal, and the first is precisely the one the main loop cannot reach.

Scope I have not covered: this is the ptpc kernel only, on FlyDSL 0.3.4.1. I have not re-read the generated ISA to confirm the barrier pairing, and I have not run a full regression across every tile / K / scale layout.

One note for anyone comparing with the mxfp8 kernels: #6407's already has the tail wait_barrier(0). #6370's does not — it has the same bare barrier — but it does not reproduce the failure, because its scale loads make the compiler insert a vmcnt(0) ahead of the final B0/A0 reads. That is an incidental data dependency, not synchronisation, so it should not be read as evidence that the bare barrier is safe.

@yangyuhuiling

Copy link
Copy Markdown
Contributor Author

Re-measured everything and rewrote the PR body. One correction to my earlier comment, and a note on method.

Correction. I quoted "445.2us against 476.3us for A+B-1" for 32768x7168x1792. Those two numbers came from two different harness runs with different preceding work, which is not a fair comparison even though both were on an idle box. Measured properly — all three arms in one pass of the same harness — it is 449.3us against 477.3us, so 5.9%. The conclusion is unchanged; the way I sourced the number was not sound.

Method. This box is shared, and I caught a neighbour's job inflating timings by 10-13% mid-session — larger than the effect being measured. The run behind the new tables is gated: it starts only after six consecutive samples show every GPU under 5%, and is thrown away if anything is running when it finishes. Two intermediate runs were discarded that way.

What the new tables say. The cost of fixing this in the main loop was up to +9.3% on the 128x256 grids and +7.8% at 32768x7168x1792, paid on every K step. In the tail it is +0.8% to +2.2% across all ten shapes — and 3-7% faster than the previous revision of this PR wherever that one hurt.

Correctness is unchanged from what I reported: main wrong on three shapes, A+B-1 wrong at K=256, this revision clean 0/8 everywhere.

One observation worth recording: which shapes main fails on moves with machine load. The contended sweep also caught 2048x3584x7168 and 2304x3584x7168 at 2/8 each, where both are clean when the box is idle. That is the interleaving dependence this bug has, and it is why the test runs each shape eight times rather than once.

xiangch and others added 2 commits October 19, 2026 02:57
Review asked for shorter comments. Each added block is now one line, the
wait_barrier docstring is dropped, and the epilogue TODO about the
unclosed half-wave stagger is removed -- it is recorded in the PR
description. No code change.

Co-Authored-By: Claude <noreply@anthropic.com>
Review preferred not adding a test file per fix. The existing
op_tests/test_gemm_a8w8.py already runs every tuned flydsl row through
gemm_a8w8_bpreshuffle, which is the only way this kernel is reached on
gfx950 (untuned shapes fall back to CK). On 32768x7168x1792 its
"ck bpreshuffle err" reads 0.05-0.09% before this fix and 0 after.

Co-Authored-By: Claude <noreply@anthropic.com>
@yangyuhuiling yangyuhuiling changed the title [FlyDSL] [gfx950] Fix 8-wave a8w8 GEMM dropping LDS writes at the K-step barrier [FlyDSL] [gfx950] Fix 8-wave a8w8 GEMM reading LDS before the last tile lands Oct 19, 2026
@solinzby1

Copy link
Copy Markdown
Contributor

LGTM

yangyuhuiling added a commit to ROCm/mori that referenced this pull request Oct 20, 2026
… the loop (#785)

The fused GEMM's peeled tail pre-reads the last K step's B0 (end of step
K_ITERS-2) and A0 (top of step K_ITERS-1), both before that step's
wait_barrier(0). Nothing drains those tiles' global->LDS loads first, so
the reads can take stale LDS: a few wrong rows, no NaN, intermittent.

The main loop's end-of-step wait was lowered from 2A+B to A+B-1 to fix
large-grid corruption, but that only made the tail's tiles land sooner.
At K=256 the main loop runs zero times, so it cannot help: ptpc
4096x7168x256 is wrong on 12/12 single-GPU launches with it in place.

Drain the producers in the tail itself, and restore the loop's original
count. With the tail fixed, 2A+B is clean on [16384, 7168] K=512 -- the
shape that motivated A+B-1, and which still corrupts 8/12 under 2A+B
without the tail change. The last step's wait_barrier(0) stays; it
still covers the final A1, issued after it.

Also add read_done_barrier() (lgkmcnt(0) + s_barrier) at the loop's S0:
the leading wave group refills B0 one stage after the barrier the
lagging group's S0 pairs with, and LDS reads retire under LGKM, which
the VM waits do not cover. This one is a statically found gap; the tail
drain alone clears every failure reproduced here, and it costs nothing
measurable.

Same diagnosis as ROCm/aiter#6368, which this pipeline was copied from.

Measured on 8x MI355X, gemm-only, idle-gated, arms alternating:
  ptpc       32768x7168x1792   437.1 -> 412.8 us   -5.5%
  blockscale 16384x7168x2048   377.9 -> 362.3 us   -4.1%
  mxfp8      32768x7168x1792   476.6 -> 462.2 us   -3.0%
  mxfp8      16384x7168x2048   246.8 -> 246.2 us   -0.2%
The fused gemm_ar path is unchanged within noise (communication-bound).

Co-authored-by: xiangch <xiangch@amd.com>
Co-authored-by: Claude <noreply@anthropic.com>
xiangch and others added 2 commits October 20, 2026 12:17
…0 read drain

Replace read_done_barrier()'s inline asm with rocdl.s_waitcnt(lgkmcnt=0) +
rocdl.s_barrier(), the form FlyDSL#1306 uses for the same fix.

Same 54 "s_waitcnt lgkmcnt(0); s_barrier" pairs at K=7168 and the same
VGPR/SGPR counts, but 212 (256x256) / 106 (128x256) fewer s_waitcnt overall:
the inline asm was opaque to SIInsertWaitcnts, which then re-inserted
lgkmcnt waits it could not prove were already satisfied.

MI355X, FlyDSL 0.4.0: 4096x7168x256 and 32768x7168x1792 are 8/8 wrong on
main and 0/8 with this change, as with the inline asm; timing is within
noise of the inline asm on six shapes.

Co-Authored-By: Claude <noreply@anthropic.com>
…efore the epilogue

The prologue's `if wave_m == 1: s_barrier()` leaves waves 4-7 one barrier
ahead in count, and the last unconditional s_barrier only paired with their
previous one, so waves 4-7 sat in a barrier until waves 0-3 had stored and
exited: the two groups' epilogues ran back to back. Issue the final barrier
on wave_m == 0 only, as ROCm/FlyDSL#1306 does for the mxfp8 kernel, so the
counts match and both epilogues overlap.

Kept unconditional at BLOCK_N=512: that tile is already at 256 VGPRs, the
branch raises spills from 4 to 11, and all three tuned 128x512 shapes measured
2.4-2.7% slower with it. The 256-wide tiles have headroom (256x256: 252 VGPRs
either way; 128x256: 152 -> 156, no spills).

MI355X, FlyDSL 0.4.0, 16 tuned shapes at their tuned tile/wpe/xcd, 7
interleaved rounds: geomean -2.1%, up to -6.1% (1472x7168x1536); large-M
multi-wave shapes -0.7% to -2.1%. 18 shapes incl. 4096x7168x256 and
32768x7168x1792 stay 0/8 wrong with NaN-poisoned output.

Co-Authored-By: Claude <noreply@anthropic.com>
…e added comments

Issue the final barrier on wave_m == 0 for every tile, BLOCK_N=512 included,
instead of keeping it unconditional there. Remove the comments this PR added.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderfeli
coderfeli merged commit 00f8f5e into ROCm:main Oct 26, 2026
yangyuhuiling added a commit to ROCm/mori that referenced this pull request Oct 28, 2026
* fix(gemm-ar): drain the peeled tail's producers instead of tightening the loop

The fused GEMM's peeled tail pre-reads the last K step's B0 (end of step
K_ITERS-2) and A0 (top of step K_ITERS-1), both before that step's
wait_barrier(0). Nothing drains those tiles' global->LDS loads first, so
the reads can take stale LDS: a few wrong rows, no NaN, intermittent.

The main loop's end-of-step wait was lowered from 2A+B to A+B-1 to fix
large-grid corruption, but that only made the tail's tiles land sooner.
At K=256 the main loop runs zero times, so it cannot help: ptpc
4096x7168x256 is wrong on 12/12 single-GPU launches with it in place.

Drain the producers in the tail itself, and restore the loop's original
count. With the tail fixed, 2A+B is clean on [16384, 7168] K=512 -- the
shape that motivated A+B-1, and which still corrupts 8/12 under 2A+B
without the tail change. The last step's wait_barrier(0) stays; it
still covers the final A1, issued after it.

Also add read_done_barrier() (lgkmcnt(0) + s_barrier) at the loop's S0:
the leading wave group refills B0 one stage after the barrier the
lagging group's S0 pairs with, and LDS reads retire under LGKM, which
the VM waits do not cover. This one is a statically found gap; the tail
drain alone clears every failure reproduced here, and it costs nothing
measurable.

Same diagnosis as ROCm/aiter#6368, which this pipeline was copied from.

Measured on 8x MI355X, gemm-only, idle-gated, arms alternating:
  ptpc       32768x7168x1792   437.1 -> 412.8 us   -5.5%
  blockscale 16384x7168x2048   377.9 -> 362.3 us   -4.1%
  mxfp8      32768x7168x1792   476.6 -> 462.2 us   -3.0%
  mxfp8      16384x7168x2048   246.8 -> 246.2 us   -0.2%
The fused gemm_ar path is unchanged within noise (communication-bound).

Co-Authored-By: Claude <noreply@anthropic.com>

* perf(gemm-ar): mxfp8 with row-major scales staged through LDS

Add quant="mxfp8_raw": the same 32-wide ue8m0 GEMM, taking A's scales as
[M, K/32] and B's as [N/32, K/32] uint8 -- what the quantiser and the
checkpoint already hold -- and staging them through LDS, after aiter's
ScaleRawLds. "mxfp8" (packed, preshuffle_a_scale) is unchanged and stays the
default, so existing callers keep working.

Reading the packed layout from global once per K step costs twice: the
MFMA's vmcnt wait on the scale also drains the tile prefetches issued before
it (63 vmcnt waits at K=2048, 20 here), and the scale registers spill in a
kernel already at 256 VGPRs (8 at K=512, none here). Each wave now copies
its share of four K steps into LDS with buffer_load ... lds, double-buffered,
and the pipeline's own wait_barrier covers the copies.

GEMM only, MI355X, CUDA-graph timed, idle-gated, modes alternating, cold
weights: -9% to -15% at M=64/1024 on the 256x128 tile, -1% to -12% on the
256x256 tile across 10 shapes, +0.9% at K=640 where five K steps do not
amortise two chunks. Fused-sdma at TP4 wo_b is unchanged (+-0.3%): that layer
is bound by the collective. No padding: any M goes in.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(gemm-ar): stage mxfp8_raw scale chunks with the offset in the VGPR

The chunk offset went in through soffset, which the raw buffer range check
does not cover, so the last row's bytes past K were read from beyond the
scale tensor rather than zero-filled. They are never consumed, but the read
could reach past the allocation. With the offset in the per-lane address the
check covers it, and the comment that claimed zero-fill is now true.

Also qualify "faster than mxfp8": it holds cold and at large M; hot at small
M or short K it can be a few percent slower.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: xiangch <xiangch@amd.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants