Skip to content

[FlyDSL] Paged mla indexer - #4221

Open
fhuizing wants to merge 52 commits into
ROCm:mainfrom
fhuizing:paged_MLA_indexer
Open

fhuizing wants to merge 52 commits into
ROCm:mainfrom
fhuizing:paged_MLA_indexer

Conversation

@fhuizing

@fhuizing fhuizing commented Jul 13, 2026

Copy link
Copy Markdown

Motivation

Follow-up to PR #3913 (dense FP8 MQA-logits FlyDSL). This PR adds the paged / decode lightning-indexer path for gfx950 (CDNA4).

The DeepSeek indexer produces sparse-attention selection logits during decode. For each query row m and KV position n:

logits[m, n] = sum_h ReLU(<Q[m, h, :], K[n, :]> * kv_scale[n]) * weights[m, h]

Q and K are fp8 e4m3; weights, kv_scale, and the output are f32. K and its per-token scale are co-packed in a preshuffled paged cache (KVBlockSize=64) and gathered through a block table.

Ragged next_n for confidence-scheduled verification

The motivation for per-sequence next_n is the same problem vLLM describes for DSpark adaptive verification [1]: a static num_speculative_tokens is not optimal across concurrencies, because “per-position acceptance decays fast” and a low-probability tail token “costs a slot in every verification batch.”

From that post: DSpark’s confidence head “scores each drafted token’s chance of surviving verification,” and the scheduler keeps a contiguous prefix per request. “Slots compete across requests: position 5 of a confident request can outrank position 1 of a low-confidence one.” The decode step is therefore varlen. vLLM captures graphs with a promised max_query_len = num_speculative_tokens + 1 so “one graph serves any mix of 1 to num_speculative_tokens + 1 tokens per request.” On SM100 they note that this needs a varlen indexer (DeepGEMM, landed with vLLM PR #47808).

The indexer on gfx950 must follow that mix. Production Gluon deepgemm_fp8_paged_mqa_logits takes one launch-wide next_n = Q.shape[1], so a trimmed batch is either padded to the longest prefix (wasted Q rows and stores) or split into homogeneous launches. This kernel accepts:

  • compact Q[B, next_n, 32, 128] with next_n ∈ {1..8} (same packed layout as Gluon; no forced pad to 8)
  • optional next_n_lens[B], each in {1..next_n}: live rows for sequence b are the leading min(next_n_lens[b], next_n) rows
  • unused logit rows stay -inf

The compiled kernel still reserves LDS for eight rows (one binary, graph-capture friendly). The compute loop early-exits when row >= nn, so a confidence-trimmed sequence does not replay MFMA / stores for discarded draft slots. That is the gfx950 counterpart of the varlen indexer vLLM cites for SM100 [1].

Scope: gfx942 / KVBlockSize=1 / H=64 were explored earlier in the branch and dropped. The landed kernel is gfx950 H=32 D=128 KVB=64 only.

Technical details

Kernel: aiter/ops/flydsl/kernels/mqa_logits/fp8_paged_mqa_logits_gfx950.py
Public entry: flydsl_fp8_paged_mqa_logits (re-exported from mqa_logits/fp8_paged_mqa_logits.py)

  • Dynamic next_n: rows_per_batch from Q.shape[1]; nn = rows_per_batch if next_n_lens is omitted, else min(next_n_lens[b], rows_per_batch). Q/W LDS fill and logit stores are indexed with pid_batch * rows_per_batch + row.
  • 8-wave CTA (512 threads): waves 0–3 each own one N=16 tile of a 64-token page and loop live Q rows; waves 4–7 NT-G2L the next page into the idle LDS bank (aux=2).
  • MFMA: v_mfma_scale_f32_16x16x128_f8f6f4 via fx.make_mma_atom + fx.gemm. H=32 is two M-atoms. ReLU·w, shuffle_xor(16/32), lanes 0–15 NT-store.
  • LDS: 50,688 B (2×8,448 K banks + 32 KiB Q + 1 KiB W) → occupancy 3 WG/CU.
  • SplitKV: min(real_pages, ceil(total_cu * 3 / B)) targeting that occupancy. Explicit SplitKV remains graph-capture safe.
  • Pages in HBM are always 64 tokens.

Test plan

  • op_tests/flydsl_tests/test_flydsl_fp8_paged_mqa_logits.py — gfx950 correctness vs torch port of vLLM fp8_paged_mqa_logits_torch. Gate: exact -inf mask + calc_diff < 1e-3.
  • op_tests/flydsl_tests/test_flydsl_fp8_paged_mqa_logits_gfx950.py — long-context / SplitKV / wide-output cases, including ragged next_n_lens.
  • op_tests/flydsl_tests/test_flydsl_fp8_paged_mqa_logits_ragged_nn.py — compact uniform next_n ∈ {1,2,4,8} and mixed next_n_lens in one batch (unused rows stay -inf; mutating padded Q rows must not change live logits).

Performance (MI355X / gfx950)

Workload is the Probe_1079 decode histogram: 212 calls, 137 unique Nq (1..256, mean 117.1), scattered randperm pages, KV pool 44362 blocks, max_model_len=258048. 10 iters / 3 warmup. Times in µs.

Ragged mix (FlyDSL-only). Per-sequence next_n_lens sampled in 1..8 (mean 4.56, mean live Q rows 528). This is the confidence-trim shape: one launch, heterogeneous prefixes. Gluon has no next_n_lens and would have to pad to 8 or split launches.

ctx=8192     FlyDSL  71.40 µs
ctx=131072   FlyDSL  1067.41 µs

Uniform compact vs Gluon (deepgemm_fp8_paged_mqa_logits, Triton 3.7.0, ChunkK=256, WavePerEU=2). Same Nq mix; both backends get Q[:, next_n]. Ratio is Gluon / FlyDSL (>1 means FlyDSL is faster).

              ctx=8192 (128 pages)              ctx=131072 (2048 pages)
 next_n     FlyDSL     Gluon    G/F          FlyDSL      Gluon     G/F
     1       37.04     23.22   0.627          585.64     296.28   0.506
     2       39.38     39.57   1.005          608.45     565.08   0.929
     3       48.54     55.66   1.147          680.81     838.12   1.231
     4       57.44     72.34   1.259          836.65    1114.54   1.332
     5       66.69     88.77   1.331          975.66    1386.87   1.421
     6       76.27    105.25   1.380         1129.98    1672.09   1.480
     7       87.19    122.00   1.399         1303.14    1947.69   1.495
     8       97.44    138.12   1.418         1471.31    2224.44   1.512

Crossover is nn≈2 at 8k and between nn=2 and nn=3 at 128k. Gluon wins at a uniform next_n=1; FlyDSL pulls ahead as more Q rows reuse each fetched page. Under the adaptive-verification mix described in [1], mean next_n is not the pad width: our ragged numbers (mean nn=4.56) sit between the uniform nn=4 and nn=5 columns rather than at padded nn=8.

References

  1. vLLM Team, “Adaptive Verification in vLLM: DSpark confidence-scheduled verification,” 14 Aug 2026.

amd-yashagar and others added 5 commits July 10, 2026 09:18
Address PR review feedback (ROCm#4136): ChunkK was hardcoded to
_BLOCK_KV in the public flydsl_fp8_paged_mqa_logits call despite
compile_fp8_paged_mqa_logits already accepting block_kv. Now ChunkK is
forwarded, defaulting to _BLOCK_KV (128) and validated to be a multiple
of MFMA_N before dispatch.
@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:triton-300x Run an additional Triton test job on MI300X in PRs; main branch always runs both MI35X and MI300X
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 4221 --add-label <label>

@fhuizing
fhuizing marked this pull request as ready for review July 14, 2026 12:41
@fhuizing
fhuizing requested a review from a team July 14, 2026 12:41
@nholmber

Copy link
Copy Markdown
Contributor

Yields a consistent ~1.5% E2E improvement for GLM5.2 MXFP4 without accuracy impact.

image

@zufayu
zufayu requested a review from yzhou103 July 15, 2026 02:20
@valarLip
valarLip requested a review from coderfeli July 17, 2026 08:32
Kernel: extend fp8_paged_mqa_logits from KVBlockSize==1 to KVBlockSize>=1
(block-flat co-packed layout: KVB fp8 key rows grouped first, then KVB f32
scales). Per-lane gather resolves each logical KV column independently
through the block table (kv_indices[b, col // KVB] -> block, col % KVB ->
token), so KVB is a compile-time constant that folds to the per-token slot
layout at KVB==1. i32 gather-offset ceiling now accounts for KVB*index_dim.

Test: add --kv-block-size sweep axis (1, 64) and reference block-flat
dequant; convert to a pure correctness test -- drop run_perftest / the
fn() wrapper for a direct kernel call, and remove the timing-derived
us/TFLOPS output columns (gate stays exact -inf mask + calc_diff < 1e-3).

Verified on gfx950 from a fresh compile cache across KVB {1,64}, dtypes
{fnuz,fn}, SplitKV {auto,1,4}, heads/head_dim {64,128}, short/long context.
…ion bench

Support the production shuffle_weight(16x16) KV data layout in the FlyDSL paged
fp8 MQA-logits kernel (Preshuffle=True, KVBlockSize % 16 == 0), making it a true
drop-in for deepgemm_fp8_paged_mqa_logits(Preshuffle=True). Only the intra-block
key-byte offset changes; the block-table gather, co-packed/token-ordered scale
gather, MFMA, and causal mask are unchanged.

- _mqa_logits_common: add load_pack_v8i32_preshuffle (closed-form offset into
  the shuffle_weight(16x16) data section; 8-byte halves stay contiguous).
- fp8_paged_mqa_logits: thread a compile-time preshuffle flag through the kernel
  builder (with a _ps name tag), compile_* cache key, and the public API; replace
  the NotImplementedError guard with the KVBlockSize % 16 == 0 assert.
- test: add a preshuffle path (oracle from the unshuffled cache, kernel fed the
  shuffled copy) and a small dedicated case set (kvb 16/64, D 64/128, FN patch).
- bench_flydsl_vs_gluon: note the A/B is fixed at KVBlockSize=1 (Gluon faults at
  KVBlockSize>1); KVBlockSize>1 / Preshuffle coverage lives in the flydsl test.
- add bench_flydsl_paged_mqa_logits.py: standalone single-config bench (no Gluon)
  for rocprofv3 isolation profiling, covering both block-flat and preshuffle paths.

Validated on gfx950: block-flat and preshuffle correctness pass the unchanged
gate (exact -inf mask + calc_diff < 1e-3); rocprofv3 traces the _ps kernel.
# Conflicts:
#	aiter/ops/flydsl/__init__.py
#	aiter/ops/flydsl/kernels/fp8_mqa_logits.py
…kage

Upstream relocated the mqa-logits kernels into
aiter/ops/flydsl/kernels/mqa_logits/. Move the paged FP8 kernel and its
shared helper module there too for consistency:

  kernels/fp8_paged_mqa_logits.py -> kernels/mqa_logits/fp8_paged_mqa_logits.py
  kernels/_mqa_logits_common.py   -> kernels/mqa_logits/_mqa_logits_common.py

Fix the sibling .tensor_shim imports to ..tensor_shim (now one level up)
and update the public __init__ import path. No behavior change; public API
(flydsl_fp8_paged_mqa_logits) is unchanged.
@zufayu
zufayu requested review from amd-ruitang3 and removed request for yzhou103 August 14, 2026 02:25
fhuizing and others added 9 commits August 14, 2026 14:56
Both paged benchmarks sized the block pool from max_model_len and then
handed blocks out with `pool[counter % num_blocks]`, so the block table
wrapped and every sequence reused the same physical blocks. At B=16,
kv_len=32768 that is a 1024-block pool serving 8192 block references: an
8.65 MB KV working set standing in for 69 MB, small enough to live in
cache. Every number measured against it was therefore a cache benchmark.

That flattered the kernel by ~13% on its own, and distorted the
head-to-head against production Gluon unevenly -- given the honest
footprint FlyDSL loses 31-51% while the reference loses only 1-4%, since
the reference is slow enough that memory is not its constraint. The
reported speedup falls from 1.3-4.2x to 1.7-3.1x.

Size the pool from the blocks the batch actually needs, build the block
table vectorized rather than one element per GPU write, and report the
touched footprint alongside the requested bytes. --pool-blocks shrinks
the pool deliberately for cache-residency studies, and the vs-gluon
bench now fails with a clear message when a shape would exceed the
kernel's 2^31 i32 gather-offset limit instead of tripping an assert
inside the launch path.

Co-authored-by: Cursor <cursoragent@cursor.com>
…apes)

SplitKV was auto-computed with the production Triton host formula, which
ignores how many waves a CTA contains and so drifts with shape: it lands
2.5 waves per SIMD at B=16 but 10 at B=128. Resident wave count is the
lever that matters here -- this kernel is bound by memory throughput
rather than per-wave latency, so too few waves leaves the memory system
idle and too many just queue. Measured across six shapes (batch 4..128,
kv_len 4k..128k) the curve is flat between 2.0 and 2.5 waves/SIMD and
rises steeply either side: 1.0/SIMD costs 33.9us and 4.0/SIMD 29.4us
against 27.6us at the optimum.

Target WavePerEU waves per SIMD directly, accounting for the CTA's wave
count, and default to paged_w1. At equal resident wave count, splitting a
CTA across 2 or 4 waves is slower on every shape measured: a 1-wave CTA
schedules more freely, and the waves have no work to share anyway since
each owns a disjoint set of KV columns with no barrier or LDS between
them.

Worth 4.6-14.4% per shape, 9.3% aggregate, and it removes the shape
sensitivity -- all six shapes now land within 1% of 27.7us for the same
total KV work, where the old defaults spread them over 29.2-32.5us.
Verified against the full 870-case correctness sweep (max error 5.3e-4,
tolerance 1e-3); the block-flat KVBlockSize=1 path is unchanged.

Two public-surface notes for reviewers. This deliberately diverges from
the deepgemm_fp8_paged_mqa_logits host formula it previously mirrored.
And WavePerEU changes meaning, from a multiplier in the old formula to
the target waves per SIMD; its default of 2 is where the curve bottoms
out. FP8_PAGED_MQA_LOGITS_DEFAULT_VARIANT changes paged_w4 -> paged_w1.

Co-authored-by: Cursor <cursoragent@cursor.com>
The kernel was written against raw MLIR builders -- 58 _to_raw() calls and
65 arith.* calls -- for arithmetic FlyDSL already overloads. The worst case
was the store predicate, 17 lines of nested arith.andi/arith.cmpi to say
`(lane_div_N == 0) & (col <= q_limit)`.

Route unsigned division and modulo through new udiv/umod helpers rather
than the bare `//` and `%` operators: fx.Int32 is signed, so `//` lowers to
floordivsi, which expands to several instructions to get negative-operand
rounding right. Every quantity divided here is non-negative, so going via
fx.Uint32 keeps the single-instruction divui/remui the hand-written calls
emitted.

Verified codegen-neutral: the ATT-decoded ISA is byte-identical to the
previous commit, 655 instructions in the same order. 870/870 correctness
cases pass at 5.3e-4, timing is unchanged at 28.0us, and ruff reports the
same findings as before.

Co-authored-by: Cursor <cursoragent@cursor.com>
_mqa_logits_common carried three helpers nothing referenced: fn_to_fnuz_i64
(45 lines -- the dense kernel defines its own local copy), load_pack_i64
(likewise), and i32_add. Removing them is the bulk of the shrink.

Comments and docstrings were 30% of the two files, much of it restating the
code or duplicating the PR description. Trimmed to what a reader cannot
derive: the co-pack layout contract, the ReLU scale-hoist identity, the i32
gather ceiling, the D-reg-to-head mapping, and the warning that a Python
`if` on KVB would become a runtime scf.if and drop branch-local addresses.

932 -> 770 lines across the two files. ISA byte-identical to the previous
commit (655 instructions), 870/870 correctness at 5.3e-4, 27.8us unchanged,
no new ruff findings.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replaces the hand-built scf.ForOp plus `with ir.InsertionPoint(body)` and
trailing scf.YieldOp with the scf.for_ generator, which owns the insertion
point and terminator. Same pattern the fmha_gfx1250 kernel already uses.

Only two lines, but it removes the last raw MLIR region plumbing from the
kernel's main loop. The masked store keeps scf.IfOp: FlyDSL exposes no `if_`
helper, and switching to a predicated store would change codegen.

ISA byte-identical (655 instructions), 870/870 correctness at 5.3e-4,
27.7us.

Also investigated expressing the preshuffle as a FlyDSL layout, as
examples/04-preshuffle_gemm.py does in two lines. Not adopted: that example
indexes a whole contiguous tensor, whereas our access is a paged gather off
a per-lane runtime base. GTensor._linear_offset is element-granular, and
while every preshuffle stride happens to be dword-aligned, a 5-D view would
just replace one multiply-add chain with another for about two lines. It
only pays off alongside FlyDSL-native tensors and copy atoms.

Co-authored-by: Cursor <cursoragent@cursor.com>
The paged gather hand-computed byte offsets and branched on Preshuffle to
pick between two of them. Both layouts are actually the same coordinate
space -- (block, token//16, token%16, K-step, sub-group, lane half) -- and
differ only in the stride vector:

    block-flat  (blk, 4D, D/4, 16,  4,  2)   token c at c*D
    shuffled    (blk, 4D, 4,   256, 64, 2)   token c at (c//16)*16D + (c%16)*16

So the choice moves into make_kv_key_view and the kernel loses its `if
preshuffle` branch entirely; one load_pack_kv serves both. The co-packed
scale tail becomes a (block, token) view whose base absorbs the KVB*D key
offset, replacing the manual scale_byte arithmetic.

load_pack_kv slices the view down to the `sub` axis once rather than
indexing all six coordinates per load. Without that the invariant part of
the address is re-derived in each of the four loads and the kernel grows 49
instructions; with it, 14.

Trade, measured rather than assumed: the kernel file drops 15 lines and a
branch, the shared module gains 21 for the two view constructors, so the
pair is +6 overall. Codegen is 14 instructions larger (669 vs 655) because
the hand-written version exploited disjoint bit-fields to fuse addresses
into v_or3_b32/v_add3_u32 that the general dot product does not reach for.

Speed holds on every path (3 reps each, layout vs previous): preshuffle
27.58 vs 27.83us, block-flat KVB=64 9.78 vs 9.98us, KVB=1 10.20 vs 10.53us.
870/870 correctness at 5.3e-4, no new ruff findings.

Co-authored-by: Cursor <cursoragent@cursor.com>
The sweep crossed seven axes into 864 cases that compiled only 12 distinct
kernels, re-running each about 72 times. The kernel specialises at compile
time on (heads, head_dim, KVBlockSize, Preshuffle); batch, next_n, context
length, SplitKV and the Q dtype are runtime values, and none of the axes
interact -- nothing couples head_dim to SplitKV or the dtype to KVBlockSize
-- so the product was testing interactions that cannot exist. q_dtype alone
cost 432 cases while selecting no different code path on gfx950, where
convert_q_fn is always False.

Now: a full cross of the compile-time axes (16 cases, one per distinct
kernel -- more preshuffle coverage than before, which only hand-picked 4),
each runtime axis varied alone off a base, and four interaction cases where
the tile-range arithmetic genuinely couples. --exhaustive still runs the
original 870 for release validation.

Validated by mutation testing rather than by the suite passing, since a
suite cut too far also passes. Eleven deliberate kernel bugs -- swapped
preshuffle and block-flat strides, causal off-by-one, dropped tail clamp,
wrong lane half, wrong block-table index, dropped kv_scale, wrong head
weight, floor-instead-of-ceil split (needs SplitKV>1), batch-blind output
row (needs batch>1), next_n-blind causal bound (needs next_n>1). The full
suite catches 11/11; the curated matrix also catches 11/11.

29 cases in 8.7s against 870 in 79s. Harness in att_profiles/mutation_check.py.

Co-authored-by: Cursor <cursoragent@cursor.com>
Neither was ever exercised: every case ran the default variant at
ChunkK=128, so paged_w2 and paged_w4 had no coverage at all, and the Tier-1
change of DEFAULT_VARIANT silently moved which one CI touched.

That hole was real, not theoretical. A mutation of the wave-to-column-tile
assignment (`wave * N_TILES_PER_WAVE` -> `wave * (N_TILES_PER_WAVE - 1)`) is
correct by construction at one wave per CTA, where `wave` is always 0, and
wrong for any multi-wave variant. It survived both the curated suite *and*
the original 870-case exhaustive sweep.

Adds variant and chunk_k parameters plumbed through to the kernel, reported
in the results table, and six cases: paged_w2 and paged_w4 at the default
tile width, ChunkK 64 and 256 against the variants that divide them, and a
preshuffle x paged_w4 cross. 35 cases in 9.0s, and the mutation set now runs
12/12 where the exhaustive suite manages 11/12.

Co-authored-by: Cursor <cursoragent@cursor.com>
Docstrings trimmed to what a reader cannot derive -- the oracle's formula
and mask rule, the co-pack byte layout, what the preshuffled copy is for --
dropping the restatement and the cross-references to other files.

_build_inputs returned an 8-tuple that was unpacked over ten lines at the
call site; it now returns a NamedTuple, so the test body reads inp.q_fp8
rather than positional unpacking, and the contract is documented by the
type. Its block table is also built vectorized instead of one element per
GPU write, which is why the suite went from 9.0s to 7.5s. Same values: the
per-case errors are unchanged to the digit.

The pool there is still sized from max_model_len, so sequences share blocks
once the batch outgrows it. Left alone deliberately, with a comment saying
why: harmless for correctness and arguably extra coverage of block reuse,
unlike in the benchmarks where it shrank the measured working set into cache.

Still 35 cases, still 12/12 on the mutation set.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot changed the title Paged mla indexer [FlyDSL] Paged mla indexer Aug 19, 2026
fhuizing and others added 2 commits August 19, 2026 11:08
Revert buffer load/store aux to the upstream IntegerAttr form expected by
flydsl 0.3.1, and route scaled MFMA through the expr wrapper so cbsz/blgp
attributes lower correctly during paged MQA kernel JIT.

Co-authored-by: Cursor <cursoragent@cursor.com>
…MQA logits

Prefetch kv_indices one tile ahead (skip reload when ind_off unchanged) and
carry K-step-0 across the inner column loop to hide VMEM latency on the
production kvb64/preshuffle decode path.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fhuizing

Copy link
Copy Markdown
Author

Update: ATT-guided prefetch on the production FlyDSL path (KVB=64 + preshuffle, B16/kv32k): ~27.6 → ~25.8 µs (−6.4%), ATT total stall −17%.

From WaveScope dev work using fp8_paged_mqa_logits as the optimization testcase (ATT → agent review → kernel change → re-bench).

fhuizing and others added 3 commits August 20, 2026 09:45
Bind loop SSA values via default args (B023) and apply Black formatting.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Some quick comments here. I didn't fully read through the kernel code. There seems to be some comments on gfx942 vs gfx950 support, does it work on both, or only gfx950? The MFMA config I saw has 32x32x64 which is gfx950.

"""Load 32 fp8 bytes as the vector<8xi32> A/B operand of the 32x32x64 MFMA.

Each lane holds 4 K-groups of 8 bytes at ``lane8 + kk*16``. Reads go out as
2-dword loads because a ``v8i8`` buffer_load fails to lower on gfx942.

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 kernel is using 32x32x64 mfma but this comment talks about gfx942, is this relevant?

try:
return torch.cuda.get_device_properties(device_index).multi_processor_count
except Exception: # noqa: BLE001
return 304

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.

should be 256 if this is a gfx950 targeting kernel?

@@ -0,0 +1,335 @@
# SPDX-License-Identifier: MIT

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.

I think benchmarks are usually part of the main tests, not separate. The test will serve both as validation and benchmark.

@@ -0,0 +1,427 @@
# SPDX-License-Identifier: MIT

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.

Separate tests/benchmarks for kernel comparisons probably shouldn't be created.

)


@benchmark()

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.

should this be decorating some other function, now this includes all input building too?

a_pack, b_col, w_frag, kv_scale, m_tiles=M_TILES, k_steps=K_STEPS
)
is_writer = _to_raw((lane_div_N == 0) & (col <= q_limit))
with ir.InsertionPoint(scf.IfOp(is_writer).then_block):

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.

ir/scf should not be usually needed

Comment on lines +20 to +21
from flydsl.expr import arith, range_constexpr, rocdl
from flydsl.expr.numeric import ArithValue

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.

Check if arith or ArithValue are really needed.

``fp8_mqa_logits`` carries its own copies and does not import this.
"""

# No `from __future__ import annotations`: FlyDSL arg typing needs real

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.

remove this comment

Comment on lines +8 to +10
Split out of ``fp8_paged_mqa_logits`` to keep the gather layouts, the MFMA
head-reduce and the output view separable from the kernel body. The dense
``fp8_mqa_logits`` carries its own copies and does not import this.

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.

Could you remove copies from elsewhere? And remove this comment.

while len(ops) < 6:
ops.append(0)
ops.extend([neutral, 0, neutral])
return rocdl.mfma_scale_f32_32x32x64_f8f6f4(result_type, ops)

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.

check if this is already supported more natively in flydsl mma

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

every higher level flydsl way explored seems to result in more sloc.

fhuizing and others added 4 commits August 25, 2026 09:29
amd-sriram added a commit to amd-sriram/vllm that referenced this pull request Aug 26, 2026
The decode half of the sparse-attention indexer runs on aiter's Triton/Gluon
deepgemm_fp8_paged_mqa_logits. ROCm/aiter#4221 adds a gfx950 FlyDSL
implementation of the same kernel, reported at 1.3-4.2x over Gluon on MI355X,
and the decode logits are the indexer's dominant per-step cost at long
context.

Route the gfx950 branch of rocm_fp8_paged_mqa_logits at it when
VLLM_ROCM_USE_AITER_FLYDSL_PAGED_MQA_LOGITS is set, so one build can run both
arms and any aiter without the PR falls back to the existing path. Off by
default while the PR is unmerged.

This is a second call site rather than the module swap used for the prefill
kernel in the preceding commit. The two launchers take the same tensors in
the same order, but the FlyDSL one's remaining knobs are keyword-only with
its own tuned defaults, so sharing a caller would mean forcing the Gluon
ChunkK=256 on it. KVBlockSize is not optional: the kernel asserts it against
the KV cache's own block dimension, so it is passed through along with
Preshuffle, exactly as the Gluon call does.

The FlyDSL path also skips the -inf prefill and the NaN scrub the Gluon path
needs. Gluon writes the full row and leaves NaN in the padding; FlyDSL writes
every column up to its causal bound `context_lens[b] - next_n + n` and
nothing past it (`is_writer = ... & (col <= q_limit)`). That bound is exactly
the decode top-k's read bound, so the untouched tail of the workspace is
never read. Its docstring asks callers for an -inf prefill because a caller
that reads the whole row would need one; this one does not.

The FlyDSL kernel is written against gfx950, so
_flydsl_paged_mqa_logits_kernel() returns None on every other architecture
and the Triton/Gluon kernel is used regardless of the flag.

Signed-off-by: Sriram Kumar <sriramkumar.kishorekumar@amd.com>
amd-sriram added a commit to amd-sriram/vllm that referenced this pull request Aug 26, 2026
The decode half of the sparse-attention indexer runs on aiter's Triton/Gluon
deepgemm_fp8_paged_mqa_logits. ROCm/aiter#4221 adds a gfx950 FlyDSL
implementation of the same kernel, reported at 1.3-4.2x over Gluon on MI355X,
and the decode logits are the indexer's dominant per-step cost at long
context.

Route the gfx950 branch of rocm_fp8_paged_mqa_logits at it when
VLLM_ROCM_USE_AITER_FLYDSL_PAGED_MQA_LOGITS is set, so one build can run both
arms and any aiter without the PR falls back to the existing path. Off by
default while the PR is unmerged.

This is a second call site rather than the module swap used for the prefill
kernel in the preceding commit. The two launchers take the same tensors in
the same order, but the FlyDSL one's remaining knobs are keyword-only with
its own tuned defaults, so sharing a caller would mean forcing the Gluon
ChunkK=256 on it. KVBlockSize is not optional: the kernel asserts it against
the KV cache's own block dimension, so it is passed through along with
Preshuffle, exactly as the Gluon call does.

The FlyDSL path skips the -inf prefill the kernel's docstring asks for. That
request is for a caller that reads the whole row; this one does not. FlyDSL
writes every column up to its causal bound `context_lens[b] - next_n + n` and
nothing past it (`is_writer = ... & (col <= q_limit)`), and that bound is the
decode top-k's read bound, so the tail the fill would cover is never written
and never read.

The sanitize is kept, and is the same bounded sanitize_decode_logits() the
Gluon path calls. Its window is that same causal bound, so it covers exactly
the columns FlyDSL wrote: no more work than the Gluon path already does, and
both paths hand the top-k a workspace in the same state. Keeping it means the
flag toggles the kernel and nothing else, which is what makes the two arms
comparable.

The FlyDSL kernel is written against gfx950, so
_flydsl_paged_mqa_logits_kernel() returns None on every other architecture
and the Triton/Gluon kernel is used regardless of the flag.

Signed-off-by: Sriram Kumar <sriramkumar.kishorekumar@amd.com>
fhuizing and others added 6 commits September 6, 2026 11:38
Land the production indexer mapping (resident Q, depth-4 NT B-ring, SplitKV)
without the v2/v3 experiments or local profiling artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
Compile a separate 16x16x128 kernel per head count so the indexer matches
gluon's ChunkQ=heads set without changing the FlyDSL page pipeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the unused second query row on next_n=1 so MFMA/VALU and store waits match the actual decode shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep next-page K in fixed register banks instead of scf.for yield copies, store one wave-wide page of logits, and prefetch the page table so gather latency stays overlapped.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…path

Route H32/H64 D128 KVB64 preshuffle next_n in {1,2} through the 16x16x128 kernel, keep the 32x32x64 implementation as the generic fallback, and satisfy Black/Ruff.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added the FlyDSL label Sep 7, 2026
fhuizing and others added 5 commits September 8, 2026 12:08
Drop the generic 32x32x64 kernel and fold helpers into the gfx950 decoder so
H32/H64 stays on the tuned 16x16x128 pipeline, with i64 output-row addressing
and idiomatic FlyDSL MMA/scheduling that still matches the previous ISA.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…QA logits

Match the Gluon contract so next_n can be 1..8 without padding Q to 8 rows;
optional next_n_lens still selects live rows per sequence.

Co-authored-by: Cursor <cursoragent@cursor.com>
amd-sriram added a commit to amd-sriram/vllm that referenced this pull request Sep 14, 2026
The decode half of the sparse-attention indexer runs on aiter's Triton/Gluon
deepgemm_fp8_paged_mqa_logits. ROCm/aiter#4221 adds a gfx950 FlyDSL
implementation of the same kernel, reported at 1.3-4.2x over Gluon on MI355X,
and the decode logits are the indexer's dominant per-step cost at long
context.

Route the gfx950 branch of rocm_fp8_paged_mqa_logits at it when
VLLM_ROCM_USE_AITER_FLYDSL_PAGED_MQA_LOGITS is set, so one build can run both
arms and any aiter without the PR falls back to the existing path. Off by
default while the PR is unmerged.

This is a second call site rather than the module swap used for the prefill
kernel in the preceding commit. The two launchers take the same tensors in
the same order, but the FlyDSL one's remaining knobs are keyword-only with
its own tuned defaults, so sharing a caller would mean forcing the Gluon
ChunkK=256 on it. KVBlockSize is not optional: the kernel asserts it against
the KV cache's own block dimension, so it is passed through along with
Preshuffle, exactly as the Gluon call does.

The FlyDSL path skips the -inf prefill the kernel's docstring asks for. That
request is for a caller that reads the whole row; this one does not. FlyDSL
writes every column up to its causal bound `context_lens[b] - next_n + n` and
nothing past it (`is_writer = ... & (col <= q_limit)`), and that bound is the
decode top-k's read bound, so the tail the fill would cover is never written
and never read.

The sanitize is kept, and is the same bounded sanitize_decode_logits() the
Gluon path calls. Its window is that same causal bound, so it covers exactly
the columns FlyDSL wrote: no more work than the Gluon path already does, and
both paths hand the top-k a workspace in the same state. Keeping it means the
flag toggles the kernel and nothing else, which is what makes the two arms
comparable.

The FlyDSL kernel is written against gfx950, so
_flydsl_paged_mqa_logits_kernel() returns None on every other architecture
and the Triton/Gluon kernel is used regardless of the flag.

Signed-off-by: Sriram Kumar <sriramkumar.kishorekumar@amd.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