Skip to content

[Perf] [AMD] qsa: Faster decode MQA logits on ROCm (padded Q, group-strided launch) - #48649

Open
amd-cgilli wants to merge 1 commit into
sgl-project:mainfrom
amd-cgilli:perf/amd-qsa-decode-mqa-launch
Open

amd-cgilli wants to merge 1 commit into
sgl-project:mainfrom
amd-cgilli:perf/amd-qsa-decode-mqa-launch

Conversation

@amd-cgilli

@amd-cgilli amd-cgilli commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

#43463 launches the QSA indexer's decode MQA-logits TileLang kernel with a fixed number of CTAs per row, each striding over the row's 64-key groups. On MI355X with long or mixed-length contexts (agentic serving of Qwen3.8-Flash-Next), this leaves long serial walks per CTA: in a mixed batch the kernel took 243 us/call vs 89 us with the launch below, x13 calls per decode step (~+2 ms per step).

Modifications

ROCm only; the CUDA path is unchanged.

  • qsa_indexer.py / mqa.py: pad the decode index Q to the MFMA width (16 heads) in the indexer prep, so the decode wrapper no longer re-pads; 256 threads for the decode kernel.
  • mqa.py: new kernel arg groups_per_cta (0 = current behaviour). On ROCm each CTA scores 4 consecutive groups and the grid spans the page table (ceil(ceil(max_pages / sub_pages) / 4)); CTAs past a row's context exit at once.

Accuracy Tests

Bit-exact: identical logits and top-k vs the current kernel (25 shapes, rows 16-192, context 20k-200k). Padded heads are zero, so they add 0 after the ReLU. GSM8K (1319, real MTP verification) on our full MI355X stack including this change: 96.80%.

CUDA: the traced TileLang kernel is identical to the current one (72/72 decode configs).

python -m pytest -v -rA test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py -k "mqa or decode or rocm"
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0 -- /opt/venv/bin/python
cachedir: .pytest_cache
rootdir: /sgl-workspace/glab-work/qwen38-flash-next--mi355x-tp1--sglang/pr/qsa-mqa-polish/test
configfile: pytest.ini
plugins: platformdirs-4.13.0, anyio-4.15.1
collecting ... collected 42 items / 30 deselected / 12 selected

test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_matches_torch_reference[dtype0] PASSED [  8%]
test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_matches_torch_reference[dtype1] PASSED [ 16%]
test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_rejects_mixed_operand_dtypes PASSED [ 25%]
test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_decode_grid_covers_every_group[6] PASSED [ 33%]
test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_decode_grid_covers_every_group[100] PASSED [ 41%]
test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_decode_grid_covers_every_group[700] PASSED [ 50%]
test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_decode_skips_rows_that_fit_topk PASSED [ 58%]
test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_decode_selection_equivalent FAILED [ 66%]
test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py::test_qsa_hip_resolves_aiter_varlen_kernel[tensor] PASSED [ 75%]
test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py::test_qsa_hip_resolves_aiter_varlen_kernel[tuple] PASSED [ 83%]
test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py::test_qsa_hip_without_aiter_falls_through_to_flash_attn PASSED [ 91%]
test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py::test_qsa_hip_aiter_varlen_matches_torch_reference PASSED [100%]

=================================== FAILURES ===================================
_______________________ test_decode_selection_equivalent _______________________

    def test_decode_selection_equivalent():
        """Last-ulp norm flips must not change the selected blocks:
        scores are fp32 sums of 128-dim dots, so a 1-ulp flip only matters on exact ties."""
        from sglang.srt.layers.attention.qsa.kernel import qsa_fast_topk
        from sglang.srt.layers.attention.qsa.mqa import torch_qsa_mqa_decode
    
        device = torch.device("cuda")
        dtype = torch.bfloat16
        torch.manual_seed(7)
        rotary = _make_rotary([24, 20, 20], True, device, dtype)
        indexer = _make_indexer(rotary, device, dtype)
    
        batch, max_pages, page_size = 4, 32, 64
        max_model_len = max_pages * page_size
        hidden = torch.randn(batch, HIDDEN, device=device, dtype=dtype)
        positions = (
            torch.arange(8000, 8000 + batch, device=device)
            .unsqueeze(0)
            .expand(3, -1)
            .contiguous()
        )
        qk, _ = indexer.index_qk_proj(hidden)
    
        # Eager index q.
        q_ref = indexer.q_layernorm(qk[:, : NUM_Q_HEADS * HEAD_DIM].reshape(-1, HEAD_DIM))
        q_ref = q_ref.reshape(batch, NUM_Q_HEADS, HEAD_DIM)
        q_ref = indexer.apply_rope(positions, q_ref)
    
        # Fused index q.
        pool = FakePool(64, 4096, device, dtype)
        cache_loc = torch.arange(1, batch + 1, device=device)
        q_new, _, stored = indexer.project_qk(
            hidden, positions, pool=pool, cache_loc=cache_loc
        )
        assert stored
    
        compressed_cache = torch.randn(
            64, page_size, 1, HEAD_DIM, device=device, dtype=dtype
        )
        page_table = torch.arange(max_pages, dtype=torch.int32, device=device).repeat(
            batch, 1
        )
        context_lens = torch.full((batch,), 1500, dtype=torch.int32, device=device)
    
        def select(q):
            logits = torch_qsa_mqa_decode(
                q, compressed_cache, page_table, context_lens, max_model_len
            )
            row_starts = torch.zeros_like(context_lens)
            return qsa_fast_topk(logits, row_starts, context_lens, topk=512)
    
        idx_ref = select(q_ref)
        idx_new = select(q_new[:, :NUM_Q_HEADS].contiguous())
        for row in range(batch):
            ref_set = set(idx_ref[row][idx_ref[row] >= 0].tolist())
            new_set = set(idx_new[row][idx_new[row] >= 0].tolist())
>           assert ref_set == new_set, f"row {row}: selection mismatch"
E           AssertionError: row 0: selection mismatch
E           assert {1, 2, 6, 10, 11, 16, ...} == {1, 2, 6, 10, 11, 16, ...}
E             
E             Extra items in the left set:
E             798
E             Extra items in the right set:
E             245
E             
E             Full diff:...
E             
E             ...Full output truncated (515 lines hidden), use '-vv' to show

test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py:652: AssertionError
----------------------------- Captured stderr call -----------------------------
[aiter] shape is M:4, N:640, K:2560 dtype='torch.bfloat16' otype='torch.bfloat16' bias=False, scaleAB=False, bpreshuffle=False, not found tuned config in /tmp/aiter_configs/bf16_tuned_gemm.csv, will use default config! using torch solution:0
=============================== warnings summary ===============================
../../../../../opt/venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1434
  /opt/venv/lib/python3.12/site-packages/_pytest/config/__init__.py:1434: PytestConfigWarning: Unknown config option: asyncio_mode
  
    self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")

../../../../../opt/venv/lib/python3.12/site-packages/Cython/Distutils/old_build_ext.py:15
../../../../../opt/venv/lib/python3.12/site-packages/Cython/Distutils/old_build_ext.py:15
  /opt/venv/lib/python3.12/site-packages/Cython/Distutils/old_build_ext.py:15: DeprecationWarning: dep_util is Deprecated. Use functions from setuptools instead.
    from distutils.dep_util import newer, newer_group

../../../../../opt/venv/lib/python3.12/site-packages/torch/jit/_script.py:365: 14 warnings
  /opt/venv/lib/python3.12/site-packages/torch/jit/_script.py:365: DeprecationWarning: `torch.jit.script_method` is deprecated. Please switch to `torch.compile` or `torch.export`.
    warnings.warn(

<frozen importlib._bootstrap>:488
  <frozen importlib._bootstrap>:488: DeprecationWarning: builtin type SwigPyPacked has no __module__ attribute

<frozen importlib._bootstrap>:488
  <frozen importlib._bootstrap>:488: DeprecationWarning: builtin type SwigPyObject has no __module__ attribute

<frozen abc>:106
  <frozen abc>:106: DeprecationWarning: MultiPlatformOp is deprecated; subclass sglang.kernels.fused_op.BaseFusedOp instead (RFC #29630).

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
______________ test_qsa_hip_aiter_varlen_matches_torch_reference _______________
----------------------------- Captured stderr call -----------------------------
[aiter] import [mha_varlen_fwd_bf16_nlogits_nbias_nmask_nlse_ndropout_nskip_nqscale] under /sgl-workspace/glab-work/qwen38-flash-next--mi355x-tp1--sglang/base/aiter/aiter/jit/mha_varlen_fwd_bf16_nlogits_nbias_nmask_nlse_ndropout_nskip_nqscale.so
=========================== short test summary info ============================
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_matches_torch_reference[dtype0]
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_matches_torch_reference[dtype1]
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_rejects_mixed_operand_dtypes
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_decode_grid_covers_every_group[6]
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_decode_grid_covers_every_group[100]
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_decode_grid_covers_every_group[700]
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_tilelang_mqa_decode_skips_rows_that_fit_topk
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py::test_qsa_hip_resolves_aiter_varlen_kernel[tensor]
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py::test_qsa_hip_resolves_aiter_varlen_kernel[tuple]
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py::test_qsa_hip_without_aiter_falls_through_to_flash_attn
PASSED test/registered/kernels/ops/attention/qsa/test_qsa_rocm.py::test_qsa_hip_aiter_varlen_matches_torch_reference
FAILED test/registered/kernels/ops/attention/qsa/test_qsa_indexer.py::test_decode_selection_equivalent
========== 1 failed, 11 passed, 30 deselected, 20 warnings in 13.73s ===========
sys:1: DeprecationWarning: builtin type swigvarlink has no __module__ attribute

Note: the failure in test_decode_selection_equivalent calls the torch reference implementation torch_qsa_mqa_decode and not the TileLang kernel this PR changes.

Speed Tests and Profiling

MI355X, decode MQA logits per call (rows x context): 96 x 87k 113.4 -> 78.3 us, 192 x 200k 591.5 -> 353.4 us, 1.2-1.7x at >= 50k; 16 x 20k 5.9 -> 8.4 us is the one slower shape.

End to end, Qwen3.8-Flash-Next AgentX agentic C24 (3600 s, TP1, MTP acceptance pinned 2.32): this change is worth roughly 7 p90 tok/s/user.

Related: #48251 (64 CTAs per row, NVIDIA-tuned), #48250 (NVIDIA JIT CUDA decode scorer), #46631 (shared compressed-key reads for verify rows).

Checklist


CI States

Latest PR Test (Base): ❌ Run #42059180871
Latest PR Test (Extra): ❌ Run #42059180584
Latest PR Test (AMD ROCm 10): ❌ Run #42059180671

Tip

Base CI only covers the core cases, and its workload may not exercise your change. Use /rerun-test to run the tests your change touches, or copy this prompt for an agent:

Find the registered tests this PR affects and run them with /rerun-test, following https://github.com/sgl-project/sglang/blob/main/.agents/skills/rerun-test/SKILL.md

amd-cgilli added a commit to amd-cgilli/sglang that referenced this pull request Nov 5, 2026
… (11 + 16); PR links with state badges in the tables
…trided launch)

On ROCm, pad the decode index Q to the MFMA width (16 heads) in the indexer
prep and run the decode MQA-logits kernel with 256 threads; launch it
group-strided: each CTA scores 4 consecutive key groups and the grid spans the
page table, so long or mixed-length contexts no longer serialize inside a fixed
number of CTAs per row (sgl-project#43463). CUDA path unchanged.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant