[HIP] [JIT] fp8_paged_mqa_logits: hand-written gfx950 decode indexer kernel - #2
Open
anhcvt wants to merge 1 commit into
Open
[HIP] [JIT] fp8_paged_mqa_logits: hand-written gfx950 decode indexer kernel#2anhcvt wants to merge 1 commit into
anhcvt wants to merge 1 commit into
Conversation
Adds the decode half of the DeepSeek-V3.2 / GLM-5 sparse-attention lightning
indexer as a HIP kernel, alongside the existing Triton/Gluon one:
logits[m, n] = sum_h relu(Q[m,h,:] . K[n,:]) * w[m,h] * kv_scale[n]
K and its per-token dequant scale are co-packed in a paged cache and gathered
through a block table. Same tensor contract as deepgemm_fp8_paged_mqa_logits.
aiter/ops/fp8_paged_mqa_logits.py the op, plus is_supported()
csrc/kernels/fp8_paged_mqa_logits.cu module_fp8_paged_mqa_logits
op_tests/test_fp8_paged_mqa_logits.py triton vs hip, one fp32 reference
K is streamed from the paged cache straight to registers -- no LDS staging on
the K path -- and contracted 32 heads x 32 tokens per
mfma_scale_f32_32x32x64_f8f6f4 tile. Heads reduce across lanes with
v_permlane32_swap_b32 rather than a __shfl_down LDS round-trip, and the
per-token scale is hoisted out of the ReLU/weight loop: kv_scale >= 0, so it
commutes, which also matches the order the torch reference applies it in.
ROWS_PER_BLOCK consecutive next_n rows share one K stream, trading redundant HBM
reads against occupancy; the host picks it, ChunkK, num_warps and SplitKV from
the total KV footprint, and each is overridable.
KVBlockSize folds in as a compile-time constant, instantiated for 1 and 64. The
two take different cache layouts, the pairing production already uses:
KVBlockSize=1 reads the plain co-packed cache, 64 the shuffle_weight((16,16))
preshuffled one.
It is gfx950-only and fixed at n_heads=32/head_dim=128 (the shipped GLM-5-FP8
indexer shape). is_supported() gates on that so a caller serving other shapes can
route them to the Triton kernel rather than trip a TORCH_CHECK.
The test sweeps both kernels over batch, next_n, context length, both block sizes
and uniform/ragged context. Block tables are built from a shuffled pool, so a
kernel that ignores the table and walks the cache linearly fails rather than
passing by accident. next_n up to 6 is what reaches the ROWS_PER_BLOCK=3
instantiation MTP decode runs on, and the reference dequantizes per sequence so
the 128K shapes fit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
PR title tags: |
| p <= context_lens[b] - next_n + n is written; the -inf outside it is the | ||
| caller's, exactly as for `deepgemm_fp8_paged_mqa_logits`. | ||
| """ | ||
| ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The DeepSeek-V3.2 / GLM-5 "lightning indexer" produces the sparse-attention
selection logits. For each query row
mand KV positionn:In the decode path K and its per-token dequant scale are co-packed in a paged
cache and gathered through a block table. This PR adds a hand-written HIP kernel
for that path on gfx950 (CDNA4), alongside the existing Triton/Gluon
deepgemm_fp8_paged_mqa_logits. The prefill half is a separate PR.The decode indexer streams the entire K cache for a handful of FLOPs per token,
so it is squarely memory-bound and the grid carries a
next_naxis -- every oneof a batch's MTP rows re-reads the same K. That redundancy, and the cost of
routing K through LDS, is what this kernel targets.
Technical Details
New op:
aiter/ops/fp8_paged_mqa_logits.pyfp8_paged_mqa_logitsis a drop-in fordeepgemm_fp8_paged_mqa_logits-- sametensors (
q_fp8,kv_cache_fp8,weights,context_lens,block_tables,max_model_len), same contract that only the causal windowp <= context_lens[b] - next_n + nis written and the-infoutside it is thecaller's. Key design points:
and contracted 32 heads x 32 tokens per
mfma_scale_f32_32x32x64_f8f6f4tile.ROWS_PER_BLOCKrows share one K stream. R consecutivenext_nrows arehandled by one block, so the cache is read once per R rows instead of once per
row; grid is
(batch, SplitKV, ceil(next_n / R)). R trades that redundancyagainst occupancy, so the host picks it -- along with
ChunkK,num_warpsandSplitKV-- from the total KV footprintbatch * max_model_len. Each isoverridable.
kv_scale >= 0and ReLU is positive-homogeneous, so thescale is hoisted out of the head sum and applied once per KV column. This also
matches the order the torch reference applies it in.
v_permlane32_swap_b32head reduce, instead of__shfl_down(x, 32)whichlowers to
ds_bpermute_b32-- an LDS round-trip plus anlgkmcntwait.KVBlockSizeis a compile-time constant, instantiated for 1 and 64. Each istied to one cache layout, the pairing production already uses: 1 reads the plain
co-packed cache, 64 the
shuffle_weight(layout=(16,16))preshuffled one.-fno-honor-nansfor the module, so the ReLU is a singlev_max_f32.Without it LLVM must assume a signalling NaN and emits an IEEE canonicalize
first -- two VALU per accumulator value, in the hottest loop of the kernel.
The kernel is gfx950-only and fixed at
n_heads=32, head_dim=128-- the shippedGLM-5-FP8 indexer shape.
is_supported(num_heads, head_dim, kv_block_size)gateson that, so a caller that also serves other shapes can route them to the Triton
kernel rather than trip a
TORCH_CHECK. This mirrors how_should_use_asm_kernelgates the head_size=128-only ASM paged-attention kernelin
aiter/ops/attention.py.Files added / changed:
aiter/ops/fp8_paged_mqa_logits.py-- the op and its support gatecsrc/kernels/fp8_paged_mqa_logits.cu-- kernel and host dispatchcsrc/include/fp8_paged_mqa_logits.h,csrc/pybind/fp8_paged_mqa_logits_pybind.cucsrc/include/rocm_ops.hpp,aiter/jit/optCompilerConfig.json--module_fp8_paged_mqa_logitsop_tests/test_fp8_paged_mqa_logits.py-- correctness + perf sweepTest Plan
op_tests/test_fp8_paged_mqa_logits.pyruns Triton and HIP on identical inputsand grades both against one fp32 torch reference (a port of vLLM's
fp8_paged_mqa_logits_torch). Gates are an exact-infmask match pluscalc_diff < 1e-3andcheckAllclose; tolerances are not widened.The sweep is the cartesian product of
batch in {1,4,16,64},next_n in {1,2,4,6},heads in {32,64},head_dim=128,kv_len in {1024, 8192, 32768, 131072},KVBlockSize in {1,64}andvar_ratio in {0.0, 0.3}-- 512 cases, 256 of which the HIP kernel supports.Points worth calling out:
next_nup to 6. The host clampsROWS_PER_BLOCKtonext_n, so a sweepstopping at 2 can never reach the R=3 instantiation MTP decode actually runs on.
var_ratio 0.3draws each length from +/-30% ofkv_len).A uniform batch gives every sequence the same tail tile and the same causal
boundary, so a kernel deriving its bounds from one sequence -- or from
max_model_len-- would pass.kernel that ignores the table and walks the cache linearly fails rather than
passing by accident.
nanrather thanreporting a wrong-but-fast number, and any case dropped for lack of memory is
logged by name so a short table cannot read as full coverage.
Test Result
All correctness gates pass on gfx950 across the sweep; per-case
hip errmatchestriton errexactly.Performance on MI355x/gfx950,
heads=32,head_dim=128, ragged context(
var_ratio=0.3),run_perfteston an otherwise idle GPU -- 72 shapes:Summarised over the full 72-shape sweep:
KVBlockSize=1KVBlockSize=64(preshuffled)The win is concentrated on the
KVBlockSize=1path, where the HIP kernel isfaster on every shape measured and the gap widens with
next_n(geomean 1.60x atnext_n=1, 1.98x at 2, 2.44x at 6) -- which is what the shared-K-stream designpredicts, since R only has rows to amortise over once
next_n > 1.On the preshuffled
KVBlockSize=64path the two are at parity overall(geomean 1.01x): the HIP kernel wins by ~1.2-1.3x at high
next_nand loses by upto 0.69x on the small end, where its prologue is not amortised. It is reported
here rather than hidden -- with
is_supported()in place a caller can pick perconfiguration, and the small-
next_npreshuffled corner is the obvious nexttarget.
Submission Checklist