| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Triton kernels for MiniMax M3 block-sparse GQA attention. |
| 4 | |
| 5 | The main heads attend only to the blocks selected by the lightning indexer (see |
| 6 | ``index_topk``). Adapted to vLLM's paged KV cache: the KV page size is forced to |
| 7 | equal the sparse block size (128), so one selected block maps to exactly one |
| 8 | page. |
| 9 | |
| 10 | Main K/V cache layout (vLLM): |
| 11 | ``(num_blocks, num_kv_heads, 128, 2 * head_dim)`` |
| 12 | K=[..., :head_dim] V=[..., head_dim:] |
| 13 | |
| 14 | Only the paths MiniMax M3 uses are implemented: no attention sink, base-2 |
| 15 | (exp2/log2) softmax. The decode kernels use split-K (flash-decoding) over the |
| 16 | selected blocks with a separate merge step, since one query token per request |
| 17 | leaves the prefill kernels (which parallelize over the query dim) idle. |
| 18 | """ |
| 19 | |
| 20 | import torch |
| 21 | |
| 22 | from vllm.platforms import current_platform |
| 23 | from vllm.triton_utils import tl, triton |
| 24 | |
| 25 | # One sparse block == one KV page. |
| 26 | SPARSE_BLOCK_SIZE = 128 |
| 27 | |
| 28 | _FP8_DTYPES = ( |
| 29 | torch.float8_e4m3fn, |
| 30 | torch.float8_e4m3fnuz, |
| 31 | torch.float8_e5m2, |
| 32 | torch.float8_e5m2fnuz, |
| 33 | ) |
| 34 | |
| 35 | |
| 36 | # --------------------------------------------------------------------------- |
| 37 | # GQA block-sparse attention (paged). Main heads attend only to the selected |
| 38 | # blocks. BLOCK_SIZE_K == 128 so each selected block is one page. |
| 39 | # --------------------------------------------------------------------------- |
| 40 | # since prefill metadata is sliced from mixed batch metadata, seq_lens and prefix_lens |
| 41 | # might lose pointer alignment, which trigger Triton recompiles. we don't actually |
| 42 | # need pointer alignment for those tensors anyway because we do scalar load. |
| 43 | @triton.heuristics( |
| 44 | { |
| 45 | "BLOCK_SIZE_D": lambda args: triton.next_power_of_2(args["head_dim"]), |
| 46 | "BLOCK_SIZE_H": lambda args: triton.next_power_of_2(args["gqa_group_size"]), |
| 47 | "BLOCK_SIZE_QH": lambda args: args["BLOCK_SIZE_Q"] |
| 48 | * triton.next_power_of_2(args["gqa_group_size"]), |
| 49 | } |
| 50 | ) |
| 51 | @triton.jit(do_not_specialize_on_alignment=["seq_lens", "prefix_lens"]) |
| 52 | def _gqa_sparse_fwd_kernel( |
| 53 | q_ptr, # [total_q, num_heads, head_dim] |
| 54 | kv_cache_ptr, # main cache: [num_blocks, num_kv_heads, 128, 2*head_dim] |
| 55 | k_scale_ptr, |
| 56 | v_scale_ptr, |
| 57 | t_ptr, # topk_idx: [num_kv_heads, total_q, topk] |
| 58 | o_ptr, # [total_q, num_heads, head_dim] |
| 59 | block_table_ptr, # [num_reqs, max_blocks] |
| 60 | cu_seqlens_q, |
| 61 | cu_seqblocks_q, |
| 62 | seq_lens, |
| 63 | prefix_lens, |
| 64 | num_kv_heads, |
| 65 | gqa_group_size, |
| 66 | head_dim, |
| 67 | max_topk, |
| 68 | num_q_loop, |
| 69 | sm_scale, |
| 70 | stride_qn, |
| 71 | stride_qh, |
| 72 | stride_qd, |
| 73 | stride_kv_blk, |
| 74 | stride_kv_h, |
| 75 | stride_kv_pos, |
| 76 | stride_kv_d, |
| 77 | stride_ks_h, |
| 78 | stride_ks_t, |
| 79 | stride_vs_h, |
| 80 | stride_vs_t, |
| 81 | stride_th, |
| 82 | stride_tn, |
| 83 | stride_tk, |
| 84 | stride_on, |
| 85 | stride_oh, |
| 86 | stride_od, |
| 87 | stride_bt_b, |
| 88 | BLOCK_SIZE_Q: tl.constexpr, |
| 89 | BLOCK_SIZE_K: tl.constexpr, # == SPARSE_BLOCK_SIZE (128) |
| 90 | BLOCK_SIZE_D: tl.constexpr, |
| 91 | BLOCK_SIZE_H: tl.constexpr, |
| 92 | BLOCK_SIZE_QH: tl.constexpr, |
| 93 | USE_FP8: tl.constexpr, # fp8 KV cache: dequantize K/V to q.dtype on load |
| 94 | KV_SCALE_MODE: tl.constexpr, # 0: none, 1: scalar, 2: [kv_head, token] |
| 95 | ): |
| 96 | sm_scale_log2e = sm_scale * 1.4426950409 |
| 97 | pid_q = tl.program_id(0) |
| 98 | pid_kh = tl.program_id(1) |
| 99 | pid_b = tl.program_id(2) |
| 100 | pid_h = pid_kh * gqa_group_size |
| 101 | q_start = tl.load(cu_seqlens_q + pid_b) |
| 102 | q_len = tl.load(cu_seqlens_q + pid_b + 1) - q_start |
| 103 | q_block_start = tl.load(cu_seqblocks_q + pid_b) |
| 104 | q_block_len = tl.load(cu_seqblocks_q + pid_b + 1) - q_block_start |
| 105 | seq_len = tl.load(seq_lens + pid_b) |
| 106 | prefix_len = tl.load(prefix_lens + pid_b) |
| 107 | if pid_q * num_q_loop >= q_block_len: |
| 108 | return |
| 109 | real_q_loop = min(num_q_loop, q_block_len - pid_q * num_q_loop) |
| 110 | bt_row = block_table_ptr + pid_b * stride_bt_b |
| 111 | off_n = tl.arange(0, BLOCK_SIZE_K) |
| 112 | off_d = tl.arange(0, BLOCK_SIZE_D) |
| 113 | d_mask = off_d < head_dim |
| 114 | for j in range(real_q_loop): |
| 115 | pid_q_j = pid_q * num_q_loop + j |
| 116 | t_ptr_j = t_ptr + (q_block_start + pid_q_j) * stride_tn + pid_kh * stride_th |
| 117 | # Valid block count from seq position (no sentinel): block_size_q == 1. |
| 118 | q_abs = prefix_len + pid_q_j * BLOCK_SIZE_Q |
| 119 | valid_blocks = (q_abs + BLOCK_SIZE_K) // BLOCK_SIZE_K |
| 120 | real_topk = tl.minimum(max_topk, valid_blocks) |
| 121 | q_ptrs = tl.make_block_ptr( |
| 122 | base=q_ptr + q_start * stride_qn + pid_h * stride_qh, |
| 123 | shape=(q_len, gqa_group_size, head_dim), |
| 124 | strides=(stride_qn, stride_qh, stride_qd), |
| 125 | offsets=(pid_q_j * BLOCK_SIZE_Q, 0, 0), |
| 126 | block_shape=(BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_D), |
| 127 | order=(2, 1, 0), |
| 128 | ) |
| 129 | q = tl.load(q_ptrs, boundary_check=(0, 1, 2), padding_option="zero") |
| 130 | off_q = ( |
| 131 | tl.arange(0, BLOCK_SIZE_Q)[:, None] |
| 132 | + pid_q_j * BLOCK_SIZE_Q |
| 133 | + prefix_len |
| 134 | - tl.arange(0, BLOCK_SIZE_K)[None, :] |
| 135 | ) |
| 136 | m_i = tl.full((BLOCK_SIZE_QH,), float("-inf"), dtype=tl.float32) |
| 137 | lse_i = tl.full((BLOCK_SIZE_QH,), float("-inf"), dtype=tl.float32) |
| 138 | acc_o = tl.zeros((BLOCK_SIZE_QH, BLOCK_SIZE_D), dtype=tl.float32) |
| 139 | q = tl.reshape(q, BLOCK_SIZE_QH, BLOCK_SIZE_D) |
| 140 | for _ in range(real_topk): |
| 141 | blk = tl.load(t_ptr_j).to(tl.int32) |
| 142 | t_ptr_j = t_ptr_j + stride_tk |
| 143 | c = blk * BLOCK_SIZE_K |
| 144 | page = tl.load(bt_row + blk).to(tl.int64) |
| 145 | pos = c + off_n |
| 146 | pos_mask = pos < seq_len |
| 147 | k = tl.load( |
| 148 | kv_cache_ptr |
| 149 | + page * stride_kv_blk |
| 150 | + pid_kh * stride_kv_h |
| 151 | + off_n[None, :] * stride_kv_pos |
| 152 | + off_d[:, None] * stride_kv_d, |
| 153 | mask=d_mask[:, None] & pos_mask[None, :], |
| 154 | other=0.0, |
| 155 | ) |
| 156 | if USE_FP8: |
| 157 | k = k.to(q.dtype) |
| 158 | if KV_SCALE_MODE == 1: |
| 159 | k = (k * tl.load(k_scale_ptr)).to(q.dtype) |
| 160 | elif KV_SCALE_MODE == 2: |
| 161 | k_scale = tl.load( |
| 162 | k_scale_ptr |
| 163 | + pid_kh * stride_ks_h |
| 164 | + (page * BLOCK_SIZE_K + off_n) * stride_ks_t, |
| 165 | mask=pos_mask, |
| 166 | other=1.0, |
| 167 | ) |
| 168 | k = (k * k_scale[None, :]).to(q.dtype) |
| 169 | qk = tl.zeros((BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_K), dtype=tl.float32) |
| 170 | # causal: q_abs_pos - k_off >= block_start (c) |
| 171 | qk += tl.where(off_q[:, None, :] >= c, 0, float("-inf")) |
| 172 | qk = tl.reshape(qk, BLOCK_SIZE_QH, BLOCK_SIZE_K) |
| 173 | qk += tl.dot(q, k) * sm_scale_log2e |
| 174 | qk += tl.where(pos_mask[None, :], 0, float("-inf")) |
| 175 | m_ij = tl.maximum(m_i, tl.max(qk, axis=1)) |
| 176 | p = tl.exp2(qk - m_ij[:, None]) |
| 177 | l_ij = tl.sum(p, axis=1) |
| 178 | acc_o = acc_o * tl.exp2(m_i - m_ij)[:, None] |
| 179 | v = tl.load( |
| 180 | kv_cache_ptr |
| 181 | + page * stride_kv_blk |
| 182 | + pid_kh * stride_kv_h |
| 183 | + off_n[:, None] * stride_kv_pos |
| 184 | + (head_dim + off_d[None, :]) * stride_kv_d, |
| 185 | mask=pos_mask[:, None] & d_mask[None, :], |
| 186 | other=0.0, |
| 187 | ) |
| 188 | if USE_FP8: |
| 189 | v = v.to(q.dtype) |
| 190 | if KV_SCALE_MODE == 1: |
| 191 | v = (v * tl.load(v_scale_ptr)).to(q.dtype) |
| 192 | elif KV_SCALE_MODE == 2: |
| 193 | v_scale = tl.load( |
| 194 | v_scale_ptr |
| 195 | + pid_kh * stride_vs_h |
| 196 | + (page * BLOCK_SIZE_K + off_n) * stride_vs_t, |
| 197 | mask=pos_mask, |
| 198 | other=1.0, |
| 199 | ) |
| 200 | v = (v * v_scale[:, None]).to(q.dtype) |
| 201 | acc_o += tl.dot(p.to(v.dtype), v) |
| 202 | m_i = m_ij |
| 203 | lse_i = m_ij + tl.log2(tl.exp2(lse_i - m_ij) + l_ij) |
| 204 | acc_o = acc_o * tl.exp2(m_i - lse_i)[:, None] |
| 205 | acc_o = tl.reshape(acc_o, BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_D) |
| 206 | o_ptrs = tl.make_block_ptr( |
| 207 | base=o_ptr + q_start * stride_on + pid_h * stride_oh, |
| 208 | shape=(q_len, gqa_group_size, head_dim), |
| 209 | strides=(stride_on, stride_oh, stride_od), |
| 210 | offsets=(pid_q_j * BLOCK_SIZE_Q, 0, 0), |
| 211 | block_shape=(BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_D), |
| 212 | order=(2, 1, 0), |
| 213 | ) |
| 214 | tl.store(o_ptrs, acc_o.to(o_ptr.dtype.element_ty), boundary_check=(0, 1, 2)) |
| 215 | |
| 216 | |
| 217 | # --------------------------------------------------------------------------- |
| 218 | # Decode kernels (split-K). Decode batches are flattened request-major, with a |
| 219 | # runtime query length used to map each query token back to its request metadata. |
| 220 | # This parallelizes over the selected top-k blocks, producing partials that the |
| 221 | # merge kernel combines (flash-decoding). All chunk counts depend only on shape |
| 222 | # constants so the grid is fixed within a cuda graph. Base-2 (exp2/log2) |
| 223 | # softmax matches the prefill kernel. |
| 224 | # --------------------------------------------------------------------------- |
| 225 | @triton.heuristics( |
| 226 | { |
| 227 | "BLOCK_SIZE_H": lambda args: max( |
| 228 | 16, triton.next_power_of_2(args["gqa_group_size"]) |
| 229 | ), |
| 230 | "BLOCK_SIZE_D": lambda args: triton.next_power_of_2(args["head_dim"]), |
| 231 | } |
| 232 | ) |
| 233 | @triton.jit(do_not_specialize=["decode_query_len"]) |
| 234 | def _gqa_sparse_decode_kernel( |
| 235 | q_ptr, # [total_q, num_heads, head_dim] |
| 236 | kv_cache_ptr, # main cache: [num_blocks, num_kv_heads, 128, 2*head_dim] |
| 237 | k_scale_ptr, |
| 238 | v_scale_ptr, |
| 239 | t_ptr, # topk_idx: [num_kv_heads, total_q, topk] |
| 240 | o_ptr, # partial out: [NUM_TOPK_CHUNKS, total_q, num_heads, head_dim] |
| 241 | lse_ptr, # partial lse (log2): [NUM_TOPK_CHUNKS, total_q, num_heads] |
| 242 | block_table_ptr, # [num_reqs, max_blocks] |
| 243 | seq_lens, # [num_reqs] |
| 244 | total_q, |
| 245 | gqa_group_size, |
| 246 | head_dim, |
| 247 | max_topk, |
| 248 | sm_scale, |
| 249 | decode_query_len, |
| 250 | stride_qn, |
| 251 | stride_qh, |
| 252 | stride_qd, |
| 253 | stride_kv_blk, |
| 254 | stride_kv_h, |
| 255 | stride_kv_pos, |
| 256 | stride_kv_d, |
| 257 | stride_ks_h, |
| 258 | stride_ks_t, |
| 259 | stride_vs_h, |
| 260 | stride_vs_t, |
| 261 | stride_th, |
| 262 | stride_tn, |
| 263 | stride_tk, |
| 264 | stride_o_c, |
| 265 | stride_o_b, |
| 266 | stride_o_h, |
| 267 | stride_o_d, |
| 268 | stride_l_c, |
| 269 | stride_l_b, |
| 270 | stride_l_h, |
| 271 | stride_bt_b, |
| 272 | BLOCK_SIZE_K: tl.constexpr, # == SPARSE_BLOCK_SIZE (128) |
| 273 | NUM_TOPK_CHUNKS: tl.constexpr, |
| 274 | BLOCK_SIZE_H: tl.constexpr, |
| 275 | BLOCK_SIZE_D: tl.constexpr, |
| 276 | USE_FP8: tl.constexpr, # fp8 KV cache: dequantize K/V to q.dtype on load |
| 277 | KV_SCALE_MODE: tl.constexpr, # 0: none, 1: scalar, 2: [kv_head, token] |
| 278 | USE_PDL: tl.constexpr, |
| 279 | ): |
| 280 | sm_scale_log2e = sm_scale * 1.4426950409 |
| 281 | # split-K over the topk dimension: pid(0) folds (query-token, chunk). |
| 282 | pid_bc, pid_kh = tl.program_id(0), tl.program_id(1) |
| 283 | pid_b = pid_bc % total_q |
| 284 | pid_c = pid_bc // total_q |
| 285 | req_id = pid_b // decode_query_len |
| 286 | q_offset = pid_b - req_id * decode_query_len |
| 287 | pid_h = pid_kh * gqa_group_size |
| 288 | chunk_size_topk = (max_topk + NUM_TOPK_CHUNKS - 1) // NUM_TOPK_CHUNKS |
| 289 | chunk_start_topk = pid_c * chunk_size_topk |
| 290 | chunk_end_compiletime = chunk_start_topk + chunk_size_topk |
| 291 | |
| 292 | if USE_PDL: |
| 293 | tl.extra.cuda.gdc_wait() |
| 294 | |
| 295 | seq_len = tl.load(seq_lens + req_id) |
| 296 | query_pos = seq_len - decode_query_len + q_offset |
| 297 | # Full-CG padding uses zero-length request rows. Clamp to an empty |
| 298 | # attention range instead of letting padded rows produce negative lengths. |
| 299 | kv_len = tl.maximum(query_pos + 1, 0) |
| 300 | |
| 301 | # Valid block count from seq_len (no sentinel): min(topk, cdiv(kv_len, blk)). |
| 302 | idx_base = t_ptr + pid_kh * stride_th + pid_b * stride_tn |
| 303 | num_blocks = (kv_len + BLOCK_SIZE_K - 1) // BLOCK_SIZE_K |
| 304 | real_topk = tl.minimum(max_topk, num_blocks) |
| 305 | chunk_end_topk = tl.minimum(chunk_end_compiletime, real_topk) |
| 306 | |
| 307 | off_n = tl.arange(0, BLOCK_SIZE_K) |
| 308 | off_d = tl.arange(0, BLOCK_SIZE_D) |
| 309 | d_mask = off_d < head_dim |
| 310 | bt_row = block_table_ptr + req_id * stride_bt_b |
| 311 | |
| 312 | m_i = tl.full((BLOCK_SIZE_H,), float("-inf"), dtype=tl.float32) |
| 313 | lse_i = tl.full((BLOCK_SIZE_H,), float("-inf"), dtype=tl.float32) |
| 314 | acc_o = tl.zeros((BLOCK_SIZE_H, BLOCK_SIZE_D), dtype=tl.float32) |
| 315 | q_ptrs = tl.make_block_ptr( |
| 316 | base=q_ptr + pid_b * stride_qn + pid_h * stride_qh, |
| 317 | shape=(gqa_group_size, head_dim), |
| 318 | strides=(stride_qh, stride_qd), |
| 319 | offsets=(0, 0), |
| 320 | block_shape=(BLOCK_SIZE_H, BLOCK_SIZE_D), |
| 321 | order=(1, 0), |
| 322 | ) |
| 323 | q = tl.load(q_ptrs, boundary_check=(0, 1), padding_option="zero") |
| 324 | |
| 325 | cur_idx_ptr = idx_base + chunk_start_topk * stride_tk |
| 326 | for _ in tl.range(chunk_start_topk, chunk_end_topk): |
| 327 | blk = tl.load(cur_idx_ptr).to(tl.int32) |
| 328 | cur_idx_ptr = cur_idx_ptr + stride_tk |
| 329 | c = blk * BLOCK_SIZE_K |
| 330 | page = tl.load(bt_row + blk).to(tl.int64) |
| 331 | pos = c + off_n |
| 332 | pos_mask = pos < kv_len |
| 333 | k = tl.load( |
| 334 | kv_cache_ptr |
| 335 | + page * stride_kv_blk |
| 336 | + pid_kh * stride_kv_h |
| 337 | + off_n[None, :] * stride_kv_pos |
| 338 | + off_d[:, None] * stride_kv_d, |
| 339 | mask=d_mask[:, None] & pos_mask[None, :], |
| 340 | other=0.0, |
| 341 | ) |
| 342 | if USE_FP8: |
| 343 | k = k.to(q.dtype) |
| 344 | if KV_SCALE_MODE == 1: |
| 345 | k = (k * tl.load(k_scale_ptr)).to(q.dtype) |
| 346 | elif KV_SCALE_MODE == 2: |
| 347 | k_scale = tl.load( |
| 348 | k_scale_ptr |
| 349 | + pid_kh * stride_ks_h |
| 350 | + (page * BLOCK_SIZE_K + off_n) * stride_ks_t, |
| 351 | mask=pos_mask, |
| 352 | other=1.0, |
| 353 | ) |
| 354 | k = (k * k_scale[None, :]).to(q.dtype) |
| 355 | qk = tl.zeros((BLOCK_SIZE_H, BLOCK_SIZE_K), dtype=tl.float32) |
| 356 | qk += tl.where(pos_mask[None, :], 0, float("-inf")) |
| 357 | qk += tl.dot(q, k) * sm_scale_log2e |
| 358 | m_ij = tl.maximum(m_i, tl.max(qk, axis=1)) |
| 359 | p = tl.exp2(qk - m_ij[:, None]) |
| 360 | l_ij = tl.sum(p, axis=1) |
| 361 | acc_o = acc_o * tl.exp2(m_i - m_ij)[:, None] |
| 362 | v = tl.load( |
| 363 | kv_cache_ptr |
| 364 | + page * stride_kv_blk |
| 365 | + pid_kh * stride_kv_h |
| 366 | + off_n[:, None] * stride_kv_pos |
| 367 | + (head_dim + off_d[None, :]) * stride_kv_d, |
| 368 | mask=pos_mask[:, None] & d_mask[None, :], |
| 369 | other=0.0, |
| 370 | ) |
| 371 | if USE_FP8: |
| 372 | v = v.to(q.dtype) |
| 373 | if KV_SCALE_MODE == 1: |
| 374 | v = (v * tl.load(v_scale_ptr)).to(q.dtype) |
| 375 | elif KV_SCALE_MODE == 2: |
| 376 | v_scale = tl.load( |
| 377 | v_scale_ptr |
| 378 | + pid_kh * stride_vs_h |
| 379 | + (page * BLOCK_SIZE_K + off_n) * stride_vs_t, |
| 380 | mask=pos_mask, |
| 381 | other=1.0, |
| 382 | ) |
| 383 | v = (v * v_scale[:, None]).to(q.dtype) |
| 384 | acc_o += tl.dot(p.to(v.dtype), v) |
| 385 | m_i = m_ij |
| 386 | lse_i = m_ij + tl.log2(tl.exp2(lse_i - m_ij) + l_ij) |
| 387 | |
| 388 | if USE_PDL: |
| 389 | tl.extra.cuda.gdc_launch_dependents() |
| 390 | |
| 391 | # Empty chunks for active rows must store zero output; otherwise the merge |
| 392 | # can hit 0 * NaN. All-empty padded rows may still produce NaNs in merge. |
| 393 | scale = tl.where(lse_i > float("-inf"), tl.exp2(m_i - lse_i), tl.zeros_like(lse_i)) |
| 394 | acc_o = acc_o * scale[:, None] |
| 395 | o_ptrs = tl.make_block_ptr( |
| 396 | base=o_ptr + pid_c * stride_o_c + pid_b * stride_o_b + pid_h * stride_o_h, |
| 397 | shape=(gqa_group_size, head_dim), |
| 398 | strides=(stride_o_h, stride_o_d), |
| 399 | offsets=(0, 0), |
| 400 | block_shape=(BLOCK_SIZE_H, BLOCK_SIZE_D), |
| 401 | order=(1, 0), |
| 402 | ) |
| 403 | tl.store(o_ptrs, acc_o.to(o_ptr.dtype.element_ty), boundary_check=(0, 1)) |
| 404 | lse_ptrs = tl.make_block_ptr( |
| 405 | base=lse_ptr + pid_c * stride_l_c + pid_b * stride_l_b + pid_h * stride_l_h, |
| 406 | shape=(gqa_group_size,), |
| 407 | strides=(stride_l_h,), |
| 408 | offsets=(0,), |
| 409 | block_shape=(BLOCK_SIZE_H,), |
| 410 | order=(0,), |
| 411 | ) |
| 412 | tl.store(lse_ptrs, lse_i.to(lse_ptr.dtype.element_ty), boundary_check=(0,)) |
| 413 | |
| 414 | |
| 415 | @triton.heuristics( |
| 416 | {"BLOCK_SIZE_D": lambda args: triton.next_power_of_2(args["head_dim"])} |
| 417 | ) |
| 418 | @triton.jit |
| 419 | def _merge_topk_attn_out_kernel( |
| 420 | o_ptr, # partials: [NUM_TOPK_CHUNKS, total_q, num_heads, head_dim] |
| 421 | lse_ptr, # partials (log2): [NUM_TOPK_CHUNKS, total_q, num_heads] |
| 422 | out_ptr, # merged out: [total_q, num_heads, head_dim] |
| 423 | head_dim, |
| 424 | stride_o_c, |
| 425 | stride_o_b, |
| 426 | stride_o_h, |
| 427 | stride_o_d, |
| 428 | stride_l_c, |
| 429 | stride_l_b, |
| 430 | stride_l_h, |
| 431 | stride_out_n, |
| 432 | stride_out_h, |
| 433 | stride_out_d, |
| 434 | NUM_TOPK_CHUNKS: tl.constexpr, |
| 435 | BLOCK_SIZE_D: tl.constexpr, |
| 436 | USE_PDL: tl.constexpr, |
| 437 | ): |
| 438 | pid_b, pid_h = tl.program_id(0), tl.program_id(1) |
| 439 | |
| 440 | # NOTE: assume seq_lens is safe to load before gdc_wait() |
| 441 | if USE_PDL: |
| 442 | tl.extra.cuda.gdc_wait() |
| 443 | tl.extra.cuda.gdc_launch_dependents() |
| 444 | |
| 445 | off_c = tl.arange(0, NUM_TOPK_CHUNKS) |
| 446 | off_d = tl.arange(0, BLOCK_SIZE_D) |
| 447 | o_ptrs = tl.make_block_ptr( |
| 448 | base=o_ptr + pid_b * stride_o_b + pid_h * stride_o_h, |
| 449 | shape=(NUM_TOPK_CHUNKS, head_dim), |
| 450 | strides=(stride_o_c, stride_o_d), |
| 451 | offsets=(0, 0), |
| 452 | block_shape=(NUM_TOPK_CHUNKS, BLOCK_SIZE_D), |
| 453 | order=(1, 0), |
| 454 | ) |
| 455 | lse_ptrs = lse_ptr + pid_b * stride_l_b + pid_h * stride_l_h + off_c * stride_l_c |
| 456 | o = tl.load(o_ptrs, boundary_check=(0, 1), padding_option="zero") |
| 457 | lse = tl.load(lse_ptrs) # empty chunks contribute -inf -> weight 0 |
| 458 | lse_max = tl.max(lse, axis=0) |
| 459 | weights = tl.exp2(lse - lse_max) |
| 460 | weights = weights / tl.sum(weights, axis=0) |
| 461 | o_merged = tl.sum(o * weights[:, None], axis=0) |
| 462 | out_ptrs = ( |
| 463 | out_ptr + pid_b * stride_out_n + pid_h * stride_out_h + off_d * stride_out_d |
| 464 | ) |
| 465 | tl.store(out_ptrs, o_merged.to(out_ptr.dtype.element_ty), mask=off_d < head_dim) |
| 466 | |
| 467 | |
| 468 | # --------------------------------------------------------------------------- |
| 469 | # Python wrappers |
| 470 | # --------------------------------------------------------------------------- |
| 471 | _KV_SCALE_NONE = 0 |
| 472 | _KV_SCALE_SCALAR = 1 |
| 473 | _KV_SCALE_PER_TOKEN_HEAD = 2 |
| 474 | |
| 475 | |
| 476 | def _kv_scale_args( |
| 477 | output: torch.Tensor, |
| 478 | num_kv_heads: int, |
| 479 | k_scale: torch.Tensor | None, |
| 480 | v_scale: torch.Tensor | None, |
| 481 | ) -> tuple[torch.Tensor, torch.Tensor, int, int, int, int, int]: |
| 482 | if k_scale is None and v_scale is None: |
| 483 | return output, output, 0, 0, 0, 0, _KV_SCALE_NONE |
| 484 | if k_scale is None or v_scale is None: |
| 485 | raise ValueError("k_scale and v_scale must be both provided or both None") |
| 486 | if k_scale.device != output.device or v_scale.device != output.device: |
| 487 | raise ValueError("k_scale and v_scale must be on the same device as output") |
| 488 | if k_scale.numel() == 1 and v_scale.numel() == 1: |
| 489 | return k_scale, v_scale, 0, 0, 0, 0, _KV_SCALE_SCALAR |
| 490 | if k_scale.dim() == 2 and v_scale.dim() == 2: |
| 491 | if k_scale.shape[0] != num_kv_heads or v_scale.shape[0] != num_kv_heads: |
| 492 | raise ValueError( |
| 493 | "per-token/head KV scales must have shape " |
| 494 | f"[{num_kv_heads}, max_kv_tokens]" |
| 495 | ) |
| 496 | if k_scale.shape != v_scale.shape: |
| 497 | raise ValueError("k_scale and v_scale must have matching shapes") |
| 498 | return ( |
| 499 | k_scale, |
| 500 | v_scale, |
| 501 | k_scale.stride(0), |
| 502 | k_scale.stride(1), |
| 503 | v_scale.stride(0), |
| 504 | v_scale.stride(1), |
| 505 | _KV_SCALE_PER_TOKEN_HEAD, |
| 506 | ) |
| 507 | raise ValueError( |
| 508 | "MiniMax-M3 sparse attention supports scalar KV scales or " |
| 509 | "[num_kv_heads, max_kv_tokens] per-token/head scales" |
| 510 | ) |
| 511 | |
| 512 | |
| 513 | @torch.no_grad() |
| 514 | def minimax_m3_sparse_attn( |
| 515 | q: torch.Tensor, # [total_q, num_heads, head_dim] |
| 516 | kv_cache: torch.Tensor, # [num_blocks, num_kv_heads, 128, 2*head_dim] |
| 517 | topk_idx: torch.Tensor, # [num_kv_heads, total_q, topk] |
| 518 | block_table: torch.Tensor, # [batch, max_blocks] |
| 519 | cu_seqlens_q: torch.Tensor, # [batch+1] int32 |
| 520 | seq_lens: torch.Tensor, # [batch] int32 |
| 521 | prefix_lens: torch.Tensor, # [batch] int32 |
| 522 | max_query_len: int, |
| 523 | num_kv_heads: int, |
| 524 | sm_scale: float, |
| 525 | output: torch.Tensor, # [total_q, num_heads, head_dim] |
| 526 | k_scale: torch.Tensor | None = None, |
| 527 | v_scale: torch.Tensor | None = None, |
| 528 | ) -> None: |
| 529 | """GQA block-sparse attention over the selected blocks. block_size_q == 1.""" |
| 530 | total_q, num_heads, head_dim = q.shape |
| 531 | batch = cu_seqlens_q.shape[0] - 1 |
| 532 | topk = topk_idx.shape[-1] |
| 533 | gqa_group_size = num_heads // num_kv_heads |
| 534 | use_fp8 = kv_cache.dtype in _FP8_DTYPES |
| 535 | ( |
| 536 | k_scale_arg, |
| 537 | v_scale_arg, |
| 538 | stride_ks_h, |
| 539 | stride_ks_t, |
| 540 | stride_vs_h, |
| 541 | stride_vs_t, |
| 542 | kv_scale_mode, |
| 543 | ) = ( |
| 544 | _kv_scale_args(output, num_kv_heads, k_scale, v_scale) |
| 545 | if use_fp8 |
| 546 | else ( |
| 547 | output, |
| 548 | output, |
| 549 | 0, |
| 550 | 0, |
| 551 | 0, |
| 552 | 0, |
| 553 | _KV_SCALE_NONE, |
| 554 | ) |
| 555 | ) |
| 556 | grid = (max_query_len, num_kv_heads, batch) |
| 557 | _gqa_sparse_fwd_kernel[grid]( |
| 558 | q, |
| 559 | kv_cache, |
| 560 | k_scale_arg, |
| 561 | v_scale_arg, |
| 562 | topk_idx, |
| 563 | output, |
| 564 | block_table, |
| 565 | cu_seqlens_q, |
| 566 | cu_seqlens_q, # cu_seqblocks_q == cu_seqlens_q when block_size_q == 1 |
| 567 | seq_lens, |
| 568 | prefix_lens, |
| 569 | num_kv_heads, |
| 570 | gqa_group_size, |
| 571 | head_dim, |
| 572 | topk, |
| 573 | 1, # num_q_loop |
| 574 | sm_scale, |
| 575 | q.stride(0), |
| 576 | q.stride(1), |
| 577 | q.stride(2), |
| 578 | kv_cache.stride(0), |
| 579 | kv_cache.stride(1), |
| 580 | kv_cache.stride(2), |
| 581 | kv_cache.stride(3), |
| 582 | stride_ks_h, |
| 583 | stride_ks_t, |
| 584 | stride_vs_h, |
| 585 | stride_vs_t, |
| 586 | topk_idx.stride(0), |
| 587 | topk_idx.stride(1), |
| 588 | topk_idx.stride(2), |
| 589 | output.stride(0), |
| 590 | output.stride(1), |
| 591 | output.stride(2), |
| 592 | block_table.stride(0), |
| 593 | BLOCK_SIZE_Q=1, |
| 594 | BLOCK_SIZE_K=SPARSE_BLOCK_SIZE, |
| 595 | USE_FP8=use_fp8, |
| 596 | KV_SCALE_MODE=kv_scale_mode, |
| 597 | ) |
| 598 | |
| 599 | |
| 600 | @torch.no_grad() |
| 601 | def minimax_m3_sparse_attn_decode( |
| 602 | q: torch.Tensor, # [total_q, num_heads, head_dim] |
| 603 | kv_cache: torch.Tensor, # [num_blocks, num_kv_heads, 128, 2*head_dim] |
| 604 | topk_idx: torch.Tensor, # [num_kv_heads, total_q, topk] |
| 605 | block_table: torch.Tensor, # [num_reqs, max_blocks] |
| 606 | seq_lens: torch.Tensor, # [num_reqs] int32 |
| 607 | num_kv_heads: int, |
| 608 | sm_scale: float, |
| 609 | output: torch.Tensor, # [total_q, num_heads, head_dim] |
| 610 | decode_query_len: int, |
| 611 | k_scale: torch.Tensor | None = None, |
| 612 | v_scale: torch.Tensor | None = None, |
| 613 | ) -> None: |
| 614 | """GQA block-sparse attention for decode (split-K over the top-k blocks).""" |
| 615 | total_q, num_heads, head_dim = q.shape |
| 616 | assert total_q == seq_lens.shape[0] * decode_query_len |
| 617 | max_topk = topk_idx.shape[-1] |
| 618 | gqa_group_size = num_heads // num_kv_heads |
| 619 | use_fp8 = kv_cache.dtype in _FP8_DTYPES |
| 620 | ( |
| 621 | k_scale_arg, |
| 622 | v_scale_arg, |
| 623 | stride_ks_h, |
| 624 | stride_ks_t, |
| 625 | stride_vs_h, |
| 626 | stride_vs_t, |
| 627 | kv_scale_mode, |
| 628 | ) = ( |
| 629 | _kv_scale_args(output, num_kv_heads, k_scale, v_scale) |
| 630 | if use_fp8 |
| 631 | else ( |
| 632 | output, |
| 633 | output, |
| 634 | 0, |
| 635 | 0, |
| 636 | 0, |
| 637 | 0, |
| 638 | _KV_SCALE_NONE, |
| 639 | ) |
| 640 | ) |
| 641 | use_pdl = current_platform.is_arch_support_pdl() |
| 642 | # `launch_pdl` is a Triton runtime kwarg only some backends accept (CUDA |
| 643 | # SM9+); this ROCm Triton rejects it even when False ("Keyword argument |
| 644 | # launch_pdl was specified but unrecognised"). Only pass it when PDL is |
| 645 | # actually supported -- on ROCm use_pdl is always False, so it's omitted. |
| 646 | pdl_launch = {"launch_pdl": True} if use_pdl else {} |
| 647 | # split-K over the selected blocks; chunk count is shape-constant (cuda graph). |
| 648 | TARGET_GRID = 256 |
| 649 | target = max(1, min(max_topk, TARGET_GRID // max(1, total_q * num_kv_heads))) |
| 650 | num_topk_chunks = 1 << (target.bit_length() - 1) |
| 651 | o_partial = torch.empty( |
| 652 | num_topk_chunks, total_q, num_heads, head_dim, dtype=q.dtype, device=q.device |
| 653 | ) |
| 654 | lse_partial = torch.empty( |
| 655 | num_topk_chunks, total_q, num_heads, dtype=torch.float32, device=q.device |
| 656 | ) |
| 657 | grid = (total_q * num_topk_chunks, num_kv_heads) |
| 658 | _gqa_sparse_decode_kernel[grid]( |
| 659 | q, |
| 660 | kv_cache, |
| 661 | k_scale_arg, |
| 662 | v_scale_arg, |
| 663 | topk_idx, |
| 664 | o_partial, |
| 665 | lse_partial, |
| 666 | block_table, |
| 667 | seq_lens, |
| 668 | total_q, |
| 669 | gqa_group_size, |
| 670 | head_dim, |
| 671 | max_topk, |
| 672 | sm_scale, |
| 673 | decode_query_len, |
| 674 | q.stride(0), |
| 675 | q.stride(1), |
| 676 | q.stride(2), |
| 677 | kv_cache.stride(0), |
| 678 | kv_cache.stride(1), |
| 679 | kv_cache.stride(2), |
| 680 | kv_cache.stride(3), |
| 681 | stride_ks_h, |
| 682 | stride_ks_t, |
| 683 | stride_vs_h, |
| 684 | stride_vs_t, |
| 685 | topk_idx.stride(0), |
| 686 | topk_idx.stride(1), |
| 687 | topk_idx.stride(2), |
| 688 | o_partial.stride(0), |
| 689 | o_partial.stride(1), |
| 690 | o_partial.stride(2), |
| 691 | o_partial.stride(3), |
| 692 | lse_partial.stride(0), |
| 693 | lse_partial.stride(1), |
| 694 | lse_partial.stride(2), |
| 695 | block_table.stride(0), |
| 696 | BLOCK_SIZE_K=SPARSE_BLOCK_SIZE, |
| 697 | NUM_TOPK_CHUNKS=num_topk_chunks, |
| 698 | USE_FP8=use_fp8, |
| 699 | KV_SCALE_MODE=kv_scale_mode, |
| 700 | USE_PDL=use_pdl, |
| 701 | **pdl_launch, |
| 702 | ) |
| 703 | merge_grid = (total_q, num_heads) |
| 704 | _merge_topk_attn_out_kernel[merge_grid]( |
| 705 | o_partial, |
| 706 | lse_partial, |
| 707 | output, |
| 708 | head_dim, |
| 709 | o_partial.stride(0), |
| 710 | o_partial.stride(1), |
| 711 | o_partial.stride(2), |
| 712 | o_partial.stride(3), |
| 713 | lse_partial.stride(0), |
| 714 | lse_partial.stride(1), |
| 715 | lse_partial.stride(2), |
| 716 | output.stride(0), |
| 717 | output.stride(1), |
| 718 | output.stride(2), |
| 719 | NUM_TOPK_CHUNKS=num_topk_chunks, |
| 720 | USE_PDL=use_pdl, |
| 721 | **pdl_launch, |
| 722 | ) |