| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Triton kernels for MiniMax M3 lightning-indexer block scoring + top-k. |
| 4 | |
| 5 | Index queries score each 128-token block of index keys (max over the block), |
| 6 | then the top-k blocks (plus forced init/local blocks) are selected per query |
| 7 | token. Adapted to vLLM's paged KV cache: the KV page size is forced to equal the |
| 8 | sparse block size (128), so one sparse block maps to exactly one page. |
| 9 | |
| 10 | Index-K cache layout (vLLM): ``(num_blocks, 128, idx_head_dim)`` (one shared |
| 11 | key vector per token). |
| 12 | |
| 13 | Only the paths MiniMax M3 uses are implemented: score_type="max", index value |
| 14 | disabled (score-only indexer), and shared index keys. Each local index-query |
| 15 | head selects its own block ids for the block-sparse attention kernels in |
| 16 | ``sparse_attn``. |
| 17 | """ |
| 18 | |
| 19 | import torch |
| 20 | |
| 21 | from vllm.models.minimax_m3.amd.ops.sparse_pa import ( |
| 22 | PAGES_PER_SPARSE_BLOCK, |
| 23 | _write_sparse_block_table_row_from_values, |
| 24 | ) |
| 25 | from vllm.platforms import current_platform |
| 26 | from vllm.triton_utils import tl, triton |
| 27 | from vllm.utils.math_utils import round_up |
| 28 | |
| 29 | # One sparse block == one KV page. |
| 30 | SPARSE_BLOCK_SIZE = 128 |
| 31 | DECODE_SCORE_BALANCED_PROGRAM_BUDGET = 1024 |
| 32 | DECODE_SCORE_HIGH_BATCH_PROGRAM_BUDGET = 768 |
| 33 | MAX_DECODE_SCORE_BALANCED_REQUESTS = 11 |
| 34 | DECODE_SCORE_DEFAULT_TARGET_GRID = 512 |
| 35 | DECODE_SCORE_GFX950_HIGH_BATCH_TARGET_GRID = 1024 |
| 36 | MIN_DECODE_SCORE_GFX950_HIGH_BATCH_REQUESTS = 20 |
| 37 | MAX_DECODE_SCORE_GFX950_HIGH_BATCH_REQUESTS = 64 |
| 38 | MAX_DECODE_SCORE_KV_CHUNKS = 256 |
| 39 | DECODE_TOPK_BLOCKS_PER_CHUNK = 512 |
| 40 | DECODE_TOPK_SHORT_BLOCKS_PER_CHUNK = 128 |
| 41 | MAX_DECODE_TOPK_FAST_CHUNKS = 16 |
| 42 | DECODE_TOPK_TARGET_GRID = 64 |
| 43 | |
| 44 | |
| 45 | def _decode_score_program_budget( |
| 46 | num_reqs: int, |
| 47 | head_dim: int, |
| 48 | query_dtype: torch.dtype, |
| 49 | cache_dtype: torch.dtype, |
| 50 | *, |
| 51 | is_gfx950: bool, |
| 52 | ) -> int | None: |
| 53 | if not ( |
| 54 | is_gfx950 |
| 55 | and head_dim == 128 |
| 56 | and query_dtype == torch.bfloat16 |
| 57 | and cache_dtype == torch.bfloat16 |
| 58 | ): |
| 59 | return None |
| 60 | if 1 <= num_reqs <= 8: |
| 61 | return DECODE_SCORE_BALANCED_PROGRAM_BUDGET |
| 62 | if 9 <= num_reqs <= MAX_DECODE_SCORE_BALANCED_REQUESTS: |
| 63 | return DECODE_SCORE_HIGH_BATCH_PROGRAM_BUDGET |
| 64 | return None |
| 65 | |
| 66 | |
| 67 | def _decode_score_split_launch_policy( |
| 68 | num_reqs: int, |
| 69 | head_dim: int, |
| 70 | query_dtype: torch.dtype, |
| 71 | cache_dtype: torch.dtype, |
| 72 | *, |
| 73 | is_gfx950: bool, |
| 74 | ) -> tuple[int, bool]: |
| 75 | """Choose the generic split-K launch and high-batch specialization.""" |
| 76 | use_high_batch_config = ( |
| 77 | is_gfx950 |
| 78 | and MIN_DECODE_SCORE_GFX950_HIGH_BATCH_REQUESTS |
| 79 | <= num_reqs |
| 80 | <= MAX_DECODE_SCORE_GFX950_HIGH_BATCH_REQUESTS |
| 81 | and head_dim == 128 |
| 82 | and query_dtype == torch.bfloat16 |
| 83 | and cache_dtype == torch.bfloat16 |
| 84 | ) |
| 85 | target_grid = ( |
| 86 | DECODE_SCORE_GFX950_HIGH_BATCH_TARGET_GRID |
| 87 | if use_high_batch_config |
| 88 | else DECODE_SCORE_DEFAULT_TARGET_GRID |
| 89 | ) |
| 90 | target = max( |
| 91 | 1, |
| 92 | min( |
| 93 | MAX_DECODE_SCORE_KV_CHUNKS, |
| 94 | target_grid // max(1, num_reqs), |
| 95 | ), |
| 96 | ) |
| 97 | return 1 << (target.bit_length() - 1), use_high_batch_config |
| 98 | |
| 99 | |
| 100 | def _decode_topk_launch_policy( |
| 101 | max_block: int, |
| 102 | total_q: int, |
| 103 | num_idx_heads: int, |
| 104 | topk: int, |
| 105 | *, |
| 106 | is_gfx950: bool, |
| 107 | ) -> tuple[int, int, int, int, bool, bool]: |
| 108 | """Choose the selector grid and compile-time launch configuration.""" |
| 109 | if ( |
| 110 | is_gfx950 |
| 111 | and topk == 16 |
| 112 | and 0 < max_block <= MAX_DECODE_TOPK_FAST_CHUNKS * DECODE_TOPK_BLOCKS_PER_CHUNK |
| 113 | ): |
| 114 | if max_block <= DECODE_TOPK_SHORT_BLOCKS_PER_CHUNK: |
| 115 | return 1, DECODE_TOPK_SHORT_BLOCKS_PER_CHUNK, 2, 1, True, True |
| 116 | return ( |
| 117 | MAX_DECODE_TOPK_FAST_CHUNKS, |
| 118 | DECODE_TOPK_BLOCKS_PER_CHUNK, |
| 119 | 4, |
| 120 | 2, |
| 121 | True, |
| 122 | True, |
| 123 | ) |
| 124 | |
| 125 | target = max( |
| 126 | 1, |
| 127 | min( |
| 128 | MAX_DECODE_TOPK_FAST_CHUNKS, |
| 129 | DECODE_TOPK_TARGET_GRID // max(1, total_q * num_idx_heads), |
| 130 | ), |
| 131 | ) |
| 132 | return ( |
| 133 | 1 << (target.bit_length() - 1), |
| 134 | DECODE_TOPK_BLOCKS_PER_CHUNK, |
| 135 | 8, |
| 136 | 2, |
| 137 | False, |
| 138 | False, |
| 139 | ) |
| 140 | |
| 141 | |
| 142 | # --------------------------------------------------------------------------- |
| 143 | # Bitonic top-k helpers (layout-agnostic). |
| 144 | # --------------------------------------------------------------------------- |
| 145 | @triton.jit |
| 146 | def _compare_and_swap(x, ids, flip, i: tl.constexpr, n_dims: tl.constexpr): |
| 147 | n_outer: tl.constexpr = x.numel >> n_dims |
| 148 | shape: tl.constexpr = [n_outer * 2**i, 2, 2 ** (n_dims - i - 1)] |
| 149 | y = tl.reshape(x, shape) |
| 150 | mask = tl.arange(0, 2)[None, :, None] |
| 151 | left = tl.broadcast_to(tl.sum(y * (1 - mask), 1)[:, None, :], shape).to(y.dtype) |
| 152 | right = tl.broadcast_to(tl.sum(y * mask, 1)[:, None, :], shape).to(y.dtype) |
| 153 | left = tl.reshape(left, x.shape) |
| 154 | right = tl.reshape(right, x.shape) |
| 155 | y_idx = tl.reshape(ids, shape) |
| 156 | left_idx = tl.broadcast_to(tl.sum(y_idx * (1 - mask), 1)[:, None, :], shape) |
| 157 | right_idx = tl.broadcast_to(tl.sum(y_idx * mask, 1)[:, None, :], shape) |
| 158 | left_idx = tl.reshape(left_idx, x.shape).to(y_idx.dtype) |
| 159 | right_idx = tl.reshape(right_idx, x.shape).to(y_idx.dtype) |
| 160 | idtype = tl.core.get_int_dtype(bitwidth=x.dtype.primitive_bitwidth, signed=True) |
| 161 | ileft = left.to(idtype, bitcast=True) |
| 162 | iright = right.to(idtype, bitcast=True) |
| 163 | ix = x.to(idtype, bitcast=True) |
| 164 | cond = (left > right) != flip |
| 165 | ret = ix ^ tl.where(cond, ileft ^ iright, tl.zeros_like(ix)) |
| 166 | new_ids = ids ^ tl.where(cond, left_idx ^ right_idx, tl.zeros_like(ids)) |
| 167 | return ret.to(x.dtype, bitcast=True), new_ids |
| 168 | |
| 169 | |
| 170 | @triton.jit |
| 171 | def _bitonic_merge( |
| 172 | x, ids, stage: tl.constexpr, order: tl.constexpr, n_dims: tl.constexpr |
| 173 | ): |
| 174 | n_outer: tl.constexpr = x.numel >> n_dims |
| 175 | tl.static_assert(stage <= n_dims) |
| 176 | if order == 2: |
| 177 | shape: tl.constexpr = [n_outer * 2 ** (n_dims - 1 - stage), 2, 2**stage] |
| 178 | flip = tl.reshape( |
| 179 | tl.broadcast_to(tl.arange(0, 2)[None, :, None], shape), x.shape |
| 180 | ) |
| 181 | else: |
| 182 | flip = order |
| 183 | for i in tl.static_range(stage): |
| 184 | x, ids = _compare_and_swap(x, ids, flip, i + (n_dims - stage), n_dims) |
| 185 | return x, ids |
| 186 | |
| 187 | |
| 188 | # --------------------------------------------------------------------------- |
| 189 | # Index block-score kernel (paged). score[h, token, block] = max over the |
| 190 | # 128-token block of (idx_q . index_k), causal-masked. BLOCK_SIZE_K == 128 so |
| 191 | # each K-tile is exactly one page (BLOCKS_PER_K_BLOCK == 1). |
| 192 | # --------------------------------------------------------------------------- |
| 193 | # since prefill metadata is sliced from mixed batch metadata, seq_lens and prefix_lens |
| 194 | # might lose pointer alignment, which trigger Triton recompiles. we don't actually |
| 195 | # need pointer alignment for those tensors anyway because we do scalar load. |
| 196 | @triton.jit(do_not_specialize_on_alignment=["seq_lens", "prefix_lens"]) |
| 197 | def _index_block_score_kernel( |
| 198 | q_ptr, # idx_q: [total_q, num_idx_heads, head_dim] |
| 199 | ik_cache_ptr, # index-K cache: [num_blocks, 128, head_dim] |
| 200 | score_ptr, # [num_idx_heads, total_q, max_block] |
| 201 | block_table_ptr, # [num_reqs, max_blocks] |
| 202 | cu_seqlens, # [batch+1] query start offsets |
| 203 | seq_lens, # [batch] total K length |
| 204 | prefix_lens, # [batch] context length before this chunk's queries |
| 205 | num_idx_heads, |
| 206 | head_dim: tl.constexpr, |
| 207 | stride_q_n, |
| 208 | stride_q_h, |
| 209 | stride_q_d, |
| 210 | stride_ik_blk, |
| 211 | stride_ik_pos, |
| 212 | stride_ik_d, |
| 213 | stride_s_h, |
| 214 | stride_s_n, |
| 215 | stride_s_k, |
| 216 | stride_bt_b, |
| 217 | BLOCK_SIZE_Q: tl.constexpr, |
| 218 | BLOCK_SIZE_K: tl.constexpr, # == SPARSE_BLOCK_SIZE (128) |
| 219 | ): |
| 220 | pid_q = tl.program_id(0) |
| 221 | pid_bh = tl.program_id(1) |
| 222 | pid_b = pid_bh // num_idx_heads |
| 223 | pid_h = pid_bh % num_idx_heads |
| 224 | |
| 225 | seq_start = tl.load(cu_seqlens + pid_b) |
| 226 | q_len = tl.load(cu_seqlens + pid_b + 1) - seq_start |
| 227 | seq_len = tl.load(seq_lens + pid_b) |
| 228 | prefix_len = tl.load(prefix_lens + pid_b) |
| 229 | if BLOCK_SIZE_Q * pid_q >= q_len: |
| 230 | return |
| 231 | |
| 232 | q_ptrs = tl.make_block_ptr( |
| 233 | base=q_ptr + seq_start * stride_q_n + pid_h * stride_q_h, |
| 234 | shape=(q_len, head_dim), |
| 235 | strides=(stride_q_n, stride_q_d), |
| 236 | offsets=(pid_q * BLOCK_SIZE_Q, 0), |
| 237 | block_shape=(BLOCK_SIZE_Q, head_dim), |
| 238 | order=(1, 0), |
| 239 | ) |
| 240 | q = tl.load(q_ptrs, boundary_check=(0,), padding_option="zero") |
| 241 | q_start = prefix_len + pid_q * BLOCK_SIZE_Q |
| 242 | |
| 243 | off_q = tl.arange(0, BLOCK_SIZE_Q) + pid_q * BLOCK_SIZE_Q + prefix_len |
| 244 | off_k = tl.arange(0, BLOCK_SIZE_K) |
| 245 | off_d = tl.arange(0, head_dim) |
| 246 | # Block table row for this request. |
| 247 | bt_row = block_table_ptr + pid_b * stride_bt_b |
| 248 | # Causal window: only blocks up to the last query token's position. |
| 249 | hi = min(seq_len, prefix_len + (pid_q + 1) * BLOCK_SIZE_Q) |
| 250 | for i in tl.range(0, hi, BLOCK_SIZE_K): |
| 251 | blk = i // BLOCK_SIZE_K |
| 252 | page = tl.load(bt_row + blk).to(tl.int64) |
| 253 | pos = i + off_k |
| 254 | # index-K for this page: [BLOCK_SIZE_D, BLOCK_SIZE_K] (transposed) |
| 255 | # we don't need masked load for K, because KV cache ensures |
| 256 | # allocation is multiple of BLOCK_SIZE_K. |
| 257 | # for tokens beyond seqlen, they will be masked in qk later. |
| 258 | k = tl.load( |
| 259 | ik_cache_ptr |
| 260 | + page * stride_ik_blk |
| 261 | + off_k[None, :] * stride_ik_pos |
| 262 | + off_d[:, None] * stride_ik_d, |
| 263 | ) |
| 264 | qk = tl.dot(q, k) |
| 265 | # apply causal mask as needed |
| 266 | if q_start < i + BLOCK_SIZE_K: |
| 267 | qk = tl.where(off_q[:, None] >= pos[None, :], qk, float("-inf")) |
| 268 | # one sparse block per K-tile -> max over the 128 positions |
| 269 | score = tl.max(qk, axis=1) # [BLOCK_SIZE_Q] |
| 270 | s_ptrs = ( |
| 271 | score_ptr |
| 272 | + pid_h * stride_s_h |
| 273 | + (seq_start + pid_q * BLOCK_SIZE_Q + tl.arange(0, BLOCK_SIZE_Q)) |
| 274 | * stride_s_n |
| 275 | + blk * stride_s_k |
| 276 | ) |
| 277 | q_store_mask = (pid_q * BLOCK_SIZE_Q + tl.arange(0, BLOCK_SIZE_Q)) < q_len |
| 278 | tl.store(s_ptrs, score, mask=q_store_mask) |
| 279 | |
| 280 | |
| 281 | # --------------------------------------------------------------------------- |
| 282 | # Top-k selection over per-token block scores (layout-agnostic). block_size_q |
| 283 | # is 1 for M3, so top-k is computed per query token. |
| 284 | # --------------------------------------------------------------------------- |
| 285 | # since prefill metadata is sliced from mixed batch metadata, prefix_lens |
| 286 | # might lose pointer alignment, which trigger Triton recompiles. we don't actually |
| 287 | # need pointer alignment for those tensors anyway because we do scalar load. |
| 288 | @triton.heuristics({"BLOCK_SIZE_T": lambda args: triton.next_power_of_2(args["topk"])}) |
| 289 | @triton.autotune( |
| 290 | configs=[ |
| 291 | triton.Config({"BLOCK_SIZE_K": 2048}, num_warps=8, num_stages=2), |
| 292 | triton.Config({"BLOCK_SIZE_K": 1024}, num_warps=8, num_stages=2), |
| 293 | triton.Config({"BLOCK_SIZE_K": 512}, num_warps=8, num_stages=2), |
| 294 | triton.Config({"BLOCK_SIZE_K": 256}, num_warps=8, num_stages=2), |
| 295 | triton.Config({"BLOCK_SIZE_K": 128}, num_warps=4, num_stages=2), |
| 296 | triton.Config({"BLOCK_SIZE_K": 64}, num_warps=2, num_stages=2), |
| 297 | ], |
| 298 | key=["BLOCK_SIZE_T"], |
| 299 | ) |
| 300 | @triton.jit(do_not_specialize_on_alignment=["prefix_lens"]) |
| 301 | def _topk_index_kernel( |
| 302 | s_ptr, # [num_heads, total_q, max_block] |
| 303 | ti_ptr, # [num_heads, total_q, topk] |
| 304 | sample_interval: tl.constexpr, # block_size_q (1 for M3) |
| 305 | block_size: tl.constexpr, # sparse block size (128) |
| 306 | cu_seqlens, |
| 307 | cu_seqblocks_q, |
| 308 | prefix_lens, |
| 309 | topk, |
| 310 | init_blocks: tl.constexpr, |
| 311 | local_blocks: tl.constexpr, |
| 312 | stride_s_h, |
| 313 | stride_s_n, |
| 314 | stride_s_k, |
| 315 | stride_ti_h, |
| 316 | stride_ti_n, |
| 317 | stride_ti_t, |
| 318 | BLOCK_SIZE_K: tl.constexpr, |
| 319 | BLOCK_SIZE_T: tl.constexpr, |
| 320 | MASK_INIT: tl.constexpr, |
| 321 | MASK_LOCAL: tl.constexpr, |
| 322 | ): |
| 323 | tl.static_assert(BLOCK_SIZE_K > BLOCK_SIZE_T) |
| 324 | pid_q = tl.program_id(0) |
| 325 | pid_b = tl.program_id(1) |
| 326 | pid_h = tl.program_id(2) |
| 327 | seq_start = tl.load(cu_seqlens + pid_b) |
| 328 | block_start = tl.load(cu_seqblocks_q + pid_b) |
| 329 | block_num = tl.load(cu_seqblocks_q + pid_b + 1) - block_start |
| 330 | prefix_len = tl.load(prefix_lens + pid_b) |
| 331 | if pid_q >= block_num: |
| 332 | return |
| 333 | off_k = tl.arange(0, BLOCK_SIZE_K) |
| 334 | off_t = tl.arange(0, BLOCK_SIZE_T) |
| 335 | s_ptrs = ( |
| 336 | s_ptr |
| 337 | + (seq_start + pid_q * sample_interval) * stride_s_n |
| 338 | + pid_h * stride_s_h |
| 339 | + off_k * stride_s_k |
| 340 | ) |
| 341 | topk_score = tl.full((BLOCK_SIZE_K,), -1e30, dtype=tl.float32) |
| 342 | topk_idx = tl.full((BLOCK_SIZE_K,), 0, dtype=tl.int32) |
| 343 | left_half_mask = tl.arange(0, BLOCK_SIZE_K) < BLOCK_SIZE_K // 2 |
| 344 | valid_blocks = (prefix_len + pid_q * sample_interval + block_size) // block_size |
| 345 | for i in tl.range(0, valid_blocks, BLOCK_SIZE_K): |
| 346 | causal_mask = i + off_k < valid_blocks |
| 347 | local_mask = i + off_k >= max(0, valid_blocks - local_blocks) |
| 348 | init_mask = i + off_k < init_blocks |
| 349 | score = tl.load(s_ptrs, mask=causal_mask, other=-1e30).to(tl.float32) |
| 350 | score = tl.where(score != score, -1e30, score) |
| 351 | s_ptrs = s_ptrs + stride_s_k * BLOCK_SIZE_K |
| 352 | if MASK_INIT: |
| 353 | score = tl.where(causal_mask & init_mask, score - 1e29, score) |
| 354 | else: |
| 355 | score = tl.where(causal_mask & init_mask, 1e30, score) |
| 356 | if MASK_LOCAL: |
| 357 | score = tl.where(causal_mask & local_mask, score - 1e28, score) |
| 358 | else: |
| 359 | score = tl.where(causal_mask & local_mask, 1e29, score) |
| 360 | topk_score, last_topk_score = score, topk_score |
| 361 | topk_idx, last_topk_idx = (tl.where(causal_mask, i + off_k + 1, 0), topk_idx) |
| 362 | n_dims: tl.constexpr = tl.standard._log2(BLOCK_SIZE_K) |
| 363 | for j in tl.static_range(1, n_dims): |
| 364 | topk_score, topk_idx = _bitonic_merge( |
| 365 | topk_score, topk_idx.to(tl.int32), j, 2, n_dims |
| 366 | ) |
| 367 | if i != 0: |
| 368 | topk_score, topk_idx = _bitonic_merge( |
| 369 | topk_score, topk_idx.to(tl.int32), n_dims, False, n_dims |
| 370 | ) |
| 371 | topk_score_new = last_topk_score * left_half_mask + topk_score * ( |
| 372 | 1 - left_half_mask |
| 373 | ) |
| 374 | topk_idx_new = last_topk_idx * left_half_mask + topk_idx * ( |
| 375 | 1 - left_half_mask |
| 376 | ) |
| 377 | topk_score, topk_idx = _bitonic_merge( |
| 378 | topk_score_new, topk_idx_new.to(tl.int32), n_dims, True, n_dims |
| 379 | ) |
| 380 | else: |
| 381 | topk_score, topk_idx = _bitonic_merge( |
| 382 | topk_score, topk_idx.to(tl.int32), n_dims, True, n_dims |
| 383 | ) |
| 384 | topk_mask = tl.arange(0, BLOCK_SIZE_K // BLOCK_SIZE_T) == 0 |
| 385 | topk_idx = tl.sum( |
| 386 | topk_mask[:, None] |
| 387 | * tl.reshape(topk_idx - 1, [BLOCK_SIZE_K // BLOCK_SIZE_T, BLOCK_SIZE_T]), |
| 388 | axis=0, |
| 389 | ) |
| 390 | ti_ptrs = ( |
| 391 | ti_ptr |
| 392 | + (block_start + pid_q) * stride_ti_n |
| 393 | + pid_h * stride_ti_h |
| 394 | + off_t * stride_ti_t |
| 395 | ) |
| 396 | store_mask = off_t < topk |
| 397 | valid_mask = off_t < valid_blocks |
| 398 | topk_idx = tl.where(store_mask & valid_mask, topk_idx, -1) |
| 399 | tl.store(ti_ptrs, topk_idx.to(ti_ptrs.dtype.element_ty), mask=store_mask) |
| 400 | |
| 401 | |
| 402 | # --------------------------------------------------------------------------- |
| 403 | # Decode index-score kernel (split-K over seq blocks). Decode batches are |
| 404 | # flattened request-major, with a runtime query length used to map each query |
| 405 | # token back to its request metadata. Chunk counts depend only on shape |
| 406 | # constants so the grid is fixed within a cuda graph. The score scale is omitted |
| 407 | # because decode only consumes block ordering. |
| 408 | # --------------------------------------------------------------------------- |
| 409 | @triton.jit(do_not_specialize=["num_kv_chunks", "decode_query_len"]) |
| 410 | def _decode_index_score_kernel( |
| 411 | q_ptr, # idx_q: [total_q, num_idx_heads, head_dim] |
| 412 | ik_cache_ptr, # index-K cache: [num_blocks, 128, head_dim] |
| 413 | score_ptr, # [num_idx_heads, total_q, max_block] |
| 414 | block_table_ptr, # [num_reqs, max_blocks] |
| 415 | seq_lens, # [num_reqs] |
| 416 | num_idx_heads: tl.constexpr, |
| 417 | head_dim: tl.constexpr, |
| 418 | init_blocks, |
| 419 | local_blocks, |
| 420 | decode_query_len, |
| 421 | stride_q_n, |
| 422 | stride_q_h, |
| 423 | stride_q_d, |
| 424 | stride_ik_blk, |
| 425 | stride_ik_pos, |
| 426 | stride_ik_d, |
| 427 | stride_s_h, |
| 428 | stride_s_n, |
| 429 | stride_s_k, |
| 430 | stride_bt_b, |
| 431 | BLOCK_SIZE_K: tl.constexpr, # == SPARSE_BLOCK_SIZE (128) |
| 432 | BLOCK_SIZE_Q: tl.constexpr, |
| 433 | num_kv_chunks, |
| 434 | USE_PDL: tl.constexpr, |
| 435 | ): |
| 436 | BLOCK_SIZE_HQ: tl.constexpr = num_idx_heads * BLOCK_SIZE_Q |
| 437 | pid_r = tl.program_id(0) |
| 438 | pid_c = tl.program_id(1) |
| 439 | hq_offsets = tl.arange(0, BLOCK_SIZE_HQ) |
| 440 | h_offsets = hq_offsets // BLOCK_SIZE_Q |
| 441 | q_offsets = hq_offsets % BLOCK_SIZE_Q |
| 442 | q_mask = q_offsets < decode_query_len |
| 443 | q_ids = pid_r * decode_query_len + q_offsets |
| 444 | |
| 445 | if USE_PDL: |
| 446 | tl.extra.cuda.gdc_wait() |
| 447 | tl.extra.cuda.gdc_launch_dependents() |
| 448 | |
| 449 | seq_len = tl.load(seq_lens + pid_r) |
| 450 | query_pos = seq_len - decode_query_len + q_offsets |
| 451 | # Full-CG padding uses zero-length request rows. Clamp to an empty |
| 452 | # attention range instead of letting padded rows produce negative lengths. |
| 453 | kv_len = tl.maximum(query_pos + 1, 0) |
| 454 | num_blocks_q = (kv_len + BLOCK_SIZE_K - 1) // BLOCK_SIZE_K |
| 455 | kv_len_max = tl.max(tl.where(q_mask, kv_len, 0), axis=0) |
| 456 | num_blocks = (kv_len_max + BLOCK_SIZE_K - 1) // BLOCK_SIZE_K |
| 457 | |
| 458 | # block-aligned fixed-count split: grid independent of seq_len (cuda graph). |
| 459 | chunk_size_blocks = (num_blocks + num_kv_chunks - 1) // num_kv_chunks |
| 460 | chunk_start_block = pid_c * chunk_size_blocks |
| 461 | chunk_end_block = tl.minimum(chunk_start_block + chunk_size_blocks, num_blocks) |
| 462 | if chunk_start_block >= chunk_end_block: |
| 463 | return |
| 464 | off_k = tl.arange(0, BLOCK_SIZE_K) # positions within a 128-block |
| 465 | off_d = tl.arange(0, head_dim) |
| 466 | bt_row = block_table_ptr + pid_r * stride_bt_b |
| 467 | # Force-select init (1e30) and local (1e29, higher priority) blocks. |
| 468 | local_start = tl.maximum(0, num_blocks_q - local_blocks) |
| 469 | # Query vectors for all index heads in a small spec-decode block. |
| 470 | q = tl.load( |
| 471 | q_ptr |
| 472 | + q_ids[None, :] * stride_q_n |
| 473 | + h_offsets[None, :] * stride_q_h |
| 474 | + off_d[:, None] * stride_q_d, |
| 475 | mask=q_mask[None, :], |
| 476 | other=0.0, |
| 477 | ) # [D,HQ] |
| 478 | for blk in tl.range(chunk_start_block, chunk_end_block): |
| 479 | page = tl.load(bt_row + blk).to(tl.int64) |
| 480 | pos = blk * BLOCK_SIZE_K + off_k |
| 481 | pos_mask = pos[:, None] < kv_len[None, :] |
| 482 | # we don't need masked load for K, because KV cache ensures |
| 483 | # allocation is multiple of BLOCK_SIZE_K. |
| 484 | # for tokens beyond seqlen, they will be masked in qk later. |
| 485 | k = tl.load( |
| 486 | ik_cache_ptr |
| 487 | + page * stride_ik_blk |
| 488 | + off_k[:, None] * stride_ik_pos |
| 489 | + off_d * stride_ik_d, |
| 490 | ) # [N,D] |
| 491 | if BLOCK_SIZE_HQ == 1: |
| 492 | # Degenerate GEMV (q is [D,1]): vectorized fp32 multiply + reduce |
| 493 | # instead of an MFMA tile. Numerically equivalent to tl.dot. |
| 494 | q_vec = tl.sum(q, axis=1).to(tl.float32) # [D] |
| 495 | kq = tl.sum(k.to(tl.float32) * q_vec[None, :], axis=1)[:, None] # [N,1] |
| 496 | else: |
| 497 | # fp32 accumulation is required for the fp8 (e4m3) index cache: q/k |
| 498 | # are loaded in their stored dtype (bf16 or e4m3) and the MMA |
| 499 | # accumulates in fp32 so the per-block max score is exact for the |
| 500 | # fp8 indexer too. |
| 501 | kq = tl.dot(k, q, out_dtype=tl.float32) # [N,HQ] |
| 502 | kq = tl.where(pos_mask & q_mask[None, :], kq, float("-inf")) |
| 503 | score = tl.max(kq, axis=0) # [HQ] |
| 504 | is_visible_block = blk < num_blocks_q |
| 505 | is_init = (blk < init_blocks) & is_visible_block |
| 506 | is_local = (blk >= local_start) & is_visible_block |
| 507 | score = tl.where(is_local, 1e29, tl.where(is_init, 1e30, score)) |
| 508 | tl.store( |
| 509 | score_ptr + h_offsets * stride_s_h + q_ids * stride_s_n + blk * stride_s_k, |
| 510 | score, |
| 511 | mask=q_mask, |
| 512 | ) |
| 513 | |
| 514 | |
| 515 | @triton.jit |
| 516 | def _ceil_div_nonnegative(value, divisor): |
| 517 | return value // divisor + tl.where(value % divisor != 0, 1, 0) |
| 518 | |
| 519 | |
| 520 | @triton.jit |
| 521 | def _decode_index_score_mapped_range( |
| 522 | q_ptr, |
| 523 | ik_cache_ptr, |
| 524 | score_ptr, |
| 525 | block_table_ptr, |
| 526 | pid_r, |
| 527 | seq_len, |
| 528 | chunk_start_block, |
| 529 | blocks_per_program, |
| 530 | num_idx_heads: tl.constexpr, |
| 531 | head_dim: tl.constexpr, |
| 532 | init_blocks, |
| 533 | local_blocks, |
| 534 | decode_query_len, |
| 535 | stride_q_n, |
| 536 | stride_q_h, |
| 537 | stride_q_d, |
| 538 | stride_ik_blk, |
| 539 | stride_ik_pos, |
| 540 | stride_ik_d, |
| 541 | stride_s_h, |
| 542 | stride_s_n, |
| 543 | stride_s_k, |
| 544 | stride_bt_b, |
| 545 | BLOCK_SIZE_K: tl.constexpr, |
| 546 | BLOCK_SIZE_Q: tl.constexpr, |
| 547 | ): |
| 548 | BLOCK_SIZE_HQ: tl.constexpr = num_idx_heads * BLOCK_SIZE_Q |
| 549 | hq_offsets = tl.arange(0, BLOCK_SIZE_HQ) |
| 550 | h_offsets = hq_offsets // BLOCK_SIZE_Q |
| 551 | q_offsets = hq_offsets % BLOCK_SIZE_Q |
| 552 | q_mask = q_offsets < decode_query_len |
| 553 | q_ids = pid_r * decode_query_len + q_offsets |
| 554 | |
| 555 | query_pos = seq_len - decode_query_len + q_offsets |
| 556 | kv_len = tl.maximum(query_pos + 1, 0) |
| 557 | num_blocks_q = (kv_len + BLOCK_SIZE_K - 1) // BLOCK_SIZE_K |
| 558 | kv_len_max = tl.max(tl.where(q_mask, kv_len, 0), axis=0) |
| 559 | num_blocks = (kv_len_max + BLOCK_SIZE_K - 1) // BLOCK_SIZE_K |
| 560 | chunk_end_block = tl.minimum( |
| 561 | chunk_start_block + blocks_per_program, |
| 562 | num_blocks, |
| 563 | ) |
| 564 | |
| 565 | if chunk_start_block < chunk_end_block: |
| 566 | off_k = tl.arange(0, BLOCK_SIZE_K) |
| 567 | off_d = tl.arange(0, head_dim) |
| 568 | bt_row = block_table_ptr + pid_r * stride_bt_b |
| 569 | local_start = tl.maximum(0, num_blocks_q - local_blocks) |
| 570 | q = tl.load( |
| 571 | q_ptr |
| 572 | + q_ids[None, :] * stride_q_n |
| 573 | + h_offsets[None, :] * stride_q_h |
| 574 | + off_d[:, None] * stride_q_d, |
| 575 | mask=q_mask[None, :], |
| 576 | other=0.0, |
| 577 | ) |
| 578 | for blk in tl.range(chunk_start_block, chunk_end_block): |
| 579 | page = tl.load(bt_row + blk).to(tl.int64) |
| 580 | pos = blk * BLOCK_SIZE_K + off_k |
| 581 | pos_mask = pos[:, None] < kv_len[None, :] |
| 582 | k = tl.load( |
| 583 | ik_cache_ptr |
| 584 | + page * stride_ik_blk |
| 585 | + off_k[:, None] * stride_ik_pos |
| 586 | + off_d * stride_ik_d, |
| 587 | ) |
| 588 | if BLOCK_SIZE_HQ == 1: |
| 589 | q_vec = tl.sum(q, axis=1).to(tl.float32) |
| 590 | kq = tl.sum( |
| 591 | k.to(tl.float32) * q_vec[None, :], |
| 592 | axis=1, |
| 593 | )[:, None] |
| 594 | else: |
| 595 | kq = tl.dot(k, q, out_dtype=tl.float32) |
| 596 | kq = tl.where( |
| 597 | pos_mask & q_mask[None, :], |
| 598 | kq, |
| 599 | float("-inf"), |
| 600 | ) |
| 601 | score = tl.max(kq, axis=0) |
| 602 | is_visible_block = blk < num_blocks_q |
| 603 | is_init = (blk < init_blocks) & is_visible_block |
| 604 | is_local = (blk >= local_start) & is_visible_block |
| 605 | score = tl.where( |
| 606 | is_local, |
| 607 | 1e29, |
| 608 | tl.where(is_init, 1e30, score), |
| 609 | ) |
| 610 | tl.store( |
| 611 | score_ptr |
| 612 | + h_offsets * stride_s_h |
| 613 | + q_ids * stride_s_n |
| 614 | + blk * stride_s_k, |
| 615 | score, |
| 616 | mask=q_mask, |
| 617 | ) |
| 618 | |
| 619 | |
| 620 | @triton.jit(do_not_specialize=["score_program_budget", "decode_query_len"]) |
| 621 | def _decode_index_score_balanced_kernel( |
| 622 | q_ptr, |
| 623 | ik_cache_ptr, |
| 624 | score_ptr, |
| 625 | block_table_ptr, |
| 626 | seq_lens, |
| 627 | num_idx_heads: tl.constexpr, |
| 628 | head_dim: tl.constexpr, |
| 629 | init_blocks, |
| 630 | local_blocks, |
| 631 | NUM_REQUESTS: tl.constexpr, |
| 632 | score_program_budget, |
| 633 | decode_query_len, |
| 634 | stride_q_n, |
| 635 | stride_q_h, |
| 636 | stride_q_d, |
| 637 | stride_ik_blk, |
| 638 | stride_ik_pos, |
| 639 | stride_ik_d, |
| 640 | stride_s_h, |
| 641 | stride_s_n, |
| 642 | stride_s_k, |
| 643 | stride_bt_b, |
| 644 | BLOCK_SIZE_K: tl.constexpr, |
| 645 | BLOCK_SIZE_Q: tl.constexpr, |
| 646 | ): |
| 647 | pid = tl.program_id(0) |
| 648 | total_blocks = tl.zeros((), dtype=tl.int32) |
| 649 | for req in tl.static_range(0, NUM_REQUESTS): |
| 650 | req_seq_len = tl.maximum(tl.load(seq_lens + req), 0) |
| 651 | total_blocks += _ceil_div_nonnegative( |
| 652 | req_seq_len, |
| 653 | BLOCK_SIZE_K, |
| 654 | ) |
| 655 | blocks_per_program = tl.maximum( |
| 656 | 1, |
| 657 | _ceil_div_nonnegative( |
| 658 | total_blocks, |
| 659 | score_program_budget, |
| 660 | ), |
| 661 | ) |
| 662 | |
| 663 | program_offset = tl.zeros((), dtype=tl.int32) |
| 664 | pid_r = tl.zeros((), dtype=tl.int32) |
| 665 | chunk_start_block = tl.zeros((), dtype=tl.int32) |
| 666 | selected_seq_len = tl.zeros((), dtype=tl.int32) |
| 667 | owns_work = tl.zeros((), dtype=tl.int32) |
| 668 | for req in tl.static_range(0, NUM_REQUESTS): |
| 669 | req_seq_len = tl.maximum(tl.load(seq_lens + req), 0) |
| 670 | req_num_blocks = _ceil_div_nonnegative( |
| 671 | req_seq_len, |
| 672 | BLOCK_SIZE_K, |
| 673 | ) |
| 674 | req_programs = _ceil_div_nonnegative( |
| 675 | req_num_blocks, |
| 676 | blocks_per_program, |
| 677 | ) |
| 678 | owns_request = (pid >= program_offset) & (pid < program_offset + req_programs) |
| 679 | pid_r = tl.where(owns_request, req, pid_r) |
| 680 | local_program = tl.where( |
| 681 | owns_request, |
| 682 | pid - program_offset, |
| 683 | 0, |
| 684 | ) |
| 685 | chunk_start_block = tl.where( |
| 686 | owns_request, |
| 687 | local_program * blocks_per_program, |
| 688 | chunk_start_block, |
| 689 | ) |
| 690 | selected_seq_len = tl.where( |
| 691 | owns_request, |
| 692 | req_seq_len, |
| 693 | selected_seq_len, |
| 694 | ) |
| 695 | owns_work += owns_request.to(tl.int32) |
| 696 | program_offset += req_programs |
| 697 | if owns_work == 0: |
| 698 | return |
| 699 | |
| 700 | _decode_index_score_mapped_range( |
| 701 | q_ptr, |
| 702 | ik_cache_ptr, |
| 703 | score_ptr, |
| 704 | block_table_ptr, |
| 705 | pid_r, |
| 706 | selected_seq_len, |
| 707 | chunk_start_block, |
| 708 | blocks_per_program, |
| 709 | num_idx_heads, |
| 710 | head_dim, |
| 711 | init_blocks, |
| 712 | local_blocks, |
| 713 | decode_query_len, |
| 714 | stride_q_n, |
| 715 | stride_q_h, |
| 716 | stride_q_d, |
| 717 | stride_ik_blk, |
| 718 | stride_ik_pos, |
| 719 | stride_ik_d, |
| 720 | stride_s_h, |
| 721 | stride_s_n, |
| 722 | stride_s_k, |
| 723 | stride_bt_b, |
| 724 | BLOCK_SIZE_K, |
| 725 | BLOCK_SIZE_Q, |
| 726 | ) |
| 727 | |
| 728 | |
| 729 | # --------------------------------------------------------------------------- |
| 730 | # Decode top-k. Each CTA keeps a packed score/id top-k. Short rows emit |
| 731 | # directly; long rows synchronize their live CTAs and the last arrival merges |
| 732 | # the partial lists and optionally builds the sparse page table. |
| 733 | # --------------------------------------------------------------------------- |
| 734 | @triton.jit |
| 735 | def _decode_topk_key(score, index, valid): |
| 736 | score = tl.where(score != score, -1e30, score) |
| 737 | score = tl.where(score == 0.0, 0.0, score) |
| 738 | bits = score.to(tl.uint32, bitcast=True) |
| 739 | ordered = bits ^ tl.where(bits >> 31 != 0, 0xFFFFFFFF, 0x80000000) |
| 740 | tie = (0xFFFF - index).to(tl.int64) |
| 741 | key = (1 << 48) | (ordered.to(tl.int64) << 16) | tie |
| 742 | return tl.where(valid, key, 0) |
| 743 | |
| 744 | |
| 745 | @triton.jit |
| 746 | def _store_decode_topk( |
| 747 | winners, |
| 748 | topk_ptr, |
| 749 | attention_block_table_ptr, |
| 750 | sparse_bt_ptr, |
| 751 | sparse_ctx_ptr, |
| 752 | pid_b, |
| 753 | pid_h, |
| 754 | req_id, |
| 755 | query_pos, |
| 756 | num_blocks, |
| 757 | topk: tl.constexpr, |
| 758 | block_size: tl.constexpr, |
| 759 | pages_per_sparse_block: tl.constexpr, |
| 760 | block_page_stride: tl.constexpr, |
| 761 | stride_topk_h, |
| 762 | stride_topk_b, |
| 763 | stride_topk_t, |
| 764 | stride_attention_bt_b, |
| 765 | stride_sparse_bt_b, |
| 766 | BLOCK_SIZE_T: tl.constexpr, |
| 767 | EMIT_SPARSE_TABLE: tl.constexpr, |
| 768 | ): |
| 769 | off_t = tl.arange(0, BLOCK_SIZE_T) |
| 770 | topk_idx = (0xFFFF - (winners & 0xFFFF)).to(tl.int32) - 1 |
| 771 | topk_idx = tl.where(off_t < tl.minimum(topk, num_blocks), topk_idx, -1) |
| 772 | tl.store( |
| 773 | topk_ptr |
| 774 | + pid_h * stride_topk_h |
| 775 | + pid_b * stride_topk_b |
| 776 | + off_t * stride_topk_t, |
| 777 | topk_idx, |
| 778 | mask=off_t < topk, |
| 779 | ) |
| 780 | if EMIT_SPARSE_TABLE: |
| 781 | _write_sparse_block_table_row_from_values( |
| 782 | topk_idx, |
| 783 | attention_block_table_ptr + req_id * stride_attention_bt_b, |
| 784 | sparse_bt_ptr + pid_b * stride_sparse_bt_b, |
| 785 | sparse_ctx_ptr + pid_b, |
| 786 | query_pos, |
| 787 | topk, |
| 788 | block_size, |
| 789 | pages_per_sparse_block, |
| 790 | block_page_stride, |
| 791 | BLOCK_SIZE_T, |
| 792 | ) |
| 793 | |
| 794 | |
| 795 | @triton.jit |
| 796 | def _merge_store_decode_topk( |
| 797 | partial_ptr, |
| 798 | counter, |
| 799 | topk_ptr, |
| 800 | attention_block_table_ptr, |
| 801 | sparse_bt_ptr, |
| 802 | sparse_ctx_ptr, |
| 803 | active_chunks, |
| 804 | pid_b, |
| 805 | pid_h, |
| 806 | req_id, |
| 807 | query_pos, |
| 808 | num_blocks, |
| 809 | stride_partial_c, |
| 810 | stride_partial_h, |
| 811 | stride_partial_b, |
| 812 | stride_partial_t, |
| 813 | stride_topk_h, |
| 814 | stride_topk_b, |
| 815 | stride_topk_t, |
| 816 | stride_attention_bt_b, |
| 817 | stride_sparse_bt_b, |
| 818 | topk: tl.constexpr, |
| 819 | block_size: tl.constexpr, |
| 820 | pages_per_sparse_block: tl.constexpr, |
| 821 | block_page_stride: tl.constexpr, |
| 822 | BLOCK_SIZE_T: tl.constexpr, |
| 823 | MERGE_CHUNKS: tl.constexpr, |
| 824 | EMIT_SPARSE_TABLE: tl.constexpr, |
| 825 | ): |
| 826 | candidate_width: tl.constexpr = MERGE_CHUNKS * BLOCK_SIZE_T |
| 827 | off_candidate = tl.arange(0, candidate_width) |
| 828 | candidate_chunk = off_candidate // BLOCK_SIZE_T |
| 829 | candidate_slot = off_candidate % BLOCK_SIZE_T |
| 830 | candidates = tl.load( |
| 831 | partial_ptr |
| 832 | + candidate_chunk * stride_partial_c |
| 833 | + pid_h * stride_partial_h |
| 834 | + pid_b * stride_partial_b |
| 835 | + candidate_slot * stride_partial_t, |
| 836 | mask=candidate_chunk < active_chunks, |
| 837 | other=0, |
| 838 | volatile=True, |
| 839 | ) |
| 840 | winners = tl.topk(candidates, BLOCK_SIZE_T) |
| 841 | _store_decode_topk( |
| 842 | winners, |
| 843 | topk_ptr, |
| 844 | attention_block_table_ptr, |
| 845 | sparse_bt_ptr, |
| 846 | sparse_ctx_ptr, |
| 847 | pid_b, |
| 848 | pid_h, |
| 849 | req_id, |
| 850 | query_pos, |
| 851 | num_blocks, |
| 852 | topk, |
| 853 | block_size, |
| 854 | pages_per_sparse_block, |
| 855 | block_page_stride, |
| 856 | stride_topk_h, |
| 857 | stride_topk_b, |
| 858 | stride_topk_t, |
| 859 | stride_attention_bt_b, |
| 860 | stride_sparse_bt_b, |
| 861 | BLOCK_SIZE_T, |
| 862 | EMIT_SPARSE_TABLE, |
| 863 | ) |
| 864 | tl.atomic_xchg(counter, 0, sem="acq_rel", scope="gpu") |
| 865 | |
| 866 | |
| 867 | @triton.jit(do_not_specialize=["decode_query_len"]) |
| 868 | def _decode_topk_fused_kernel( |
| 869 | score_ptr, # [num_idx_heads, total_q, max_block] |
| 870 | partial_ptr, # [NUM_TOPK_CHUNKS, num_idx_heads, total_q, BLOCK_SIZE_T] |
| 871 | counter_ptr, # [num_idx_heads, total_q] |
| 872 | topk_ptr, # [num_idx_heads, total_q, topk] |
| 873 | seq_lens_ptr, # [num_reqs] |
| 874 | attention_block_table_ptr, # [num_reqs, max_blocks] |
| 875 | sparse_bt_ptr, # [total_q, topk * PAGES_PER_SPARSE_BLOCK] |
| 876 | sparse_ctx_ptr, # [total_q] |
| 877 | decode_query_len, |
| 878 | stride_s_h, |
| 879 | stride_s_b, |
| 880 | stride_s_k, |
| 881 | stride_partial_c, |
| 882 | stride_partial_h, |
| 883 | stride_partial_b, |
| 884 | stride_partial_t, |
| 885 | stride_counter_h, |
| 886 | stride_counter_b, |
| 887 | stride_topk_h, |
| 888 | stride_topk_b, |
| 889 | stride_topk_t, |
| 890 | stride_attention_bt_b, |
| 891 | stride_sparse_bt_b, |
| 892 | topk: tl.constexpr, |
| 893 | block_size: tl.constexpr, |
| 894 | pages_per_sparse_block: tl.constexpr, |
| 895 | block_page_stride: tl.constexpr, |
| 896 | NUM_TOPK_CHUNKS: tl.constexpr, |
| 897 | BLOCK_SIZE_K: tl.constexpr, |
| 898 | BLOCK_SIZE_T: tl.constexpr, |
| 899 | EMIT_SPARSE_TABLE: tl.constexpr, |
| 900 | SINGLE_TILE_GUARANTEED: tl.constexpr, |
| 901 | ADAPTIVE_FINAL_MERGE: tl.constexpr, |
| 902 | ): |
| 903 | tl.static_assert(topk <= BLOCK_SIZE_T) |
| 904 | tl.static_assert(BLOCK_SIZE_T <= BLOCK_SIZE_K) |
| 905 | pid_b = tl.program_id(0) |
| 906 | pid_h = tl.program_id(1) |
| 907 | pid_chunk = tl.program_id(2) |
| 908 | req_id = pid_b // decode_query_len |
| 909 | q_offset = pid_b - req_id * decode_query_len |
| 910 | |
| 911 | seq_len = tl.load(seq_lens_ptr + req_id) |
| 912 | query_pos = seq_len - decode_query_len + q_offset |
| 913 | kv_len = tl.maximum(query_pos + 1, 0) |
| 914 | num_blocks = (kv_len + block_size - 1) // block_size |
| 915 | |
| 916 | desired_chunks = (num_blocks + BLOCK_SIZE_K - 1) // BLOCK_SIZE_K |
| 917 | active_chunks = tl.maximum(1, tl.minimum(NUM_TOPK_CHUNKS, desired_chunks)) |
| 918 | if pid_chunk >= active_chunks: |
| 919 | return |
| 920 | |
| 921 | blocks_per_chunk = num_blocks // active_chunks |
| 922 | extra_chunks = num_blocks - blocks_per_chunk * active_chunks |
| 923 | chunk_start = pid_chunk * blocks_per_chunk + tl.minimum(pid_chunk, extra_chunks) |
| 924 | chunk_blocks = blocks_per_chunk + (pid_chunk < extra_chunks) |
| 925 | |
| 926 | off_k = tl.arange(0, BLOCK_SIZE_K) |
| 927 | off_t = tl.arange(0, BLOCK_SIZE_T) |
| 928 | if SINGLE_TILE_GUARANTEED: |
| 929 | valid = off_k < chunk_blocks |
| 930 | score = tl.load( |
| 931 | score_ptr |
| 932 | + pid_h * stride_s_h |
| 933 | + pid_b * stride_s_b |
| 934 | + (chunk_start + off_k) * stride_s_k, |
| 935 | mask=valid, |
| 936 | other=-1e30, |
| 937 | ).to(tl.float32) |
| 938 | index = (chunk_start + off_k + 1).to(tl.int32) |
| 939 | local_topk = tl.topk( |
| 940 | _decode_topk_key(score, index, valid), |
| 941 | BLOCK_SIZE_T, |
| 942 | ) |
| 943 | else: |
| 944 | score_ptrs = ( |
| 945 | score_ptr |
| 946 | + pid_h * stride_s_h |
| 947 | + pid_b * stride_s_b |
| 948 | + (chunk_start + off_k) * stride_s_k |
| 949 | ) |
| 950 | valid = off_k < chunk_blocks |
| 951 | score = tl.load(score_ptrs, mask=valid, other=-1e30).to(tl.float32) |
| 952 | index = (chunk_start + off_k + 1).to(tl.int32) |
| 953 | local_topk = tl.topk( |
| 954 | _decode_topk_key(score, index, valid), |
| 955 | BLOCK_SIZE_T, |
| 956 | ) |
| 957 | score_ptrs += BLOCK_SIZE_K * stride_s_k |
| 958 | for offset in tl.range(BLOCK_SIZE_K, chunk_blocks, BLOCK_SIZE_K): |
| 959 | valid = off_k < chunk_blocks - offset |
| 960 | score = tl.load(score_ptrs, mask=valid, other=-1e30).to(tl.float32) |
| 961 | index = (chunk_start + offset + off_k + 1).to(tl.int32) |
| 962 | tile_topk = tl.topk( |
| 963 | _decode_topk_key(score, index, valid), |
| 964 | BLOCK_SIZE_T, |
| 965 | ) |
| 966 | local_topk = tl.topk( |
| 967 | tl.cat(local_topk, tile_topk, can_reorder=True), |
| 968 | BLOCK_SIZE_T, |
| 969 | ) |
| 970 | score_ptrs += BLOCK_SIZE_K * stride_s_k |
| 971 | |
| 972 | if active_chunks == 1: |
| 973 | _store_decode_topk( |
| 974 | local_topk, |
| 975 | topk_ptr, |
| 976 | attention_block_table_ptr, |
| 977 | sparse_bt_ptr, |
| 978 | sparse_ctx_ptr, |
| 979 | pid_b, |
| 980 | pid_h, |
| 981 | req_id, |
| 982 | query_pos, |
| 983 | num_blocks, |
| 984 | topk, |
| 985 | block_size, |
| 986 | pages_per_sparse_block, |
| 987 | block_page_stride, |
| 988 | stride_topk_h, |
| 989 | stride_topk_b, |
| 990 | stride_topk_t, |
| 991 | stride_attention_bt_b, |
| 992 | stride_sparse_bt_b, |
| 993 | BLOCK_SIZE_T, |
| 994 | EMIT_SPARSE_TABLE, |
| 995 | ) |
| 996 | return |
| 997 | |
| 998 | tl.store( |
| 999 | partial_ptr |
| 1000 | + pid_chunk * stride_partial_c |
| 1001 | + pid_h * stride_partial_h |
| 1002 | + pid_b * stride_partial_b |
| 1003 | + off_t * stride_partial_t, |
| 1004 | local_topk, |
| 1005 | ) |
| 1006 | counter = counter_ptr + pid_h * stride_counter_h + pid_b * stride_counter_b |
| 1007 | arrival = tl.atomic_add(counter, 1, sem="acq_rel", scope="gpu") |
| 1008 | if arrival != active_chunks - 1: |
| 1009 | return |
| 1010 | |
| 1011 | if ADAPTIVE_FINAL_MERGE: |
| 1012 | if active_chunks <= 2: |
| 1013 | _merge_store_decode_topk( |
| 1014 | partial_ptr, |
| 1015 | counter, |
| 1016 | topk_ptr, |
| 1017 | attention_block_table_ptr, |
| 1018 | sparse_bt_ptr, |
| 1019 | sparse_ctx_ptr, |
| 1020 | active_chunks, |
| 1021 | pid_b, |
| 1022 | pid_h, |
| 1023 | req_id, |
| 1024 | query_pos, |
| 1025 | num_blocks, |
| 1026 | stride_partial_c, |
| 1027 | stride_partial_h, |
| 1028 | stride_partial_b, |
| 1029 | stride_partial_t, |
| 1030 | stride_topk_h, |
| 1031 | stride_topk_b, |
| 1032 | stride_topk_t, |
| 1033 | stride_attention_bt_b, |
| 1034 | stride_sparse_bt_b, |
| 1035 | topk, |
| 1036 | block_size, |
| 1037 | pages_per_sparse_block, |
| 1038 | block_page_stride, |
| 1039 | BLOCK_SIZE_T, |
| 1040 | 2, |
| 1041 | EMIT_SPARSE_TABLE, |
| 1042 | ) |
| 1043 | return |
| 1044 | if active_chunks <= 4: |
| 1045 | _merge_store_decode_topk( |
| 1046 | partial_ptr, |
| 1047 | counter, |
| 1048 | topk_ptr, |
| 1049 | attention_block_table_ptr, |
| 1050 | sparse_bt_ptr, |
| 1051 | sparse_ctx_ptr, |
| 1052 | active_chunks, |
| 1053 | pid_b, |
| 1054 | pid_h, |
| 1055 | req_id, |
| 1056 | query_pos, |
| 1057 | num_blocks, |
| 1058 | stride_partial_c, |
| 1059 | stride_partial_h, |
| 1060 | stride_partial_b, |
| 1061 | stride_partial_t, |
| 1062 | stride_topk_h, |
| 1063 | stride_topk_b, |
| 1064 | stride_topk_t, |
| 1065 | stride_attention_bt_b, |
| 1066 | stride_sparse_bt_b, |
| 1067 | topk, |
| 1068 | block_size, |
| 1069 | pages_per_sparse_block, |
| 1070 | block_page_stride, |
| 1071 | BLOCK_SIZE_T, |
| 1072 | 4, |
| 1073 | EMIT_SPARSE_TABLE, |
| 1074 | ) |
| 1075 | return |
| 1076 | if active_chunks <= 8: |
| 1077 | _merge_store_decode_topk( |
| 1078 | partial_ptr, |
| 1079 | counter, |
| 1080 | topk_ptr, |
| 1081 | attention_block_table_ptr, |
| 1082 | sparse_bt_ptr, |
| 1083 | sparse_ctx_ptr, |
| 1084 | active_chunks, |
| 1085 | pid_b, |
| 1086 | pid_h, |
| 1087 | req_id, |
| 1088 | query_pos, |
| 1089 | num_blocks, |
| 1090 | stride_partial_c, |
| 1091 | stride_partial_h, |
| 1092 | stride_partial_b, |
| 1093 | stride_partial_t, |
| 1094 | stride_topk_h, |
| 1095 | stride_topk_b, |
| 1096 | stride_topk_t, |
| 1097 | stride_attention_bt_b, |
| 1098 | stride_sparse_bt_b, |
| 1099 | topk, |
| 1100 | block_size, |
| 1101 | pages_per_sparse_block, |
| 1102 | block_page_stride, |
| 1103 | BLOCK_SIZE_T, |
| 1104 | 8, |
| 1105 | EMIT_SPARSE_TABLE, |
| 1106 | ) |
| 1107 | return |
| 1108 | _merge_store_decode_topk( |
| 1109 | partial_ptr, |
| 1110 | counter, |
| 1111 | topk_ptr, |
| 1112 | attention_block_table_ptr, |
| 1113 | sparse_bt_ptr, |
| 1114 | sparse_ctx_ptr, |
| 1115 | active_chunks, |
| 1116 | pid_b, |
| 1117 | pid_h, |
| 1118 | req_id, |
| 1119 | query_pos, |
| 1120 | num_blocks, |
| 1121 | stride_partial_c, |
| 1122 | stride_partial_h, |
| 1123 | stride_partial_b, |
| 1124 | stride_partial_t, |
| 1125 | stride_topk_h, |
| 1126 | stride_topk_b, |
| 1127 | stride_topk_t, |
| 1128 | stride_attention_bt_b, |
| 1129 | stride_sparse_bt_b, |
| 1130 | topk, |
| 1131 | block_size, |
| 1132 | pages_per_sparse_block, |
| 1133 | block_page_stride, |
| 1134 | BLOCK_SIZE_T, |
| 1135 | 16, |
| 1136 | EMIT_SPARSE_TABLE, |
| 1137 | ) |
| 1138 | return |
| 1139 | |
| 1140 | candidate_width: tl.constexpr = NUM_TOPK_CHUNKS * BLOCK_SIZE_T |
| 1141 | off_candidate = tl.arange(0, candidate_width) |
| 1142 | candidate_chunk = off_candidate // BLOCK_SIZE_T |
| 1143 | candidate_slot = off_candidate % BLOCK_SIZE_T |
| 1144 | candidates = tl.load( |
| 1145 | partial_ptr |
| 1146 | + candidate_chunk * stride_partial_c |
| 1147 | + pid_h * stride_partial_h |
| 1148 | + pid_b * stride_partial_b |
| 1149 | + candidate_slot * stride_partial_t, |
| 1150 | mask=candidate_chunk < active_chunks, |
| 1151 | other=0, |
| 1152 | volatile=True, |
| 1153 | ) |
| 1154 | winners = tl.topk(candidates, BLOCK_SIZE_T) |
| 1155 | _store_decode_topk( |
| 1156 | winners, |
| 1157 | topk_ptr, |
| 1158 | attention_block_table_ptr, |
| 1159 | sparse_bt_ptr, |
| 1160 | sparse_ctx_ptr, |
| 1161 | pid_b, |
| 1162 | pid_h, |
| 1163 | req_id, |
| 1164 | query_pos, |
| 1165 | num_blocks, |
| 1166 | topk, |
| 1167 | block_size, |
| 1168 | pages_per_sparse_block, |
| 1169 | block_page_stride, |
| 1170 | stride_topk_h, |
| 1171 | stride_topk_b, |
| 1172 | stride_topk_t, |
| 1173 | stride_attention_bt_b, |
| 1174 | stride_sparse_bt_b, |
| 1175 | BLOCK_SIZE_T, |
| 1176 | EMIT_SPARSE_TABLE, |
| 1177 | ) |
| 1178 | tl.atomic_xchg(counter, 0, sem="acq_rel", scope="gpu") |
| 1179 | |
| 1180 | |
| 1181 | # --------------------------------------------------------------------------- |
| 1182 | # Python wrappers |
| 1183 | # --------------------------------------------------------------------------- |
| 1184 | @torch.no_grad() |
| 1185 | def minimax_m3_index_score( |
| 1186 | idx_q: torch.Tensor, # [total_q, num_idx_heads, head_dim] |
| 1187 | index_kv_cache: torch.Tensor, # [num_blocks, 128, head_dim] |
| 1188 | block_table: torch.Tensor, # [batch, max_blocks] |
| 1189 | cu_seqlens_q: torch.Tensor, # [batch+1] int32 |
| 1190 | seq_lens: torch.Tensor, # [batch] int32 |
| 1191 | prefix_lens: torch.Tensor, # [batch] int32 |
| 1192 | max_query_len: int, |
| 1193 | max_seq_len: int, |
| 1194 | num_kv_heads: int, |
| 1195 | ) -> torch.Tensor: |
| 1196 | """Compute per-token index scores for each visible sparse block. |
| 1197 | |
| 1198 | Returns score [num_kv_heads, total_q, max_block], where each score is the |
| 1199 | max over a 128-token index-K block. M3 has num_idx_heads == num_kv_heads. |
| 1200 | """ |
| 1201 | total_q, num_idx_heads, head_dim = idx_q.shape |
| 1202 | assert num_idx_heads == num_kv_heads, ( |
| 1203 | "M3 expects num_idx_heads == num_kv_heads (no topk index reduce)" |
| 1204 | ) |
| 1205 | batch = cu_seqlens_q.shape[0] - 1 |
| 1206 | max_block = triton.cdiv(max_seq_len, SPARSE_BLOCK_SIZE) |
| 1207 | |
| 1208 | # Keep score strides 16-divisible to avoid Triton recompiles. |
| 1209 | score_block_stride = round_up(max_block, 16) |
| 1210 | score = torch.empty( |
| 1211 | (num_idx_heads, total_q, score_block_stride), |
| 1212 | dtype=torch.float32, |
| 1213 | device=idx_q.device, |
| 1214 | ) |
| 1215 | BLOCK_SIZE_Q = 64 |
| 1216 | grid_score = (triton.cdiv(max_query_len, BLOCK_SIZE_Q), batch * num_idx_heads) |
| 1217 | _index_block_score_kernel[grid_score]( |
| 1218 | idx_q, |
| 1219 | index_kv_cache, |
| 1220 | score, |
| 1221 | block_table, |
| 1222 | cu_seqlens_q, |
| 1223 | seq_lens, |
| 1224 | prefix_lens, |
| 1225 | num_idx_heads, |
| 1226 | head_dim, |
| 1227 | idx_q.stride(0), |
| 1228 | idx_q.stride(1), |
| 1229 | idx_q.stride(2), |
| 1230 | index_kv_cache.stride(0), |
| 1231 | index_kv_cache.stride(1), |
| 1232 | index_kv_cache.stride(2), |
| 1233 | score.stride(0), |
| 1234 | score.stride(1), |
| 1235 | score.stride(2), |
| 1236 | block_table.stride(0), |
| 1237 | BLOCK_SIZE_Q=BLOCK_SIZE_Q, |
| 1238 | BLOCK_SIZE_K=SPARSE_BLOCK_SIZE, |
| 1239 | ) |
| 1240 | return score |
| 1241 | |
| 1242 | |
| 1243 | @torch.no_grad() |
| 1244 | def minimax_m3_index_topk( |
| 1245 | score: torch.Tensor, # [num_idx_heads, total_q, max_block] |
| 1246 | cu_seqlens_q: torch.Tensor, # [batch+1] int32 |
| 1247 | prefix_lens: torch.Tensor, # [batch] int32 |
| 1248 | max_query_len: int, |
| 1249 | topk: int, |
| 1250 | init_blocks: int, |
| 1251 | local_blocks: int, |
| 1252 | out: torch.Tensor | None = None, |
| 1253 | ) -> torch.Tensor: |
| 1254 | """Select index top-k from a precomputed score tensor. |
| 1255 | |
| 1256 | When ``out`` is provided (a ``[num_idx_heads, >=total_q, topk]`` buffer), the |
| 1257 | result is written into ``out[:, :total_q, :]`` instead of a fresh tensor -- |
| 1258 | used to keep the top-k output at a stable address for cudagraph capture. |
| 1259 | """ |
| 1260 | num_idx_heads = score.shape[0] |
| 1261 | batch = cu_seqlens_q.shape[0] - 1 |
| 1262 | total_q = score.shape[1] |
| 1263 | if out is not None: |
| 1264 | topk_idx = out[:, :total_q, :] |
| 1265 | else: |
| 1266 | topk_idx = torch.empty( |
| 1267 | (num_idx_heads, total_q, topk), |
| 1268 | dtype=torch.int32, |
| 1269 | device=score.device, |
| 1270 | ) |
| 1271 | # block_size_q == 1 -> query blocks coincide with query tokens. |
| 1272 | grid_topk = (max_query_len, batch, num_idx_heads) |
| 1273 | _topk_index_kernel[grid_topk]( |
| 1274 | score, |
| 1275 | topk_idx, |
| 1276 | 1, # sample_interval (block_size_q) |
| 1277 | SPARSE_BLOCK_SIZE, |
| 1278 | cu_seqlens_q, |
| 1279 | cu_seqlens_q, # cu_seqblocks_q == cu_seqlens_q when block_size_q == 1 |
| 1280 | prefix_lens, |
| 1281 | topk, |
| 1282 | init_blocks, |
| 1283 | local_blocks, |
| 1284 | score.stride(0), |
| 1285 | score.stride(1), |
| 1286 | score.stride(2), |
| 1287 | topk_idx.stride(0), |
| 1288 | topk_idx.stride(1), |
| 1289 | topk_idx.stride(2), |
| 1290 | MASK_INIT=False, |
| 1291 | MASK_LOCAL=False, |
| 1292 | ) |
| 1293 | return topk_idx |
| 1294 | |
| 1295 | |
| 1296 | @torch.no_grad() |
| 1297 | def minimax_m3_index_decode( |
| 1298 | idx_q: torch.Tensor, # [total_q, num_idx_heads, head_dim] |
| 1299 | index_kv_cache: torch.Tensor, # [num_blocks, 128, head_dim] |
| 1300 | block_table: torch.Tensor, # [num_reqs, max_blocks] |
| 1301 | seq_lens: torch.Tensor, # [num_reqs] int32 |
| 1302 | max_seq_len: int, |
| 1303 | topk: int, |
| 1304 | init_blocks: int, |
| 1305 | local_blocks: int, |
| 1306 | num_kv_heads: int, |
| 1307 | decode_query_len: int, |
| 1308 | max_decode_query_len: int, |
| 1309 | out: torch.Tensor | None = None, |
| 1310 | *, |
| 1311 | attention_block_table: torch.Tensor | None = None, |
| 1312 | sparse_block_table_out: torch.Tensor | None = None, |
| 1313 | sparse_context_lens_out: torch.Tensor | None = None, |
| 1314 | block_page_stride: int | None = None, |
| 1315 | completion_counter: torch.Tensor | None = None, |
| 1316 | ) -> torch.Tensor: |
| 1317 | """Decode index block-score followed by fused adaptive top-k selection. |
| 1318 | |
| 1319 | Returns topk_idx [num_kv_heads, total_q, topk] (0-indexed block ids, -1 pad). |
| 1320 | When ``out`` ([num_kv_heads, >=total_q, topk]) is given, writes into |
| 1321 | ``out[:, :total_q, :]`` (stable address for cudagraph) instead of allocating. |
| 1322 | The optional sparse-table arguments fuse current-layer table construction |
| 1323 | into the selector. They must be provided together. ``completion_counter`` |
| 1324 | provides stable per-query synchronization storage for CUDA graphs. It must |
| 1325 | be zero before its first launch and must not be shared by overlapping |
| 1326 | selector invocations; every completed launch resets its active entries. |
| 1327 | """ |
| 1328 | total_q, num_idx_heads, head_dim = idx_q.shape |
| 1329 | assert num_idx_heads == num_kv_heads, ( |
| 1330 | "M3 expects num_idx_heads == num_kv_heads (no topk index reduce)" |
| 1331 | ) |
| 1332 | assert 0 < decode_query_len <= max_decode_query_len |
| 1333 | assert total_q == seq_lens.shape[0] * decode_query_len |
| 1334 | batch = total_q |
| 1335 | emit_sparse_table = attention_block_table is not None |
| 1336 | if emit_sparse_table and ( |
| 1337 | sparse_block_table_out is None |
| 1338 | or sparse_context_lens_out is None |
| 1339 | or block_page_stride is None |
| 1340 | ): |
| 1341 | raise ValueError( |
| 1342 | "MiniMax-M3 fused decode sparse-table arguments must be provided together" |
| 1343 | ) |
| 1344 | if not emit_sparse_table and ( |
| 1345 | sparse_block_table_out is not None |
| 1346 | or sparse_context_lens_out is not None |
| 1347 | or block_page_stride is not None |
| 1348 | ): |
| 1349 | raise ValueError( |
| 1350 | "MiniMax-M3 fused decode sparse-table arguments must be provided together" |
| 1351 | ) |
| 1352 | if emit_sparse_table: |
| 1353 | assert attention_block_table is not None |
| 1354 | assert sparse_block_table_out is not None |
| 1355 | assert sparse_context_lens_out is not None |
| 1356 | assert block_page_stride is not None |
| 1357 | if num_idx_heads != 1: |
| 1358 | raise ValueError( |
| 1359 | "MiniMax-M3 fused decode sparse-table construction requires " |
| 1360 | f"one index head, got {num_idx_heads}" |
| 1361 | ) |
| 1362 | if block_page_stride not in ( |
| 1363 | PAGES_PER_SPARSE_BLOCK, |
| 1364 | 2 * PAGES_PER_SPARSE_BLOCK, |
| 1365 | ): |
| 1366 | raise ValueError( |
| 1367 | "MiniMax-M3 fused decode sparse-table page stride must be " |
| 1368 | f"{PAGES_PER_SPARSE_BLOCK} or {2 * PAGES_PER_SPARSE_BLOCK}, " |
| 1369 | f"got {block_page_stride}" |
| 1370 | ) |
| 1371 | expected_sparse_shape = (total_q, topk * PAGES_PER_SPARSE_BLOCK) |
| 1372 | if sparse_block_table_out.shape != expected_sparse_shape: |
| 1373 | raise ValueError( |
| 1374 | "MiniMax-M3 fused decode sparse block table has shape " |
| 1375 | f"{tuple(sparse_block_table_out.shape)}, expected " |
| 1376 | f"{expected_sparse_shape}" |
| 1377 | ) |
| 1378 | if sparse_context_lens_out.shape != (total_q,): |
| 1379 | raise ValueError( |
| 1380 | "MiniMax-M3 fused decode sparse context lengths have shape " |
| 1381 | f"{tuple(sparse_context_lens_out.shape)}, expected {(total_q,)}" |
| 1382 | ) |
| 1383 | if ( |
| 1384 | attention_block_table.dim() != 2 |
| 1385 | or attention_block_table.shape[0] != seq_lens.shape[0] |
| 1386 | ): |
| 1387 | raise ValueError( |
| 1388 | "MiniMax-M3 fused decode attention block table must be " |
| 1389 | "[num_requests, max_blocks]" |
| 1390 | ) |
| 1391 | int32_tensors = ( |
| 1392 | attention_block_table, |
| 1393 | sparse_block_table_out, |
| 1394 | sparse_context_lens_out, |
| 1395 | ) |
| 1396 | if any(tensor.dtype != torch.int32 for tensor in int32_tensors): |
| 1397 | raise ValueError("MiniMax-M3 fused decode sparse tables require int32") |
| 1398 | if any(tensor.device != idx_q.device for tensor in int32_tensors): |
| 1399 | raise ValueError( |
| 1400 | "MiniMax-M3 fused decode sparse tables must share the query device" |
| 1401 | ) |
| 1402 | if ( |
| 1403 | attention_block_table.stride(1) != 1 |
| 1404 | or sparse_block_table_out.stride(1) != 1 |
| 1405 | or sparse_context_lens_out.stride(0) != 1 |
| 1406 | ): |
| 1407 | raise ValueError( |
| 1408 | "MiniMax-M3 fused decode sparse tables require contiguous rows" |
| 1409 | ) |
| 1410 | max_block = triton.cdiv(max_seq_len, SPARSE_BLOCK_SIZE) |
| 1411 | if max_block >= 0xFFFF: |
| 1412 | raise ValueError( |
| 1413 | "MiniMax-M3 decode top-k supports fewer than 65535 sparse blocks" |
| 1414 | ) |
| 1415 | if emit_sparse_table: |
| 1416 | assert attention_block_table is not None |
| 1417 | if attention_block_table.shape[1] < max_block: |
| 1418 | raise ValueError( |
| 1419 | "MiniMax-M3 fused decode attention block table is shorter " |
| 1420 | f"than the required {max_block} sparse blocks" |
| 1421 | ) |
| 1422 | use_pdl = current_platform.is_arch_support_pdl() |
| 1423 | # `launch_pdl` is a Triton runtime kwarg only some backends accept (CUDA |
| 1424 | # SM9+); this ROCm Triton rejects it even when False ("Keyword argument |
| 1425 | # launch_pdl was specified but unrecognised"). Only pass it when PDL is |
| 1426 | # actually supported -- on ROCm use_pdl is always False, so it's omitted. |
| 1427 | pdl_kwargs: dict[str, bool | int] = {} |
| 1428 | if use_pdl: |
| 1429 | pdl_kwargs.update({"launch_pdl": True}) |
| 1430 | is_gfx950 = False |
| 1431 | if current_platform.is_rocm(): |
| 1432 | from vllm.platforms.rocm import on_gfx950 |
| 1433 | |
| 1434 | is_gfx950 = on_gfx950() |
| 1435 | # Multi-head spec decode scores a wider head-position tile per K block; |
| 1436 | # reduce stages to ease memory/register pressure on the fallback path. |
| 1437 | score_kwargs = pdl_kwargs.copy() |
| 1438 | if num_idx_heads > 1 and max_decode_query_len > 1: |
| 1439 | score_kwargs.update({"num_warps": 4, "num_stages": 2}) |
| 1440 | |
| 1441 | # Keep score strides 16-divisible to avoid Triton recompiles. |
| 1442 | score_block_stride = round_up(max_block, 16) |
| 1443 | score = torch.empty( |
| 1444 | (num_idx_heads, total_q, score_block_stride), |
| 1445 | dtype=torch.float32, |
| 1446 | device=idx_q.device, |
| 1447 | ) |
| 1448 | # Use the configured max decode length to avoid Triton recompiles when |
| 1449 | # switching between qlen=1 and spec-decode verification batches. |
| 1450 | BLOCK_SIZE_Q = triton.next_power_of_2(max_decode_query_len) |
| 1451 | num_reqs = seq_lens.shape[0] |
| 1452 | score_program_budget = _decode_score_program_budget( |
| 1453 | num_reqs, |
| 1454 | head_dim, |
| 1455 | idx_q.dtype, |
| 1456 | index_kv_cache.dtype, |
| 1457 | is_gfx950=is_gfx950, |
| 1458 | ) |
| 1459 | grid_score: tuple[int, ...] |
| 1460 | if score_program_budget is not None: |
| 1461 | grid_score = (score_program_budget + num_reqs - 1,) |
| 1462 | _decode_index_score_balanced_kernel[grid_score]( |
| 1463 | idx_q, |
| 1464 | index_kv_cache, |
| 1465 | score, |
| 1466 | block_table, |
| 1467 | seq_lens, |
| 1468 | num_idx_heads, |
| 1469 | head_dim, |
| 1470 | init_blocks, |
| 1471 | local_blocks, |
| 1472 | num_reqs, |
| 1473 | score_program_budget, |
| 1474 | decode_query_len, |
| 1475 | idx_q.stride(0), |
| 1476 | idx_q.stride(1), |
| 1477 | idx_q.stride(2), |
| 1478 | index_kv_cache.stride(0), |
| 1479 | index_kv_cache.stride(1), |
| 1480 | index_kv_cache.stride(2), |
| 1481 | score.stride(0), |
| 1482 | score.stride(1), |
| 1483 | score.stride(2), |
| 1484 | block_table.stride(0), |
| 1485 | BLOCK_SIZE_K=SPARSE_BLOCK_SIZE, |
| 1486 | BLOCK_SIZE_Q=BLOCK_SIZE_Q, |
| 1487 | num_warps=2, |
| 1488 | num_stages=1, |
| 1489 | ) |
| 1490 | else: |
| 1491 | # Increase independent work for the measured high-batch gfx950 decode |
| 1492 | # range while preserving the deployed split for every other shape. |
| 1493 | num_kv_chunks, use_high_batch_config = _decode_score_split_launch_policy( |
| 1494 | num_reqs, |
| 1495 | head_dim, |
| 1496 | idx_q.dtype, |
| 1497 | index_kv_cache.dtype, |
| 1498 | is_gfx950=is_gfx950, |
| 1499 | ) |
| 1500 | if use_high_batch_config and not ( |
| 1501 | num_idx_heads > 1 and max_decode_query_len > 1 |
| 1502 | ): |
| 1503 | score_kwargs.update({"num_warps": 2, "num_stages": 1}) |
| 1504 | grid_score = (num_reqs, num_kv_chunks) |
| 1505 | _decode_index_score_kernel[grid_score]( |
| 1506 | idx_q, |
| 1507 | index_kv_cache, |
| 1508 | score, |
| 1509 | block_table, |
| 1510 | seq_lens, |
| 1511 | num_idx_heads, |
| 1512 | head_dim, |
| 1513 | init_blocks, |
| 1514 | local_blocks, |
| 1515 | decode_query_len, |
| 1516 | idx_q.stride(0), |
| 1517 | idx_q.stride(1), |
| 1518 | idx_q.stride(2), |
| 1519 | index_kv_cache.stride(0), |
| 1520 | index_kv_cache.stride(1), |
| 1521 | index_kv_cache.stride(2), |
| 1522 | score.stride(0), |
| 1523 | score.stride(1), |
| 1524 | score.stride(2), |
| 1525 | block_table.stride(0), |
| 1526 | BLOCK_SIZE_K=SPARSE_BLOCK_SIZE, |
| 1527 | BLOCK_SIZE_Q=BLOCK_SIZE_Q, |
| 1528 | num_kv_chunks=num_kv_chunks, |
| 1529 | USE_PDL=use_pdl, |
| 1530 | **score_kwargs, |
| 1531 | ) |
| 1532 | |
| 1533 | if out is not None: |
| 1534 | topk_idx = out[:, :total_q, :] |
| 1535 | else: |
| 1536 | topk_idx = torch.empty( |
| 1537 | (num_idx_heads, total_q, topk), |
| 1538 | dtype=torch.int32, |
| 1539 | device=idx_q.device, |
| 1540 | ) |
| 1541 | # The launch grid remains shape-constant for CUDA graphs. Each query uses |
| 1542 | # only the number of chunks needed for its live context. |
| 1543 | ( |
| 1544 | num_topk_chunks, |
| 1545 | topk_block_size, |
| 1546 | selector_num_warps, |
| 1547 | selector_num_stages, |
| 1548 | single_tile_guaranteed, |
| 1549 | adaptive_final_merge, |
| 1550 | ) = _decode_topk_launch_policy( |
| 1551 | max_block, |
| 1552 | batch, |
| 1553 | num_idx_heads, |
| 1554 | topk, |
| 1555 | is_gfx950=is_gfx950, |
| 1556 | ) |
| 1557 | block_size_t = triton.next_power_of_2(topk) |
| 1558 | topk_partial = torch.empty( |
| 1559 | num_topk_chunks, |
| 1560 | num_idx_heads, |
| 1561 | batch, |
| 1562 | block_size_t, |
| 1563 | dtype=torch.int64, |
| 1564 | device=idx_q.device, |
| 1565 | ) |
| 1566 | if completion_counter is None: |
| 1567 | active_counter = torch.zeros( |
| 1568 | (num_idx_heads, batch), |
| 1569 | dtype=torch.int32, |
| 1570 | device=idx_q.device, |
| 1571 | ) |
| 1572 | else: |
| 1573 | if ( |
| 1574 | completion_counter.dim() != 2 |
| 1575 | or completion_counter.shape[0] != num_idx_heads |
| 1576 | or completion_counter.shape[1] < batch |
| 1577 | or completion_counter.dtype != torch.int32 |
| 1578 | or completion_counter.device != idx_q.device |
| 1579 | or not completion_counter.is_contiguous() |
| 1580 | ): |
| 1581 | raise ValueError( |
| 1582 | "MiniMax-M3 completion counter must be contiguous int32 " |
| 1583 | "[num_idx_heads, >=total_q] on the query device" |
| 1584 | ) |
| 1585 | active_counter = completion_counter[:, :batch] |
| 1586 | |
| 1587 | selector_attention_block_table = block_table |
| 1588 | selector_sparse_block_table = topk_idx |
| 1589 | selector_sparse_context_lens = seq_lens |
| 1590 | selector_block_page_stride = PAGES_PER_SPARSE_BLOCK |
| 1591 | selector_sparse_block_stride = topk_idx.stride(1) |
| 1592 | if emit_sparse_table: |
| 1593 | assert attention_block_table is not None |
| 1594 | assert sparse_block_table_out is not None |
| 1595 | assert sparse_context_lens_out is not None |
| 1596 | assert block_page_stride is not None |
| 1597 | selector_attention_block_table = attention_block_table |
| 1598 | selector_sparse_block_table = sparse_block_table_out |
| 1599 | selector_sparse_context_lens = sparse_context_lens_out |
| 1600 | selector_block_page_stride = block_page_stride |
| 1601 | selector_sparse_block_stride = sparse_block_table_out.stride(0) |
| 1602 | _decode_topk_fused_kernel[(batch, num_idx_heads, num_topk_chunks)]( |
| 1603 | score, |
| 1604 | topk_partial, |
| 1605 | active_counter, |
| 1606 | topk_idx, |
| 1607 | seq_lens, |
| 1608 | selector_attention_block_table, |
| 1609 | selector_sparse_block_table, |
| 1610 | selector_sparse_context_lens, |
| 1611 | decode_query_len, |
| 1612 | score.stride(0), |
| 1613 | score.stride(1), |
| 1614 | score.stride(2), |
| 1615 | topk_partial.stride(0), |
| 1616 | topk_partial.stride(1), |
| 1617 | topk_partial.stride(2), |
| 1618 | topk_partial.stride(3), |
| 1619 | active_counter.stride(0), |
| 1620 | active_counter.stride(1), |
| 1621 | topk_idx.stride(0), |
| 1622 | topk_idx.stride(1), |
| 1623 | topk_idx.stride(2), |
| 1624 | selector_attention_block_table.stride(0), |
| 1625 | selector_sparse_block_stride, |
| 1626 | topk=topk, |
| 1627 | block_size=SPARSE_BLOCK_SIZE, |
| 1628 | pages_per_sparse_block=PAGES_PER_SPARSE_BLOCK, |
| 1629 | block_page_stride=selector_block_page_stride, |
| 1630 | NUM_TOPK_CHUNKS=num_topk_chunks, |
| 1631 | BLOCK_SIZE_K=topk_block_size, |
| 1632 | BLOCK_SIZE_T=block_size_t, |
| 1633 | EMIT_SPARSE_TABLE=emit_sparse_table, |
| 1634 | SINGLE_TILE_GUARANTEED=single_tile_guaranteed, |
| 1635 | ADAPTIVE_FINAL_MERGE=adaptive_final_merge, |
| 1636 | num_warps=selector_num_warps, |
| 1637 | num_stages=selector_num_stages, |
| 1638 | ) |
| 1639 | return topk_idx |