Skip to content

[FlyDSL] fp8_mqa_logits gfx942: add LDS-staged variant, use it by default - #6164

Open
akii96 wants to merge 6 commits into
ROCm:mainfrom
akii96:flydsl/fp8-mqa-logits-gfx942-lds-pipeline
Open

akii96 wants to merge 6 commits into
ROCm:mainfrom
akii96:flydsl/fp8-mqa-logits-gfx942-lds-pipeline

Conversation

@akii96

@akii96 akii96 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds an LDS-staged fp8_mqa_logits variant for gfx942 and makes it the default for ≥16 query rows. gfx950 is unchanged.

Motivation

On gfx942 the direct-load kernel is bound by memory traffic: every wave loads its own slice of each KV tile and holds all of the block's rows in registers, capping occupancy at 2 waves/SIMD.

Changes

  • New variant mfma_bkv64_r{1,2,4}_w4_lds2 (fp8_mqa_logits_gfx942_lds.py): the block's 4 waves share each KV tile through LDS, and each wave keeps only its own rows, giving 3 waves/SIMD.
  • Rows per wave are picked from the shape; calls under 16 rows keep the direct-load variant.
  • The new variant handles a short last block in-kernel, so the host does not pad rows for it (same as main for every variant).
  • gfx950 hard-gated: the shared direct-load builder (fp8_mqa_logits.py) is byte-identical to main; all new variants, hints and selection are behind _ARCH == "gfx942".
  • head_size guard: LDS variant auto-selects only when head_size ≤ 256 (D=512 would overflow 64 KB LDS). Falls back to direct-load for larger heads.
  • LDS kernel schedule and launch policy (by @aghamari):
    • Software-pipelined MFMA chains: each chain's ReLU × weight epilogue runs under the next chain's MFMAs, and the next tile's DMA is issued after the tile's last LDS read and spread over the last chains, so the loop-top vmcnt is exact.
    • Rows pre-permuted per lane group, so the head reduce-scatter needs no selects; it is staged across MFMA chains, and each tile's last reduce + store is carried past the next barrier.
    • One DMA descriptor per KV tile, window loads issued up front, paired RPW=2 stores and a dwordx4 -inf fill.
    • grid.y slices of decreasing size, so the last blocks to start are the smallest.
    • LDS variants compile with the SLP vectorizer off (the epilogue stays scalar v_fmac under the MFMAs); 128 heads with an FN operand compile at waves_per_eu=2, removing its VGPR spills.
    • Long cached-prefix windows (KV ≥ 16k) split grid.y further, sized from the variant's occupancy so graph capture and eager calls pick the same split.
    • RPW=2 from 4096 rows without a cached prefix, for every head count.

Rollback: set FLYDSL_FP8_MQA_LOGITS_VARIANT=mfma_r4_w2 (or pass variant=) to pin the previous direct-load kernel for every shape.

Performance

MI325X (gfx942), vLLM 0.34.0rc1.dev206 (nightly-f5d93296c8ec), flydsl 0.4.2.

Kernel benchmarks (graph-replay timing, L2 flushed per replay)

Check Result
Verify (--full / --packed-shapes / default / FN Q+KV) 1618 / 94 / 133 / 133, all pass
Long context, cached-prefix chunks 32k–128k KV (23 shapes, H32/H64, FN and FNUZ Q) 1.77× vs main (1.54–2.29×)
Model shapes (--model-shapes, 24 shapes) 1.73× vs main (1.43–2.06×)
FlyDSL-over-triton geomean (--model-shapes): main → PR 4.66× → 8.00×
Regressions 0

Long-context chunks are the shapes the DSv4 indexer issues at 128k+ prompts under the default 512 MB logits budget (e.g. 1024 rows × 128k KV, 2048 × 64k):

Shape (rows × KV, heads) main (µs) PR (µs) Speedup
1024 × 128k, H64 2236 1407 1.59×
1024 × 128k, H32 1384 729 1.90×
1365 × 96k, H64 2243 1410 1.59×
2048 × 64k, H64 2193 1394 1.57×
2048 × 64k, H64, FN Q 2145 1389 1.54×
4096 × 32k, H64 2469 1327 1.86×

End-to-end serving (prefix caching off)

DeepSeek-V4-Flash, 4× MI325X, TP=4, random dataset, --ignore-eos, OSL 256. Previous revision of this PR (cd113ea, 2 runs) → this revision (2–4 runs), medians. TPOT is unchanged within noise.

ISL / conc Total tok/s TTFT (ms) TPOT (ms)
128k / 4 15218 → 15414 (+1.3%) 17421 → 17156 66.5 → 65.8
192k / 2 13000 → 13208 (+1.6%) 17909 → 17575 48.2 → 47.7
248k / 1 11008 → 11215 (+1.9%) 16579 → 16135 25.5 → 25.6

GSM8K (8-shot, 1319 questions)

Metric Before After
strict-match 0.9234 0.9287
flexible-extract 0.9227 0.9295

No accuracy regression.

gfx950 (MI355X): unchanged vs main

  • Cross-compiled LLVM IR and final ISA identical to main for 28/28 variant × clean_logits combos.
  • FlyDSL timings on unaligned shapes match main within 0.2% (e.g. 1023² H32 26.86 → 26.80 µs, 4095×36863 H64 1638 → 1635 µs).

Testing

  • Unit tests added or updated
  • Performance benchmarks run

Documentation

  • Docstrings updated
  • No documentation change is needed

Dependencies

  • No new third-party dependencies added

Breaking Changes

None; the PR only modifies FlyDSL kernel files and their test.

PR Self-Review

Reviewed revision

107aaeb

Behavior and intent

Adds a new LDS-staged kernel variant for gfx942 without changing gfx950 or other archs; no variant gets host-side row padding, matching main after #6001. The direct-load variant stays and is auto-selected for short calls (<16 rows) or large heads (D>256). The LDS variant lets 4 waves share one KV tile through LDS instead of each loading its own copy, bumping occupancy from 2→3 waves/SIMD. Also adds a head_size guard so D=512 doesn't overflow the 64 KB LDS budget — it falls back to the existing direct-load path. The latest commit reworks the LDS kernel's schedule and its launch policy (grid.y split sizing, slice shape, compile hints) and moves the RPW=2 threshold for causal chunks to 4096 rows for every head count.

Validation and unverified scope

On MI325X at the latest commit: test_flydsl_fp8_mqa_logits.py default 133/133, --packed-shapes 94/94, --full 1618/1618, FN Q+KV 133/133, plus 78/78 extra H16/H128 and head_dim 256 cases. All 54 LDS kernel builds compile. Kernel bench: 1.73× vs main on the 24 model shapes and 1.77× on 23 long-context cached-prefix shapes (32k–128k KV), 0 regressions; the final launch-policy cleanup measured 1.00× against the revision those numbers come from. E2E with DeepSeek-V4-Flash on 4× MI325X (TP=4, prefix caching off) at 128k–248k ISL: +1.3–1.9% total throughput and 1.5–2.7% lower TTFT vs the previous revision of this PR. GSM8K 8-shot: 0.9234→0.9287 strict, no regression. black/ruff pass.

gfx950: cross-compiled IR/ISA identical to main for 28/28 variant × clean_logits combos, and on MI355X FlyDSL timings on unaligned shapes match main.

Unverified: on gfx950, --packed-shapes fails at H64 on both main and this PR because the Gluon reference it compares against is wrong there (single-warp race, fixed separately in #7823); FlyDSL had no errors. GSM8K and the gfx950 checks are from the previous revision (gfx950 code is unchanged since).

PR-specific question

The LDS variants register under mfma_bkv64_r{1,2,4}_w4_lds2 in _VARIANT_BUILDERS. _resolve_variant auto-selects them only when arch=gfx942, num_heads∈{16,32,64,128}, seq_len≥16, and head_size≤256. They are also reachable through variant=, the FLYDSL_FP8_MQA_LOGITS_VARIANT env var, and the aiter.mqa_logits tuner, which enumerates every registered variant. Direct-load variants stay registered and reachable the same way.

Changed op tests

test_flydsl_fp8_mqa_logits.py: added --packed-shapes for multi-sequence chunked-prefill shapes and pins for the LDS variant choice, including the 4096-row RPW boundary at 16/32/64 heads. --packed-shapes covers short per-sequence windows and short last blocks at the model head counts. Added verify_launcher_contract, a launch-free check on every arch that fails on any host-side row padding and, for the gfx942 LDS variants, on a grid.y split count whose slice bounds overflow i32 (1.5M- and 1M-column cases). The default lane also runs one 16-row × 1.5M-column case end to end. No existing test removed.

@github-actions

github-actions Bot commented Oct 6, 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 (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
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 6164 --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.

@akii96
akii96 force-pushed the flydsl/fp8-mqa-logits-gfx942-lds-pipeline branch 4 times, most recently from ea761a4 to ed5cf9f Compare October 14, 2026 23:39
@akii96 akii96 changed the title # [FlyDSL] fp8_mqa_logits gfx942: stage KV through LDS, split rows across waves [FlyDSL] fp8_mqa_logits gfx942: add LDS-staged variant, use it by default Oct 15, 2026
@juuso-oskari

juuso-oskari commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

I tested on MI325X, and the PR seems a solid one: all tests pass, and I measured similar speedups. Couple things though:

  • Needs rebase to main as couple commits there overlap. Remeasure after.
  • Regression: head size 512 now fails on gfx942 (LDS overflow). Add checks for head size.
  • The design described in the docstring (lines 98–101) and at lines 412–415 isn't what runs. In the dumped assembly, every variant has s_waitcnt vmcnt(4); s_barrier; s_waitcnt vmcnt(0). Compiler seems to override stuff.

Also a suggestion: LDS seems to be capping the occupancy now to 3 workgroups per CU. Scales are what tips it over, 4 x KV tiles itself would fit the 64kB. Could the scales be loaded directly to registers?

@akii96
akii96 force-pushed the flydsl/fp8-mqa-logits-gfx942-lds-pipeline branch from ed5cf9f to 2a3c08f Compare November 4, 2026 23:59
@aiter-bot

aiter-bot commented Nov 5, 2026 •

Copy link
Copy Markdown

NOT READY FOR HUMAN REVIEW

PR Self-Review

Revision 107aaeb12ef2 · mode shadow

Author self-review accepted. Readiness report:

Author answers

  • PASS — current-SHA answers are present and structurally specific
Show the author's self-review answers

Behavior and intent

Adds a new LDS-staged kernel variant for gfx942 without changing gfx950 or other archs; no variant gets host-side row padding, matching main after #6001. The direct-load variant stays and is auto-selected for short calls (<16 rows) or large heads (D>256). The LDS variant lets 4 waves share one KV tile through LDS instead of each loading its own copy, bumping occupancy from 2→3 waves/SIMD. Also adds a head_size guard so D=512 doesn't overflow the 64 KB LDS budget — it falls back to the existing direct-load path. The latest commit reworks the LDS kernel's schedule and its launch policy (grid.y split sizing, slice shape, compile hints) and moves the RPW=2 threshold for causal chunks to 4096 rows for every head count.

Validation and unverified scope

On MI325X at the latest commit: `test_flydsl_fp8_mqa_logits.py` default 133/133, `--packed-shapes` 94/94, `--full` 1618/1618, FN Q+KV 133/133, plus 78/78 extra H16/H128 and head_dim 256 cases. All 54 LDS kernel builds compile. Kernel bench: 1.73× vs main on the 24 model shapes and 1.77× on 23 long-context cached-prefix shapes (32k–128k KV), 0 regressions; the final launch-policy cleanup measured 1.00× against the revision those numbers come from. E2E with DeepSeek-V4-Flash on 4× MI325X (TP=4, prefix caching off) at 128k–248k ISL: +1.3–1.9% total throughput and 1.5–2.7% lower TTFT vs the previous revision of this PR. GSM8K 8-shot: 0.9234→0.9287 strict, no regression. black/ruff pass.

gfx950: cross-compiled IR/ISA identical to main for 28/28 variant × clean_logits combos, and on MI355X FlyDSL timings on unaligned shapes match main.

Unverified: on gfx950, `--packed-shapes` fails at H64 on both main and this PR because the Gluon reference it compares against is wrong there (single-warp race, fixed separately in #7823); FlyDSL had no errors. GSM8K and the gfx950 checks are from the previous revision (gfx950 code is unchanged since).

PR-specific question

The LDS variants register under `mfma_bkv64_r{1,2,4}_w4_lds2` in `_VARIANT_BUILDERS`. `_resolve_variant` auto-selects them only when arch=gfx942, num_heads∈{16,32,64,128}, seq_len≥16, and head_size≤256. They are also reachable through `variant=`, the `FLYDSL_FP8_MQA_LOGITS_VARIANT` env var, and the `aiter.mqa_logits` tuner, which enumerates every registered variant. Direct-load variants stay registered and reachable the same way.

Changed op tests

`test_flydsl_fp8_mqa_logits.py`: added `--packed-shapes` for multi-sequence chunked-prefill shapes and pins for the LDS variant choice, including the 4096-row RPW boundary at 16/32/64 heads. `--packed-shapes` covers short per-sequence windows and short last blocks at the model head counts. Added `verify_launcher_contract`, a launch-free check on every arch that fails on any host-side row padding and, for the gfx942 LDS variants, on a grid.y split count whose slice bounds overflow i32 (1.5M- and 1M-column cases). The default lane also runs one 16-row × 1.5M-column case end to end. No existing test removed.

PR state

  • PASS — pull request is marked ready for review

Copilot findings

  • PASS — no active Copilot finding lacks a substantive author reply
  • ADVISORY — Copilot did not review the current head SHA

Code and op-test hygiene

  • PASS — no committed artifact or conflict marker detected

Required CI

  • PASS — Black: passed
  • PASS — Ruff: passed
  • PASS — Repository dependency: passed
  • FAIL — Aiter Test Gate: failed
  • Required CI failed; fix it before requesting human review.

Shadow mode reports what would block but keeps the status successful. Set AITER_PR_SELF_REVIEW_MODE=enforce only after reviewing real false positives.

…ss waves

On gfx942 the direct-load kernel is limited by memory traffic, not MFMA
throughput: each wave loads its own slice of every KV tile and holds the Q
and weights of all of the block's rows, which keeps the VMEM queue full and
caps occupancy at 2 waves/SIMD.

Add an LDS-staged variant, mfma_bkv64_r{1,2,4}_w4_lds2
(kernels/mqa_logits/fp8_mqa_logits_gfx942_lds.py), and auto-select it on
gfx942. The block's 4 waves DMA one 64-column KV tile into LDS and all read
it back, so each tile is loaded once per block, and each wave keeps only its
own rows in registers (waves_per_eu=3). The variant handles a short last
block itself, so the host does not pad rows for it.

Selection: rows per wave start from a num_heads register cap and drop for
short or packed windows and for few rows against a long KV range; calls
with fewer than 16 rows keep the direct-load variant. The new variant has
its own _SplitPolicy entry. variant= and FLYDSL_FP8_MQA_LOGITS_VARIANT still
force any variant.

Rows with an empty window no longer widen a block's KV range, in both the
new and the direct-load kernel. gfx950 selection is unchanged.

op_tests: --packed-shapes, a small packed subset in the default lane, and
pins for the gfx942 variant selection.

Signed-off-by: Aakif Nawaz <aakif.nawaz@amd.com>
@akii96
akii96 force-pushed the flydsl/fp8-mqa-logits-gfx942-lds-pipeline branch from 2a3c08f to 910d863 Compare November 5, 2026 07:46
@akii96

akii96 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for testing and the feedback @juuso-oskari , I had forgotten about this but your review got me to pick this back up 😄

The newest pushes fixes some of the issues you raised with one todo:

  • Rebased onto main, conflicts sorted out
  • Added a guard so head_size 512 doesn't blow up LDS, just falls back to the old kernel
  • Fixed the docstring to not pretend the compiler follows our pipeline schedule
  • Looked at moving scales out of LDS to fit 4 workgroups per CU, but it's a bigger change than I want here. Left a TODO.

(opening this now coz I am happy with how it looks for now but happy to iterate further)

@akii96
akii96 marked this pull request as ready for review November 5, 2026 07:53
@akii96
akii96 requested a review from a team November 5, 2026 07:53
@github-actions github-actions Bot added the FlyDSL label Nov 5, 2026

@zhiding512 zhiding512 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.

Request changes: host-side row padding regresses gfx950

Thanks for the LDS-staged gfx942 work. On a gfx950 box I verified that the new mfma_bkv64_r*_w4_lds2 path itself is not selected, but the host launcher change still lands on every gfx950 call and regresses latency whenever seq_len is not a multiple of the rows-per-block of the auto-selected variant.

Root cause

After rebase onto main, this PR reintroduces the host pad of Q / weights / cu_starts / cu_ends for every variant that is not in _LDS942_VARIANTS:

# Most kernels require seq_len padded to a multiple of the rows a block
# owns, so every block owns exactly that many. Padded rows get empty windows
# (start == end == 0) so the kernel writes nothing for them; the output is
# sliced back to the original seq_len after the launch. The gfx942 LDS
# variants handle a short last block in-kernel and skip the copies.
lds942 = variant in _LDS942_VARIANTS
if lds942:
seq_len_padded = seq_len
else:
seq_len_padded = (
(seq_len + _ROWS_PER_BLOCK - 1) // _ROWS_PER_BLOCK
) * _ROWS_PER_BLOCK
if seq_len_padded != seq_len:
pad = seq_len_padded - seq_len
Q = torch.cat([Q, Q.new_zeros((pad, num_heads, head_size))], dim=0)
weights = torch.cat([weights, weights.new_zeros((pad, num_heads))], dim=0)
cu_starts = torch.cat([cu_starts, cu_starts.new_zeros(pad)], dim=0)
cu_ends = torch.cat([cu_ends, cu_ends.new_zeros(pad)], dim=0)

Main already dropped that padding in #6001: every builder (including all gfx950 LDS variants) masks a short last block in-kernel, and the module docstring still says "Query rows are not padded". The new gfx942 LDS kernels correctly skip the pad; everything else, including all gfx950 variants, gets the old host copies back.

On an unaligned seq_len one call therefore goes from 1 GPU kernel to 9 (zeros + cat for the four tensors, then the real kernel). Outputs stay bitwise identical to main.

Reproduce on gfx950 (MI355X)

# worktrees at the PR merge-base and at this PR tip
git fetch origin main pull/6164/head:pr/6164
MB=$(git merge-base origin/main pr/6164)   # a97e99922...
git worktree add --detach /tmp/aiter-base $MB
git worktree add --detach /tmp/aiter-pr   pr/6164

# flydsl matching aiter's pin
pip install --target /tmp/flydsl042 'flydsl==0.4.2'

# copy the PR test harness onto the base worktree so both sides run the same cases
cp /tmp/aiter-pr/op_tests/test_flydsl_fp8_mqa_logits.py \
   /tmp/aiter-base/op_tests/test_flydsl_fp8_mqa_logits_pr6164harness.py

Correctness (both sides pass):

# default + packed-shapes verify
(cd /tmp/aiter-pr && HIP_VISIBLE_DEVICES=0 PYTHONPATH=/tmp/flydsl042 \
  FLYDSL_RUNTIME_CACHE_DIR=/tmp/flydsl_cache_pr \
  python3 op_tests/test_flydsl_fp8_mqa_logits.py --scenario verify)
# -> all 73 case runs passed

(cd /tmp/aiter-base && HIP_VISIBLE_DEVICES=0 PYTHONPATH=/tmp/flydsl042 \
  FLYDSL_RUNTIME_CACHE_DIR=/tmp/flydsl_cache_base \
  python3 op_tests/test_flydsl_fp8_mqa_logits_pr6164harness.py --scenario verify)
# -> all 73 case runs passed

# same for --packed-shapes (36/36 both sides)

Latency A/B with the PR's own graph-replay bench (aligned control vs unaligned / packed):

# model shapes are all multiples of RPB -> geomean PR/base ≈ 1.00 (control)
(cd /tmp/aiter-base && HIP_VISIBLE_DEVICES=0 PYTHONPATH=/tmp/flydsl042 \
  FLYDSL_RUNTIME_CACHE_DIR=/tmp/flydsl_cache_base \
  python3 op_tests/test_flydsl_fp8_mqa_logits_pr6164harness.py --scenario bench --model-shapes)
(cd /tmp/aiter-pr && HIP_VISIBLE_DEVICES=0 PYTHONPATH=/tmp/flydsl042 \
  FLYDSL_RUNTIME_CACHE_DIR=/tmp/flydsl_cache_pr \
  python3 op_tests/test_flydsl_fp8_mqa_logits.py --scenario bench --model-shapes)

# packed multi-seq chunks (most s_q not a multiple of RPB) -> geomean ~1.20× slower
... --scenario bench --packed-shapes

# deliberately odd rows -> geomean ~1.31× slower
... --scenario bench \
  -s 1023,1023 4095,4095 8191,8191 16383,16383 4095,36863 8191,40959 \
  -w causal --head-dim 128 --clean-logits 0 1 --num-heads 64

Spot measurements I got on one MI355X (steady-state CUDA-graph replay, no LLC flush; same seed / inputs):

shape base µs PR µs note
8192×8192 H64 326 326 aligned control
8191×8191 H64 330 431 +30%, 1→9 kernels
8191×8191 H32 193 260 +35%
16383×16383 H128 2203 2508 +14%
1023×1023 H64 18 47 2.5×
7×20007 H64 12 29 2.4×

A profiler on the unaligned shapes shows the PR launching zeros / vectorized_elementwise cats around the FlyDSL kernel; base launches only the FlyDSL kernel.

Requested fix

Please drop the host pad again and always pass the real seq_len, the same way main does after #6001. Keep _LDS942_SPLIT_POLICY for the new gfx942 LDS variants only. That is a ~27-line deletion in flydsl_fp8_mqa_logits; after that change on this box, the unaligned shapes above matched main again (73/73 and 36/36 still green).

Sketch of the resulting launcher body:

# No torch.full even when clean_logits: the kernel writes -inf itself.
aligned_size = 256
seq_len_kv_aligned = (seq_len_kv + aligned_size - 1) // aligned_size * aligned_size
logits = torch.empty(
    (seq_len, seq_len_kv_aligned),
    dtype=torch.float32,
    device=Q.device,
)[:, :seq_len_kv]
...
scalars = (int(seq_len), int(seq_len_kv), int(logits.stride(0)))
...
split_pol = _LDS942_SPLIT_POLICY if variant in _LDS942_VARIANTS else None
num_splits = _auto_num_splits(
    seq_len, seq_len_kv, _ROWS_PER_BLOCK, _BKV, Q.device.index,
    split_pol, blocks_per_cu,
)

Happy to re-check on gfx950 after the push.

…tical to main

The merge with main reintroduced host-side padding of Q/weights/cu_starts/
cu_ends to a multiple of the block's rows for every variant outside the gfx942
LDS set. main dropped that in ROCm#6001 (every builder masks a short last block
in-kernel), so on gfx950 an unaligned seq_len went from one launch to nine and
regressed (reported on MI355X). Pass the real seq_len to every variant again,
as main does; only the split policy stays variant-specific (_LDS942_SPLIT_POLICY
for the gfx942 LDS variants).

Also hard-gate the PR to gfx942: revert the shared direct-load builder
(_build_kernel_mfma_r_w, used by gfx950's direct-load variants) to main's
version. The empty-row union-window change only existed to absorb padded rows,
and the hoisted _fn_to_fnuz_i64 now lives in the gfx942 LDS module, which is
its only other user. fp8_mqa_logits.py is now byte-identical to main.

Add a launch-free launcher-contract check to the op test (real seq_len, no
host cat/new_zeros) that runs on every arch.

Signed-off-by: Aakif Nawaz <aakif.nawaz@amd.com>
@zufayu
zufayu requested review from zhiding512 and removed request for coderfeli November 11, 2026 06:54
@zufayu zufayu assigned zhiding512 and unassigned coderfeli Nov 11, 2026
@zufayu
zufayu requested a review from coderfeli November 11, 2026 06:54
@akii96

akii96 commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @zhiding512, good catch, and thanks for the detailed repro. You were right: the merge with main brought the host-side row padding back for every non-LDS variant.

Fixed in cd113ea:

  • Dropped the host pad entirely, so every variant gets the real seq_len again, same as main after [FlyDSL] fp8_mqa_logits: drop the host row-pad copies #6001. _LDS942_SPLIT_POLICY is kept for the gfx942 LDS variants only.
  • Also reverted the shared direct-load builder (fp8_mqa_logits.py), which is now byte-identical to main. All the new stuff (variants, hints, auto-selection) sits behind _ARCH == "gfx942", so gfx950 should be exactly main.
  • Added a small launch-free verify_launcher_contract check to the op test that fails if the launcher ever pads rows again.

On an MI355X, your unaligned shapes now match main (e.g. 1023² H32 26.86 → 26.80 µs, 4095×36863 H64 1638 → 1635 µs), and cross-compiled IR/ISA is identical to main for all 28 variant × clean_logits combos I checked. A rerun on your box would be great whenever you get a chance.

… policy

Rework the gfx942 LDS-staged kernel's inner loop and launch policy, keeping
its algorithm and variants:

- Software-pipelined MFMA chains: B fragments read one n-tile ahead, each
  chain's ReLU x weight epilogue interleaved under the next chain's MFMAs,
  and the next tile's DMA issued after the tile's last LDS read and spread
  over the last MFMA chains, so the loop-top vmcnt is exact.
- Rows pre-permuted per lane group, so the head reduce-scatter needs no
  selects; it is staged across MFMA chains, and each tile's last reduce and
  store is carried past the next barrier.
- One DMA descriptor per KV tile, all window loads issued up front, and
  paired non-temporal RPW=2 stores and a dwordx4 -inf fill.
- grid.y slices of decreasing size, so the last blocks to start are the
  smallest.
- Compile the LDS variants with the SLP vectorizer off, so the epilogue
  stays scalar v_fmac that issues under the MFMAs. 128 heads with an FN
  operand compile at waves_per_eu=2, which removes its VGPR spills.
- Split grid.y further for long cached-prefix windows, sized from the
  variant's waves_per_eu, so graph capture and eager calls pick the same
  split; cap the split count so the slice bounds fit in i32.
- RPW=2 from 4096 rows without a cached prefix, for every head count.

gfx950 and the direct-load kernel are unchanged.

Signed-off-by: Aakif Nawaz <aakif.nawaz@amd.com>
@akii96

akii96 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a new commit from Amir (@aghamari) that makes the gfx942 LDS kernel faster (thanks for the great work!). Variant names and gfx950 are unchanged.

  • The matrix math and the ReLU × weight step now overlap instead of waiting on each other.
  • The next KV tile loads while the current one is still being computed.
  • Fewer instructions per tile for the head sum, the stores and the -inf fill.
  • Long-context calls (cached prefix ≥16k) are split across more blocks, so the GPU stays busy until the end.
  • 128 heads with FN inputs no longer spill registers.
  • At 4096+ rows without a prefix, 2 rows per wave is now used for every head count (the test pin is updated).

Results on MI325X vs the previous commit:

  • Long-context indexer shapes (32k–128k KV): ~1.3× faster.
  • --model-shapes bench: ~1.25× faster (FlyDSL over Triton goes from 5.7× to 7.1×).
  • All verify suites pass (default, --packed-shapes, --full, FN inputs).

will add E2E validation numbers soon!

@akii96
akii96 requested a review from aghamari November 12, 2026 09:42

This branch has not been deployed

No deployments
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.

6 participants