| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """ROCm gfx942/gfx950 block-sparse GQA prefill kernel for MiniMax-M3. |
| 4 | |
| 5 | Only the prefill path is specialized on CDNA: each 128-token KV block is split |
| 6 | into SUB_K-token sub-tiles to right-size the per-block QK/PV MFMAs. Everything |
| 7 | else -- the decode split-K kernels, the FP8 dtype set, the sparse block size -- |
| 8 | is reused unchanged from ``common.ops.sparse_attn``. |
| 9 | """ |
| 10 | |
| 11 | import torch |
| 12 | |
| 13 | from vllm.models.minimax_m3.common.ops.sparse_attn import ( |
| 14 | _FP8_DTYPES, |
| 15 | _KV_SCALE_NONE, |
| 16 | SPARSE_BLOCK_SIZE, |
| 17 | _kv_scale_args, |
| 18 | minimax_m3_sparse_attn_decode, |
| 19 | ) |
| 20 | from vllm.platforms.rocm import on_gfx950, on_mi3xx |
| 21 | from vllm.triton_utils import tl, triton |
| 22 | |
| 23 | __all__ = ["minimax_m3_sparse_attn", "minimax_m3_sparse_attn_decode"] |
| 24 | |
| 25 | |
| 26 | # Sub-tile width for the prefill kernel's per-block QK/PV GEMMs. gfx950 -> 64, |
| 27 | # gfx942 -> 32 (re-tune with tune_sparse_attn.py). Must divide SPARSE_BLOCK_SIZE. |
| 28 | _SPARSE_ATTN_SUB_K = SPARSE_BLOCK_SIZE // 2 if on_gfx950() else SPARSE_BLOCK_SIZE // 4 |
| 29 | |
| 30 | _SPARSE_ATTN_PREFILL_KWARG: dict | None = None |
| 31 | |
| 32 | |
| 33 | def _sparse_attn_prefill_kwargs() -> dict: |
| 34 | """MFMA + pipeline launch params for the sub-tiled prefill kernel. |
| 35 | |
| 36 | gfx942 and gfx950 share the same params: ``num_warps=1`` keeps one wave |
| 37 | resident on the small per-sub-tile GEMM, ``matrix_instr_nonkdim=16`` / |
| 38 | ``kpack=2`` select the MFMA_16x16 path, and ``num_stages=1`` fits LDS and is |
| 39 | fastest in the sweep. Only the sub-tile width (``_SPARSE_ATTN_SUB_K``) |
| 40 | differs by arch. Empty on other AMD archs. Cached: arch is fixed per process. |
| 41 | """ |
| 42 | global _SPARSE_ATTN_PREFILL_KWARG |
| 43 | if _SPARSE_ATTN_PREFILL_KWARG is None: |
| 44 | kwarg: dict = {} |
| 45 | if on_mi3xx(): |
| 46 | kwarg = { |
| 47 | "num_warps": 1, |
| 48 | "matrix_instr_nonkdim": 16, |
| 49 | "kpack": 2, |
| 50 | "num_stages": 1, |
| 51 | } |
| 52 | _SPARSE_ATTN_PREFILL_KWARG = kwarg |
| 53 | return _SPARSE_ATTN_PREFILL_KWARG |
| 54 | |
| 55 | |
| 56 | # --------------------------------------------------------------------------- |
| 57 | # GQA block-sparse attention (paged). Main heads attend only to the selected |
| 58 | # blocks. BLOCK_SIZE_K == 128 so each selected block is one page. |
| 59 | # --------------------------------------------------------------------------- |
| 60 | # since prefill metadata is sliced from mixed batch metadata, seq_lens and prefix_lens |
| 61 | # might lose pointer alignment, which trigger Triton recompiles. we don't actually |
| 62 | # need pointer alignment for those tensors anyway because we do scalar load. |
| 63 | @triton.heuristics( |
| 64 | { |
| 65 | "BLOCK_SIZE_D": lambda args: triton.next_power_of_2(args["head_dim"]), |
| 66 | "BLOCK_SIZE_H": lambda args: triton.next_power_of_2(args["gqa_group_size"]), |
| 67 | "BLOCK_SIZE_T": lambda args: triton.next_power_of_2(args["max_topk"]), |
| 68 | "BLOCK_SIZE_QH": lambda args: args["BLOCK_SIZE_Q"] |
| 69 | * triton.next_power_of_2(args["gqa_group_size"]), |
| 70 | } |
| 71 | ) |
| 72 | @triton.jit(do_not_specialize_on_alignment=["seq_lens", "prefix_lens"]) |
| 73 | def _gqa_sparse_fwd_kernel( |
| 74 | q_ptr, # [total_q, num_heads, head_dim] |
| 75 | kv_cache_ptr, # main cache: [num_blocks, num_kv_heads, 128, 2*head_dim] |
| 76 | k_scale_ptr, |
| 77 | v_scale_ptr, |
| 78 | t_ptr, # topk_idx: [num_kv_heads, total_q, topk] |
| 79 | o_ptr, # [total_q, num_heads, head_dim] |
| 80 | block_table_ptr, # [num_reqs, max_blocks] |
| 81 | cu_seqlens_q, |
| 82 | cu_seqblocks_q, |
| 83 | seq_lens, |
| 84 | prefix_lens, |
| 85 | num_kv_heads, |
| 86 | gqa_group_size, |
| 87 | head_dim, |
| 88 | max_topk, |
| 89 | num_q_loop, |
| 90 | sm_scale, |
| 91 | stride_qn, |
| 92 | stride_qh, |
| 93 | stride_qd, |
| 94 | stride_kv_blk, |
| 95 | stride_kv_h, |
| 96 | stride_kv_pos, |
| 97 | stride_kv_d, |
| 98 | stride_ks_h, |
| 99 | stride_ks_t, |
| 100 | stride_vs_h, |
| 101 | stride_vs_t, |
| 102 | stride_th, |
| 103 | stride_tn, |
| 104 | stride_tk, |
| 105 | stride_on, |
| 106 | stride_oh, |
| 107 | stride_od, |
| 108 | stride_bt_b, |
| 109 | BLOCK_SIZE_Q: tl.constexpr, |
| 110 | BLOCK_SIZE_K: tl.constexpr, # == SPARSE_BLOCK_SIZE (128) |
| 111 | BLOCK_SIZE_D: tl.constexpr, |
| 112 | BLOCK_SIZE_H: tl.constexpr, |
| 113 | BLOCK_SIZE_T: tl.constexpr, |
| 114 | BLOCK_SIZE_QH: tl.constexpr, |
| 115 | USE_FP8: tl.constexpr, # fp8 KV cache: dequantize K/V to q.dtype on load |
| 116 | KV_SCALE_MODE: tl.constexpr, # 0: none, 1: scalar, 2: [kv_head, token] |
| 117 | SUB_K: tl.constexpr, # CDNA only: KV sub-tile width (see _IS_MI3XX) |
| 118 | ): |
| 119 | sm_scale_log2e = sm_scale * 1.4426950409 |
| 120 | pid_q = tl.program_id(0) |
| 121 | pid_kh = tl.program_id(1) |
| 122 | pid_b = tl.program_id(2) |
| 123 | pid_h = pid_kh * gqa_group_size |
| 124 | q_start = tl.load(cu_seqlens_q + pid_b) |
| 125 | q_len = tl.load(cu_seqlens_q + pid_b + 1) - q_start |
| 126 | q_block_start = tl.load(cu_seqblocks_q + pid_b) |
| 127 | q_block_len = tl.load(cu_seqblocks_q + pid_b + 1) - q_block_start |
| 128 | seq_len = tl.load(seq_lens + pid_b) |
| 129 | prefix_len = tl.load(prefix_lens + pid_b) |
| 130 | if pid_q * num_q_loop >= q_block_len: |
| 131 | return |
| 132 | real_q_loop = min(num_q_loop, q_block_len - pid_q * num_q_loop) |
| 133 | bt_row = block_table_ptr + pid_b * stride_bt_b |
| 134 | off_d = tl.arange(0, BLOCK_SIZE_D) |
| 135 | d_mask = off_d < head_dim |
| 136 | for j in range(real_q_loop): |
| 137 | pid_q_j = pid_q * num_q_loop + j |
| 138 | t_ptr_j = t_ptr + (q_block_start + pid_q_j) * stride_tn + pid_kh * stride_th |
| 139 | off_t = tl.arange(0, BLOCK_SIZE_T) |
| 140 | topk_idx = tl.load(t_ptr_j + off_t * stride_tk, mask=off_t < max_topk, other=-1) |
| 141 | real_topk = tl.sum((topk_idx >= 0).to(tl.int32), axis=0) |
| 142 | q_ptrs = tl.make_block_ptr( |
| 143 | base=q_ptr + q_start * stride_qn + pid_h * stride_qh, |
| 144 | shape=(q_len, gqa_group_size, head_dim), |
| 145 | strides=(stride_qn, stride_qh, stride_qd), |
| 146 | offsets=(pid_q_j * BLOCK_SIZE_Q, 0, 0), |
| 147 | block_shape=(BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_D), |
| 148 | order=(2, 1, 0), |
| 149 | ) |
| 150 | q = tl.load(q_ptrs, boundary_check=(0, 1, 2), padding_option="zero") |
| 151 | m_i = tl.full((BLOCK_SIZE_QH,), float("-inf"), dtype=tl.float32) |
| 152 | lse_i = tl.full((BLOCK_SIZE_QH,), float("-inf"), dtype=tl.float32) |
| 153 | acc_o = tl.zeros((BLOCK_SIZE_QH, BLOCK_SIZE_D), dtype=tl.float32) |
| 154 | q = tl.reshape(q, BLOCK_SIZE_QH, BLOCK_SIZE_D) |
| 155 | |
| 156 | # CDNA: process each 128-token KV block in SUB_K-token sub-tiles so |
| 157 | # each QK/PV MFMA is right-sized. Numerically equivalent to the dense |
| 158 | # path below (flash-softmax reassociation). |
| 159 | NUM_SUB: tl.constexpr = BLOCK_SIZE_K // SUB_K |
| 160 | for _ in tl.range(real_topk): |
| 161 | blk = tl.load(t_ptr_j).to(tl.int32) |
| 162 | t_ptr_j = t_ptr_j + stride_tk |
| 163 | c = blk * BLOCK_SIZE_K |
| 164 | page = tl.load(bt_row + blk).to(tl.int64) |
| 165 | kv_base = kv_cache_ptr + page * stride_kv_blk + pid_kh * stride_kv_h |
| 166 | for sub_i in range(NUM_SUB): |
| 167 | off_sub = tl.arange(0, SUB_K) + sub_i * SUB_K |
| 168 | pos_sub = c + off_sub |
| 169 | pos_mask_sub = pos_sub < seq_len |
| 170 | k_sub = tl.load( |
| 171 | kv_base |
| 172 | + off_sub[None, :] * stride_kv_pos |
| 173 | + off_d[:, None] * stride_kv_d, |
| 174 | mask=d_mask[:, None] & pos_mask_sub[None, :], |
| 175 | other=0.0, |
| 176 | ) |
| 177 | if USE_FP8: |
| 178 | k_sub = k_sub.to(q.dtype) |
| 179 | if KV_SCALE_MODE == 1: |
| 180 | k_sub = (k_sub * tl.load(k_scale_ptr)).to(q.dtype) |
| 181 | elif KV_SCALE_MODE == 2: |
| 182 | k_scale = tl.load( |
| 183 | k_scale_ptr |
| 184 | + pid_kh * stride_ks_h |
| 185 | + (page * BLOCK_SIZE_K + off_sub) * stride_ks_t, |
| 186 | mask=pos_mask_sub, |
| 187 | other=1.0, |
| 188 | ) |
| 189 | k_sub = (k_sub * k_scale[None, :]).to(q.dtype) |
| 190 | off_q_sub = ( |
| 191 | tl.arange(0, BLOCK_SIZE_Q)[:, None] |
| 192 | + pid_q_j * BLOCK_SIZE_Q |
| 193 | + prefix_len |
| 194 | - off_sub[None, :] |
| 195 | ) |
| 196 | qk_sub = tl.zeros((BLOCK_SIZE_Q, BLOCK_SIZE_H, SUB_K), dtype=tl.float32) |
| 197 | # causal: q_abs_pos - k_off >= block_start (c) |
| 198 | qk_sub += tl.where(off_q_sub[:, None, :] >= c, 0, float("-inf")) |
| 199 | qk_sub = tl.reshape(qk_sub, BLOCK_SIZE_QH, SUB_K) |
| 200 | qk_sub += tl.dot(q, k_sub) * sm_scale_log2e |
| 201 | qk_sub += tl.where(pos_mask_sub[None, :], 0, float("-inf")) |
| 202 | m_ij = tl.maximum(m_i, tl.max(qk_sub, axis=1)) |
| 203 | p_sub = tl.exp2(qk_sub - m_ij[:, None]) |
| 204 | l_ij = tl.sum(p_sub, axis=1) |
| 205 | acc_o = acc_o * tl.exp2(m_i - m_ij)[:, None] |
| 206 | v_sub = tl.load( |
| 207 | kv_base |
| 208 | + off_sub[:, None] * stride_kv_pos |
| 209 | + (head_dim + off_d[None, :]) * stride_kv_d, |
| 210 | mask=pos_mask_sub[:, None] & d_mask[None, :], |
| 211 | other=0.0, |
| 212 | ) |
| 213 | if USE_FP8: |
| 214 | v_sub = v_sub.to(q.dtype) |
| 215 | if KV_SCALE_MODE == 1: |
| 216 | v_sub = (v_sub * tl.load(v_scale_ptr)).to(q.dtype) |
| 217 | elif KV_SCALE_MODE == 2: |
| 218 | v_scale = tl.load( |
| 219 | v_scale_ptr |
| 220 | + pid_kh * stride_vs_h |
| 221 | + (page * BLOCK_SIZE_K + off_sub) * stride_vs_t, |
| 222 | mask=pos_mask_sub, |
| 223 | other=1.0, |
| 224 | ) |
| 225 | v_sub = (v_sub * v_scale[:, None]).to(q.dtype) |
| 226 | acc_o += tl.dot(p_sub.to(v_sub.dtype), v_sub) |
| 227 | m_i = m_ij |
| 228 | lse_i = m_ij + tl.log2(tl.exp2(lse_i - m_ij) + l_ij) |
| 229 | acc_o = acc_o * tl.exp2(m_i - lse_i)[:, None] |
| 230 | acc_o = tl.reshape(acc_o, BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_D) |
| 231 | o_ptrs = tl.make_block_ptr( |
| 232 | base=o_ptr + q_start * stride_on + pid_h * stride_oh, |
| 233 | shape=(q_len, gqa_group_size, head_dim), |
| 234 | strides=(stride_on, stride_oh, stride_od), |
| 235 | offsets=(pid_q_j * BLOCK_SIZE_Q, 0, 0), |
| 236 | block_shape=(BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_D), |
| 237 | order=(2, 1, 0), |
| 238 | ) |
| 239 | tl.store(o_ptrs, acc_o.to(o_ptr.dtype.element_ty), boundary_check=(0, 1, 2)) |
| 240 | |
| 241 | |
| 242 | @torch.no_grad() |
| 243 | def minimax_m3_sparse_attn( |
| 244 | q: torch.Tensor, # [total_q, num_heads, head_dim] |
| 245 | kv_cache: torch.Tensor, # [num_blocks, num_kv_heads, 128, 2*head_dim] |
| 246 | topk_idx: torch.Tensor, # [num_kv_heads, total_q, topk] |
| 247 | block_table: torch.Tensor, # [batch, max_blocks] |
| 248 | cu_seqlens_q: torch.Tensor, # [batch+1] int32 |
| 249 | seq_lens: torch.Tensor, # [batch] int32 |
| 250 | prefix_lens: torch.Tensor, # [batch] int32 |
| 251 | max_query_len: int, |
| 252 | num_kv_heads: int, |
| 253 | sm_scale: float, |
| 254 | output: torch.Tensor, # [total_q, num_heads, head_dim] |
| 255 | k_scale: torch.Tensor | None = None, |
| 256 | v_scale: torch.Tensor | None = None, |
| 257 | ) -> None: |
| 258 | """GQA block-sparse attention over the selected blocks. block_size_q == 1.""" |
| 259 | total_q, num_heads, head_dim = q.shape |
| 260 | batch = cu_seqlens_q.shape[0] - 1 |
| 261 | topk = topk_idx.shape[-1] |
| 262 | gqa_group_size = num_heads // num_kv_heads |
| 263 | use_fp8 = kv_cache.dtype in _FP8_DTYPES |
| 264 | ( |
| 265 | k_scale_arg, |
| 266 | v_scale_arg, |
| 267 | stride_ks_h, |
| 268 | stride_ks_t, |
| 269 | stride_vs_h, |
| 270 | stride_vs_t, |
| 271 | kv_scale_mode, |
| 272 | ) = ( |
| 273 | _kv_scale_args(output, num_kv_heads, k_scale, v_scale) |
| 274 | if use_fp8 |
| 275 | else ( |
| 276 | output, |
| 277 | output, |
| 278 | 0, |
| 279 | 0, |
| 280 | 0, |
| 281 | 0, |
| 282 | _KV_SCALE_NONE, |
| 283 | ) |
| 284 | ) |
| 285 | grid = (max_query_len, num_kv_heads, batch) |
| 286 | _gqa_sparse_fwd_kernel[grid]( |
| 287 | q, |
| 288 | kv_cache, |
| 289 | k_scale_arg, |
| 290 | v_scale_arg, |
| 291 | topk_idx, |
| 292 | output, |
| 293 | block_table, |
| 294 | cu_seqlens_q, |
| 295 | cu_seqlens_q, # cu_seqblocks_q == cu_seqlens_q when block_size_q == 1 |
| 296 | seq_lens, |
| 297 | prefix_lens, |
| 298 | num_kv_heads, |
| 299 | gqa_group_size, |
| 300 | head_dim, |
| 301 | topk, |
| 302 | 1, # num_q_loop |
| 303 | sm_scale, |
| 304 | q.stride(0), |
| 305 | q.stride(1), |
| 306 | q.stride(2), |
| 307 | kv_cache.stride(0), |
| 308 | kv_cache.stride(1), |
| 309 | kv_cache.stride(2), |
| 310 | kv_cache.stride(3), |
| 311 | stride_ks_h, |
| 312 | stride_ks_t, |
| 313 | stride_vs_h, |
| 314 | stride_vs_t, |
| 315 | topk_idx.stride(0), |
| 316 | topk_idx.stride(1), |
| 317 | topk_idx.stride(2), |
| 318 | output.stride(0), |
| 319 | output.stride(1), |
| 320 | output.stride(2), |
| 321 | block_table.stride(0), |
| 322 | BLOCK_SIZE_Q=1, |
| 323 | BLOCK_SIZE_K=SPARSE_BLOCK_SIZE, |
| 324 | USE_FP8=use_fp8, |
| 325 | KV_SCALE_MODE=kv_scale_mode, |
| 326 | SUB_K=_SPARSE_ATTN_SUB_K, |
| 327 | **_sparse_attn_prefill_kwargs(), |
| 328 | ) |