Skip to content

Commit e262860

Browse files
anhcvtclaude
andcommitted
[HIP] fp8_mqa_logits: hand-written gfx950 prefill indexer kernel
Adds the prefill 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] for n in [ks[m], ke[m]), -inf elsewhere K is already gathered into a contiguous [N, 128] buffer, so there is no block table. Same call contract as aiter.ops.triton.attention.fp8_mqa_logits. aiter/ops/fp8_mqa_logits.py the op, plus is_supported() csrc/kernels/fp8_mqa_logits.cu module_fp8_mqa_logits op_tests/test_fp8_mqa_logits.py triton vs hip, one fp32 reference The kernel streams K HBM->register with no LDS staging and contracts 32 heads x 32 tokens per mfma_scale_f32_32x32x64_f8f6f4 tile. Heads reduce across lanes with v_permlane32_swap_b32, two query rows at a time, and the per-token scale is applied once after that reduction (kv_scale >= 0, so it commutes with the ReLU). BLOCK_M query rows share one K stream; the warps of a block split the tile range rather than the rows, which keeps several K streams in flight per block. With clean_logits the kernel writes the -inf outside each window itself, so the caller needs no separate fill pass over the whole output. Row groups are dispatched high-m first for N > 2048. Under causal masking a row group's work grows with m, so in natural order the longest-running blocks are dispatched LAST and become the tail; reversing starts them first and lets the short ones fill in behind. Worth +6% at (4096, 4096) and +3% at (8192, 8192), neutral elsewhere, and free -- it only reorders block dispatch. `unroll2` and `reverse_rows` are tri-state (-1 heuristic, 0/1 off/on) so the host can tell "off" from "caller did not choose". 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 the same shapes and six window modes. Beyond causal and cp it covers misaligned, empty, past-end and multi-request windows -- all legal indexer input at a chunk boundary, and where the masking and the -inf fill are easiest to get wrong. It poisons the output buffer with NaN first, so a position no kernel writes fails the -inf mask check instead of passing on allocator leftovers -- that poisoning is a separate call from the timed one, so neither candidate is charged a full-output memset per iteration. Shapes include the real chunk sizes a request is split into, and the reference runs in query-row chunks so those fit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0beb459 commit e262860

7 files changed

Lines changed: 976 additions & 0 deletions

File tree

aiter/jit/optCompilerConfig.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1872,5 +1872,16 @@
18721872
"extra_include": [],
18731873
"verbose": "False",
18741874
"blob_gen_cmd": "''"
1875+
},
1876+
"module_fp8_mqa_logits": {
1877+
"srcs": [
1878+
"f'{AITER_CSRC_DIR}/pybind/fp8_mqa_logits_pybind.cu'",
1879+
"f'{AITER_CSRC_DIR}/kernels/fp8_mqa_logits.cu'"
1880+
],
1881+
"flags_extra_cc": [],
1882+
"flags_extra_hip": ["'-fno-honor-nans'"],
1883+
"extra_ldflags": "None",
1884+
"extra_include": [],
1885+
"verbose": "False"
18751886
}
18761887
}

aiter/ops/fp8_mqa_logits.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (C) 2024-2026, Advanced Micro Devices, Inc. All rights reserved.
3+
"""Hand-written HIP prefill-phase FP8 MQA indexer logits kernel for gfx950.
4+
5+
The dense half of the DeepSeek-V3.2 / GLM-5 sparse-attention lightning indexer:
6+
7+
logits[m, n] = sum_h relu(Q[m,h,:] . K[n,:]) * w[m,h] * kv_scale[n]
8+
9+
K has already been gathered into a contiguous ``[N, 128]`` buffer, so there is no
10+
block table. Drop-in for ``aiter.ops.triton.attention.fp8_mqa_logits``, so the two
11+
can be A/B'd on identical inputs.
12+
13+
Fixed at n_heads=32, head_dim=128 -- the shipped GLM-5-FP8 indexer shape.
14+
``is_supported()`` gates on that, so a caller that also has to serve other shapes
15+
can route them to the Triton kernel rather than trip a TORCH_CHECK.
16+
"""
17+
18+
from torch import Tensor
19+
20+
from ..jit.core import compile_ops
21+
from ..jit.utils.chip_info import get_gfx
22+
23+
MD_NAME = "module_fp8_mqa_logits"
24+
25+
SUPPORTED_GFX = ("gfx950",)
26+
NUM_HEADS = 32
27+
HEAD_DIM = 128
28+
29+
30+
def is_supported(num_heads: int, head_dim: int) -> bool:
31+
"""True when this kernel can run this shape on this device."""
32+
return get_gfx() in SUPPORTED_GFX and num_heads == NUM_HEADS and head_dim == HEAD_DIM
33+
34+
35+
@compile_ops(MD_NAME, fc_name="fp8_mqa_logits")
36+
def fp8_mqa_logits(
37+
q_fp8: Tensor,
38+
k_fp8: Tensor,
39+
kv_scale: Tensor,
40+
weights: Tensor,
41+
cu_seqlen_ks: Tensor,
42+
cu_seqlen_ke: Tensor,
43+
BlockM: int = 0,
44+
SplitN: int = 0,
45+
num_warps: int = 0,
46+
TotalCuCount: int = 256,
47+
clean_logits: bool = True,
48+
unroll2: int = -1,
49+
reverse_rows: int = -1,
50+
out: Tensor | None = None,
51+
) -> Tensor:
52+
"""Prefill indexer logits over a contiguous K buffer.
53+
54+
q_fp8 [M, 32, 128] fp8 k_fp8 [N, 128] fp8
55+
kv_scale [N] f32 weights [M, 32] f32
56+
cu_seqlen_ks/ke [M] i32 -- row m is valid on [ks[m], ke[m]). Either bound may
57+
legally sit outside [0, N); the row is then empty over the part that does.
58+
59+
The zero-valued tunables (BlockM, SplitN, num_warps) mean "use the host
60+
heuristic"; unroll2 and reverse_rows are tri-state, -1 for the heuristic and
61+
0/1 to force off/on. Writes into `out` when given (and returns it), otherwise allocates
62+
[M, N] f32; outside each row's window the kernel writes -inf when clean_logits,
63+
and leaves the buffer untouched otherwise.
64+
"""
65+
...

csrc/include/fp8_mqa_logits.h

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#pragma once
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (C) 2024-2026, Advanced Micro Devices, Inc. All rights reserved.
4+
//
5+
// Hand-written HIP prefill-phase FP8 MQA indexer logits kernel for gfx950.
6+
//
7+
// The dense half of the DeepSeek-V3.2 / GLM-5 sparse-attention lightning indexer:
8+
//
9+
// logits[m, n] = sum_h relu(Q[m,h,:] . K[n,:]) * w[m,h] * kv_scale[n]
10+
// for n in [ks[m], ke[m]), -inf elsewhere
11+
//
12+
// K has already been gathered out of the paged cache into a contiguous [N, 128]
13+
// buffer, so there is no block table. Same contract as
14+
// `aiter.ops.triton.attention.fp8_mqa_logits`, so it drops into the same call site.
15+
//
16+
// Fixed at n_heads=32, head_dim=128 -- the shipped GLM-5-FP8 indexer shape.
17+
18+
// aiter builds csrc without hipify, so the ATen CUDA headers (which pull in
19+
// cuda_runtime_api.h) are unavailable -- use the HIP-flavoured ones, as the rest
20+
// of csrc does.
21+
#include <ATen/hip/HIPContext.h>
22+
#include <ATen/hip/impl/HIPGuardImplMasqueradingAsCUDA.h>
23+
#include <c10/util/Optional.h>
24+
#include <torch/all.h>
25+
#include <torch/extension.h>
26+
#include <cstdlib>
27+
28+
// Writes into `out` when given (and returns it), otherwise allocates [M, N] f32.
29+
// Positions outside a row's [ks, ke) window read -inf when `clean_logits`, and are
30+
// left untouched otherwise.
31+
torch::Tensor fp8_mqa_logits(torch::Tensor q_fp8, // [M, 32, 128] fp8
32+
torch::Tensor k_fp8, // [N, 128] fp8
33+
torch::Tensor kv_scale, // [N] f32
34+
torch::Tensor weights, // [M, 32] f32
35+
torch::Tensor cu_seqlen_ks, // [M] i32
36+
torch::Tensor cu_seqlen_ke, // [M] i32
37+
int64_t BlockM,
38+
int64_t SplitN,
39+
int64_t num_warps,
40+
int64_t TotalCuCount,
41+
bool clean_logits,
42+
int64_t Unroll2, // -1 = host heuristic, 0/1 = off/on
43+
int64_t ReverseRows, // -1 = host heuristic, 0/1 = off/on
44+
std::optional<torch::Tensor> out);

csrc/include/rocm_ops.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2780,3 +2780,21 @@ namespace py = pybind11;
27802780
py::arg("final_lse"), \
27812781
py::arg("q_scale"), \
27822782
py::arg("kv_scale"));
2783+
2784+
#define FP8_MQA_LOGITS_PYBIND \
2785+
m.def("fp8_mqa_logits", \
2786+
&fp8_mqa_logits, \
2787+
py::arg("q_fp8"), \
2788+
py::arg("k_fp8"), \
2789+
py::arg("kv_scale"), \
2790+
py::arg("weights"), \
2791+
py::arg("cu_seqlen_ks"), \
2792+
py::arg("cu_seqlen_ke"), \
2793+
py::arg("BlockM") = 0, \
2794+
py::arg("SplitN") = 0, \
2795+
py::arg("num_warps") = 0, \
2796+
py::arg("TotalCuCount") = 256, \
2797+
py::arg("clean_logits") = true, \
2798+
py::arg("unroll2") = -1, \
2799+
py::arg("reverse_rows") = -1, \
2800+
py::arg("out") = std::nullopt);

0 commit comments

Comments
 (0)