Skip to content

[Test] Sweep the MQA logits indexer's M and N across prefill and decode - #5434

Open
zhiding512 wants to merge 2 commits into
mainfrom
zhiding512/mqa-logits-op-test
Open

zhiding512 wants to merge 2 commits into
mainfrom
zhiding512/mqa-logits-op-test

Conversation

@zhiding512

@zhiding512 zhiding512 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds op_tests/test_mqa_logits.py, an M x N sweep for the MQA-logits lightning
indexer, with one table for prefill and one for decode.

Motivation

The model fixes the indexer's head geometry -- KV is a single head, head_dim
is 128, Q has 32 or 64 heads -- so M (query rows) and N (KV length) are the axes
that decide which kernel config actually runs. Nothing in-tree sweeps them:

  • op_tests/triton_tests/attention/test_fp8_mqa_logits.py is a correctness UT
    that tops out at (s_q, s_k) = (1024, 1560), plus two >2 GiB regression shapes.
  • bench_fp8_mqa_logits.py and bench_deepgemm_attention.py each take one
    shape per process and report no accuracy.

So there was no single place to answer "what does this op do across the shapes
a model actually serves, and is it still correct there".

Changes

One new file. Prefill and decode are different kernels with different calling
conventions, so they get one @benchmark function and one table each:

phase kernel shape mapping
prefill fp8_mqa_logits contiguous KV [N, 128], Q [M, H, 128], causal chunked-prefill window
decode deepgemm_fp8_paged_mqa_logits paged cache, Q [B, next_n, H, 128], M = B * next_n

Decode reproduces ATOM's Indexer._score_topk_decode convention: preshuffled
64-token KV blocks, ChunkK=256, WavePerEU=2, caller-owned output buffer, and
randomized (fragmented) block tables rather than a sequential one a real KV
cache never hands the kernel.

Two things worth flagging for review:

  • err measures the kernel, not the quantization. Both references
    dequantize the same fp8 bytes the kernels read, so a non-zero err column is
    a real bug rather than fp8 rounding noise.
  • Prefill's reference is row-sampled, deliberately. The full [H, M, N]
    fp32 score tensor is ~69 TB at H=32/M=8192/N=65664. The sample takes the
    grid edges and the BLOCK_M=2 block seam first (where a row-indexing bug
    lands), then an even spread; the ref_rows column records how many rows were
    actually compared.

TB/s is compulsory traffic only. Prefill re-reads KV per query row, so that
column is a lower bound there and TFLOPS is the metric to read; decode is
genuinely bandwidth-bound and TB/s is the one that matters.

How to run it

python op_tests/test_mqa_logits.py                          # both tables, ~24 s
python op_tests/test_mqa_logits.py -p prefill -hq 64        # one phase, one head count
python op_tests/test_mqa_logits.py -p decode -b 128 256 -n 131072
python op_tests/test_mqa_logits.py -m 4096 -n 8192 -hq 32   # one shape

Every flag is a swept list (-m, -n, -b, -hq, -dh, -mtp, -kb, -c,
-p), so a different model's shapes go in without touching the file. Prefill
skips n < m; -n is shared by both phases.

Performance

Environment: MI355X (gfx950, 256 CU, 288 GB HBM), run on this branch. All 76 shapes report err == 0. Both tables below come from one invocation:

$ python op_tests/test_mqa_logits.py

Prefill: 64 heads is the faster configuration at every shared shape (18/18 of them), at 1215-1990 TFLOPS against 32 heads' 927-1567. Both climb with M -- BLOCK_M=2 engages above M=4096, and the slowest column is M=1024 where the grid cannot fill 256 CUs.

Decode is bandwidth-bound: 1.9-6.2 TB/s of compulsory traffic once batch >= 16. 64 heads roughly doubles decode TFLOPS there (229-740 against 130-373) at the same TB/s, so head count is nearly free and KV traffic is the whole story. batch=1 never reaches that band -- it is launch-bound, with N from 4K to 128K moving latency only from 3.8 to 4.9 us at 32 heads, and 4.3 to 5.8 us at 64.

prefill -- fp8_mqa_logits, 36 shapes

m n num_heads head_dim clean_logits gfx ref_rows triton us triton TFLOPS triton TB/s triton err
1024 4096 32 128 True gfx950 64 32.4379 926.97 0.667221 0
1024 16384 32 128 True gfx950 64 117.634 1131.88 0.625641 0
1024 65664 32 128 True gfx950 64 558.219 979.075 0.506032 0
1024 131072 32 128 True gfx950 64 1072.25 1021.42 0.520865 0
2048 4096 32 128 True gfx950 64 53.2467 968.097 0.802789 0
2048 16384 32 128 True gfx950 64 223.62 1152.43 0.64856 0
2048 65664 32 128 True gfx950 64 1084.19 1000.28 0.51309 0
2048 131072 32 128 True gfx950 64 2281.39 956.37 0.482029 0
4096 4096 32 128 True gfx950 64 71.8233 957.02 1.18278 0
4096 16384 32 128 True gfx950 64 392.175 1226.63 0.734111 0
4096 65664 32 128 True gfx950 64 1793.44 1190.23 0.615522 0
4096 131072 32 128 True gfx950 64 3803.69 1138.2 0.573677 0
8192 16384 32 128 True gfx950 64 538.96 1530.11 1.06434 0
8192 65664 32 128 True gfx950 64 2643.38 1563.07 0.831944 0
8192 131072 32 128 True gfx950 64 5596.76 1522.53 0.776676 0
16384 16384 32 128 True gfx950 64 744.609 1476.72 1.53787 0
16384 65664 32 128 True gfx950 64 4923.7 1566.67 0.891529 0
16384 131072 32 128 True gfx950 64 10841 1521.32 0.800333 0
1024 4096 64 128 True gfx950 64 49.4829 1215.33 0.5248 0
1024 16384 64 128 True gfx950 64 182.682 1457.71 0.426547 0
1024 65664 64 128 True gfx950 63 676.575 1615.6 0.423903 0
1024 131072 64 128 True gfx950 32 1353.72 1618.1 0.415762 0
2048 4096 64 128 True gfx950 64 75.216 1370.67 0.68332 0
2048 16384 64 128 True gfx950 64 311.347 1655.43 0.493603 0
2048 65664 64 128 True gfx950 63 1251.82 1732.66 0.451294 0
2048 131072 64 128 True gfx950 32 2639.77 1653.06 0.419864 0
4096 4096 64 128 True gfx950 64 95.2557 1443.19 1.07345 0
4096 16384 64 128 True gfx950 64 527.009 1825.6 0.57912 0
4096 65664 64 128 True gfx950 63 2301.81 1854.73 0.487098 0
4096 131072 64 128 True gfx950 32 4779.48 1811.64 0.460173 0
8192 16384 64 128 True gfx950 64 849.885 1940.66 0.715673 0
8192 65664 64 128 True gfx950 63 4206.47 1964.49 0.531026 0
8192 131072 64 128 True gfx950 32 8686.85 1961.87 0.50438 0
16384 16384 64 128 True gfx950 64 1170.54 1878.75 1.0374 0
16384 65664 64 128 True gfx950 63 7751.11 1990.38 0.57525 0
16384 131072 64 128 True gfx950 32 16691.4 1976.19 0.52396 0

decode -- deepgemm_fp8_paged_mqa_logits, 40 shapes

batch n num_heads head_dim next_n kv_block gfx m triton us triton TFLOPS triton TB/s triton err
1 4096 32 128 1 64 gfx950 1 3.83215 8.75603 0.146466 0
1 16384 32 128 1 64 gfx950 1 3.81764 35.1573 0.584772 0
1 65664 32 128 1 64 gfx950 1 4.01804 133.876 2.2236 0
1 131072 32 128 1 64 gfx950 1 4.87587 220.215 3.65679 0
16 4096 32 128 1 64 gfx950 16 4.13443 129.854 2.17212 0
16 16384 32 128 1 64 gfx950 16 8.52784 251.82 4.18853 0
16 65664 32 128 1 64 gfx950 16 26.6348 323.138 5.36714 0
16 131072 32 128 1 64 gfx950 16 50.9795 336.995 5.59598 0
64 4096 32 128 1 64 gfx950 64 8.14781 263.566 4.40878 0
64 16384 32 128 1 64 gfx950 64 26.6238 322.642 5.36651 0
64 65664 32 128 1 64 gfx950 64 130.42 263.968 4.38436 0
64 131072 32 128 1 64 gfx950 64 253.459 271.127 4.5022 0
128 4096 32 128 1 64 gfx950 128 15.2951 280.806 4.69717 0
128 16384 32 128 1 64 gfx950 128 46.0177 373.332 6.20964 0
128 65664 32 128 1 64 gfx950 128 238.966 288.131 4.78569 0
128 131072 32 128 1 64 gfx950 128 469.553 292.702 4.86045 0
256 4096 32 128 1 64 gfx950 256 24.7088 347.647 5.81525 0
256 16384 32 128 1 64 gfx950 256 113.332 303.178 5.04278 0
256 65664 32 128 1 64 gfx950 256 458.305 300.471 4.99065 0
256 131072 32 128 1 64 gfx950 256 902.956 304.42 5.05504 0
1 4096 64 128 1 64 gfx950 1 4.31619 15.5482 0.131019 0
1 16384 64 128 1 64 gfx950 1 4.40654 60.9175 0.50758 0
1 65664 64 128 1 64 gfx950 1 4.76812 225.632 1.87469 0
1 131072 64 128 1 64 gfx950 1 5.83102 368.286 3.05851 0
16 4096 64 128 1 64 gfx950 16 4.6917 228.86 1.92852 0
16 16384 64 128 1 64 gfx950 16 10.2378 419.519 3.49554 0
16 65664 64 128 1 64 gfx950 16 28.0953 612.679 5.09053 0
16 131072 64 128 1 64 gfx950 16 52.5745 653.544 5.42749 0
64 4096 64 128 1 64 gfx950 64 9.98877 429.979 3.62329 0
64 16384 64 128 1 64 gfx950 64 27.5884 622.721 5.18867 0
64 65664 64 128 1 64 gfx950 64 135.347 508.72 4.22677 0
64 131072 64 128 1 64 gfx950 64 262.21 524.156 4.35297 0
128 4096 64 128 1 64 gfx950 128 16.981 505.856 4.26268 0
128 16384 64 128 1 64 gfx950 128 46.4103 740.347 6.16875 0
128 65664 64 128 1 64 gfx950 128 241.639 569.889 4.735 0
128 131072 64 128 1 64 gfx950 128 465.772 590.156 4.90108 0
256 4096 64 128 1 64 gfx950 256 27.1009 633.923 5.34185 0
256 16384 64 128 1 64 gfx950 256 116.821 588.247 4.90142 0
256 65664 64 128 1 64 gfx950 256 466.432 590.472 4.90601 0
256 131072 64 128 1 64 gfx950 256 913.856 601.578 4.99594 0

Testing

  • Unit tests added (this is the test)
  • Performance benchmarks run
  • Tested on MI355X (gfx950): 76/76 shapes, err == 0, on this branch
  • black --check and ruff check (0.16.0, the version CI pins) clean on the
    new file
  • Not tested on gfx942 -- SUPPORTED_GFX gates it in and the Triton wrappers
    dispatch per arch, but op_tests CI only has MI35x runners, so nobody has
    run it there yet.

Follow-ups (not in this PR)

  • Variable context lengths -- decode currently runs all sequences at the same
    length, so the kernel's varctx scheduling path is never exercised.
  • MTP: -mtp defaults to 1, so the next_n > 1 decode layout is untested.
  • A FlyDSL candidate alongside the Triton one in the prefill table. It is
    gfx942-only and I have no gfx942 to validate it on, so it is left out rather
    than shipped unrun.

The MQA-logits lightning indexer has its head geometry fixed by the model --
single-head KV, head_dim 128, 32 or 64 Q heads -- so M (query rows) and N (KV
length) are the axes that decide which kernel config runs. Nothing in-tree
swept them: the Triton UT tops out at (1024, 1560) and the two benchmarks take
one shape per process.

Add op_tests/test_mqa_logits.py, one table per phase, because prefill and
decode are different kernels with different calling conventions:

  prefill  fp8_mqa_logits, contiguous KV, causal chunked-prefill window
  decode   deepgemm_fp8_paged_mqa_logits, paged cache, ATOM's convention
           (preshuffled 64-token blocks, ChunkK=256, WavePerEU=2, caller-owned
           output buffer, fragmented block tables)

Both references dequantize the same fp8 bytes the kernels read, so err
isolates the kernel from the quantization. Prefill's reference is row-sampled:
the full [H, M, N] fp32 score tensor is ~69 TB at H=32/M=8192/N=65536, so the
sample takes the grid edges and the BLOCK_M=2 block seam first.
@zhiding512
zhiding512 requested review from a team and a lite review from Copilot September 11, 2026 03:26
@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; main branch always runs both MI35X and MI300X
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 5434 --add-label <label>

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 opt this PR out.

Copilot AI 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.

🟡 Changes recommended

Correctness assertions, decode mask validation, and benchmark methodology/reporting require fixes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a configurable M×N correctness and performance sweep for MQA logits prefill and decode kernels.

Changes:

  • Adds sampled prefill and paged decode validation.
  • Adds randomized cache construction and configurable shape sweeps.
  • Reports correctness and benchmark metrics.
File summaries
File Description
op_tests/test_mqa_logits.py New prefill/decode sweep, validation harness, and reporting.
Review details

Suppressed comments (4)

op_tests/test_mqa_logits.py:192

  • [verified] The PR description says 64-head prefill at M=1024 is 480–630 TFLOPS and below the 32-head results, but the included sweep table reports 1252–1618 TFLOPS for those same rows; this code computes the column directly as flops / us / 1e6. The tuning conclusion is therefore not reproducible from the stated sweep. Author must reconcile the table, measurement run, and performance narrative.
        ret[f"{name} TFLOPS"] = flops / us / 1e6
        ret[f"{name} TB/s"] = nbytes / us / 1e6

op_tests/test_mqa_logits.py:50

  • The stated size is incorrect: [32, 8192, 65536] fp32 elements occupy 68,719,476,736 bytes (about 64 GiB or 69 GB), not 69 TB. The row-sampling rationale remains valid, but this comment overstates the reference memory footprint by three orders of magnitude. Author must correct the unit.
# The full [H, M, N] fp32 score tensor a naive reference builds is ~69 TB at
# H=32/M=8192/N=65536, so prefill accuracy is always checked on a row sample.

op_tests/test_mqa_logits.py:193

  • checkAllclose returns a mismatch ratio and only raises for catastrophic errors; with its default catastrophic_check=False, even a 100% mismatch merely populates this err field. Since this is the numerical correctness check for the sampled prefill rows, the sweep can report a nonzero error and still exit successfully. Author must assert the returned ratio is within the intended tolerance before recording the row.
        ret[f"{name} err"] = err

op_tests/test_mqa_logits.py:268

  • checkAllclose returns a mismatch ratio and only raises for catastrophic errors; with its default catastrophic_check=False, even a 100% mismatch merely populates this err field. The decode sweep therefore can log a failed accuracy result and still exit successfully. Author must assert the returned ratio is within the intended tolerance before recording the row.
        ret[f"{name} err"] = err
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +159 to +175
candidates = {
"triton": lambda: fp8_mqa_logits(
q_fp8, kv_fp8, kv_scales, weights, ks, ke, clean_logits
),
}

valid = int((ke.clamp(max=n) - ks).clamp(min=0).sum().item())
n_pad = (n + 255) // 256 * 256
flops = 2.0 * num_heads * head_dim * valid
read_bytes = q_fp8.nbytes + kv_fp8.nbytes + kv_scales.nbytes + weights.nbytes
# Compulsory traffic only. KV is re-read per query row, so once N*128 stops
# fitting in L2 the achieved bandwidth is well above this number.
nbytes = read_bytes + (m * n_pad * 4 if clean_logits else valid * 4)

ret = {"gfx": get_gfx(), "ref_rows": len(rows)}
for name, fn in candidates.items():
out, us = run_perftest(fn, num_rotate_args=_rotate_args(read_bytes))
Comment on lines +258 to +264
err = checkAllclose(
ref[inside],
out[inside],
rtol=1e-2,
atol=1e-2,
msg=f"{name}: decode b={batch} n={n} h={num_heads} mtp={next_n}",
)
black 26.5.1 and ruff 0.15.18 disagree on the assert wrap, and running
ruff format after black undid black's choice. CI checks black and
ruff check (pinned 0.16.0), not ruff format, so black goes last.
0.16.0 also adds I001, which wants aiter in its own first-party block.
Copilot AI review requested due to automatic review settings September 11, 2026 04:15

Copilot AI 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.

🟡 Changes recommended

Four unresolved findings remain, including three critical issues and one moderate benchmark-validity issue.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

op_tests/test_mqa_logits.py:263

  • [verified] The decode result is likewise only reported: for example, one completely wrong row at batch=64,next_n=1 is below checkAllclose's 5% non-raising threshold, so the advertised correctness sweep can pass with a bad row. Author must assert err == 0 (or otherwise make any mismatch fail) before returning the row.
        err = checkAllclose(
            ref[inside],
            out[inside],
            rtol=1e-2,
            atol=1e-2,

op_tests/test_mqa_logits.py:263

  • [verified] This decode validation has two holes: it only compares ref[inside]/out[inside], ignoring any finite logits outside the causal window, and it never asserts the nonzero error fraction returned by checkAllclose. A future-token write or any numeric mismatch can therefore pass CI. Author must compare out == -float("inf") with ~inside and fail when err != 0.
        err = checkAllclose(
            ref[inside],
            out[inside],
            rtol=1e-2,
            atol=1e-2,

op_tests/test_mqa_logits.py:176

  • [verified] _rotate_args() is passed to run_perftest with zero-argument closures here (and at the decode call on line 258), but run_perftest only deep-copies tensors in its explicit args; it never sees the captured Q/KV/weights/output. The intended cache rotation is therefore never performed, so small shapes can measure warm-cache/L2-hit latency and make the reported bandwidth numbers unreliable. Author must pass the benchmark tensors as explicit run_perftest arguments or explicitly rotate the captured inputs.
        out, us = run_perftest(fn, num_rotate_args=_rotate_args(read_bytes))
  • Files reviewed: 1/1 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment on lines +184 to +190
err = checkAllclose(
ref[inside],
got[inside].float(),
rtol=1e-2,
atol=1e-2,
msg=f"{name}: prefill m={m} n={n} h={num_heads}",
)


@benchmark()
def test_mqa_logits_decode(batch, n, num_heads, head_dim, next_n, kv_block):
Comment on lines +233 to +234
out_logits = torch.full((rows, n), -float("inf"), dtype=dtypes.fp32)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants