| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | import functools |
| 4 | import importlib |
| 5 | import math |
| 6 | from collections.abc import Callable |
| 7 | from importlib.util import find_spec |
| 8 | |
| 9 | import torch |
| 10 | import torch.nn.functional as F |
| 11 | |
| 12 | import vllm.envs as envs |
| 13 | from vllm.compilation.breakable_cudagraph import eager_break_during_capture |
| 14 | from vllm.config import CUDAGraphMode, get_current_vllm_config |
| 15 | from vllm.forward_context import get_forward_context |
| 16 | from vllm.logger import init_logger |
| 17 | from vllm.platforms import current_platform |
| 18 | from vllm.triton_utils import tl, triton |
| 19 | from vllm.utils.torch_utils import LayerNameType, direct_register_custom_op |
| 20 | from vllm.v1.attention.backends.mla.indexer import DeepseekV32IndexerMetadata |
| 21 | from vllm.v1.attention.ops.common import pack_seq_triton, unpack_seq_triton |
| 22 | from vllm.v1.worker.workspace import current_workspace_manager |
| 23 | |
| 24 | if current_platform.is_rocm(): |
| 25 | from vllm.platforms.rocm import _ON_GFX942, _ON_GFX950 |
| 26 | else: |
| 27 | _ON_GFX942 = False |
| 28 | _ON_GFX950 = False |
| 29 | |
| 30 | logger = init_logger(__name__) |
| 31 | |
| 32 | FP8_DTYPE = current_platform.fp8_dtype() |
| 33 | |
| 34 | |
| 35 | @functools.cache |
| 36 | def _get_aiter_sparse_prefill_opus() -> Callable[..., torch.Tensor] | None: |
| 37 | from vllm._aiter_ops import rocm_aiter_ops |
| 38 | |
| 39 | if not rocm_aiter_ops.is_mla_enabled(): |
| 40 | return None |
| 41 | try: |
| 42 | from aiter.ops.pa_sparse_prefill_opus import pa_sparse_prefill_opus |
| 43 | except ImportError: |
| 44 | return None |
| 45 | logger.info_once("Using AITER OPUS for large sparse MLA prefill on gfx950") |
| 46 | return pa_sparse_prefill_opus |
| 47 | |
| 48 | |
| 49 | # Conservative perf gate, not a correctness bound: OPUS is correct for any query |
| 50 | # count, but Triton stays faster below this measured crossover. |
| 51 | _GFX950_AITER_SPARSE_PREFILL_OPUS_MIN_QUERIES = 1024 |
| 52 | |
| 53 | |
| 54 | def _indexer_k_is_c4a_block_flat(compress_ratio: int) -> bool: |
| 55 | """V4.0 C4A is block-flat (NORMAL). Ratio 1 and 2 are 16×16 SHUFFLE.""" |
| 56 | return compress_ratio == 4 |
| 57 | |
| 58 | |
| 59 | @triton.jit |
| 60 | def _localize_aiter_prefill_topk_kernel( |
| 61 | indices_ptr, |
| 62 | row_starts_ptr, |
| 63 | indices_stride, |
| 64 | num_topk, |
| 65 | BLOCK_SIZE: tl.constexpr, |
| 66 | ): |
| 67 | row_idx = tl.program_id(0) |
| 68 | offsets = tl.program_id(1) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) |
| 69 | mask = offsets < num_topk |
| 70 | indices = tl.load(indices_ptr + row_idx * indices_stride + offsets, mask=mask) |
| 71 | row_start = tl.load(row_starts_ptr + row_idx) |
| 72 | localized = tl.where(indices < 0, indices, indices - row_start) |
| 73 | tl.store( |
| 74 | indices_ptr + row_idx * indices_stride + offsets, |
| 75 | localized, |
| 76 | mask=mask, |
| 77 | ) |
| 78 | |
| 79 | |
| 80 | def _localize_aiter_prefill_topk( |
| 81 | indices: torch.Tensor, |
| 82 | row_starts: torch.Tensor, |
| 83 | ) -> None: |
| 84 | num_rows, num_topk = indices.shape |
| 85 | block_size = 256 |
| 86 | _localize_aiter_prefill_topk_kernel[(num_rows, triton.cdiv(num_topk, block_size))]( |
| 87 | indices, |
| 88 | row_starts, |
| 89 | indices.stride(0), |
| 90 | num_topk, |
| 91 | BLOCK_SIZE=block_size, |
| 92 | ) |
| 93 | |
| 94 | |
| 95 | def _launch_aiter_top_k_per_row_prefill( |
| 96 | logits: torch.Tensor, |
| 97 | row_starts: torch.Tensor, |
| 98 | row_ends: torch.Tensor, |
| 99 | indices: torch.Tensor, |
| 100 | topk_tokens: int, |
| 101 | ) -> None: |
| 102 | from vllm._aiter_ops import rocm_aiter_ops |
| 103 | |
| 104 | rocm_aiter_ops.indexer_top_k_prefill( |
| 105 | logits, row_starts, row_ends, indices, topk_tokens |
| 106 | ) |
| 107 | _localize_aiter_prefill_topk(indices, row_starts) |
| 108 | |
| 109 | |
| 110 | @triton.jit |
| 111 | def _indexer_k_quant_and_cache_kernel( |
| 112 | k_ptr, # [num_tokens, head_dim] |
| 113 | kv_cache_ptr, # [n_blks, blk_size//tile_block, head_dim // 16B, tile_block, 16B] |
| 114 | # [n_blocks, blk_size, head_dim] |
| 115 | kv_cache_scale_ptr, # [n_blks, blk_size] |
| 116 | slot_mapping_ptr, # [num_tokens] |
| 117 | kv_cache_scale_stride, |
| 118 | kv_cache_value_stride, |
| 119 | block_size, |
| 120 | num_tokens, |
| 121 | head_dim: tl.constexpr, |
| 122 | LAYOUT: tl.constexpr, |
| 123 | BLOCK_TILE_SIZE: tl.constexpr, |
| 124 | HEAD_TILE_SIZE: tl.constexpr, |
| 125 | IS_FNUZ: tl.constexpr, |
| 126 | USE_UE8M0: tl.constexpr, |
| 127 | ): |
| 128 | tid = tl.program_id(0) |
| 129 | offset = tl.arange(0, head_dim) |
| 130 | if LAYOUT == "SHUFFLE": |
| 131 | tile_offset = ( |
| 132 | offset // HEAD_TILE_SIZE * BLOCK_TILE_SIZE * HEAD_TILE_SIZE |
| 133 | + offset % HEAD_TILE_SIZE |
| 134 | ) |
| 135 | else: |
| 136 | tile_offset = offset |
| 137 | tile_store_offset = tile_offset |
| 138 | # for idx in tl.range(tid, num_tokens, n_program): |
| 139 | src_ptr = k_ptr + tid * head_dim |
| 140 | slot_id = tl.load(slot_mapping_ptr + tid) |
| 141 | if slot_id < 0: |
| 142 | return |
| 143 | # The packed KV layout makes per-block strides large |
| 144 | # enough that block_id * stride can exceed 32-bit range. |
| 145 | block_id = (slot_id // block_size).to(tl.int64) |
| 146 | block_offset = slot_id % block_size |
| 147 | tile_block_id = block_offset // BLOCK_TILE_SIZE |
| 148 | tile_block_offset = block_offset % BLOCK_TILE_SIZE |
| 149 | val = tl.load(src_ptr + offset) |
| 150 | amax = tl.max(val.abs(), axis=-1).to(tl.float32) |
| 151 | if IS_FNUZ: |
| 152 | scale = tl.maximum(1e-4, amax) / 224.0 |
| 153 | else: |
| 154 | scale = tl.maximum(1e-4, amax) / 448.0 |
| 155 | |
| 156 | if USE_UE8M0: |
| 157 | scale = tl.exp2(tl.ceil(tl.log2(scale))) |
| 158 | |
| 159 | fp8_val = (val.to(tl.float32) / scale).to(kv_cache_ptr.type.element_ty) |
| 160 | if LAYOUT == "SHUFFLE": |
| 161 | dst_ptr = ( |
| 162 | kv_cache_ptr |
| 163 | + block_id * kv_cache_value_stride |
| 164 | + tile_block_id * BLOCK_TILE_SIZE * head_dim |
| 165 | + tile_block_offset * HEAD_TILE_SIZE |
| 166 | ) |
| 167 | else: |
| 168 | dst_ptr = ( |
| 169 | kv_cache_ptr + block_id * kv_cache_value_stride + block_offset * head_dim |
| 170 | ) |
| 171 | tl.store(dst_ptr + tile_store_offset, fp8_val) |
| 172 | dst_scale_ptr = kv_cache_scale_ptr + block_id * kv_cache_scale_stride + block_offset |
| 173 | tl.store(dst_scale_ptr, scale) |
| 174 | |
| 175 | |
| 176 | def indexer_k_quant_and_cache_triton( |
| 177 | k: torch.Tensor, |
| 178 | kv_cache: torch.Tensor, # [num_blocks, block_size, head_dim + 4] |
| 179 | slot_mapping: torch.Tensor, |
| 180 | quant_block_size, |
| 181 | scale_fmt, |
| 182 | block_tile_size=16, |
| 183 | head_tile_size=16, |
| 184 | ): |
| 185 | num_blocks = kv_cache.shape[0] |
| 186 | head_dim = k.shape[-1] |
| 187 | num_tokens = slot_mapping.shape[0] |
| 188 | block_size = kv_cache.shape[1] |
| 189 | # In real layout, we store the first portion as kv cache value |
| 190 | # and second portion as kv cache scale |
| 191 | kv_cache = kv_cache.view(num_blocks, -1) |
| 192 | kv_cache_value = kv_cache[:, : block_size * head_dim].view(FP8_DTYPE) |
| 193 | kv_cache_scale = kv_cache[:, block_size * head_dim :].view(torch.float32) |
| 194 | head_tile_size = head_tile_size // kv_cache.element_size() |
| 195 | layout = "NORMAL" if block_size == 1 else "SHUFFLE" |
| 196 | grid = (num_tokens,) |
| 197 | _indexer_k_quant_and_cache_kernel[grid]( |
| 198 | k, |
| 199 | kv_cache_value, |
| 200 | kv_cache_scale, |
| 201 | slot_mapping, |
| 202 | kv_cache_scale.stride(0), |
| 203 | kv_cache_value.stride(0), |
| 204 | block_size, |
| 205 | num_tokens, |
| 206 | head_dim, |
| 207 | layout, |
| 208 | block_tile_size, |
| 209 | head_tile_size, |
| 210 | IS_FNUZ=torch.float8_e4m3fnuz == FP8_DTYPE, |
| 211 | USE_UE8M0=scale_fmt == "ue8m0", |
| 212 | ) |
| 213 | |
| 214 | |
| 215 | @triton.jit |
| 216 | def _cp_gather_indexer_quant_cache_kernel( |
| 217 | kv_cache_ptr, # [n_blks,blk_size//tile_blk,head_dim//16B,tile_blk,16B] |
| 218 | # [n_blks, blk_size, head_dim] |
| 219 | kv_cache_scale_ptr, # [n_blks, blk_size] |
| 220 | k_fp8_ptr, # [num_tokens, head_dim] |
| 221 | k_scale_ptr, # [num_tokens] |
| 222 | block_table_ptr, # [batch_size, block_table_stride] |
| 223 | cu_seqlen_ptr, # [batch_size + 1] |
| 224 | token_to_seq_ptr, # [num_tokens] |
| 225 | block_size, |
| 226 | block_table_stride, |
| 227 | kv_cache_stride, |
| 228 | kv_cache_scale_stride, |
| 229 | LAYOUT: tl.constexpr, |
| 230 | HEAD_DIM: tl.constexpr, |
| 231 | BLOCK_TILE_SIZE: tl.constexpr, |
| 232 | HEAD_TILE_SIZE: tl.constexpr, |
| 233 | num_tokens, |
| 234 | num_batches, |
| 235 | block_table_width, |
| 236 | num_blocks, |
| 237 | ): |
| 238 | tid = tl.program_id(0) |
| 239 | offset = tl.arange(0, HEAD_DIM) |
| 240 | valid_tid = tid < num_tokens |
| 241 | batch_id = tl.load(token_to_seq_ptr + tid, mask=valid_tid, other=-1) |
| 242 | valid_batch = (batch_id >= 0) & (batch_id < num_batches) |
| 243 | safe_batch_id = tl.where(valid_batch, batch_id, 0) |
| 244 | batch_start = tl.load(cu_seqlen_ptr + safe_batch_id, mask=valid_batch, other=0) |
| 245 | batch_end = tl.load(cu_seqlen_ptr + safe_batch_id + 1, mask=valid_batch, other=0) |
| 246 | batch_offset = tid - batch_start |
| 247 | valid_token = valid_tid & valid_batch & (tid >= batch_start) & (tid < batch_end) |
| 248 | if not valid_token: |
| 249 | return |
| 250 | block_table_id = batch_offset // block_size |
| 251 | block_offset = batch_offset % block_size |
| 252 | valid_block_table = ( |
| 253 | valid_token |
| 254 | & (block_table_id >= 0) |
| 255 | & (block_table_id < block_table_width) |
| 256 | & (block_offset >= 0) |
| 257 | & (block_offset < block_size) |
| 258 | ) |
| 259 | safe_block_table_id = tl.where(valid_block_table, block_table_id, 0) |
| 260 | block_table_offset = safe_batch_id * block_table_stride + safe_block_table_id |
| 261 | block_id = tl.load( |
| 262 | block_table_ptr + block_table_offset, mask=valid_block_table, other=-1 |
| 263 | ) |
| 264 | valid_block = valid_block_table & (block_id >= 0) & (block_id < num_blocks) |
| 265 | # The packed KV layout makes per-block strides large |
| 266 | # enough that block_id * stride can exceed 32-bit range. |
| 267 | safe_block_id = tl.where(valid_block, block_id, 0).to(tl.int64) |
| 268 | safe_block_offset = tl.where(valid_block, block_offset, 0) |
| 269 | tiled_block_offset = safe_block_offset % BLOCK_TILE_SIZE |
| 270 | if LAYOUT == "SHUFFLE": |
| 271 | src_cache_offset = ( |
| 272 | safe_block_id * kv_cache_stride |
| 273 | + (safe_block_offset // BLOCK_TILE_SIZE) * HEAD_DIM * BLOCK_TILE_SIZE |
| 274 | + tiled_block_offset * HEAD_TILE_SIZE |
| 275 | ) |
| 276 | else: |
| 277 | src_cache_offset = ( |
| 278 | safe_block_id * kv_cache_stride + safe_block_offset * HEAD_DIM |
| 279 | ) |
| 280 | src_scale_offset = safe_block_id * kv_cache_scale_stride + safe_block_offset |
| 281 | dst_offset = tid * HEAD_DIM |
| 282 | src_scale_ptr = kv_cache_scale_ptr + src_scale_offset |
| 283 | src_cache_ptr = kv_cache_ptr + src_cache_offset |
| 284 | dst_k_ptr = k_fp8_ptr + dst_offset |
| 285 | scale_val = tl.load(src_scale_ptr, mask=valid_block, other=0.0) |
| 286 | tl.store(k_scale_ptr + tid, scale_val) |
| 287 | if LAYOUT == "SHUFFLE": |
| 288 | tiled_src_offset = ( |
| 289 | offset // HEAD_TILE_SIZE * HEAD_TILE_SIZE * BLOCK_TILE_SIZE |
| 290 | + offset % HEAD_TILE_SIZE |
| 291 | ) |
| 292 | else: |
| 293 | tiled_src_offset = offset |
| 294 | val = tl.load(src_cache_ptr + tiled_src_offset) |
| 295 | tl.store(dst_k_ptr + offset, val, mask=valid_block) |
| 296 | |
| 297 | |
| 298 | @triton.jit(do_not_specialize=["num_batches"]) |
| 299 | def _cp_gather_indexer_quant_cache_gfx950_kernel( |
| 300 | kv_cache_ptr, # [n_blks,blk_size//tile_blk,head_dim//16B,tile_blk,16B] |
| 301 | # [n_blks, blk_size, head_dim] |
| 302 | kv_cache_scale_ptr, # [n_blks, blk_size] |
| 303 | k_fp8_ptr, # [num_tokens, head_dim] |
| 304 | k_scale_ptr, # [num_tokens] |
| 305 | block_table_ptr, # [batch_size, block_table_stride] |
| 306 | cu_seqlen_ptr, # [batch_size + 1] |
| 307 | token_to_seq_ptr, # [num_tokens] |
| 308 | block_size, |
| 309 | block_table_stride, |
| 310 | kv_cache_stride, |
| 311 | kv_cache_scale_stride, |
| 312 | LAYOUT: tl.constexpr, |
| 313 | HEAD_DIM: tl.constexpr, |
| 314 | BLOCK_TILE_SIZE: tl.constexpr, |
| 315 | HEAD_TILE_SIZE: tl.constexpr, |
| 316 | num_batches, |
| 317 | BLOCK_TABLE_WIDTH: tl.constexpr, |
| 318 | NUM_BLOCKS: tl.constexpr, |
| 319 | ): |
| 320 | tid = tl.program_id(0) |
| 321 | offset = tl.arange(0, HEAD_DIM) |
| 322 | batch_id = tl.load(token_to_seq_ptr + tid) |
| 323 | valid_batch = (batch_id >= 0) & (batch_id < num_batches) |
| 324 | safe_batch_id = tl.where(valid_batch, batch_id, 0) |
| 325 | batch_start = tl.load(cu_seqlen_ptr + safe_batch_id, mask=valid_batch, other=0) |
| 326 | batch_end = tl.load(cu_seqlen_ptr + safe_batch_id + 1, mask=valid_batch, other=0) |
| 327 | batch_offset = tid - batch_start |
| 328 | valid_token = valid_batch & (tid >= batch_start) & (tid < batch_end) |
| 329 | if not valid_token: |
| 330 | return |
| 331 | block_table_id = batch_offset // block_size |
| 332 | block_offset = batch_offset % block_size |
| 333 | valid_block_table = ( |
| 334 | valid_token |
| 335 | & (block_table_id >= 0) |
| 336 | & (block_table_id < BLOCK_TABLE_WIDTH) |
| 337 | & (block_offset >= 0) |
| 338 | & (block_offset < block_size) |
| 339 | ) |
| 340 | safe_block_table_id = tl.where(valid_block_table, block_table_id, 0) |
| 341 | block_table_offset = safe_batch_id * block_table_stride + safe_block_table_id |
| 342 | block_id = tl.load( |
| 343 | block_table_ptr + block_table_offset, mask=valid_block_table, other=-1 |
| 344 | ) |
| 345 | valid_block = valid_block_table & (block_id >= 0) & (block_id < NUM_BLOCKS) |
| 346 | # The packed KV layout makes per-block strides large |
| 347 | # enough that block_id * stride can exceed 32-bit range. |
| 348 | safe_block_id = tl.where(valid_block, block_id, 0).to(tl.int64) |
| 349 | safe_block_offset = tl.where(valid_block, block_offset, 0) |
| 350 | tiled_block_offset = safe_block_offset % BLOCK_TILE_SIZE |
| 351 | if LAYOUT == "SHUFFLE": |
| 352 | src_cache_offset = ( |
| 353 | safe_block_id * kv_cache_stride |
| 354 | + (safe_block_offset // BLOCK_TILE_SIZE) * HEAD_DIM * BLOCK_TILE_SIZE |
| 355 | + tiled_block_offset * HEAD_TILE_SIZE |
| 356 | ) |
| 357 | else: |
| 358 | src_cache_offset = ( |
| 359 | safe_block_id * kv_cache_stride + safe_block_offset * HEAD_DIM |
| 360 | ) |
| 361 | src_scale_offset = safe_block_id * kv_cache_scale_stride + safe_block_offset |
| 362 | dst_offset = tid * HEAD_DIM |
| 363 | src_scale_ptr = kv_cache_scale_ptr + src_scale_offset |
| 364 | src_cache_ptr = kv_cache_ptr + src_cache_offset |
| 365 | dst_k_ptr = k_fp8_ptr + dst_offset |
| 366 | scale_val = tl.load(src_scale_ptr, mask=valid_block, other=0.0) |
| 367 | tl.store(k_scale_ptr + tid, scale_val) |
| 368 | if LAYOUT == "SHUFFLE": |
| 369 | tiled_src_offset = ( |
| 370 | offset // HEAD_TILE_SIZE * HEAD_TILE_SIZE * BLOCK_TILE_SIZE |
| 371 | + offset % HEAD_TILE_SIZE |
| 372 | ) |
| 373 | else: |
| 374 | tiled_src_offset = offset |
| 375 | val = tl.load(src_cache_ptr + tiled_src_offset) |
| 376 | tl.store(dst_k_ptr + offset, val, mask=valid_block) |
| 377 | |
| 378 | |
| 379 | def cp_gather_indexer_k_quant_cache_triton( |
| 380 | k_cache: torch.Tensor, # [num_blocks, block_size, head_dim + 4] |
| 381 | k_fp8: torch.Tensor, |
| 382 | k_fp8_scale: torch.Tensor, |
| 383 | block_table: torch.Tensor, |
| 384 | cu_seqlen: torch.Tensor, |
| 385 | token_to_seq: torch.Tensor, |
| 386 | block_tile_size: int = 16, |
| 387 | head_tile_size: int = 16, |
| 388 | cache_layout: str | None = None, |
| 389 | ): |
| 390 | num_tokens = k_fp8.size(0) |
| 391 | block_size = k_cache.size(1) |
| 392 | block_table_stride = block_table.stride(0) |
| 393 | head_dim = k_fp8.shape[-1] |
| 394 | num_blocks = k_cache.shape[0] |
| 395 | # we assume the kv cache already been split to 2 portion |
| 396 | k_cache = k_cache.view(num_blocks, -1) |
| 397 | k_cache_value = k_cache[:, : block_size * head_dim].view(FP8_DTYPE) |
| 398 | k_cache_scale = k_cache[:, block_size * head_dim :].view(torch.float32) |
| 399 | grid = (num_tokens,) |
| 400 | k_fp8_scale = k_fp8_scale.view(torch.float32) |
| 401 | if cache_layout is None: |
| 402 | layout = "NORMAL" if block_size == 1 else "SHUFFLE" |
| 403 | else: |
| 404 | layout = cache_layout |
| 405 | kernel_args = ( |
| 406 | k_cache_value, |
| 407 | k_cache_scale, |
| 408 | k_fp8, |
| 409 | k_fp8_scale, |
| 410 | block_table, |
| 411 | cu_seqlen, |
| 412 | token_to_seq, |
| 413 | block_size, |
| 414 | block_table_stride, |
| 415 | k_cache_value.stride(0), |
| 416 | k_cache_scale.stride(0), |
| 417 | layout, |
| 418 | head_dim, |
| 419 | block_tile_size, |
| 420 | head_tile_size, |
| 421 | ) |
| 422 | if _ON_GFX950: |
| 423 | _cp_gather_indexer_quant_cache_gfx950_kernel[grid]( |
| 424 | *kernel_args, |
| 425 | cu_seqlen.shape[0] - 1, |
| 426 | block_table.shape[1], |
| 427 | num_blocks, |
| 428 | ) |
| 429 | else: |
| 430 | _cp_gather_indexer_quant_cache_kernel[grid]( |
| 431 | *kernel_args, |
| 432 | num_tokens, |
| 433 | cu_seqlen.shape[0] - 1, |
| 434 | block_table.shape[1], |
| 435 | num_blocks, |
| 436 | ) |
| 437 | |
| 438 | |
| 439 | # Taken from https://github.com/deepseek-ai/DeepGEMM/blob/main/tests/test_attention.py#L156 |
| 440 | def fp8_paged_mqa_logits_torch( |
| 441 | q: torch.Tensor, |
| 442 | kv_cache: torch.Tensor, |
| 443 | weights: torch.Tensor, |
| 444 | context_lens: torch.Tensor, |
| 445 | block_tables: torch.Tensor, |
| 446 | max_model_len: int, |
| 447 | ): |
| 448 | from vllm.utils.math_utils import cdiv |
| 449 | |
| 450 | batch_size, next_n, _, dim = q.size() |
| 451 | if next_n == 1: |
| 452 | block_size = kv_cache.shape[1] |
| 453 | logits = torch.full( |
| 454 | [batch_size, max_model_len], |
| 455 | float("-inf"), |
| 456 | device=q.device, |
| 457 | dtype=torch.float32, |
| 458 | ) |
| 459 | if context_lens.dim() > 1: |
| 460 | context_lens = context_lens.squeeze(-1) |
| 461 | kv_cache_flat = kv_cache.view(-1, block_size * (dim + 4)) |
| 462 | for i in range(batch_size): |
| 463 | q_i = q[i, 0].to(torch.float32) |
| 464 | q_scale = weights[i] |
| 465 | seq_len = int(context_lens[i].item()) |
| 466 | assert seq_len <= max_model_len |
| 467 | num_pages = cdiv(seq_len, block_size) |
| 468 | padded_seq_len = num_pages * block_size |
| 469 | pages = block_tables[i, :num_pages] |
| 470 | cache = kv_cache_flat[pages] |
| 471 | scale_offset = block_size * dim |
| 472 | cache_value = ( |
| 473 | cache[..., :scale_offset].view(dtype=FP8_DTYPE).to(torch.float32) |
| 474 | ) |
| 475 | cache_scale = ( |
| 476 | cache[..., scale_offset:].view(dtype=torch.float32).contiguous() |
| 477 | ) |
| 478 | cache_value = cache_value.view(padded_seq_len, dim) |
| 479 | cache_scale = cache_scale.view(padded_seq_len) |
| 480 | score = F.linear(cache_value, q_i) |
| 481 | score = F.relu(score) |
| 482 | score *= q_scale[None, :] |
| 483 | score = score.sum(dim=1) |
| 484 | score *= cache_scale |
| 485 | logits[i, :seq_len] = score[:seq_len] |
| 486 | return logits |
| 487 | |
| 488 | kv_cache, scale = kv_cache[..., :dim], kv_cache[..., dim:] |
| 489 | scale = scale.contiguous().view(torch.float) |
| 490 | q = q.float() |
| 491 | kv_cache = kv_cache.view(FP8_DTYPE).float() * scale |
| 492 | num_block, block_size, _, dim = kv_cache.size() |
| 493 | logits = torch.full( |
| 494 | [batch_size * next_n, max_model_len], |
| 495 | float("-inf"), |
| 496 | device=q.device, |
| 497 | dtype=torch.float32, |
| 498 | ) |
| 499 | for i in range(batch_size): |
| 500 | context_len = context_lens[i] |
| 501 | if context_len.ndim == 0: |
| 502 | context_len_i = int(context_len.item()) |
| 503 | q_offsets = torch.arange( |
| 504 | context_len_i - next_n, context_len_i, device=q.device |
| 505 | ) |
| 506 | context_limit = torch.full( |
| 507 | (next_n,), context_len_i, dtype=torch.int32, device=q.device |
| 508 | ) |
| 509 | else: |
| 510 | context_limit = context_len.to(device=q.device, dtype=torch.int32) |
| 511 | q_offsets = context_limit - 1 |
| 512 | weight_slice = ( |
| 513 | weights[i * next_n : (i + 1) * next_n, :].transpose(0, 1).contiguous() |
| 514 | ) |
| 515 | max_context_len = int(context_limit.max().item()) |
| 516 | for block_rk in range(cdiv(max_context_len, block_size)): |
| 517 | block_idx = block_tables[i][block_rk] |
| 518 | qx, kx = q[i], kv_cache[block_idx] |
| 519 | k_offsets = torch.arange( |
| 520 | block_rk * block_size, (block_rk + 1) * block_size, device=q.device |
| 521 | ) |
| 522 | mask = (k_offsets[None, :] < context_limit[:, None]) & ( |
| 523 | k_offsets[None, :] <= q_offsets[:, None] |
| 524 | ) |
| 525 | s = torch.where( |
| 526 | mask[None, :, :], |
| 527 | (qx.transpose(0, 1) @ kx.transpose(0, 1).transpose(1, 2)).to( |
| 528 | logits.dtype |
| 529 | ), |
| 530 | float("-inf"), |
| 531 | ) |
| 532 | s = torch.relu(s) * weight_slice[..., None] |
| 533 | s = s.sum(dim=0) |
| 534 | logits[ |
| 535 | i * next_n : (i + 1) * next_n, |
| 536 | block_rk * block_size : (block_rk + 1) * block_size, |
| 537 | ] = torch.where(k_offsets[None, :] <= q_offsets[:, None], s, float("-inf")) |
| 538 | return logits |
| 539 | |
| 540 | |
| 541 | @triton.jit |
| 542 | def _fp8_paged_mqa_logits_decode_kernel( |
| 543 | q_ptr, # fp8 [B, NEXT_N, H, D] |
| 544 | kv_val_ptr, # fp8, block-flat: [num_blocks, block_size*(D+4)] |
| 545 | kv_scale_ptr, # fp32, block-flat: [num_blocks, block_size*(D+4)//4] |
| 546 | weights_ptr, # fp32 [B*NEXT_N, H] |
| 547 | ctx_lens_ptr, # int32 [B*NEXT_N] if CTX_PER_ROW else [B] |
| 548 | block_tables_ptr, # int32 [B, max_blocks] |
| 549 | logits_ptr, # fp32 [B*NEXT_N, max_model_len] |
| 550 | stride_q_b, |
| 551 | stride_q_n, |
| 552 | stride_q_h, |
| 553 | stride_w_row, |
| 554 | stride_kvblk_fp8, |
| 555 | stride_kvblk_f32, |
| 556 | scale_region_off, # block_size*D // 4 (fp32 offset of scale region within a block) |
| 557 | stride_bt_b, |
| 558 | stride_logits_row, |
| 559 | max_blocks, # block_tables width; guards the block-table gather |
| 560 | max_model_len, |
| 561 | NUM_HEADS: tl.constexpr, |
| 562 | HEAD_SIZE: tl.constexpr, |
| 563 | BLOCK_SIZE: tl.constexpr, # paged-cache page size |
| 564 | BLOCK_KV: tl.constexpr, # positions per tile; must divide BLOCK_SIZE |
| 565 | N_SPLITS: tl.constexpr, # KV-tile parallelism factor (grid dim 1) |
| 566 | NEXT_N: tl.constexpr, # query positions per batch (1 = decode; >1 = MTP verify) |
| 567 | CTX_PER_ROW: tl.constexpr, # ctx_lens is already per-(b, n) |
| 568 | ): |
| 569 | # scale_region_off = block_size*D//4 needs D % 4 == 0 (D=128). |
| 570 | tl.static_assert(HEAD_SIZE % 4 == 0) |
| 571 | # Grid (B*NEXT_N, N_SPLITS): disjoint KV tiles per program, no host sync. |
| 572 | row = tl.program_id(0) |
| 573 | split = tl.program_id(1) |
| 574 | b = row // NEXT_N |
| 575 | n = row % NEXT_N |
| 576 | if CTX_PER_ROW: |
| 577 | seq_len = tl.load(ctx_lens_ptr + row) |
| 578 | else: |
| 579 | seq_len = tl.load(ctx_lens_ptr + b) - NEXT_N + n + 1 |
| 580 | seq_len = tl.minimum(tl.maximum(seq_len, 0), max_model_len) |
| 581 | |
| 582 | h = tl.arange(0, NUM_HEADS) |
| 583 | d = tl.arange(0, HEAD_SIZE) |
| 584 | # keep q/kv fp8 -> fp8 MFMA (f32 accum), not the slow f32 path |
| 585 | q = tl.load( |
| 586 | q_ptr + b * stride_q_b + n * stride_q_n + h[:, None] * stride_q_h + d[None, :] |
| 587 | ) |
| 588 | w = tl.load(weights_ptr + row * stride_w_row + h).to(tl.float32) # [H] |
| 589 | |
| 590 | kv_col = tl.arange(0, BLOCK_KV) |
| 591 | for kv_start in tl.range(split * BLOCK_KV, seq_len, N_SPLITS * BLOCK_KV): |
| 592 | pos = kv_start + kv_col |
| 593 | # Tiles are page-aligned (BLOCK_KV | BLOCK_SIZE), so a tile is one page. |
| 594 | logical_blk = kv_start // BLOCK_SIZE |
| 595 | blk_ok = logical_blk < max_blocks # guard against a wild page -> fault |
| 596 | mask_pos = (pos < seq_len) & blk_ok |
| 597 | page = tl.load( |
| 598 | block_tables_ptr + b * stride_bt_b + logical_blk, mask=blk_ok, other=0 |
| 599 | ).to(tl.int64) |
| 600 | pos_in_blk = pos - logical_blk * BLOCK_SIZE # [BLOCK_KV] == pos % BLOCK_SIZE |
| 601 | |
| 602 | # block-flat layout: values region, then scales region. |
| 603 | val_off = page * stride_kvblk_fp8 + pos_in_blk[:, None] * HEAD_SIZE + d[None, :] |
| 604 | kv = tl.load( |
| 605 | kv_val_ptr + val_off, mask=mask_pos[:, None], other=0.0 |
| 606 | ) # [BLOCK_KV, D] fp8 |
| 607 | sc = tl.load( |
| 608 | kv_scale_ptr + page * stride_kvblk_f32 + scale_region_off + pos_in_blk, |
| 609 | mask=mask_pos, |
| 610 | other=0.0, |
| 611 | ) # [BLOCK_KV] |
| 612 | |
| 613 | # fp8 upcasts exactly; bit-identical on gfx942, ~1e-5 rel on gfx950 |
| 614 | # (accum order), top-k unchanged. |
| 615 | s = tl.dot(kv, tl.trans(q)) # [BLOCK_KV, H] |
| 616 | s = tl.maximum(s, 0.0) |
| 617 | s = s * w[None, :] |
| 618 | s = tl.sum(s, axis=1) # [BLOCK_KV] |
| 619 | s = s * sc |
| 620 | tl.store(logits_ptr + row * stride_logits_row + pos, s, mask=mask_pos) |
| 621 | |
| 622 | |
| 623 | def rocm_fp8_paged_mqa_logits_triton( |
| 624 | q_fp8: torch.Tensor, |
| 625 | kv_cache_fp8: torch.Tensor, |
| 626 | weights: torch.Tensor, |
| 627 | context_lens: torch.Tensor, |
| 628 | block_tables: torch.Tensor, |
| 629 | max_model_len: int, |
| 630 | ) -> torch.Tensor: |
| 631 | """Triton paged MQA-logits for decode and MTP; matches the torch ref but |
| 632 | has no host sync, so it is safe to capture under a full CUDA graph.""" |
| 633 | batch_size, next_n, num_heads, head_size = q_fp8.shape |
| 634 | block_size = kv_cache_fp8.shape[1] |
| 635 | BLOCK_KV = 64 |
| 636 | assert block_size % BLOCK_KV == 0 |
| 637 | |
| 638 | fp8_dtype = current_platform.fp8_dtype() |
| 639 | num_blocks = kv_cache_fp8.shape[0] |
| 640 | kv_flat = kv_cache_fp8.reshape( |
| 641 | num_blocks, -1 |
| 642 | ) # uint8 [num_blocks, block_size*(D+4)] |
| 643 | kv_val = kv_flat.view(fp8_dtype) # [num_blocks, block_size*(D+4)] fp8 |
| 644 | kv_scale = kv_flat.view(torch.float32) # [num_blocks, block_size*(D+4)//4] fp32 |
| 645 | |
| 646 | cl = context_lens.reshape(-1) |
| 647 | ctx_per_row = not (next_n > 1 and cl.numel() == batch_size) |
| 648 | |
| 649 | max_blocks = block_tables.shape[1] |
| 650 | (out_logits,) = current_workspace_manager().get_simultaneous( |
| 651 | ((batch_size * next_n, max_model_len), torch.float32), |
| 652 | ) |
| 653 | |
| 654 | # Memory-bound over the KV range: split each row's keys across programs so |
| 655 | # few-row / long-context launches still fill the GPU. Cap splits at the |
| 656 | # device CU count (304 on gfx942, 256 on gfx950) rather than a gfx950-sized |
| 657 | # constant. All terms are static at launch, so the grid stays CUDA-graph-safe. |
| 658 | rows = batch_size * next_n |
| 659 | tiles_cap = (max_model_len + BLOCK_KV - 1) // BLOCK_KV |
| 660 | N_SPLITS = max(1, min(max(1, _decode_cu_count()), tiles_cap, 1024 // rows)) |
| 661 | _fp8_paged_mqa_logits_decode_kernel[(rows, N_SPLITS)]( |
| 662 | q_fp8, |
| 663 | kv_val, |
| 664 | kv_scale, |
| 665 | weights, |
| 666 | cl, |
| 667 | block_tables, |
| 668 | out_logits, |
| 669 | q_fp8.stride(0), |
| 670 | q_fp8.stride(1), |
| 671 | q_fp8.stride(2), |
| 672 | weights.stride(0), |
| 673 | kv_val.stride(0), |
| 674 | kv_scale.stride(0), |
| 675 | (block_size * head_size) // 4, |
| 676 | block_tables.stride(0), |
| 677 | out_logits.stride(0), |
| 678 | max_blocks, |
| 679 | max_model_len, |
| 680 | NUM_HEADS=num_heads, |
| 681 | HEAD_SIZE=head_size, |
| 682 | BLOCK_SIZE=block_size, |
| 683 | BLOCK_KV=BLOCK_KV, |
| 684 | N_SPLITS=N_SPLITS, |
| 685 | NEXT_N=next_n, |
| 686 | CTX_PER_ROW=ctx_per_row, |
| 687 | num_warps=4, |
| 688 | num_stages=2, |
| 689 | ) |
| 690 | return out_logits |
| 691 | |
| 692 | |
| 693 | @functools.lru_cache |
| 694 | def paged_mqa_logits_module(): |
| 695 | paged_mqa_logits_module_path = None |
| 696 | if find_spec("aiter.ops.triton.pa_mqa_logits") is not None: |
| 697 | paged_mqa_logits_module_path = "aiter.ops.triton.pa_mqa_logits" |
| 698 | elif find_spec("aiter.ops.triton.attention.pa_mqa_logits") is not None: |
| 699 | paged_mqa_logits_module_path = "aiter.ops.triton.attention.pa_mqa_logits" |
| 700 | |
| 701 | if paged_mqa_logits_module_path is not None: |
| 702 | try: |
| 703 | module = importlib.import_module(paged_mqa_logits_module_path) |
| 704 | return module |
| 705 | except ImportError: |
| 706 | return None |
| 707 | return None |
| 708 | |
| 709 | |
| 710 | def rocm_fp8_paged_mqa_logits( |
| 711 | q_fp8: torch.Tensor, |
| 712 | kv_cache_fp8: torch.Tensor, |
| 713 | weights: torch.Tensor, |
| 714 | context_lens: torch.Tensor, |
| 715 | block_tables: torch.Tensor, |
| 716 | schedule_metadata: torch.Tensor, |
| 717 | max_model_len: int, |
| 718 | *, |
| 719 | compress_ratio: int = 1, |
| 720 | ) -> torch.Tensor: |
| 721 | """Compute FP8 MQA logits using paged KV-cache. |
| 722 | |
| 723 | Args: |
| 724 | q_fp8: Query tensor of shape [B, next_n, H, D]. Casted to |
| 725 | `torch.float8_e4m3fn` by caller. |
| 726 | kv_cache_fp8: Paged KV-cache in packed FP8+scale layout with shape |
| 727 | [num_blocks, block_size, 1, D+4], dtype `torch.uint8`. |
| 728 | weights: Tensor of shape [B * next_n, H], dtype `torch.float32`. |
| 729 | context_lens: Tensor of shape [B], dtype int32; effective context length |
| 730 | for each batch element. |
| 731 | block_tables: Tensor of shape [B, max_blocks], dtype int32; maps logical |
| 732 | block indices to physical blocks in the paged cache. |
| 733 | schedule_metadata: Returned by `get_paged_mqa_logits_metadata`; |
| 734 | used to distribute work across SMs. |
| 735 | max_model_len: Maximum sequence length used to size the logits output. |
| 736 | compress_ratio: C4A (4) takes block-flat Triton; 1 and 2 stay on AITER. |
| 737 | |
| 738 | Returns: |
| 739 | Logits tensor of shape [B * next_n, max_model_len], dtype |
| 740 | `torch.float32`. |
| 741 | |
| 742 | """ |
| 743 | from vllm._aiter_ops import rocm_aiter_ops |
| 744 | |
| 745 | batch_size, next_n = q_fp8.shape[:2] |
| 746 | block_size = kv_cache_fp8.shape[1] |
| 747 | |
| 748 | # C4A only: Flash/DSv3.2 also skip insert but still write SHUFFLE. |
| 749 | if ( |
| 750 | (_ON_GFX950 or _ON_GFX942) |
| 751 | and _indexer_k_is_c4a_block_flat(compress_ratio) |
| 752 | and block_size > 1 |
| 753 | ): |
| 754 | if block_size % 64 == 0: |
| 755 | return rocm_fp8_paged_mqa_logits_triton( |
| 756 | q_fp8, kv_cache_fp8, weights, context_lens, block_tables, max_model_len |
| 757 | ) |
| 758 | # Non 64-aligned page size (not used in prod): eager torch ref. |
| 759 | return fp8_paged_mqa_logits_torch( |
| 760 | q_fp8, kv_cache_fp8, weights, context_lens, block_tables, max_model_len |
| 761 | ) |
| 762 | |
| 763 | aiter_paged_mqa_logits_module = None |
| 764 | |
| 765 | if rocm_aiter_ops.is_enabled() or rocm_aiter_ops.is_rdna_aiter_enabled(): |
| 766 | aiter_paged_mqa_logits_module = paged_mqa_logits_module() |
| 767 | |
| 768 | if aiter_paged_mqa_logits_module is not None: |
| 769 | if _ON_GFX942 or _ON_GFX950: |
| 770 | deepgemm_fp8_paged_mqa_logits = ( |
| 771 | aiter_paged_mqa_logits_module.deepgemm_fp8_paged_mqa_logits |
| 772 | ) |
| 773 | batch_size, next_n, heads, _ = q_fp8.shape |
| 774 | (out_logits,) = current_workspace_manager().get_simultaneous( |
| 775 | ((batch_size * next_n, max_model_len), torch.float32), |
| 776 | ) |
| 777 | deepgemm_fp8_paged_mqa_logits( |
| 778 | q_fp8, |
| 779 | kv_cache_fp8, |
| 780 | weights, |
| 781 | out_logits, |
| 782 | context_lens, |
| 783 | block_tables, |
| 784 | max_model_len, |
| 785 | ChunkK=256, |
| 786 | Preshuffle=block_size > 1, |
| 787 | KVBlockSize=block_size, |
| 788 | WavePerEU=2, |
| 789 | ) |
| 790 | return out_logits |
| 791 | deepgemm_fp8_paged_mqa_logits_stage1 = ( |
| 792 | aiter_paged_mqa_logits_module.deepgemm_fp8_paged_mqa_logits_stage1 |
| 793 | ) |
| 794 | batch_size, next_n, heads, _ = q_fp8.shape |
| 795 | (out_qk,) = current_workspace_manager().get_simultaneous( |
| 796 | ((heads, batch_size * next_n, max_model_len), torch.float32), |
| 797 | ) |
| 798 | out_qk.fill_(float("-inf")) |
| 799 | deepgemm_fp8_paged_mqa_logits_stage1( |
| 800 | q_fp8, |
| 801 | kv_cache_fp8, |
| 802 | weights, |
| 803 | out_qk, |
| 804 | context_lens, |
| 805 | block_tables, |
| 806 | max_model_len, |
| 807 | ChunkQ=heads, |
| 808 | ) |
| 809 | return out_qk.sum(dim=0) |
| 810 | else: |
| 811 | return fp8_paged_mqa_logits_torch( |
| 812 | q_fp8, kv_cache_fp8, weights, context_lens, block_tables, max_model_len |
| 813 | ) |
| 814 | |
| 815 | |
| 816 | # Take from https://github.com/deepseek-ai/DeepGEMM/blob/main/tests/test_attention.py#L84 |
| 817 | def fp8_mqa_logits_torch( |
| 818 | q: torch.Tensor, |
| 819 | kv: tuple[torch.Tensor, torch.Tensor], |
| 820 | weights: torch.Tensor, |
| 821 | cu_seqlen_ks: torch.Tensor, |
| 822 | cu_seqlen_ke: torch.Tensor, |
| 823 | ) -> torch.Tensor: |
| 824 | """Compute FP8 MQA logits for a single sequence without KV paging. |
| 825 | |
| 826 | Args: |
| 827 | q: Query tensor of shape [M, H, D]. Casted to |
| 828 | `torch.float8_e4m3fn` by caller. |
| 829 | kv: Tuple `(k_fp8, k_scales)` where `k_fp8` has shape [N, D] with |
| 830 | dtype `torch.float8_e4m3fn` and `k_scales` has shape [N] (or |
| 831 | [N, 1]) with dtype `torch.float32`. |
| 832 | weights: weights of shape [M, H], dtype `torch.float32`. |
| 833 | cu_seqlen_ks: Start indices (inclusive) for valid K per query position, |
| 834 | shape [M], dtype int32. |
| 835 | cu_seqlen_ke: End indices (exclusive) for valid K per query position, |
| 836 | shape [M], dtype int32. |
| 837 | |
| 838 | Returns: |
| 839 | Logits tensor of shape [M, N], dtype `torch.float32`. |
| 840 | |
| 841 | """ |
| 842 | k_fp8, scale = kv |
| 843 | seq_len_kv = k_fp8.shape[0] |
| 844 | k = k_fp8.to(torch.bfloat16) |
| 845 | q = q.to(torch.bfloat16) |
| 846 | device = q.device |
| 847 | |
| 848 | mask_lo = ( |
| 849 | torch.arange(0, seq_len_kv, device=device)[None, :] >= cu_seqlen_ks[:, None] |
| 850 | ) |
| 851 | mask_hi = ( |
| 852 | torch.arange(0, seq_len_kv, device=device)[None, :] < cu_seqlen_ke[:, None] |
| 853 | ) |
| 854 | mask = mask_lo & mask_hi |
| 855 | |
| 856 | # ``score`` is [H, M, N]; ``scale`` is the per-KV-token scale, which |
| 857 | # vLLM callers hand us as ``[N, 1]`` (a ``[N, 4]`` uint8 buffer cast |
| 858 | # to fp32). PyTorch right-aligns dimensions for broadcasting, so a |
| 859 | # naked ``score * scale`` would align ``scale``'s leading dim with |
| 860 | # ``score``'s M dim and raise a shape mismatch. Flatten to ``[N]`` so |
| 861 | # broadcasting lines up with the last dim of ``score``. |
| 862 | score = torch.einsum("mhd,nd->hmn", q, k).float() * scale.reshape(-1) |
| 863 | logits = (score.relu() * weights.unsqueeze(-1).transpose(0, 1)).sum(dim=0) |
| 864 | logits = logits.masked_fill(~mask, float("-inf")) |
| 865 | |
| 866 | return logits |
| 867 | |
| 868 | |
| 869 | @functools.lru_cache |
| 870 | def mqa_logits_module(): |
| 871 | mqa_logits_module_path = None |
| 872 | if find_spec("aiter.ops.triton.fp8_mqa_logits") is not None: |
| 873 | mqa_logits_module_path = "aiter.ops.triton.fp8_mqa_logits" |
| 874 | elif find_spec("aiter.ops.triton.attention.fp8_mqa_logits") is not None: |
| 875 | mqa_logits_module_path = "aiter.ops.triton.attention.fp8_mqa_logits" |
| 876 | |
| 877 | if mqa_logits_module_path is not None: |
| 878 | try: |
| 879 | module = importlib.import_module(mqa_logits_module_path) |
| 880 | return module |
| 881 | except ImportError: |
| 882 | return None |
| 883 | return None |
| 884 | |
| 885 | |
| 886 | def rocm_fp8_mqa_logits( |
| 887 | q: torch.Tensor, |
| 888 | kv: tuple[torch.Tensor, torch.Tensor], |
| 889 | weights: torch.Tensor, |
| 890 | cu_seqlen_ks: torch.Tensor, |
| 891 | cu_seqlen_ke: torch.Tensor, |
| 892 | ) -> torch.Tensor: |
| 893 | """Compute FP8 MQA logits for a single sequence without KV paging. |
| 894 | |
| 895 | Args: |
| 896 | q: Query tensor of shape [M, H, D]. Casted to |
| 897 | `torch.float8_e4m3fn` by caller. |
| 898 | kv: Tuple `(k_fp8, k_scales)` where `k_fp8` has shape [N, D] with |
| 899 | dtype `torch.float8_e4m3fn` and `k_scales` has shape [N] (or |
| 900 | [N, 1]) with dtype `torch.float32`. |
| 901 | weights: weights of shape [M, H], dtype `torch.float32`. |
| 902 | cu_seqlen_ks: Start indices (inclusive) for valid K per query position, |
| 903 | shape [M], dtype int32. |
| 904 | cu_seqlen_ke: End indices (exclusive) for valid K per query position, |
| 905 | shape [M], dtype int32. |
| 906 | |
| 907 | Returns: |
| 908 | Logits tensor of shape [M, N], dtype `torch.float32`. |
| 909 | |
| 910 | """ |
| 911 | from vllm._aiter_ops import rocm_aiter_ops |
| 912 | |
| 913 | k_fp8, scale = kv |
| 914 | |
| 915 | if _ON_GFX942 and rocm_aiter_ops.is_enabled(): |
| 916 | from aiter.ops.flydsl import flydsl_fp8_mqa_logits |
| 917 | |
| 918 | return flydsl_fp8_mqa_logits( |
| 919 | q, k_fp8, scale, weights, cu_seqlen_ks, cu_seqlen_ke |
| 920 | ) |
| 921 | |
| 922 | aiter_mqa_logits_module = None |
| 923 | if rocm_aiter_ops.is_enabled() or rocm_aiter_ops.is_rdna_aiter_enabled(): |
| 924 | aiter_mqa_logits_module = mqa_logits_module() |
| 925 | |
| 926 | if aiter_mqa_logits_module is not None: |
| 927 | fp8_mqa_logits = aiter_mqa_logits_module.fp8_mqa_logits |
| 928 | return fp8_mqa_logits(q, k_fp8, scale, weights, cu_seqlen_ks, cu_seqlen_ke) |
| 929 | else: |
| 930 | return fp8_mqa_logits_torch(q, kv, weights, cu_seqlen_ks, cu_seqlen_ke) |
| 931 | |
| 932 | |
| 933 | # Programs along the column axis of the decode mask grid. The shared kernel in |
| 934 | # model_executor/kernels/attention/dsa/candidate_blocks.py launches one program |
| 935 | # per 1024-column tile, which is 1024 of them per row at max_model_len 1048576 |
| 936 | # with only the first few doing work. This is a fixed count that strides |
| 937 | # instead, measured on gfx950; it is not a portable choice, which is why this |
| 938 | # variant lives here rather than replacing the shared one. |
| 939 | _MASK_GRID_COLS = 128 |
| 940 | # Columns each program handles per iteration. Work stops at the tile boundary |
| 941 | # containing a row's end rather than at the end itself; the overshoot is |
| 942 | # masked the same way the shared kernel masks it, so it costs a tile and |
| 943 | # changes nothing. |
| 944 | _MASK_TILE = 1024 |
| 945 | |
| 946 | |
| 947 | @triton.jit(do_not_specialize=["width", "nblocks"]) |
| 948 | def _mask_candidates_strided_kernel( |
| 949 | logits, |
| 950 | starts, |
| 951 | ends, |
| 952 | flags, |
| 953 | stride_row, |
| 954 | stride_col, |
| 955 | stride_start, |
| 956 | stride_end, |
| 957 | width, |
| 958 | nblocks, |
| 959 | BLOCK_SIZE: tl.constexpr, |
| 960 | HAS_STARTS: tl.constexpr, |
| 961 | ROW_REPEAT: tl.constexpr, |
| 962 | TILE: tl.constexpr, |
| 963 | ): |
| 964 | row = tl.program_id(0).to(tl.int64) |
| 965 | start = tl.load(starts + row // ROW_REPEAT * stride_start) if HAS_STARTS else 0 |
| 966 | end = tl.load(ends + row // ROW_REPEAT * stride_end) |
| 967 | edge = tl.load(flags + row * (nblocks + 1) + nblocks) |
| 968 | step = tl.num_programs(1) * TILE |
| 969 | tile_start = tl.program_id(1) * TILE |
| 970 | # The shared kernel also sanitizes columns at or past `end`. Nothing reads |
| 971 | # them: top_k_per_row_decode bounds its scan by the same row ends passed |
| 972 | # here, and persistent_topk/cooperative_topk clamp by the same lengths. |
| 973 | # Stopping at `end` is what makes the cost track the live context rather |
| 974 | # than max_model_len. |
| 975 | while tile_start < end: |
| 976 | cols = tile_start + tl.arange(0, TILE) |
| 977 | valid = (cols >= start) & (cols < end) & (cols < width) |
| 978 | block = (cols - start) // BLOCK_SIZE |
| 979 | keep = tl.load(flags + row * (nblocks + 1) + block, valid, other=0) |
| 980 | keep = (keep != 0) | ((cols == width - 1) & (edge != 0)) |
| 981 | tl.store( |
| 982 | logits + row * stride_row + cols * stride_col, |
| 983 | -float("inf"), |
| 984 | (cols < width) & ~(valid & keep), |
| 985 | ) |
| 986 | tile_start += step |
| 987 | |
| 988 | |
| 989 | def _apply_candidate_mask_strided( |
| 990 | logits: torch.Tensor, |
| 991 | row_ks: torch.Tensor | None, |
| 992 | row_ke: torch.Tensor, |
| 993 | candidate_blocks: torch.Tensor, |
| 994 | block_size: int, |
| 995 | row_repeat: int = 1, |
| 996 | ) -> None: |
| 997 | """ROCm decode variant of ``apply_candidate_mask``. |
| 998 | |
| 999 | Same masking semantics over ``[0, end)``, but the grid is sized by a fixed |
| 1000 | program count rather than by the logits width. Only worth using where the |
| 1001 | width is the ``max_model_len`` workspace and the live context is far |
| 1002 | shorter, i.e. the paged decode path below; the prefill chunks pass |
| 1003 | chunk-sized logits and stay on the shared kernel. |
| 1004 | """ |
| 1005 | from vllm.model_executor.kernels.attention.dsa.candidate_blocks import ( |
| 1006 | _candidate_flags_kernel, |
| 1007 | ) |
| 1008 | |
| 1009 | rows, width = logits.shape |
| 1010 | if not rows or not width: |
| 1011 | return |
| 1012 | nblocks = triton.cdiv(width, block_size) |
| 1013 | flags = torch.empty((rows, nblocks + 1), device=logits.device, dtype=torch.uint8) |
| 1014 | start_stride = row_ks.stride(0) if row_ks is not None else 0 |
| 1015 | _candidate_flags_kernel[(rows,)]( |
| 1016 | candidate_blocks, |
| 1017 | row_ks, |
| 1018 | flags, |
| 1019 | *candidate_blocks.stride(), |
| 1020 | start_stride, |
| 1021 | width, |
| 1022 | nblocks, |
| 1023 | block_size, |
| 1024 | candidate_blocks.shape[1], |
| 1025 | row_ks is not None, |
| 1026 | row_repeat, |
| 1027 | ) |
| 1028 | # Derived from width, which is a tensor shape, so the grid stays static and |
| 1029 | # a FULL cudagraph capture remains valid across replays; only the loop trip |
| 1030 | # count inside the kernel is data-dependent. The min keeps narrow widths |
| 1031 | # from launching programs that would only fall through. |
| 1032 | grid_cols = min(_MASK_GRID_COLS, triton.cdiv(width, _MASK_TILE)) |
| 1033 | _mask_candidates_strided_kernel[(rows, grid_cols)]( |
| 1034 | logits, |
| 1035 | row_ks, |
| 1036 | row_ke, |
| 1037 | flags, |
| 1038 | *logits.stride(), |
| 1039 | start_stride, |
| 1040 | row_ke.stride(0), |
| 1041 | width, |
| 1042 | nblocks, |
| 1043 | block_size, |
| 1044 | row_ks is not None, |
| 1045 | row_repeat, |
| 1046 | _MASK_TILE, |
| 1047 | ) |
| 1048 | |
| 1049 | |
| 1050 | def _max_decode_logits_rows(num_batched_tokens: int) -> int: |
| 1051 | """Upper bound on decode rows the paged-MQA logits buffer can ever hold. |
| 1052 | |
| 1053 | ``rocm_fp8_paged_mqa_logits`` sizes its workspace as |
| 1054 | ``(batch_size * next_n, max_model_len)``. ``batch_size`` is bounded by |
| 1055 | ``max_num_seqs`` and ``next_n`` by ``1 + num_speculative_tokens``, which is |
| 1056 | far tighter than ``max_num_batched_tokens`` -- 192 vs 16384 for a typical |
| 1057 | 32-seq DSpark-5 deployment. The loose bound is harmless at short contexts |
| 1058 | but scales with ``max_model_len``, so at the model's full context it asks |
| 1059 | for tens of TiB and the engine cannot start. Take whichever valid bound is |
| 1060 | smaller; the workspace is locked after profiling, so it must not be under- |
| 1061 | estimated. |
| 1062 | """ |
| 1063 | try: |
| 1064 | vllm_config = get_current_vllm_config() |
| 1065 | except Exception: |
| 1066 | return num_batched_tokens |
| 1067 | scheduler_config = getattr(vllm_config, "scheduler_config", None) |
| 1068 | max_num_seqs = getattr(scheduler_config, "max_num_seqs", None) |
| 1069 | if not max_num_seqs: |
| 1070 | return num_batched_tokens |
| 1071 | speculative_config = getattr(vllm_config, "speculative_config", None) |
| 1072 | num_spec = getattr(speculative_config, "num_speculative_tokens", 0) or 0 |
| 1073 | return min(num_batched_tokens, max_num_seqs * (1 + num_spec)) |
| 1074 | |
| 1075 | |
| 1076 | def rocm_aiter_sparse_attn_indexer_fake( |
| 1077 | hidden_states: torch.Tensor, |
| 1078 | k_cache_prefix: LayerNameType, |
| 1079 | kv_cache: torch.Tensor, |
| 1080 | q_fp8: torch.Tensor, |
| 1081 | k: torch.Tensor | None, |
| 1082 | weights: torch.Tensor, |
| 1083 | quant_block_size: int, |
| 1084 | scale_fmt: str | None, |
| 1085 | topk_tokens: int, |
| 1086 | head_dim: int, |
| 1087 | max_model_len: int, |
| 1088 | total_seq_lens: int, |
| 1089 | topk_indices_buffer: torch.Tensor | None, |
| 1090 | skip_k_cache_insert: bool = False, |
| 1091 | compress_ratio: int = 1, |
| 1092 | candidate_blocks: torch.Tensor | None = None, |
| 1093 | candidate_block_size: int = 0, |
| 1094 | candidate_write: bool = False, |
| 1095 | ) -> torch.Tensor: |
| 1096 | return topk_indices_buffer |
| 1097 | |
| 1098 | |
| 1099 | @eager_break_during_capture |
| 1100 | def rocm_aiter_sparse_attn_indexer( |
| 1101 | hidden_states: torch.Tensor, |
| 1102 | k_cache_prefix: LayerNameType, |
| 1103 | kv_cache: torch.Tensor, |
| 1104 | q_fp8: torch.Tensor, |
| 1105 | k: torch.Tensor | None, |
| 1106 | weights: torch.Tensor, |
| 1107 | quant_block_size: int, |
| 1108 | scale_fmt: str | None, |
| 1109 | topk_tokens: int, |
| 1110 | head_dim: int, |
| 1111 | max_model_len: int, |
| 1112 | total_seq_lens: int, |
| 1113 | topk_indices_buffer: torch.Tensor | None, |
| 1114 | skip_k_cache_insert: bool = False, |
| 1115 | compress_ratio: int = 1, |
| 1116 | candidate_blocks: torch.Tensor | None = None, |
| 1117 | candidate_block_size: int = 0, |
| 1118 | candidate_write: bool = False, |
| 1119 | ) -> torch.Tensor: |
| 1120 | from vllm._aiter_ops import rocm_aiter_ops |
| 1121 | |
| 1122 | # careful! this will be None in dummy run |
| 1123 | forward_context = get_forward_context() |
| 1124 | attn_metadata = forward_context.attn_metadata |
| 1125 | from vllm.utils.torch_utils import _resolve_layer_name |
| 1126 | |
| 1127 | k_cache_prefix = _resolve_layer_name(k_cache_prefix) |
| 1128 | # assert isinstance(attn_metadata, dict) |
| 1129 | if not isinstance(attn_metadata, dict): |
| 1130 | # Profiling early-exit: reserve memory to account for runtime |
| 1131 | # allocations. Must be in the real impl, not the fake impl — |
| 1132 | # torch.compile calls the fake impl under FakeTensor mode where |
| 1133 | # workspace manager operations on the locked real workspace |
| 1134 | # would corrupt PyTorch's dispatch state. |
| 1135 | workspace_manager = current_workspace_manager() |
| 1136 | |
| 1137 | # Prefill k_fp8 and k_scale buffers, used by |
| 1138 | # rocm_aiter_sparse_attn_indexer's prefill path |
| 1139 | workspace_manager.get_simultaneous( |
| 1140 | ((total_seq_lens, head_dim), FP8_DTYPE), |
| 1141 | ((total_seq_lens, 4), torch.uint8), |
| 1142 | ) |
| 1143 | |
| 1144 | # Decode logits buffer, used by rocm_fp8_paged_mqa_logits. |
| 1145 | decode_rows = _max_decode_logits_rows(hidden_states.shape[0]) |
| 1146 | if _ON_GFX942 or _ON_GFX950: |
| 1147 | workspace_manager.get_simultaneous( |
| 1148 | ((decode_rows, max_model_len), torch.float32), |
| 1149 | ) |
| 1150 | else: |
| 1151 | workspace_manager.get_simultaneous( |
| 1152 | ( |
| 1153 | (q_fp8.shape[1], decode_rows, max_model_len), |
| 1154 | torch.float32, |
| 1155 | ), |
| 1156 | ) |
| 1157 | # Transient logits tensor peak memory, produced by |
| 1158 | # rocm_fp8_mqa_logits (prefill) and rocm_fp8_paged_mqa_logits |
| 1159 | # (decode). Prefill logits are bounded by |
| 1160 | # VLLM_SPARSE_INDEXER_MAX_LOGITS_MB via chunking in |
| 1161 | # split_indexer_prefill_chunks; decode logits are smaller. |
| 1162 | max_logits_elems = envs.VLLM_SPARSE_INDEXER_MAX_LOGITS_MB * 1024 * 1024 |
| 1163 | _ = torch.empty( |
| 1164 | max_logits_elems, dtype=torch.uint8, device=hidden_states.device |
| 1165 | ) |
| 1166 | |
| 1167 | return rocm_aiter_sparse_attn_indexer_fake( |
| 1168 | hidden_states, |
| 1169 | k_cache_prefix, |
| 1170 | kv_cache, |
| 1171 | q_fp8, |
| 1172 | k, |
| 1173 | weights, |
| 1174 | quant_block_size, |
| 1175 | scale_fmt, |
| 1176 | topk_tokens, |
| 1177 | head_dim, |
| 1178 | max_model_len, |
| 1179 | total_seq_lens, |
| 1180 | topk_indices_buffer, |
| 1181 | skip_k_cache_insert, |
| 1182 | compress_ratio, |
| 1183 | candidate_blocks, |
| 1184 | candidate_block_size, |
| 1185 | candidate_write, |
| 1186 | ) |
| 1187 | layer_attn_metadata = attn_metadata[k_cache_prefix] |
| 1188 | assert isinstance(layer_attn_metadata, DeepseekV32IndexerMetadata) |
| 1189 | assert topk_indices_buffer is not None |
| 1190 | assert scale_fmt is not None |
| 1191 | slot_mapping = layer_attn_metadata.slot_mapping |
| 1192 | has_decode = layer_attn_metadata.num_decodes > 0 |
| 1193 | has_prefill = layer_attn_metadata.num_prefills > 0 |
| 1194 | num_decode_tokens = layer_attn_metadata.num_decode_tokens |
| 1195 | topk_indices_buffer[: hidden_states.shape[0]] = -1 |
| 1196 | |
| 1197 | # during speculative decoding, k may be padded to the CUDA graph batch |
| 1198 | # size while slot_mapping only covers actual tokens. |
| 1199 | num_tokens = slot_mapping.shape[0] |
| 1200 | if k is not None: |
| 1201 | k = k[:num_tokens] |
| 1202 | elif not skip_k_cache_insert: |
| 1203 | raise ValueError("k must be provided when skip_k_cache_insert is False") |
| 1204 | |
| 1205 | if not skip_k_cache_insert: |
| 1206 | indexer_k_quant_and_cache_triton( |
| 1207 | k, |
| 1208 | kv_cache, |
| 1209 | slot_mapping, |
| 1210 | quant_block_size, |
| 1211 | scale_fmt, |
| 1212 | ) |
| 1213 | |
| 1214 | if has_prefill: |
| 1215 | prefill_metadata = layer_attn_metadata.prefill |
| 1216 | assert prefill_metadata is not None |
| 1217 | |
| 1218 | workspace_manager = current_workspace_manager() |
| 1219 | k_fp8_full, k_scale_full = workspace_manager.get_simultaneous( |
| 1220 | ((total_seq_lens, head_dim), FP8_DTYPE), |
| 1221 | ((total_seq_lens, 4), torch.uint8), |
| 1222 | ) |
| 1223 | for chunk in prefill_metadata.chunks: |
| 1224 | k_fp8 = k_fp8_full[: chunk.total_seq_lens] |
| 1225 | k_scale = k_scale_full[: chunk.total_seq_lens] |
| 1226 | cp_gather_indexer_k_quant_cache_triton( |
| 1227 | kv_cache, |
| 1228 | k_fp8, |
| 1229 | k_scale, |
| 1230 | chunk.block_table, |
| 1231 | chunk.cu_seq_lens, |
| 1232 | token_to_seq=chunk.token_to_seq, |
| 1233 | cache_layout=( |
| 1234 | "NORMAL" if _indexer_k_is_c4a_block_flat(compress_ratio) else None |
| 1235 | ), |
| 1236 | ) |
| 1237 | logits = rocm_fp8_mqa_logits( |
| 1238 | q_fp8[chunk.token_start : chunk.token_end], |
| 1239 | (k_fp8, k_scale.view(torch.float32)), |
| 1240 | weights[chunk.token_start : chunk.token_end], |
| 1241 | chunk.cu_seqlen_ks, |
| 1242 | chunk.cu_seqlen_ke, |
| 1243 | ) |
| 1244 | if candidate_blocks is not None: |
| 1245 | from vllm.model_executor.layers.sparse_attn_indexer import ( |
| 1246 | _apply_candidate_mask, |
| 1247 | _select_candidate_blocks, |
| 1248 | ) |
| 1249 | |
| 1250 | chunk_candidates = candidate_blocks[chunk.token_start : chunk.token_end] |
| 1251 | if candidate_write: |
| 1252 | _select_candidate_blocks( |
| 1253 | logits, |
| 1254 | chunk.cu_seqlen_ks, |
| 1255 | chunk.cu_seqlen_ke, |
| 1256 | chunk_candidates.shape[1], |
| 1257 | candidate_block_size, |
| 1258 | chunk_candidates, |
| 1259 | ) |
| 1260 | else: |
| 1261 | _apply_candidate_mask( |
| 1262 | logits, |
| 1263 | chunk.cu_seqlen_ks, |
| 1264 | chunk.cu_seqlen_ke, |
| 1265 | chunk_candidates, |
| 1266 | candidate_block_size, |
| 1267 | ) |
| 1268 | topk_indices = topk_indices_buffer[ |
| 1269 | chunk.token_start : chunk.token_end, :topk_tokens |
| 1270 | ] |
| 1271 | |
| 1272 | num_rows = logits.shape[0] |
| 1273 | |
| 1274 | if rocm_aiter_ops.is_indexer_top_k_supported( |
| 1275 | is_prefill=True, |
| 1276 | compress_ratio=compress_ratio, |
| 1277 | num_rows=num_rows, |
| 1278 | ): |
| 1279 | _launch_aiter_top_k_per_row_prefill( |
| 1280 | logits, |
| 1281 | chunk.cu_seqlen_ks, |
| 1282 | chunk.cu_seqlen_ke, |
| 1283 | topk_indices, |
| 1284 | topk_tokens, |
| 1285 | ) |
| 1286 | else: |
| 1287 | torch.ops._C.top_k_per_row_prefill( |
| 1288 | logits, |
| 1289 | chunk.cu_seqlen_ks, |
| 1290 | chunk.cu_seqlen_ke, |
| 1291 | topk_indices, |
| 1292 | num_rows, |
| 1293 | logits.stride(0), |
| 1294 | logits.stride(1), |
| 1295 | topk_tokens, |
| 1296 | ) |
| 1297 | |
| 1298 | if has_decode: |
| 1299 | decode_metadata = layer_attn_metadata.decode |
| 1300 | assert decode_metadata is not None |
| 1301 | # kv_cache size requirement [num_block, block_size, n_head, head_dim], |
| 1302 | # we only have [num_block, block_size, head_dim], |
| 1303 | kv_cache = kv_cache.unsqueeze(-2) |
| 1304 | decode_lens = decode_metadata.decode_lens |
| 1305 | if decode_metadata.requires_padding: |
| 1306 | # pad in edge case where we have short chunked prefill length < |
| 1307 | # decode_threshold since we unstrictly split |
| 1308 | # prefill and decode by decode_threshold |
| 1309 | # (currently set to 1 + speculative tokens) |
| 1310 | padded_q_fp8_decode_tokens = pack_seq_triton( |
| 1311 | q_fp8[:num_decode_tokens], decode_lens |
| 1312 | ) |
| 1313 | else: |
| 1314 | padded_q_fp8_decode_tokens = q_fp8[:num_decode_tokens].reshape( |
| 1315 | decode_lens.shape[0], -1, *q_fp8.shape[1:] |
| 1316 | ) |
| 1317 | # TODO: move and optimize below logic with triton kernels |
| 1318 | batch_size = padded_q_fp8_decode_tokens.shape[0] |
| 1319 | next_n = padded_q_fp8_decode_tokens.shape[1] |
| 1320 | assert batch_size == decode_metadata.seq_lens.shape[0] |
| 1321 | num_padded_tokens = batch_size * next_n |
| 1322 | |
| 1323 | logits = rocm_fp8_paged_mqa_logits( |
| 1324 | padded_q_fp8_decode_tokens, |
| 1325 | kv_cache, |
| 1326 | weights[:num_padded_tokens], |
| 1327 | decode_metadata.seq_lens, |
| 1328 | decode_metadata.block_table, |
| 1329 | decode_metadata.schedule_metadata, |
| 1330 | max_model_len=max_model_len, |
| 1331 | compress_ratio=compress_ratio, |
| 1332 | ) |
| 1333 | |
| 1334 | if candidate_blocks is not None: |
| 1335 | from vllm.model_executor.layers.sparse_attn_indexer import ( |
| 1336 | _select_candidate_blocks, |
| 1337 | ) |
| 1338 | |
| 1339 | num_rows = logits.shape[0] |
| 1340 | visible = decode_metadata.seq_lens.reshape(-1) |
| 1341 | if visible.numel() != num_rows: |
| 1342 | visible = visible.repeat_interleave(next_n) |
| 1343 | visible = visible[:num_rows].to(torch.int64) |
| 1344 | row_starts = torch.zeros_like(visible) |
| 1345 | decode_candidates = candidate_blocks[:num_rows] |
| 1346 | if candidate_write: |
| 1347 | _select_candidate_blocks( |
| 1348 | logits, |
| 1349 | row_starts, |
| 1350 | visible, |
| 1351 | decode_candidates.shape[1], |
| 1352 | candidate_block_size, |
| 1353 | decode_candidates, |
| 1354 | ) |
| 1355 | else: |
| 1356 | _apply_candidate_mask_strided( |
| 1357 | logits, |
| 1358 | row_starts, |
| 1359 | visible, |
| 1360 | decode_candidates, |
| 1361 | candidate_block_size, |
| 1362 | ) |
| 1363 | |
| 1364 | topk_indices = topk_indices_buffer[:num_padded_tokens, :topk_tokens] |
| 1365 | num_rows = logits.shape[0] |
| 1366 | |
| 1367 | # FULL graphs are not keyed by context length, so use a replay-safe |
| 1368 | # upper bound when choosing the captured kernel. |
| 1369 | if forward_context.cudagraph_runtime_mode == CUDAGraphMode.FULL: |
| 1370 | max_compressed_seq_len = max_model_len |
| 1371 | else: |
| 1372 | max_compressed_seq_len = layer_attn_metadata.max_seq_len // compress_ratio |
| 1373 | if rocm_aiter_ops.is_indexer_top_k_supported( |
| 1374 | is_prefill=False, |
| 1375 | compress_ratio=compress_ratio, |
| 1376 | num_rows=num_rows, |
| 1377 | max_valid_seq_len=max_compressed_seq_len, |
| 1378 | ) and not rocm_aiter_ops.dsv4_indexer_prefers_native_top_k( |
| 1379 | num_rows=num_rows, |
| 1380 | num_columns=logits.shape[1], |
| 1381 | topk_tokens=topk_tokens, |
| 1382 | ): |
| 1383 | rocm_aiter_ops.indexer_top_k_decode( |
| 1384 | logits, |
| 1385 | 1, |
| 1386 | decode_metadata.seq_lens.reshape(-1), |
| 1387 | topk_indices, |
| 1388 | topk_tokens, |
| 1389 | ) |
| 1390 | else: |
| 1391 | torch.ops._C.top_k_per_row_decode( |
| 1392 | logits, |
| 1393 | next_n, |
| 1394 | decode_metadata.seq_lens, |
| 1395 | topk_indices, |
| 1396 | num_rows, |
| 1397 | logits.stride(0), |
| 1398 | logits.stride(1), |
| 1399 | topk_tokens, |
| 1400 | ) |
| 1401 | |
| 1402 | if decode_metadata.requires_padding: |
| 1403 | # if padded, we need to unpack |
| 1404 | # the topk indices removing padded tokens |
| 1405 | topk_indices = unpack_seq_triton( |
| 1406 | topk_indices.reshape(batch_size, next_n, topk_indices.shape[-1]), |
| 1407 | decode_lens, |
| 1408 | ) |
| 1409 | topk_indices_buffer[:num_decode_tokens, : topk_indices.shape[-1]] = ( |
| 1410 | topk_indices |
| 1411 | ) |
| 1412 | |
| 1413 | return topk_indices_buffer |
| 1414 | |
| 1415 | |
| 1416 | def _decode_e8m0_scales(scale: torch.Tensor) -> torch.Tensor: |
| 1417 | if scale.dtype == torch.float8_e8m0fnu: |
| 1418 | from vllm.model_executor.layers.quantization.utils.fp8_utils import ( |
| 1419 | _upcast_e8m0_to_fp32, |
| 1420 | ) |
| 1421 | |
| 1422 | return _upcast_e8m0_to_fp32(scale).contiguous() |
| 1423 | if scale.dtype == torch.uint8: |
| 1424 | # MXFP8 parameters preserve E8M0 scales as their raw exponent bytes. |
| 1425 | # They are biased exponents, not numeric uint8 scale values. |
| 1426 | return torch.exp2(scale.to(torch.int16).to(torch.float32) - 127.0) |
| 1427 | return scale.to(torch.float32) |
| 1428 | |
| 1429 | |
| 1430 | def _expand_2d_block_scales( |
| 1431 | scale: torch.Tensor, |
| 1432 | rows: int, |
| 1433 | cols: int, |
| 1434 | ) -> torch.Tensor: |
| 1435 | scale = _decode_e8m0_scales(scale) |
| 1436 | row_blocks, col_blocks = scale.shape[-2:] |
| 1437 | row_block = math.ceil(rows / row_blocks) |
| 1438 | col_block = math.ceil(cols / col_blocks) |
| 1439 | scale = torch.repeat_interleave(scale, row_block, dim=-2)[..., :rows, :] |
| 1440 | scale = torch.repeat_interleave(scale, col_block, dim=-1)[..., :, :cols] |
| 1441 | return scale |
| 1442 | |
| 1443 | |
| 1444 | @triton.jit |
| 1445 | def _inverse_rope_gptj_kernel( |
| 1446 | o_ptr, # [T, H, D] input |
| 1447 | out_ptr, # [T, H, D] bf16 output |
| 1448 | pos_ptr, # [T] positions |
| 1449 | cos_sin_ptr, # [P, rope_dim] fp32 (cos[:half] | sin[half:]) |
| 1450 | s_t, |
| 1451 | s_h, # input row strides (last dim contiguous) |
| 1452 | os_t, |
| 1453 | os_h, # output row strides |
| 1454 | cs_stride, # cos_sin_cache row stride |
| 1455 | NOPE: tl.constexpr, # non-rope head dims (passed through) |
| 1456 | HALF: tl.constexpr, # rope_dim // 2 |
| 1457 | BLOCK_NOPE: tl.constexpr, |
| 1458 | BLOCK_HALF: tl.constexpr, |
| 1459 | ): |
| 1460 | """Fused inverse GPT-J RoPE on the trailing rope_dim of each (token, head). |
| 1461 | |
| 1462 | Mirrors ``DeepseekV4ScalingRotaryEmbedding.forward_native(inverse=True)`` |
| 1463 | for the GPT-J (non-neox) layout, writing bf16 directly. Replaces the |
| 1464 | clone + index_select + repeat_interleave + neg + stack + cat + cast chain |
| 1465 | (~10 small kernels) with a single launch. |
| 1466 | """ |
| 1467 | t = tl.program_id(0) |
| 1468 | h = tl.program_id(1) |
| 1469 | in_base = t * s_t + h * s_h |
| 1470 | out_base = t * os_t + h * os_h |
| 1471 | |
| 1472 | # NoPE lanes pass through unchanged (only cast to bf16). |
| 1473 | n = tl.arange(0, BLOCK_NOPE) |
| 1474 | nmask = n < NOPE |
| 1475 | vals = tl.load(o_ptr + in_base + n, mask=nmask) |
| 1476 | tl.store(out_ptr + out_base + n, vals.to(tl.bfloat16), mask=nmask) |
| 1477 | |
| 1478 | # RoPE lanes: out_even = a*cos + b*sin, out_odd = b*cos - a*sin |
| 1479 | # (a = even lane, b = odd lane; sin negated for the inverse rotation). |
| 1480 | pos = tl.load(pos_ptr + t).to(tl.int64) |
| 1481 | k = tl.arange(0, BLOCK_HALF) |
| 1482 | kmask = k < HALF |
| 1483 | a = tl.load(o_ptr + in_base + NOPE + 2 * k, mask=kmask).to(tl.float32) |
| 1484 | b = tl.load(o_ptr + in_base + NOPE + 2 * k + 1, mask=kmask).to(tl.float32) |
| 1485 | cos = tl.load(cos_sin_ptr + pos * cs_stride + k, mask=kmask) |
| 1486 | sin = tl.load(cos_sin_ptr + pos * cs_stride + HALF + k, mask=kmask) |
| 1487 | out_even = a * cos + b * sin |
| 1488 | out_odd = b * cos - a * sin |
| 1489 | tl.store(out_ptr + out_base + NOPE + 2 * k, out_even.to(tl.bfloat16), mask=kmask) |
| 1490 | tl.store(out_ptr + out_base + NOPE + 2 * k + 1, out_odd.to(tl.bfloat16), mask=kmask) |
| 1491 | |
| 1492 | |
| 1493 | def _fused_inverse_rope_gptj( |
| 1494 | o: torch.Tensor, |
| 1495 | positions: torch.Tensor, |
| 1496 | cos_sin_cache: torch.Tensor, |
| 1497 | rope_head_dim: int, |
| 1498 | out: torch.Tensor | None = None, |
| 1499 | ) -> torch.Tensor: |
| 1500 | """bf16 inverse GPT-J RoPE via a single fused Triton kernel. |
| 1501 | |
| 1502 | ``out`` may alias ``o``: the rotation is a per-row bijection whose kernel |
| 1503 | reads both lanes of a pair before storing either. |
| 1504 | """ |
| 1505 | assert o.dim() == 3 and o.stride(-1) == 1, ( |
| 1506 | "_fused_inverse_rope_gptj expects a [T, H, D] input with a contiguous last dim" |
| 1507 | ) |
| 1508 | assert rope_head_dim > 0 and rope_head_dim % 2 == 0, ( |
| 1509 | f"_fused_inverse_rope_gptj expects an even rope_head_dim, got {rope_head_dim}" |
| 1510 | ) |
| 1511 | assert cos_sin_cache.shape[-1] == rope_head_dim, ( |
| 1512 | "_fused_inverse_rope_gptj expects cos_sin_cache laid out as " |
| 1513 | f"[P, {rope_head_dim}] = cos | sin, got {tuple(cos_sin_cache.shape)}" |
| 1514 | ) |
| 1515 | num_tokens, num_heads, head_dim = o.shape |
| 1516 | if out is None: |
| 1517 | out = torch.empty( |
| 1518 | (num_tokens, num_heads, head_dim), dtype=torch.bfloat16, device=o.device |
| 1519 | ) |
| 1520 | else: |
| 1521 | assert out.dtype == torch.bfloat16, ( |
| 1522 | f"inverse RoPE writes bf16, got an output buffer of {out.dtype}" |
| 1523 | ) |
| 1524 | if num_tokens == 0: |
| 1525 | return out |
| 1526 | _inverse_rope_gptj_kernel[(num_tokens, num_heads)]( |
| 1527 | o, |
| 1528 | out, |
| 1529 | positions, |
| 1530 | cos_sin_cache, |
| 1531 | o.stride(0), |
| 1532 | o.stride(1), |
| 1533 | out.stride(0), |
| 1534 | out.stride(1), |
| 1535 | cos_sin_cache.stride(0), |
| 1536 | NOPE=head_dim - rope_head_dim, |
| 1537 | HALF=rope_head_dim // 2, |
| 1538 | BLOCK_NOPE=triton.next_power_of_2(head_dim - rope_head_dim), |
| 1539 | BLOCK_HALF=triton.next_power_of_2(rope_head_dim // 2), |
| 1540 | ) |
| 1541 | return out |
| 1542 | |
| 1543 | |
| 1544 | def rocm_inverse_rope_rows_( |
| 1545 | o: torch.Tensor, |
| 1546 | positions: torch.Tensor, |
| 1547 | cos_sin_cache: torch.Tensor, |
| 1548 | rope_head_dim: int, |
| 1549 | ) -> None: |
| 1550 | """Inverse-RoPE attention output rows in place. |
| 1551 | |
| 1552 | For rows no attention kernel rotated in its epilogue. Call it from the |
| 1553 | eager attention segment: which rows still owe a rotation depends on the |
| 1554 | prefill/decode split, and the o_proj that used to do this runs inside the |
| 1555 | compiled region, where a batch-dependent Python value would be frozen at |
| 1556 | trace time. |
| 1557 | """ |
| 1558 | if o.shape[0] == 0: |
| 1559 | return |
| 1560 | _fused_inverse_rope_gptj(o, positions, cos_sin_cache, rope_head_dim, out=o) |
| 1561 | |
| 1562 | |
| 1563 | @triton.jit |
| 1564 | def _inverse_rope_mxfp8_quant_kernel( |
| 1565 | o_ptr, # [T, H, D] bf16 |
| 1566 | q_ptr, # [T, H * D] e4m3 |
| 1567 | s_ptr, # [T, H * D // 32] uint8 (E8M0) |
| 1568 | pos_ptr, |
| 1569 | cos_sin_ptr, |
| 1570 | s_t, |
| 1571 | s_h, |
| 1572 | qs_t, |
| 1573 | ss_t, |
| 1574 | cs_stride, |
| 1575 | HEAD_DIM: tl.constexpr, |
| 1576 | NOPE: tl.constexpr, |
| 1577 | HALF: tl.constexpr, |
| 1578 | ): |
| 1579 | t = tl.program_id(0) |
| 1580 | h = tl.program_id(1) |
| 1581 | lanes = tl.arange(0, HEAD_DIM) |
| 1582 | x = tl.load(o_ptr + t * s_t + h * s_h + lanes).to(tl.float32) |
| 1583 | # Same lane-pair formulation as the reduce epilogue: NoPE pairs take |
| 1584 | # cos=1 / sin=0, so one expression rotates the whole row. |
| 1585 | pos = tl.load(pos_ptr + t).to(tl.int64) |
| 1586 | pair_idx = tl.arange(0, HEAD_DIM // 2) - (NOPE // 2) |
| 1587 | is_rope = pair_idx >= 0 |
| 1588 | k = tl.where(is_rope, pair_idx, 0) |
| 1589 | cos = tl.where(is_rope, tl.load(cos_sin_ptr + pos * cs_stride + k), 1.0) |
| 1590 | sin = tl.where(is_rope, tl.load(cos_sin_ptr + pos * cs_stride + HALF + k), 0.0) |
| 1591 | even, odd = tl.split(tl.reshape(x, (HEAD_DIM // 2, 2))) |
| 1592 | x = tl.reshape( |
| 1593 | tl.join(even * cos + odd * sin, odd * cos - even * sin), (1, HEAD_DIM) |
| 1594 | ) |
| 1595 | xq, bits = _mxfp8_quantize_rows(x, 1, HEAD_DIM) |
| 1596 | tl.store( |
| 1597 | q_ptr + t * qs_t + h * HEAD_DIM + lanes, |
| 1598 | tl.reshape(xq, (HEAD_DIM,)).to(q_ptr.dtype.element_ty), |
| 1599 | ) |
| 1600 | scale_offsets = tl.arange(0, HEAD_DIM // 32) |
| 1601 | tl.store( |
| 1602 | s_ptr + t * ss_t + h * (HEAD_DIM // 32) + scale_offsets, |
| 1603 | tl.reshape(bits, (HEAD_DIM // 32,)).to(tl.uint8), |
| 1604 | ) |
| 1605 | |
| 1606 | |
| 1607 | def rocm_inverse_rope_mxfp8_rows( |
| 1608 | o: torch.Tensor, |
| 1609 | positions: torch.Tensor, |
| 1610 | cos_sin_cache: torch.Tensor, |
| 1611 | rope_head_dim: int, |
| 1612 | out_data: torch.Tensor, |
| 1613 | out_scale: torch.Tensor, |
| 1614 | ) -> None: |
| 1615 | """Inverse-RoPE bf16 attention rows and MXFP8-quantize them for wo_a. |
| 1616 | |
| 1617 | The counterpart of ``rocm_inverse_rope_rows_`` for layers whose attention |
| 1618 | output is MXFP8: rows the decode reduce did not emit (prefill) go through |
| 1619 | here. ``o`` is [T, H, D]; ``out_data`` [T, H * D] e4m3 and ``out_scale`` |
| 1620 | [T, H * D // 32] E8M0, the layout the reduce epilogue writes. |
| 1621 | """ |
| 1622 | num_tokens, num_heads, head_dim = o.shape |
| 1623 | if num_tokens == 0: |
| 1624 | return |
| 1625 | assert o.stride(-1) == 1 and out_data.stride(-1) == 1 and out_scale.stride(-1) == 1 |
| 1626 | assert out_data.shape == (num_tokens, num_heads * head_dim) |
| 1627 | assert out_scale.shape == (num_tokens, num_heads * head_dim // 32) |
| 1628 | assert cos_sin_cache.shape[-1] == rope_head_dim |
| 1629 | _inverse_rope_mxfp8_quant_kernel[(num_tokens, num_heads)]( |
| 1630 | o, |
| 1631 | out_data, |
| 1632 | out_scale, |
| 1633 | positions, |
| 1634 | cos_sin_cache, |
| 1635 | o.stride(0), |
| 1636 | o.stride(1), |
| 1637 | out_data.stride(0), |
| 1638 | out_scale.stride(0), |
| 1639 | cos_sin_cache.stride(0), |
| 1640 | HEAD_DIM=head_dim, |
| 1641 | NOPE=head_dim - rope_head_dim, |
| 1642 | HALF=rope_head_dim // 2, |
| 1643 | num_warps=4, |
| 1644 | ) |
| 1645 | |
| 1646 | |
| 1647 | def _get_cached_wo_a_bf16( |
| 1648 | wo_a: torch.nn.Module, |
| 1649 | n_local_groups: int, |
| 1650 | o_lora_rank: int, |
| 1651 | hidden_dim: int, |
| 1652 | ) -> torch.Tensor: |
| 1653 | """Dequantize wo_a to bf16 once and cache it on the module. |
| 1654 | |
| 1655 | wo_a weights are static, so the fp8 -> fp32 -> (* block scale) -> bf16 |
| 1656 | dequant only needs to run once. Recomputing it every decode step shows up |
| 1657 | in the profile as the largest copy/mul kernels (``direct_copy float`` ~55us |
| 1658 | and ``MulFunctor float`` ~31us per two layers). SGLang / ATOM keep wo_a in |
| 1659 | bf16 and feed a plain bf16 GEMM; this mirrors that. |
| 1660 | """ |
| 1661 | cached = getattr(wo_a, "_dsv4_wo_a_bf16", None) |
| 1662 | if cached is not None: |
| 1663 | return cached |
| 1664 | from vllm.model_executor.layers.quantization.utils.fp8_utils import ( |
| 1665 | get_fp8_block_weight_scale, |
| 1666 | ) |
| 1667 | |
| 1668 | wo_a_scale_param = get_fp8_block_weight_scale(wo_a) |
| 1669 | if wo_a_scale_param is None: |
| 1670 | # ModelOpt MXFP8 stores the multiplicative E8M0 scale without the |
| 1671 | # historical ``_inv`` suffix. |
| 1672 | wo_a_scale_param = getattr(wo_a, "weight_scale", None) |
| 1673 | # Emulated MXFP8 kernels can replace the original one-byte weight with an |
| 1674 | # already-dequantized BF16 tensor while retaining the scale attribute for |
| 1675 | # metadata. Applying that retained scale again would double-dequantize the |
| 1676 | # weight. Block scaling is only valid while the one-byte FP8 storage remains. |
| 1677 | if wo_a_scale_param is not None and wo_a.weight.element_size() == 1: |
| 1678 | wo_a_weight = wo_a.weight.view(n_local_groups, o_lora_rank, hidden_dim).to( |
| 1679 | torch.float32 |
| 1680 | ) |
| 1681 | wo_a_scale = _expand_2d_block_scales( |
| 1682 | wo_a_scale_param.view(n_local_groups, -1, wo_a_scale_param.shape[-1]), |
| 1683 | o_lora_rank, |
| 1684 | hidden_dim, |
| 1685 | ) |
| 1686 | cached = (wo_a_weight * wo_a_scale).to(torch.bfloat16) |
| 1687 | else: |
| 1688 | cached = wo_a.weight.view(n_local_groups, o_lora_rank, hidden_dim).to( |
| 1689 | torch.bfloat16 |
| 1690 | ) |
| 1691 | wo_a._dsv4_wo_a_bf16 = cached |
| 1692 | return cached |
| 1693 | |
| 1694 | |
| 1695 | def rocm_inv_rope_einsum( |
| 1696 | rotary_emb: torch.nn.Module, |
| 1697 | o: torch.Tensor, |
| 1698 | positions: torch.Tensor, |
| 1699 | rope_head_dim: int, |
| 1700 | n_local_groups: int, |
| 1701 | o_lora_rank: int, |
| 1702 | wo_a: torch.nn.Module, |
| 1703 | inverse_rope: bool = True, |
| 1704 | ) -> torch.Tensor: |
| 1705 | """Inverse-RoPE + WO_A bmm path used on ROCm. |
| 1706 | |
| 1707 | Fuses the inverse GPT-J RoPE into one Triton kernel and caches the bf16 |
| 1708 | wo_a weight so the per-step dequant disappears. Callers whose attention |
| 1709 | already rotated every row pass ``inverse_rope=False``; that is a property |
| 1710 | of the attention backend, not of the batch, so it stays constant across |
| 1711 | steps and is safe to read from compiled code. |
| 1712 | """ |
| 1713 | if inverse_rope: |
| 1714 | o_ref = _fused_inverse_rope_gptj( |
| 1715 | o, positions, rotary_emb.cos_sin_cache, rope_head_dim |
| 1716 | ) |
| 1717 | else: |
| 1718 | assert o.dtype == torch.bfloat16, ( |
| 1719 | "a pre-rotated attention output feeds the wo_a bmm directly, so it " |
| 1720 | f"must already be bf16, got {o.dtype}" |
| 1721 | ) |
| 1722 | o_ref = o |
| 1723 | o_ref = o_ref.reshape(o.shape[0], n_local_groups, -1) |
| 1724 | |
| 1725 | wo_a_weight = _get_cached_wo_a_bf16( |
| 1726 | wo_a, n_local_groups, o_lora_rank, o_ref.shape[-1] |
| 1727 | ) |
| 1728 | |
| 1729 | return torch.einsum("tgd,grd->tgr", o_ref, wo_a_weight) |
| 1730 | |
| 1731 | |
| 1732 | @triton.jit |
| 1733 | def _mxfp8_wo_a_bmm_kernel( |
| 1734 | a_ptr, # [T, G * K] e4m3 |
| 1735 | as_ptr, # [T, G * K // 32] E8M0 |
| 1736 | w_ptr, # [G * R, K] e4m3 |
| 1737 | ws_ptr, # [G * R // SCALE_ROWS, K // 32] E8M0 |
| 1738 | out_ptr, # [T, G * R] |
| 1739 | num_tokens, |
| 1740 | stride_at, |
| 1741 | stride_ast, |
| 1742 | stride_wn, |
| 1743 | stride_wsn, |
| 1744 | stride_out, |
| 1745 | R: tl.constexpr, |
| 1746 | K: tl.constexpr, |
| 1747 | SCALE_ROWS: tl.constexpr, |
| 1748 | BLOCK_M: tl.constexpr, |
| 1749 | BLOCK_N: tl.constexpr, |
| 1750 | BLOCK_K: tl.constexpr, |
| 1751 | ): |
| 1752 | # Output columns are group-major, so a BLOCK_N tile never straddles two |
| 1753 | # groups (R % BLOCK_N == 0) and picks its group's K slice of A. |
| 1754 | pid_n = tl.program_id(0) |
| 1755 | pid_m = tl.program_id(1) |
| 1756 | group = (pid_n * BLOCK_N) // R |
| 1757 | offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) |
| 1758 | offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) |
| 1759 | m_mask = offs_m < num_tokens |
| 1760 | offs_k = tl.arange(0, BLOCK_K) |
| 1761 | offs_sk = tl.arange(0, BLOCK_K // 32) |
| 1762 | |
| 1763 | a_ptrs = a_ptr + offs_m[:, None] * stride_at + (group * K + offs_k)[None, :] |
| 1764 | as_ptrs = ( |
| 1765 | as_ptr + offs_m[:, None] * stride_ast + (group * (K // 32) + offs_sk)[None, :] |
| 1766 | ) |
| 1767 | w_ptrs = w_ptr + offs_n[:, None] * stride_wn + offs_k[None, :] |
| 1768 | ws_ptrs = ws_ptr + (offs_n // SCALE_ROWS)[:, None] * stride_wsn + offs_sk[None, :] |
| 1769 | |
| 1770 | acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) |
| 1771 | for _ in range(K // BLOCK_K): |
| 1772 | a = tl.load(a_ptrs, mask=m_mask[:, None], other=0.0) |
| 1773 | a_s = tl.load(as_ptrs, mask=m_mask[:, None], other=127) |
| 1774 | w = tl.load(w_ptrs) |
| 1775 | w_s = tl.load(ws_ptrs) |
| 1776 | acc += tl.dot_scaled(a, a_s, "e4m3", w.T, w_s, "e4m3") |
| 1777 | a_ptrs += BLOCK_K |
| 1778 | as_ptrs += BLOCK_K // 32 |
| 1779 | w_ptrs += BLOCK_K |
| 1780 | ws_ptrs += BLOCK_K // 32 |
| 1781 | |
| 1782 | tl.store( |
| 1783 | out_ptr + offs_m[:, None] * stride_out + offs_n[None, :], |
| 1784 | acc.to(out_ptr.dtype.element_ty), |
| 1785 | mask=m_mask[:, None], |
| 1786 | ) |
| 1787 | |
| 1788 | |
| 1789 | def _mxfp8_wo_a_bmm_config(num_tokens: int, n_groups: int) -> tuple[int, ...]: |
| 1790 | """(BLOCK_M, BLOCK_N, BLOCK_K, num_warps, num_stages) for gfx950. |
| 1791 | |
| 1792 | Tuned under HIP graphs with a cold weight at G = 4 and 2, over every |
| 1793 | decode shape of conc 1-128 x 0-5 spec tokens plus prefill chunks up to |
| 1794 | 8K tokens. The best tile tracks the total work T * G, so the tiers are |
| 1795 | keyed on it. |
| 1796 | |
| 1797 | This will be replaced after new GEMM kernel from AITER with proper 32x32 scale |
| 1798 | shape GEMM fp8 enabled. |
| 1799 | """ |
| 1800 | work = num_tokens * n_groups |
| 1801 | if work <= 64: |
| 1802 | return 16, 16, 1024, 2, 3 |
| 1803 | if work <= 128: |
| 1804 | return 32, 16, 1024, 2, 3 |
| 1805 | if work <= 256: |
| 1806 | return 32, 32, 512, 2, 3 |
| 1807 | if work <= 512: |
| 1808 | return 64, 32, 512, 2, 3 |
| 1809 | if work <= 1024: |
| 1810 | return 64, 64, 512, 4, 2 |
| 1811 | if work <= 2048: |
| 1812 | return 64, 64, 256, 4, 2 |
| 1813 | if work <= 3072: |
| 1814 | return 64, 64, 256, 2, 1 |
| 1815 | if work <= 4096: |
| 1816 | return 128, 128, 256, 8, 2 |
| 1817 | return 128, 128, 128, 4, 2 |
| 1818 | |
| 1819 | |
| 1820 | def _rocm_mxfp8_wo_a_bmm_impl( |
| 1821 | a: torch.Tensor, |
| 1822 | a_scale: torch.Tensor, |
| 1823 | weight: torch.Tensor, |
| 1824 | weight_scale: torch.Tensor, |
| 1825 | n_groups: int, |
| 1826 | o_lora_rank: int, |
| 1827 | ) -> torch.Tensor: |
| 1828 | num_tokens = a.shape[0] |
| 1829 | group_dim = a.shape[1] // n_groups |
| 1830 | out = torch.empty( |
| 1831 | (num_tokens, n_groups * o_lora_rank), dtype=torch.bfloat16, device=a.device |
| 1832 | ) |
| 1833 | if num_tokens == 0: |
| 1834 | return out |
| 1835 | block_m, block_n, block_k, num_warps, num_stages = _mxfp8_wo_a_bmm_config( |
| 1836 | num_tokens, n_groups |
| 1837 | ) |
| 1838 | grid = (n_groups * o_lora_rank // block_n, triton.cdiv(num_tokens, block_m)) |
| 1839 | _mxfp8_wo_a_bmm_kernel[grid]( |
| 1840 | a, |
| 1841 | a_scale, |
| 1842 | weight, |
| 1843 | weight_scale, |
| 1844 | out, |
| 1845 | num_tokens, |
| 1846 | a.stride(0), |
| 1847 | a_scale.stride(0), |
| 1848 | weight.stride(0), |
| 1849 | weight_scale.stride(0), |
| 1850 | out.stride(0), |
| 1851 | R=o_lora_rank, |
| 1852 | K=group_dim, |
| 1853 | SCALE_ROWS=weight.shape[0] // weight_scale.shape[0], |
| 1854 | BLOCK_M=block_m, |
| 1855 | BLOCK_N=block_n, |
| 1856 | BLOCK_K=block_k, |
| 1857 | num_warps=num_warps, |
| 1858 | num_stages=num_stages, |
| 1859 | ) |
| 1860 | return out |
| 1861 | |
| 1862 | |
| 1863 | def _rocm_mxfp8_wo_a_bmm_fake( |
| 1864 | a: torch.Tensor, |
| 1865 | a_scale: torch.Tensor, |
| 1866 | weight: torch.Tensor, |
| 1867 | weight_scale: torch.Tensor, |
| 1868 | n_groups: int, |
| 1869 | o_lora_rank: int, |
| 1870 | ) -> torch.Tensor: |
| 1871 | return a.new_empty((a.shape[0], n_groups * o_lora_rank), dtype=torch.bfloat16) |
| 1872 | |
| 1873 | |
| 1874 | # An opaque op: the tile choice branches on the token count, which the |
| 1875 | # compiled o_proj must not freeze at its trace-time value. |
| 1876 | direct_register_custom_op( |
| 1877 | op_name="rocm_dsv41_mxfp8_wo_a_bmm", |
| 1878 | op_func=_rocm_mxfp8_wo_a_bmm_impl, |
| 1879 | fake_impl=_rocm_mxfp8_wo_a_bmm_fake, |
| 1880 | ) |
| 1881 | |
| 1882 | |
| 1883 | def rocm_mxfp8_wo_a_bmm( |
| 1884 | a: torch.Tensor, |
| 1885 | a_scale: torch.Tensor, |
| 1886 | wo_a: torch.nn.Module, |
| 1887 | n_groups: int, |
| 1888 | o_lora_rank: int, |
| 1889 | ) -> torch.Tensor: |
| 1890 | """Grouped MXFP8 wo_a: ``out[t, g, :] = a[t, g, :] @ W[g].T``, bf16 out. |
| 1891 | |
| 1892 | ``a`` is the [T, G * K] e4m3 attention output and ``a_scale`` its |
| 1893 | [T, G * K // 32] E8M0 scales, as the sparse decode reduce writes them. |
| 1894 | The weight is the checkpoint's MXFP8 ``wo_a`` as loaded, [G * R, K] with |
| 1895 | either [G * R // 32, K // 32] block scales or [G * R, K // 32] per-row |
| 1896 | scales, so there is no dequantized copy to keep. |
| 1897 | Returns [T, G * R]. |
| 1898 | """ |
| 1899 | return torch.ops.vllm.rocm_dsv41_mxfp8_wo_a_bmm( |
| 1900 | a, a_scale, wo_a.weight, wo_a.weight_scale, n_groups, o_lora_rank |
| 1901 | ) |
| 1902 | |
| 1903 | |
| 1904 | _DSV4_SPARSE_NOPE_DIM = 448 |
| 1905 | _DSV4_SPARSE_ROPE_DIM = 64 |
| 1906 | |
| 1907 | |
| 1908 | def _validate_sparse_dims( |
| 1909 | head_dim: int, |
| 1910 | nope_head_dim: int, |
| 1911 | rope_head_dim: int, |
| 1912 | op_name: str, |
| 1913 | ) -> None: |
| 1914 | assert head_dim > 0, f"{op_name} expected a positive head_dim, got {head_dim}" |
| 1915 | assert nope_head_dim > 0, ( |
| 1916 | f"{op_name} expected a positive NoPE dimension, got {nope_head_dim}" |
| 1917 | ) |
| 1918 | assert rope_head_dim >= 0, ( |
| 1919 | f"{op_name} expected a non-negative RoPE dimension, got {rope_head_dim}" |
| 1920 | ) |
| 1921 | assert head_dim == nope_head_dim + rope_head_dim, ( |
| 1922 | f"{op_name} expected head_dim={nope_head_dim + rope_head_dim}, got {head_dim}" |
| 1923 | ) |
| 1924 | |
| 1925 | |
| 1926 | def _validate_dsv4_sparse_dims( |
| 1927 | head_dim: int, |
| 1928 | nope_head_dim: int, |
| 1929 | rope_head_dim: int, |
| 1930 | op_name: str, |
| 1931 | ) -> None: |
| 1932 | _validate_sparse_dims(head_dim, nope_head_dim, rope_head_dim, op_name) |
| 1933 | assert ( |
| 1934 | nope_head_dim == _DSV4_SPARSE_NOPE_DIM |
| 1935 | and rope_head_dim == _DSV4_SPARSE_ROPE_DIM |
| 1936 | ), ( |
| 1937 | f"{op_name} expects {_DSV4_SPARSE_NOPE_DIM} NoPE dims and " |
| 1938 | f"{_DSV4_SPARSE_ROPE_DIM} RoPE dims" |
| 1939 | ) |
| 1940 | |
| 1941 | |
| 1942 | @triton.jit |
| 1943 | def _count_valid_row_entries_kernel( |
| 1944 | indices_ptr, |
| 1945 | lengths_ptr, |
| 1946 | counts_ptr, |
| 1947 | indptr_ptr, |
| 1948 | indices_stride0, |
| 1949 | num_rows_limit, |
| 1950 | num_queries, |
| 1951 | row_width, |
| 1952 | BLOCK_R: tl.constexpr, |
| 1953 | BLOCK_W: tl.constexpr, |
| 1954 | ): |
| 1955 | pid = tl.program_id(0) |
| 1956 | # The scan fills indptr[1:], so write indptr[0] here and skip a zero fill. |
| 1957 | if pid == 0: |
| 1958 | tl.store(indptr_ptr, 0) |
| 1959 | |
| 1960 | rows = pid * BLOCK_R + tl.arange(0, BLOCK_R) |
| 1961 | row_ok = rows < num_queries |
| 1962 | row_len = tl.load(lengths_ptr + rows, mask=row_ok, other=0) |
| 1963 | |
| 1964 | acc = tl.zeros((BLOCK_R,), dtype=tl.int32) |
| 1965 | for start in range(0, row_width, BLOCK_W): |
| 1966 | cols = start + tl.arange(0, BLOCK_W) |
| 1967 | in_row = cols[None, :] < row_width |
| 1968 | vals = tl.load( |
| 1969 | indices_ptr + rows[:, None] * indices_stride0 + cols[None, :], |
| 1970 | mask=row_ok[:, None] & in_row, |
| 1971 | other=-1, |
| 1972 | ).to(tl.int32) |
| 1973 | keep = ( |
| 1974 | row_ok[:, None] & in_row & (cols[None, :] < row_len[:, None]) & (vals >= 0) |
| 1975 | ) |
| 1976 | if num_rows_limit >= 0: |
| 1977 | keep = keep & (vals < num_rows_limit) |
| 1978 | acc += tl.sum(keep.to(tl.int32), axis=1) |
| 1979 | |
| 1980 | tl.store(counts_ptr + rows, acc, mask=row_ok) |
| 1981 | |
| 1982 | |
| 1983 | @triton.jit |
| 1984 | def _compact_dense_row_to_ragged_kernel( |
| 1985 | indices_ptr, |
| 1986 | lengths_ptr, |
| 1987 | indptr_ptr, |
| 1988 | out_ptr, |
| 1989 | indices_stride0, |
| 1990 | num_rows_limit, |
| 1991 | row_width, |
| 1992 | BLOCK_W: tl.constexpr, |
| 1993 | ): |
| 1994 | row_idx = tl.program_id(0) |
| 1995 | row_len = tl.load(lengths_ptr + row_idx) |
| 1996 | out_start = tl.load(indptr_ptr + row_idx) |
| 1997 | |
| 1998 | written = 0 |
| 1999 | for start in range(0, row_width, BLOCK_W): |
| 2000 | cols = start + tl.arange(0, BLOCK_W) |
| 2001 | in_row = cols < row_width |
| 2002 | vals = tl.load( |
| 2003 | indices_ptr + row_idx * indices_stride0 + cols, mask=in_row, other=-1 |
| 2004 | ).to(tl.int32) |
| 2005 | keep = in_row & (cols < row_len) & (vals >= 0) |
| 2006 | if num_rows_limit >= 0: |
| 2007 | keep = keep & (vals < num_rows_limit) |
| 2008 | rank = tl.cumsum(keep.to(tl.int32), axis=0) - 1 |
| 2009 | tl.store(out_ptr + out_start + written + rank, vals, mask=keep) |
| 2010 | written += tl.sum(keep.to(tl.int32), axis=0) |
| 2011 | |
| 2012 | |
| 2013 | def build_ragged_indices_from_dense( |
| 2014 | indices: torch.Tensor, |
| 2015 | lengths: torch.Tensor, |
| 2016 | num_rows: int = -1, |
| 2017 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 2018 | indices = indices.reshape(indices.shape[0], -1) |
| 2019 | lengths = lengths.to(device=indices.device, dtype=torch.int32).reshape(-1) |
| 2020 | assert lengths.numel() == indices.shape[0], ( |
| 2021 | f"Expected one length per row, got {lengths.shape} for indices {indices.shape}" |
| 2022 | ) |
| 2023 | |
| 2024 | max_width = indices.shape[1] if indices.ndim == 2 else 0 |
| 2025 | # Both kernels bound the row by row_width and row_len, so no clamp needed. |
| 2026 | lengths = lengths.contiguous() |
| 2027 | |
| 2028 | # Dense rows reserve slots they may not fill, leaving -1 anywhere in the |
| 2029 | # first `lengths` entries. Keep only the valid ones, since the consumers |
| 2030 | # index the KV pool without checking the sign. |
| 2031 | num_queries = indices.shape[0] |
| 2032 | # max_width is whatever the caller packed (align(topk + window, 128) for the |
| 2033 | # V4.1 indexer) and nothing caps it, so loop the row rather than size the |
| 2034 | # block by it. |
| 2035 | block = min(triton.next_power_of_2(max_width), 1024) if max_width > 0 else 1 |
| 2036 | counts = torch.empty(num_queries, dtype=torch.int32, device=indices.device) |
| 2037 | indptr = torch.empty(num_queries + 1, dtype=torch.int32, device=indices.device) |
| 2038 | if num_queries > 0 and max_width > 0: |
| 2039 | # A program per row only gets row_width elements, which is launch |
| 2040 | # bound for narrow rows. Tile rows to fill the block. |
| 2041 | block_r = max(1, 1024 // block) |
| 2042 | _count_valid_row_entries_kernel[(triton.cdiv(num_queries, block_r),)]( |
| 2043 | indices, |
| 2044 | lengths, |
| 2045 | counts, |
| 2046 | indptr, |
| 2047 | indices.stride(0), |
| 2048 | int(num_rows), |
| 2049 | num_queries, |
| 2050 | max_width, |
| 2051 | BLOCK_R=block_r, |
| 2052 | BLOCK_W=block, |
| 2053 | ) |
| 2054 | else: |
| 2055 | counts.zero_() |
| 2056 | indptr.zero_() |
| 2057 | |
| 2058 | torch.cumsum(counts, dim=0, out=indptr[1:]) |
| 2059 | |
| 2060 | if indices.numel() == 0: |
| 2061 | flat = torch.empty(0, dtype=torch.int32, device=indices.device) |
| 2062 | else: |
| 2063 | flat = torch.empty( |
| 2064 | indices.shape[0] * max_width, |
| 2065 | dtype=torch.int32, |
| 2066 | device=indices.device, |
| 2067 | ) |
| 2068 | if flat.numel() > 0: |
| 2069 | _compact_dense_row_to_ragged_kernel[(indices.shape[0],)]( |
| 2070 | indices, |
| 2071 | lengths, |
| 2072 | indptr, |
| 2073 | flat, |
| 2074 | indices.stride(0), |
| 2075 | int(num_rows), |
| 2076 | max_width, |
| 2077 | BLOCK_W=block, |
| 2078 | ) |
| 2079 | |
| 2080 | return flat, indptr |
| 2081 | |
| 2082 | |
| 2083 | def _as_int32_contiguous_1d(x: torch.Tensor) -> torch.Tensor: |
| 2084 | if x.dtype == torch.int32 and x.ndim == 1 and x.is_contiguous(): |
| 2085 | return x |
| 2086 | return x.to(torch.int32).contiguous() |
| 2087 | |
| 2088 | |
| 2089 | @triton.jit |
| 2090 | def _sparse_kv_row_offset(slot, stride): |
| 2091 | # A global token slot fits in int32, but its byte/element offset may not. |
| 2092 | return slot.to(tl.int64) * stride |
| 2093 | |
| 2094 | |
| 2095 | @triton.jit |
| 2096 | def _sparse_attn_prefill_ragged_kernel( |
| 2097 | q_ptr, |
| 2098 | kv_ptr, |
| 2099 | kv_indices_ptr, |
| 2100 | kv_indptr_ptr, |
| 2101 | attn_sink_ptr, |
| 2102 | out_ptr, |
| 2103 | q_stride_t, |
| 2104 | q_stride_h, |
| 2105 | q_stride_d, |
| 2106 | kv_stride_n, |
| 2107 | kv_stride_d, |
| 2108 | out_stride_t, |
| 2109 | out_stride_h, |
| 2110 | out_stride_d, |
| 2111 | num_heads, |
| 2112 | head_dim, |
| 2113 | num_kv, |
| 2114 | scale, |
| 2115 | HAS_ATTN_SINK: tl.constexpr, |
| 2116 | OUT_DV: tl.constexpr, |
| 2117 | BLOCK_H: tl.constexpr, |
| 2118 | BLOCK_D: tl.constexpr, |
| 2119 | BLOCK_K: tl.constexpr, |
| 2120 | ): |
| 2121 | query_idx = tl.program_id(0) |
| 2122 | pid_h = tl.program_id(1) |
| 2123 | |
| 2124 | head_offsets = pid_h * BLOCK_H + tl.arange(0, BLOCK_H) |
| 2125 | dim_offsets = tl.arange(0, BLOCK_D) |
| 2126 | head_mask = head_offsets < num_heads |
| 2127 | dim_mask = dim_offsets < head_dim |
| 2128 | |
| 2129 | q = tl.load( |
| 2130 | q_ptr |
| 2131 | + query_idx * q_stride_t |
| 2132 | + head_offsets[:, None] * q_stride_h |
| 2133 | + dim_offsets[None, :] * q_stride_d, |
| 2134 | mask=head_mask[:, None] & dim_mask[None, :], |
| 2135 | other=0.0, |
| 2136 | ) |
| 2137 | |
| 2138 | neg_large = -3.4028234663852886e38 |
| 2139 | m_i = tl.full((BLOCK_H,), neg_large, dtype=tl.float32) |
| 2140 | l_i = tl.zeros((BLOCK_H,), dtype=tl.float32) |
| 2141 | acc = tl.zeros((BLOCK_H, BLOCK_D), dtype=tl.float32) |
| 2142 | |
| 2143 | kv_start = tl.load(kv_indptr_ptr + query_idx) |
| 2144 | kv_end = tl.load(kv_indptr_ptr + query_idx + 1) |
| 2145 | kv_len = kv_end - kv_start |
| 2146 | |
| 2147 | k_offsets = tl.arange(0, BLOCK_K) |
| 2148 | slot = tl.load( |
| 2149 | kv_indices_ptr + kv_start + k_offsets, mask=k_offsets < kv_len, other=-1 |
| 2150 | ) |
| 2151 | for k_start in tl.range(0, kv_len, BLOCK_K): |
| 2152 | k_pos = k_start + k_offsets |
| 2153 | in_range = k_pos < kv_len |
| 2154 | valid = in_range & (slot >= 0) & (slot < num_kv) |
| 2155 | safe_slot = tl.where(valid, slot, 0) |
| 2156 | |
| 2157 | kv = tl.load( |
| 2158 | kv_ptr |
| 2159 | + _sparse_kv_row_offset(safe_slot[:, None], kv_stride_n) |
| 2160 | + dim_offsets[None, :] * kv_stride_d, |
| 2161 | mask=valid[:, None] & dim_mask[None, :], |
| 2162 | other=0.0, |
| 2163 | ) |
| 2164 | |
| 2165 | next_k_pos = k_start + BLOCK_K + k_offsets |
| 2166 | slot = tl.load( |
| 2167 | kv_indices_ptr + kv_start + next_k_pos, mask=next_k_pos < kv_len, other=-1 |
| 2168 | ) |
| 2169 | |
| 2170 | scores = tl.dot(q, tl.trans(kv)) * scale |
| 2171 | scores = tl.where(head_mask[:, None] & valid[None, :], scores, neg_large) |
| 2172 | |
| 2173 | m_block = tl.max(scores, axis=1) |
| 2174 | m_new = tl.maximum(m_i, m_block) |
| 2175 | alpha = tl.exp(m_i - m_new) |
| 2176 | p = tl.exp(scores - m_new[:, None]) |
| 2177 | p = tl.where(head_mask[:, None] & valid[None, :], p, 0.0) |
| 2178 | l_new = l_i * alpha + tl.sum(p, axis=1) |
| 2179 | |
| 2180 | acc = acc * alpha[:, None] + tl.dot(p.to(kv.dtype), kv) |
| 2181 | m_i = m_new |
| 2182 | l_i = l_new |
| 2183 | |
| 2184 | if HAS_ATTN_SINK: |
| 2185 | sink = tl.load( |
| 2186 | attn_sink_ptr + head_offsets, mask=head_mask, other=neg_large |
| 2187 | ).to(tl.float32) |
| 2188 | m_final = tl.maximum(m_i, sink) |
| 2189 | alpha = tl.exp(m_i - m_final) |
| 2190 | l_final = l_i * alpha + tl.exp(sink - m_final) |
| 2191 | denom = tl.maximum(l_final, 1.0e-30) |
| 2192 | out = tl.where( |
| 2193 | l_final[:, None] > 0.0, |
| 2194 | (acc * alpha[:, None]) / denom[:, None], |
| 2195 | 0.0, |
| 2196 | ) |
| 2197 | else: |
| 2198 | denom = tl.maximum(l_i, 1.0e-30) |
| 2199 | out = tl.where(l_i[:, None] > 0.0, acc / denom[:, None], 0.0) |
| 2200 | |
| 2201 | tl.store( |
| 2202 | out_ptr |
| 2203 | + query_idx * out_stride_t |
| 2204 | + head_offsets[:, None] * out_stride_h |
| 2205 | + dim_offsets[None, :] * out_stride_d, |
| 2206 | out, |
| 2207 | mask=head_mask[:, None] & (dim_offsets[None, :] < OUT_DV), |
| 2208 | ) |
| 2209 | |
| 2210 | |
| 2211 | @triton.jit |
| 2212 | def _decode_e8m0_scales_triton(encoded_scales): |
| 2213 | scale_bits = encoded_scales.to(tl.int32) << 23 |
| 2214 | scale_bits = tl.where(encoded_scales == 0, 1 << 22, scale_bits) |
| 2215 | return scale_bits.to(tl.float32, bitcast=True) |
| 2216 | |
| 2217 | |
| 2218 | @triton.jit |
| 2219 | def _load_fp8_ds_mla_gfx950_nope_exact_chunk( |
| 2220 | token_data_ptr, |
| 2221 | token_scale_ptr, |
| 2222 | valid, |
| 2223 | CHUNK_START: tl.constexpr, |
| 2224 | CHUNK_SIZE: tl.constexpr, |
| 2225 | BLOCK_K: tl.constexpr, |
| 2226 | IS_FNUZ: tl.constexpr, |
| 2227 | ): |
| 2228 | offsets = CHUNK_START + tl.arange(0, CHUNK_SIZE) |
| 2229 | x_uint8 = tl.load( |
| 2230 | token_data_ptr[:, None] + offsets[None, :], |
| 2231 | mask=valid[:, None], |
| 2232 | other=0, |
| 2233 | ) |
| 2234 | scale_offsets = CHUNK_START // 64 + tl.arange(0, CHUNK_SIZE // 64) |
| 2235 | encoded_scales = tl.load( |
| 2236 | token_scale_ptr[:, None] + scale_offsets[None, :], |
| 2237 | mask=valid[:, None], |
| 2238 | other=127, |
| 2239 | ) |
| 2240 | scales = _decode_e8m0_scales_triton(encoded_scales) |
| 2241 | scales = tl.broadcast_to(scales[:, :, None], (BLOCK_K, CHUNK_SIZE // 64, 64)) |
| 2242 | scales = tl.reshape(scales, (BLOCK_K, CHUNK_SIZE)) |
| 2243 | if IS_FNUZ: |
| 2244 | x_f32 = x_uint8.to(tl.float8e4b8, bitcast=True).to(tl.bfloat16).to(tl.float32) |
| 2245 | else: |
| 2246 | x_f32 = x_uint8.to(tl.float8e4nv, bitcast=True).to(tl.float32) |
| 2247 | value = (x_f32 * scales).to(tl.bfloat16) |
| 2248 | zero = tl.zeros((BLOCK_K, CHUNK_SIZE), dtype=tl.bfloat16) |
| 2249 | return tl.where(valid[:, None], value, zero) |
| 2250 | |
| 2251 | |
| 2252 | @triton.jit |
| 2253 | def _load_fp8_ds_mla_gfx950_tail128( |
| 2254 | token_data_ptr, |
| 2255 | token_scale_ptr, |
| 2256 | valid, |
| 2257 | NOPE_DIM: tl.constexpr, |
| 2258 | BLOCK_K: tl.constexpr, |
| 2259 | IS_FNUZ: tl.constexpr, |
| 2260 | ): |
| 2261 | tail_offsets = 384 + tl.arange(0, 128) |
| 2262 | nope_mask = tail_offsets < NOPE_DIM |
| 2263 | x_uint8 = tl.load( |
| 2264 | token_data_ptr[:, None] + tail_offsets[None, :], |
| 2265 | mask=valid[:, None] & nope_mask[None, :], |
| 2266 | other=0, |
| 2267 | ) |
| 2268 | scale_offsets = 6 + tl.arange(0, 2) |
| 2269 | scale_mask = scale_offsets < NOPE_DIM // 64 |
| 2270 | encoded_scales = tl.load( |
| 2271 | token_scale_ptr[:, None] + scale_offsets[None, :], |
| 2272 | mask=valid[:, None] & scale_mask[None, :], |
| 2273 | other=127, |
| 2274 | ) |
| 2275 | scales = _decode_e8m0_scales_triton(encoded_scales) |
| 2276 | scales = tl.broadcast_to(scales[:, :, None], (BLOCK_K, 2, 64)) |
| 2277 | scales = tl.reshape(scales, (BLOCK_K, 128)) |
| 2278 | if IS_FNUZ: |
| 2279 | x_f32 = x_uint8.to(tl.float8e4b8, bitcast=True).to(tl.bfloat16).to(tl.float32) |
| 2280 | else: |
| 2281 | x_f32 = x_uint8.to(tl.float8e4nv, bitcast=True).to(tl.float32) |
| 2282 | nope = (x_f32 * scales).to(tl.bfloat16) |
| 2283 | |
| 2284 | rope_ptr = (token_data_ptr + NOPE_DIM).to(tl.pointer_type(tl.bfloat16)) |
| 2285 | rope = tl.load( |
| 2286 | rope_ptr[:, None] + (tail_offsets[None, :] - NOPE_DIM), |
| 2287 | mask=valid[:, None] & ~nope_mask[None, :], |
| 2288 | other=0.0, |
| 2289 | ) |
| 2290 | value = tl.where(nope_mask[None, :], nope, rope) |
| 2291 | zero = tl.zeros((BLOCK_K, 128), dtype=tl.bfloat16) |
| 2292 | return tl.where(valid[:, None], value, zero) |
| 2293 | |
| 2294 | |
| 2295 | @triton.jit |
| 2296 | def _sparse_attn_decode_ragged_kernel( |
| 2297 | q_ptr, |
| 2298 | main_cache_ptr, |
| 2299 | main_indices_ptr, |
| 2300 | main_indptr_ptr, |
| 2301 | extra_cache_ptr, |
| 2302 | extra_indices_ptr, |
| 2303 | extra_indptr_ptr, |
| 2304 | attn_sink_ptr, |
| 2305 | out_ptr, |
| 2306 | q_stride0, |
| 2307 | q_stride1, |
| 2308 | out_stride0, |
| 2309 | out_stride1, |
| 2310 | main_cache_stride0, |
| 2311 | extra_cache_stride0, |
| 2312 | main_num_rows, |
| 2313 | extra_num_rows, |
| 2314 | main_block_size, |
| 2315 | extra_block_size, |
| 2316 | scale, |
| 2317 | num_heads, |
| 2318 | HAS_ATTN_SINK: tl.constexpr, |
| 2319 | HAS_EXTRA: tl.constexpr, |
| 2320 | NOPE_DIM: tl.constexpr, |
| 2321 | NOPE_BLOCK: tl.constexpr, |
| 2322 | ROPE_DIM: tl.constexpr, |
| 2323 | # SWA K-cache (main): C++ encoder writes FNUZ on gfx942, OCP on gfx950. |
| 2324 | # Compressed K-cache (extra): Triton encoder writes OCP everywhere. |
| 2325 | IS_FNUZ_MAIN: tl.constexpr, |
| 2326 | IS_FNUZ_EXTRA: tl.constexpr, |
| 2327 | BLOCK_H: tl.constexpr, |
| 2328 | BLOCK_K: tl.constexpr, |
| 2329 | ): |
| 2330 | query_idx = tl.program_id(0) |
| 2331 | pid_h = tl.program_id(1) |
| 2332 | |
| 2333 | head_offsets = pid_h * BLOCK_H + tl.arange(0, BLOCK_H) |
| 2334 | head_mask = head_offsets < num_heads |
| 2335 | nope_offsets = tl.arange(0, NOPE_BLOCK) |
| 2336 | nope_mask = nope_offsets < NOPE_DIM |
| 2337 | rope_offsets = tl.arange(0, ROPE_DIM) |
| 2338 | |
| 2339 | q_row_ptr = q_ptr + query_idx * q_stride0 + head_offsets[:, None] * q_stride1 |
| 2340 | q_nope = tl.load( |
| 2341 | q_row_ptr + nope_offsets[None, :], |
| 2342 | mask=head_mask[:, None] & nope_mask[None, :], |
| 2343 | other=0.0, |
| 2344 | ) |
| 2345 | q_rope = tl.load( |
| 2346 | q_row_ptr + NOPE_DIM + rope_offsets[None, :], |
| 2347 | mask=head_mask[:, None], |
| 2348 | other=0.0, |
| 2349 | ) |
| 2350 | |
| 2351 | neg_large = -3.4028234663852886e38 |
| 2352 | m_i = tl.full((BLOCK_H,), neg_large, dtype=tl.float32) |
| 2353 | l_i = tl.zeros((BLOCK_H,), dtype=tl.float32) |
| 2354 | acc_nope = tl.zeros((BLOCK_H, NOPE_BLOCK), dtype=tl.float32) |
| 2355 | acc_rope = tl.zeros((BLOCK_H, ROPE_DIM), dtype=tl.float32) |
| 2356 | k_offsets = tl.arange(0, BLOCK_K) |
| 2357 | |
| 2358 | main_start = tl.load(main_indptr_ptr + query_idx) |
| 2359 | main_end = tl.load(main_indptr_ptr + query_idx + 1) |
| 2360 | main_len = main_end - main_start |
| 2361 | |
| 2362 | zero_nope = tl.zeros((BLOCK_K, NOPE_BLOCK), dtype=tl.bfloat16) |
| 2363 | zero_rope = tl.zeros((BLOCK_K, ROPE_DIM), dtype=tl.bfloat16) |
| 2364 | |
| 2365 | for k_start in tl.range(0, main_len, BLOCK_K): |
| 2366 | k_pos = k_start + k_offsets |
| 2367 | in_range = k_pos < main_len |
| 2368 | slot = tl.load(main_indices_ptr + main_start + k_pos, mask=in_range, other=-1) |
| 2369 | valid = in_range & (slot >= 0) & (slot < main_num_rows) |
| 2370 | safe_slot = tl.where(valid, slot, 0) |
| 2371 | |
| 2372 | block_idx = safe_slot // main_block_size |
| 2373 | pos_in_block = safe_slot % main_block_size |
| 2374 | cache_block_ptr = main_cache_ptr + block_idx.to(tl.int64) * main_cache_stride0 |
| 2375 | token_data_ptr = cache_block_ptr + pos_in_block * 576 |
| 2376 | token_scale_ptr = cache_block_ptr + main_block_size * 576 + pos_in_block * 8 |
| 2377 | |
| 2378 | x_uint8 = tl.load( |
| 2379 | token_data_ptr[:, None] + nope_offsets[None, :], |
| 2380 | mask=valid[:, None] & nope_mask[None, :], |
| 2381 | other=0, |
| 2382 | ) |
| 2383 | if IS_FNUZ_MAIN: |
| 2384 | x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True) |
| 2385 | else: |
| 2386 | x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True) |
| 2387 | encoded_scales = tl.load( |
| 2388 | token_scale_ptr[:, None] + nope_offsets[None, :] // 64, |
| 2389 | mask=valid[:, None] & nope_mask[None, :], |
| 2390 | other=127, |
| 2391 | ) |
| 2392 | scales = tl.exp2(encoded_scales.to(tl.float32) - 127.0) |
| 2393 | k_nope = x_fp8.to(tl.bfloat16) * scales.to(tl.bfloat16) |
| 2394 | k_nope = tl.where(valid[:, None] & nope_mask[None, :], k_nope, zero_nope) |
| 2395 | k_nope = tl.where(k_nope == k_nope, k_nope, zero_nope) |
| 2396 | |
| 2397 | rope_ptr = (token_data_ptr + NOPE_DIM).to(tl.pointer_type(tl.bfloat16)) |
| 2398 | k_rope = tl.load( |
| 2399 | rope_ptr[:, None] + rope_offsets[None, :], |
| 2400 | mask=valid[:, None], |
| 2401 | other=0.0, |
| 2402 | ) |
| 2403 | k_rope = tl.where(valid[:, None], k_rope, zero_rope) |
| 2404 | k_rope = tl.where(k_rope == k_rope, k_rope, zero_rope) |
| 2405 | |
| 2406 | scores = tl.dot(q_nope, tl.trans(k_nope)) + tl.dot(q_rope, tl.trans(k_rope)) |
| 2407 | scores *= scale |
| 2408 | scores = tl.where(head_mask[:, None] & valid[None, :], scores, neg_large) |
| 2409 | |
| 2410 | m_block = tl.max(scores, axis=1) |
| 2411 | m_new = tl.maximum(m_i, m_block) |
| 2412 | alpha = tl.exp(m_i - m_new) |
| 2413 | p = tl.exp(scores - m_new[:, None]) |
| 2414 | p = tl.where(head_mask[:, None] & valid[None, :], p, 0.0) |
| 2415 | l_new = l_i * alpha + tl.sum(p, axis=1) |
| 2416 | |
| 2417 | acc_nope = acc_nope * alpha[:, None] + tl.dot(p.to(k_nope.dtype), k_nope) |
| 2418 | acc_rope = acc_rope * alpha[:, None] + tl.dot(p.to(k_rope.dtype), k_rope) |
| 2419 | m_i = m_new |
| 2420 | l_i = l_new |
| 2421 | |
| 2422 | if HAS_EXTRA: |
| 2423 | extra_start = tl.load(extra_indptr_ptr + query_idx) |
| 2424 | extra_end = tl.load(extra_indptr_ptr + query_idx + 1) |
| 2425 | extra_len = extra_end - extra_start |
| 2426 | |
| 2427 | for k_start in tl.range(0, extra_len, BLOCK_K): |
| 2428 | k_pos = k_start + k_offsets |
| 2429 | in_range = k_pos < extra_len |
| 2430 | slot = tl.load( |
| 2431 | extra_indices_ptr + extra_start + k_pos, mask=in_range, other=-1 |
| 2432 | ) |
| 2433 | valid = in_range & (slot >= 0) & (slot < extra_num_rows) |
| 2434 | safe_slot = tl.where(valid, slot, 0) |
| 2435 | |
| 2436 | block_idx = safe_slot // extra_block_size |
| 2437 | pos_in_block = safe_slot % extra_block_size |
| 2438 | cache_block_ptr = ( |
| 2439 | extra_cache_ptr + block_idx.to(tl.int64) * extra_cache_stride0 |
| 2440 | ) |
| 2441 | token_data_ptr = cache_block_ptr + pos_in_block * 576 |
| 2442 | token_scale_ptr = ( |
| 2443 | cache_block_ptr + extra_block_size * 576 + pos_in_block * 8 |
| 2444 | ) |
| 2445 | |
| 2446 | x_uint8 = tl.load( |
| 2447 | token_data_ptr[:, None] + nope_offsets[None, :], |
| 2448 | mask=valid[:, None] & nope_mask[None, :], |
| 2449 | other=0, |
| 2450 | ) |
| 2451 | if IS_FNUZ_EXTRA: |
| 2452 | x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True) |
| 2453 | else: |
| 2454 | x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True) |
| 2455 | encoded_scales = tl.load( |
| 2456 | token_scale_ptr[:, None] + nope_offsets[None, :] // 64, |
| 2457 | mask=valid[:, None] & nope_mask[None, :], |
| 2458 | other=127, |
| 2459 | ) |
| 2460 | scales = tl.exp2(encoded_scales.to(tl.float32) - 127.0) |
| 2461 | k_nope = x_fp8.to(tl.bfloat16) * scales.to(tl.bfloat16) |
| 2462 | k_nope = tl.where(valid[:, None] & nope_mask[None, :], k_nope, zero_nope) |
| 2463 | k_nope = tl.where(k_nope == k_nope, k_nope, zero_nope) |
| 2464 | |
| 2465 | rope_ptr = (token_data_ptr + NOPE_DIM).to(tl.pointer_type(tl.bfloat16)) |
| 2466 | k_rope = tl.load( |
| 2467 | rope_ptr[:, None] + rope_offsets[None, :], |
| 2468 | mask=valid[:, None], |
| 2469 | other=0.0, |
| 2470 | ) |
| 2471 | k_rope = tl.where(valid[:, None], k_rope, zero_rope) |
| 2472 | k_rope = tl.where(k_rope == k_rope, k_rope, zero_rope) |
| 2473 | |
| 2474 | scores = tl.dot(q_nope, tl.trans(k_nope)) + tl.dot( |
| 2475 | q_rope, |
| 2476 | tl.trans(k_rope), |
| 2477 | ) |
| 2478 | scores *= scale |
| 2479 | scores = tl.where(head_mask[:, None] & valid[None, :], scores, neg_large) |
| 2480 | |
| 2481 | m_block = tl.max(scores, axis=1) |
| 2482 | m_new = tl.maximum(m_i, m_block) |
| 2483 | alpha = tl.exp(m_i - m_new) |
| 2484 | p = tl.exp(scores - m_new[:, None]) |
| 2485 | p = tl.where(head_mask[:, None] & valid[None, :], p, 0.0) |
| 2486 | l_new = l_i * alpha + tl.sum(p, axis=1) |
| 2487 | |
| 2488 | acc_nope = acc_nope * alpha[:, None] + tl.dot(p.to(k_nope.dtype), k_nope) |
| 2489 | acc_rope = acc_rope * alpha[:, None] + tl.dot(p.to(k_rope.dtype), k_rope) |
| 2490 | m_i = m_new |
| 2491 | l_i = l_new |
| 2492 | |
| 2493 | if HAS_ATTN_SINK: |
| 2494 | sink = tl.load( |
| 2495 | attn_sink_ptr + head_offsets, mask=head_mask, other=neg_large |
| 2496 | ).to(tl.float32) |
| 2497 | m_final = tl.maximum(m_i, sink) |
| 2498 | alpha = tl.exp(m_i - m_final) |
| 2499 | l_final = l_i * alpha + tl.exp(sink - m_final) |
| 2500 | denom = tl.maximum(l_final, 1.0e-30) |
| 2501 | out_nope = tl.where( |
| 2502 | l_final[:, None] > 0.0, |
| 2503 | (acc_nope * alpha[:, None]) / denom[:, None], |
| 2504 | 0.0, |
| 2505 | ) |
| 2506 | out_rope = tl.where( |
| 2507 | l_final[:, None] > 0.0, |
| 2508 | (acc_rope * alpha[:, None]) / denom[:, None], |
| 2509 | 0.0, |
| 2510 | ) |
| 2511 | else: |
| 2512 | denom = tl.maximum(l_i, 1.0e-30) |
| 2513 | out_nope = tl.where(l_i[:, None] > 0.0, acc_nope / denom[:, None], 0.0) |
| 2514 | out_rope = tl.where(l_i[:, None] > 0.0, acc_rope / denom[:, None], 0.0) |
| 2515 | |
| 2516 | out_row_ptr = ( |
| 2517 | out_ptr + query_idx * out_stride0 + head_offsets[:, None] * out_stride1 |
| 2518 | ) |
| 2519 | tl.store( |
| 2520 | out_row_ptr + nope_offsets[None, :], |
| 2521 | out_nope, |
| 2522 | mask=head_mask[:, None] & nope_mask[None, :], |
| 2523 | ) |
| 2524 | tl.store( |
| 2525 | out_row_ptr + NOPE_DIM + rope_offsets[None, :], |
| 2526 | out_rope, |
| 2527 | mask=head_mask[:, None], |
| 2528 | ) |
| 2529 | |
| 2530 | |
| 2531 | @triton.jit |
| 2532 | def _sparse_attn_decode_partial_kernel( |
| 2533 | q_ptr, |
| 2534 | main_cache_ptr, |
| 2535 | main_indices_ptr, |
| 2536 | main_indptr_ptr, |
| 2537 | extra_cache_ptr, |
| 2538 | extra_indices_ptr, |
| 2539 | extra_indptr_ptr, |
| 2540 | part_m_ptr, |
| 2541 | part_l_ptr, |
| 2542 | part_acc_ptr, |
| 2543 | q_stride0, |
| 2544 | q_stride1, |
| 2545 | main_cache_stride0, |
| 2546 | extra_cache_stride0, |
| 2547 | pm_stride0, |
| 2548 | pm_stride_s, |
| 2549 | pa_stride0, |
| 2550 | pa_stride_s, |
| 2551 | pa_stride_h, |
| 2552 | main_num_rows, |
| 2553 | extra_num_rows, |
| 2554 | main_block_size, |
| 2555 | extra_block_size, |
| 2556 | scale, |
| 2557 | num_heads, |
| 2558 | HAS_EXTRA: tl.constexpr, |
| 2559 | NOPE_DIM: tl.constexpr, |
| 2560 | NOPE_BLOCK: tl.constexpr, |
| 2561 | ROPE_DIM: tl.constexpr, |
| 2562 | # `main_cache` is the SWA K-cache (written by the C++ encoder, FNUZ on |
| 2563 | # gfx942 / OCP on gfx950). `extra_cache` is the compressed K-cache |
| 2564 | # (Triton encoder, OCP on every platform). Reading both with the same |
| 2565 | # `IS_FNUZ` would decode one of them with the wrong FNUZ/OCP scale ratio. |
| 2566 | IS_FNUZ_MAIN: tl.constexpr, |
| 2567 | IS_FNUZ_EXTRA: tl.constexpr, |
| 2568 | BLOCK_H: tl.constexpr, |
| 2569 | BLOCK_K: tl.constexpr, |
| 2570 | NUM_SPLITS: tl.constexpr, |
| 2571 | NUM_STAGES: tl.constexpr, |
| 2572 | ): |
| 2573 | query_idx = tl.program_id(0) |
| 2574 | split_id = tl.program_id(1) |
| 2575 | pid_h = tl.program_id(2) |
| 2576 | |
| 2577 | head_offsets = pid_h * BLOCK_H + tl.arange(0, BLOCK_H) |
| 2578 | head_mask = head_offsets < num_heads |
| 2579 | nope_offsets = tl.arange(0, NOPE_BLOCK) |
| 2580 | nope_mask = nope_offsets < NOPE_DIM |
| 2581 | rope_offsets = tl.arange(0, ROPE_DIM) |
| 2582 | |
| 2583 | q_row_ptr = q_ptr + query_idx * q_stride0 + head_offsets[:, None] * q_stride1 |
| 2584 | q_nope = tl.load( |
| 2585 | q_row_ptr + nope_offsets[None, :], |
| 2586 | mask=head_mask[:, None] & nope_mask[None, :], |
| 2587 | other=0.0, |
| 2588 | ) |
| 2589 | q_rope = tl.load( |
| 2590 | q_row_ptr + NOPE_DIM + rope_offsets[None, :], |
| 2591 | mask=head_mask[:, None], |
| 2592 | other=0.0, |
| 2593 | ) |
| 2594 | |
| 2595 | neg_large = -3.4028234663852886e38 |
| 2596 | m_i = tl.full((BLOCK_H,), neg_large, dtype=tl.float32) |
| 2597 | l_i = tl.zeros((BLOCK_H,), dtype=tl.float32) |
| 2598 | acc_nope = tl.zeros((BLOCK_H, NOPE_BLOCK), dtype=tl.float32) |
| 2599 | acc_rope = tl.zeros((BLOCK_H, ROPE_DIM), dtype=tl.float32) |
| 2600 | k_offsets = tl.arange(0, BLOCK_K) |
| 2601 | |
| 2602 | zero_nope = tl.zeros((BLOCK_K, NOPE_BLOCK), dtype=tl.bfloat16) |
| 2603 | zero_rope = tl.zeros((BLOCK_K, ROPE_DIM), dtype=tl.bfloat16) |
| 2604 | |
| 2605 | # Each split processes a contiguous slice of this query's main (SWA) and |
| 2606 | # extra (topk) segments. Slices are handled independently so a block never |
| 2607 | # straddles the main/extra boundary. |
| 2608 | main_start = tl.load(main_indptr_ptr + query_idx) |
| 2609 | main_end = tl.load(main_indptr_ptr + query_idx + 1) |
| 2610 | main_len = main_end - main_start |
| 2611 | main_chunk = (main_len + NUM_SPLITS - 1) // NUM_SPLITS |
| 2612 | main_lo = split_id * main_chunk |
| 2613 | main_hi = tl.minimum(main_lo + main_chunk, main_len) |
| 2614 | |
| 2615 | for k_start in tl.range(main_lo, main_hi, BLOCK_K, num_stages=NUM_STAGES): |
| 2616 | k_pos = k_start + k_offsets |
| 2617 | in_range = k_pos < main_hi |
| 2618 | slot = tl.load(main_indices_ptr + main_start + k_pos, mask=in_range, other=-1) |
| 2619 | valid = in_range & (slot >= 0) & (slot < main_num_rows) |
| 2620 | safe_slot = tl.where(valid, slot, 0) |
| 2621 | |
| 2622 | block_idx = safe_slot // main_block_size |
| 2623 | pos_in_block = safe_slot % main_block_size |
| 2624 | cache_block_ptr = main_cache_ptr + block_idx.to(tl.int64) * main_cache_stride0 |
| 2625 | token_data_ptr = cache_block_ptr + pos_in_block * 576 |
| 2626 | token_scale_ptr = cache_block_ptr + main_block_size * 576 + pos_in_block * 8 |
| 2627 | |
| 2628 | x_uint8 = tl.load( |
| 2629 | token_data_ptr[:, None] + nope_offsets[None, :], |
| 2630 | mask=valid[:, None] & nope_mask[None, :], |
| 2631 | other=0, |
| 2632 | ) |
| 2633 | if IS_FNUZ_MAIN: |
| 2634 | x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True) |
| 2635 | else: |
| 2636 | x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True) |
| 2637 | encoded_scales = tl.load( |
| 2638 | token_scale_ptr[:, None] + nope_offsets[None, :] // 64, |
| 2639 | mask=valid[:, None] & nope_mask[None, :], |
| 2640 | other=127, |
| 2641 | ) |
| 2642 | scales = tl.exp2(encoded_scales.to(tl.float32) - 127.0) |
| 2643 | k_nope = x_fp8.to(tl.bfloat16) * scales.to(tl.bfloat16) |
| 2644 | k_nope = tl.where(valid[:, None] & nope_mask[None, :], k_nope, zero_nope) |
| 2645 | k_nope = tl.where(k_nope == k_nope, k_nope, zero_nope) |
| 2646 | |
| 2647 | rope_ptr = (token_data_ptr + NOPE_DIM).to(tl.pointer_type(tl.bfloat16)) |
| 2648 | k_rope = tl.load( |
| 2649 | rope_ptr[:, None] + rope_offsets[None, :], |
| 2650 | mask=valid[:, None], |
| 2651 | other=0.0, |
| 2652 | ) |
| 2653 | k_rope = tl.where(valid[:, None], k_rope, zero_rope) |
| 2654 | k_rope = tl.where(k_rope == k_rope, k_rope, zero_rope) |
| 2655 | |
| 2656 | scores = tl.dot(q_nope, tl.trans(k_nope)) + tl.dot(q_rope, tl.trans(k_rope)) |
| 2657 | scores *= scale |
| 2658 | scores = tl.where(head_mask[:, None] & valid[None, :], scores, neg_large) |
| 2659 | |
| 2660 | m_block = tl.max(scores, axis=1) |
| 2661 | m_new = tl.maximum(m_i, m_block) |
| 2662 | alpha = tl.exp(m_i - m_new) |
| 2663 | p = tl.exp(scores - m_new[:, None]) |
| 2664 | p = tl.where(head_mask[:, None] & valid[None, :], p, 0.0) |
| 2665 | l_new = l_i * alpha + tl.sum(p, axis=1) |
| 2666 | |
| 2667 | acc_nope = acc_nope * alpha[:, None] + tl.dot(p.to(k_nope.dtype), k_nope) |
| 2668 | acc_rope = acc_rope * alpha[:, None] + tl.dot(p.to(k_rope.dtype), k_rope) |
| 2669 | m_i = m_new |
| 2670 | l_i = l_new |
| 2671 | |
| 2672 | if HAS_EXTRA: |
| 2673 | extra_start = tl.load(extra_indptr_ptr + query_idx) |
| 2674 | extra_end = tl.load(extra_indptr_ptr + query_idx + 1) |
| 2675 | extra_len = extra_end - extra_start |
| 2676 | extra_chunk = (extra_len + NUM_SPLITS - 1) // NUM_SPLITS |
| 2677 | extra_lo = split_id * extra_chunk |
| 2678 | extra_hi = tl.minimum(extra_lo + extra_chunk, extra_len) |
| 2679 | |
| 2680 | for k_start in tl.range(extra_lo, extra_hi, BLOCK_K, num_stages=NUM_STAGES): |
| 2681 | k_pos = k_start + k_offsets |
| 2682 | in_range = k_pos < extra_hi |
| 2683 | slot = tl.load( |
| 2684 | extra_indices_ptr + extra_start + k_pos, mask=in_range, other=-1 |
| 2685 | ) |
| 2686 | valid = in_range & (slot >= 0) & (slot < extra_num_rows) |
| 2687 | safe_slot = tl.where(valid, slot, 0) |
| 2688 | |
| 2689 | block_idx = safe_slot // extra_block_size |
| 2690 | pos_in_block = safe_slot % extra_block_size |
| 2691 | cache_block_ptr = ( |
| 2692 | extra_cache_ptr + block_idx.to(tl.int64) * extra_cache_stride0 |
| 2693 | ) |
| 2694 | token_data_ptr = cache_block_ptr + pos_in_block * 576 |
| 2695 | token_scale_ptr = ( |
| 2696 | cache_block_ptr + extra_block_size * 576 + pos_in_block * 8 |
| 2697 | ) |
| 2698 | |
| 2699 | x_uint8 = tl.load( |
| 2700 | token_data_ptr[:, None] + nope_offsets[None, :], |
| 2701 | mask=valid[:, None] & nope_mask[None, :], |
| 2702 | other=0, |
| 2703 | ) |
| 2704 | if IS_FNUZ_EXTRA: |
| 2705 | x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True) |
| 2706 | else: |
| 2707 | x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True) |
| 2708 | encoded_scales = tl.load( |
| 2709 | token_scale_ptr[:, None] + nope_offsets[None, :] // 64, |
| 2710 | mask=valid[:, None] & nope_mask[None, :], |
| 2711 | other=127, |
| 2712 | ) |
| 2713 | scales = tl.exp2(encoded_scales.to(tl.float32) - 127.0) |
| 2714 | k_nope = x_fp8.to(tl.bfloat16) * scales.to(tl.bfloat16) |
| 2715 | k_nope = tl.where(valid[:, None] & nope_mask[None, :], k_nope, zero_nope) |
| 2716 | k_nope = tl.where(k_nope == k_nope, k_nope, zero_nope) |
| 2717 | |
| 2718 | rope_ptr = (token_data_ptr + NOPE_DIM).to(tl.pointer_type(tl.bfloat16)) |
| 2719 | k_rope = tl.load( |
| 2720 | rope_ptr[:, None] + rope_offsets[None, :], |
| 2721 | mask=valid[:, None], |
| 2722 | other=0.0, |
| 2723 | ) |
| 2724 | k_rope = tl.where(valid[:, None], k_rope, zero_rope) |
| 2725 | k_rope = tl.where(k_rope == k_rope, k_rope, zero_rope) |
| 2726 | |
| 2727 | scores = tl.dot(q_nope, tl.trans(k_nope)) + tl.dot( |
| 2728 | q_rope, |
| 2729 | tl.trans(k_rope), |
| 2730 | ) |
| 2731 | scores *= scale |
| 2732 | scores = tl.where(head_mask[:, None] & valid[None, :], scores, neg_large) |
| 2733 | |
| 2734 | m_block = tl.max(scores, axis=1) |
| 2735 | m_new = tl.maximum(m_i, m_block) |
| 2736 | alpha = tl.exp(m_i - m_new) |
| 2737 | p = tl.exp(scores - m_new[:, None]) |
| 2738 | p = tl.where(head_mask[:, None] & valid[None, :], p, 0.0) |
| 2739 | l_new = l_i * alpha + tl.sum(p, axis=1) |
| 2740 | |
| 2741 | acc_nope = acc_nope * alpha[:, None] + tl.dot(p.to(k_nope.dtype), k_nope) |
| 2742 | acc_rope = acc_rope * alpha[:, None] + tl.dot(p.to(k_rope.dtype), k_rope) |
| 2743 | m_i = m_new |
| 2744 | l_i = l_new |
| 2745 | |
| 2746 | # Store raw (un-normalized) partial state for this split. Softmax sink and |
| 2747 | # final normalization happen in the reduce kernel. |
| 2748 | pm_base = query_idx * pm_stride0 + split_id * pm_stride_s + head_offsets |
| 2749 | tl.store(part_m_ptr + pm_base, m_i, mask=head_mask) |
| 2750 | tl.store(part_l_ptr + pm_base, l_i, mask=head_mask) |
| 2751 | acc_base = ( |
| 2752 | part_acc_ptr |
| 2753 | + query_idx * pa_stride0 |
| 2754 | + split_id * pa_stride_s |
| 2755 | + head_offsets[:, None] * pa_stride_h |
| 2756 | ) |
| 2757 | tl.store( |
| 2758 | acc_base + nope_offsets[None, :], |
| 2759 | acc_nope, |
| 2760 | mask=head_mask[:, None] & nope_mask[None, :], |
| 2761 | ) |
| 2762 | tl.store( |
| 2763 | acc_base + NOPE_DIM + rope_offsets[None, :], |
| 2764 | acc_rope, |
| 2765 | mask=head_mask[:, None], |
| 2766 | ) |
| 2767 | |
| 2768 | |
| 2769 | @triton.jit |
| 2770 | def _sparse_attn_decode_gfx950_partial_loaded_tile( |
| 2771 | q_combined, |
| 2772 | cache_ptr, |
| 2773 | slot, |
| 2774 | valid, |
| 2775 | cache_stride0, |
| 2776 | scale: tl.constexpr, |
| 2777 | head_mask, |
| 2778 | m_i, |
| 2779 | l_i, |
| 2780 | acc_nope_0a, |
| 2781 | acc_nope_0b, |
| 2782 | acc_nope_1, |
| 2783 | acc_tail, |
| 2784 | BLOCK_SIZE: tl.constexpr, |
| 2785 | NOPE_DIM: tl.constexpr, |
| 2786 | BLOCK_K: tl.constexpr, |
| 2787 | IS_FNUZ: tl.constexpr, |
| 2788 | TRUST_EXTRA_CACHE_NAN_FREE: tl.constexpr, |
| 2789 | ): |
| 2790 | safe_slot = tl.where(valid, slot, 0) |
| 2791 | block_idx = safe_slot // BLOCK_SIZE |
| 2792 | pos_in_block = safe_slot % BLOCK_SIZE |
| 2793 | cache_block_ptr = cache_ptr + block_idx.to(tl.int64) * cache_stride0 |
| 2794 | token_data_ptr = cache_block_ptr + pos_in_block * 576 |
| 2795 | token_scale_ptr = cache_block_ptr + BLOCK_SIZE * 576 + pos_in_block * 8 |
| 2796 | k_nope_0a = _load_fp8_ds_mla_gfx950_nope_exact_chunk( |
| 2797 | token_data_ptr, |
| 2798 | token_scale_ptr, |
| 2799 | valid, |
| 2800 | 0, |
| 2801 | 128, |
| 2802 | BLOCK_K, |
| 2803 | IS_FNUZ, |
| 2804 | ) |
| 2805 | k_nope_0b = _load_fp8_ds_mla_gfx950_nope_exact_chunk( |
| 2806 | token_data_ptr, |
| 2807 | token_scale_ptr, |
| 2808 | valid, |
| 2809 | 128, |
| 2810 | 128, |
| 2811 | BLOCK_K, |
| 2812 | IS_FNUZ, |
| 2813 | ) |
| 2814 | k_nope_1 = _load_fp8_ds_mla_gfx950_nope_exact_chunk( |
| 2815 | token_data_ptr, |
| 2816 | token_scale_ptr, |
| 2817 | valid, |
| 2818 | 256, |
| 2819 | 128, |
| 2820 | BLOCK_K, |
| 2821 | IS_FNUZ, |
| 2822 | ) |
| 2823 | k_tail = _load_fp8_ds_mla_gfx950_tail128( |
| 2824 | token_data_ptr, |
| 2825 | token_scale_ptr, |
| 2826 | valid, |
| 2827 | NOPE_DIM, |
| 2828 | BLOCK_K, |
| 2829 | IS_FNUZ, |
| 2830 | ) |
| 2831 | if not TRUST_EXTRA_CACHE_NAN_FREE: |
| 2832 | zero = tl.zeros((BLOCK_K, 128), dtype=tl.bfloat16) |
| 2833 | k_nope_0a = tl.where(k_nope_0a == k_nope_0a, k_nope_0a, zero) |
| 2834 | k_nope_0b = tl.where(k_nope_0b == k_nope_0b, k_nope_0b, zero) |
| 2835 | k_nope_1 = tl.where(k_nope_1 == k_nope_1, k_nope_1, zero) |
| 2836 | k_tail = tl.where(k_tail == k_tail, k_tail, zero) |
| 2837 | k_nope_0 = tl.cat(k_nope_0a, k_nope_0b, dim=1) |
| 2838 | k_tail_256 = tl.cat(k_nope_1, k_tail, dim=1) |
| 2839 | k_combined = tl.cat(k_nope_0, k_tail_256, dim=1) |
| 2840 | |
| 2841 | scores = tl.dot(q_combined, tl.trans(k_combined)) |
| 2842 | scores *= scale * 1.4426950408889634 |
| 2843 | scores = tl.where( |
| 2844 | head_mask[:, None] & valid[None, :], |
| 2845 | scores, |
| 2846 | -3.4028234663852886e38, |
| 2847 | ) |
| 2848 | m_block = tl.max(scores, axis=1) |
| 2849 | m_new = tl.maximum(m_i, m_block) |
| 2850 | alpha = tl.exp2(m_i - m_new) |
| 2851 | p = tl.exp2(scores - m_new[:, None]) |
| 2852 | p = tl.where(head_mask[:, None] & valid[None, :], p, 0.0) |
| 2853 | l_new = l_i * alpha + tl.sum(p, axis=1) |
| 2854 | p_bf16 = p.to(k_nope_0a.dtype) |
| 2855 | acc_nope_0a = acc_nope_0a * alpha[:, None] + tl.dot(p_bf16, k_nope_0a) |
| 2856 | acc_nope_0b = acc_nope_0b * alpha[:, None] + tl.dot(p_bf16, k_nope_0b) |
| 2857 | acc_nope_1 = acc_nope_1 * alpha[:, None] + tl.dot(p_bf16, k_nope_1) |
| 2858 | acc_tail = acc_tail * alpha[:, None] + tl.dot(p_bf16, k_tail) |
| 2859 | return ( |
| 2860 | m_new, |
| 2861 | l_new, |
| 2862 | acc_nope_0a, |
| 2863 | acc_nope_0b, |
| 2864 | acc_nope_1, |
| 2865 | acc_tail, |
| 2866 | ) |
| 2867 | |
| 2868 | |
| 2869 | @triton.jit |
| 2870 | def _sparse_attn_decode_gfx950_partial_kernel( |
| 2871 | q_ptr, |
| 2872 | main_cache_ptr, |
| 2873 | main_indices_ptr, |
| 2874 | main_indptr_ptr, |
| 2875 | extra_cache_ptr, |
| 2876 | extra_indices_ptr, |
| 2877 | extra_indptr_ptr, |
| 2878 | part_m_ptr, |
| 2879 | part_l_ptr, |
| 2880 | part_acc_ptr, |
| 2881 | q_stride0: tl.constexpr, |
| 2882 | q_stride1: tl.constexpr, |
| 2883 | main_cache_stride0: tl.constexpr, |
| 2884 | extra_cache_stride0: tl.constexpr, |
| 2885 | main_num_rows, |
| 2886 | extra_num_rows, |
| 2887 | MAIN_BLOCK_SIZE: tl.constexpr, |
| 2888 | EXTRA_BLOCK_SIZE: tl.constexpr, |
| 2889 | scale: tl.constexpr, |
| 2890 | num_heads: tl.constexpr, |
| 2891 | HAS_EXTRA: tl.constexpr, |
| 2892 | NOPE_DIM: tl.constexpr, |
| 2893 | ROPE_DIM: tl.constexpr, |
| 2894 | IS_FNUZ_MAIN: tl.constexpr, |
| 2895 | IS_FNUZ_EXTRA: tl.constexpr, |
| 2896 | TRUST_EXTRA_CACHE_NAN_FREE: tl.constexpr, |
| 2897 | ADAPTIVE_SPLITS: tl.constexpr, |
| 2898 | ONE_WAVE_SPLITS: tl.constexpr, |
| 2899 | BLOCK_H: tl.constexpr, |
| 2900 | BLOCK_K: tl.constexpr, |
| 2901 | NUM_SPLITS: tl.constexpr, |
| 2902 | NUM_STAGES: tl.constexpr, |
| 2903 | ): |
| 2904 | query_idx = tl.program_id(0) |
| 2905 | split_id = tl.program_id(1) |
| 2906 | pid_h = tl.program_id(2) |
| 2907 | |
| 2908 | tl.static_assert(NOPE_DIM == 448) |
| 2909 | tl.static_assert(ROPE_DIM == 64) |
| 2910 | |
| 2911 | head_offsets = pid_h * BLOCK_H + tl.arange(0, BLOCK_H) |
| 2912 | if num_heads % BLOCK_H == 0: |
| 2913 | head_mask = tl.full((BLOCK_H,), True, tl.int1) |
| 2914 | else: |
| 2915 | head_mask = head_offsets < num_heads |
| 2916 | neg_large = -3.4028234663852886e38 |
| 2917 | |
| 2918 | if ADAPTIVE_SPLITS: |
| 2919 | main_start = tl.load(main_indptr_ptr + query_idx) |
| 2920 | main_end = tl.load(main_indptr_ptr + query_idx + 1) |
| 2921 | main_len = main_end - main_start |
| 2922 | if HAS_EXTRA: |
| 2923 | extra_start = tl.load(extra_indptr_ptr + query_idx) |
| 2924 | extra_end = tl.load(extra_indptr_ptr + query_idx + 1) |
| 2925 | extra_len = extra_end - extra_start |
| 2926 | else: |
| 2927 | extra_start = 0 |
| 2928 | extra_len = 0 |
| 2929 | split4_span: tl.constexpr = 4 * BLOCK_K |
| 2930 | split4_iters = (main_len + split4_span - 1) // split4_span |
| 2931 | split4_iters += (extra_len + split4_span - 1) // split4_span |
| 2932 | use_four_splits = split4_iters <= 3 |
| 2933 | work_splits = NUM_SPLITS |
| 2934 | if ONE_WAVE_SPLITS > 4 and ONE_WAVE_SPLITS < NUM_SPLITS: |
| 2935 | one_wave_span: tl.constexpr = ONE_WAVE_SPLITS * BLOCK_K |
| 2936 | one_wave_iters = (main_len + one_wave_span - 1) // one_wave_span |
| 2937 | one_wave_iters += (extra_len + one_wave_span - 1) // one_wave_span |
| 2938 | work_splits = tl.where(one_wave_iters <= 3, ONE_WAVE_SPLITS, work_splits) |
| 2939 | work_splits = tl.where(use_four_splits, 4, work_splits) |
| 2940 | if split_id >= work_splits: |
| 2941 | pm_base = (query_idx * NUM_SPLITS + split_id) * num_heads + head_offsets |
| 2942 | tl.store(part_m_ptr + pm_base, neg_large, mask=head_mask) |
| 2943 | tl.store(part_l_ptr + pm_base, 0.0, mask=head_mask) |
| 2944 | return |
| 2945 | else: |
| 2946 | work_splits = NUM_SPLITS |
| 2947 | |
| 2948 | nope_offsets_0a = tl.arange(0, 128) |
| 2949 | nope_offsets_0b = 128 + tl.arange(0, 128) |
| 2950 | nope_offsets_0 = tl.arange(0, 256) |
| 2951 | tail_offsets = 256 + tl.arange(0, 256) |
| 2952 | nope_offsets_1 = 256 + tl.arange(0, 128) |
| 2953 | tail_offsets_128 = 384 + tl.arange(0, 128) |
| 2954 | |
| 2955 | q_row_ptr = q_ptr + query_idx * q_stride0 + head_offsets[:, None] * q_stride1 |
| 2956 | q_nope_0 = tl.load( |
| 2957 | q_row_ptr + nope_offsets_0[None, :], |
| 2958 | mask=head_mask[:, None], |
| 2959 | other=0.0, |
| 2960 | ) |
| 2961 | q_tail = tl.load( |
| 2962 | q_row_ptr + tail_offsets[None, :], |
| 2963 | mask=head_mask[:, None], |
| 2964 | other=0.0, |
| 2965 | ) |
| 2966 | q_combined = tl.cat(q_nope_0, q_tail, dim=1) |
| 2967 | |
| 2968 | m_i = tl.full((BLOCK_H,), neg_large, dtype=tl.float32) |
| 2969 | l_i = tl.zeros((BLOCK_H,), dtype=tl.float32) |
| 2970 | acc_nope_0a = tl.zeros((BLOCK_H, 128), dtype=tl.float32) |
| 2971 | acc_nope_0b = tl.zeros((BLOCK_H, 128), dtype=tl.float32) |
| 2972 | acc_nope_1 = tl.zeros((BLOCK_H, 128), dtype=tl.float32) |
| 2973 | acc_tail = tl.zeros((BLOCK_H, 128), dtype=tl.float32) |
| 2974 | k_offsets = tl.arange(0, BLOCK_K) |
| 2975 | |
| 2976 | if not ADAPTIVE_SPLITS: |
| 2977 | main_start = tl.load(main_indptr_ptr + query_idx) |
| 2978 | main_end = tl.load(main_indptr_ptr + query_idx + 1) |
| 2979 | main_len = main_end - main_start |
| 2980 | main_chunk = (main_len + work_splits - 1) // work_splits |
| 2981 | main_lo = split_id * main_chunk |
| 2982 | main_hi = tl.minimum(main_lo + main_chunk, main_len) |
| 2983 | |
| 2984 | for k_start in tl.range( |
| 2985 | main_lo, |
| 2986 | main_hi, |
| 2987 | BLOCK_K, |
| 2988 | num_stages=NUM_STAGES, |
| 2989 | ): |
| 2990 | k_pos = k_start + k_offsets |
| 2991 | in_range = k_pos < main_hi |
| 2992 | slot = tl.load(main_indices_ptr + main_start + k_pos, mask=in_range, other=-1) |
| 2993 | valid = in_range & (slot >= 0) & (slot < main_num_rows) |
| 2994 | ( |
| 2995 | m_i, |
| 2996 | l_i, |
| 2997 | acc_nope_0a, |
| 2998 | acc_nope_0b, |
| 2999 | acc_nope_1, |
| 3000 | acc_tail, |
| 3001 | ) = _sparse_attn_decode_gfx950_partial_loaded_tile( |
| 3002 | q_combined, |
| 3003 | main_cache_ptr, |
| 3004 | slot, |
| 3005 | valid, |
| 3006 | main_cache_stride0, |
| 3007 | scale, |
| 3008 | head_mask, |
| 3009 | m_i, |
| 3010 | l_i, |
| 3011 | acc_nope_0a, |
| 3012 | acc_nope_0b, |
| 3013 | acc_nope_1, |
| 3014 | acc_tail, |
| 3015 | MAIN_BLOCK_SIZE, |
| 3016 | NOPE_DIM, |
| 3017 | BLOCK_K, |
| 3018 | IS_FNUZ_MAIN, |
| 3019 | False, |
| 3020 | ) |
| 3021 | |
| 3022 | if HAS_EXTRA: |
| 3023 | if not ADAPTIVE_SPLITS: |
| 3024 | extra_start = tl.load(extra_indptr_ptr + query_idx) |
| 3025 | extra_end = tl.load(extra_indptr_ptr + query_idx + 1) |
| 3026 | extra_len = extra_end - extra_start |
| 3027 | extra_chunk = (extra_len + work_splits - 1) // work_splits |
| 3028 | extra_lo = split_id * extra_chunk |
| 3029 | extra_hi = tl.minimum(extra_lo + extra_chunk, extra_len) |
| 3030 | |
| 3031 | outer_block_k: tl.constexpr = 2 * BLOCK_K |
| 3032 | outer_k_offsets = tl.arange(0, outer_block_k) |
| 3033 | extra_hi_full = ( |
| 3034 | extra_lo + ((extra_hi - extra_lo) // outer_block_k) * outer_block_k |
| 3035 | ) |
| 3036 | for k_start in tl.range( |
| 3037 | extra_lo, |
| 3038 | extra_hi_full, |
| 3039 | outer_block_k, |
| 3040 | num_stages=NUM_STAGES, |
| 3041 | ): |
| 3042 | slot = tl.load(extra_indices_ptr + extra_start + k_start + outer_k_offsets) |
| 3043 | valid = (slot >= 0) & (slot < extra_num_rows) |
| 3044 | slot_pairs = tl.trans(tl.reshape(slot, (2, BLOCK_K))) |
| 3045 | valid_pairs = tl.trans(tl.reshape(valid, (2, BLOCK_K))) |
| 3046 | slot_lo, slot_hi = tl.split(slot_pairs) |
| 3047 | valid_lo, valid_hi = tl.split(valid_pairs) |
| 3048 | ( |
| 3049 | m_i, |
| 3050 | l_i, |
| 3051 | acc_nope_0a, |
| 3052 | acc_nope_0b, |
| 3053 | acc_nope_1, |
| 3054 | acc_tail, |
| 3055 | ) = _sparse_attn_decode_gfx950_partial_loaded_tile( |
| 3056 | q_combined, |
| 3057 | extra_cache_ptr, |
| 3058 | slot_lo, |
| 3059 | valid_lo, |
| 3060 | extra_cache_stride0, |
| 3061 | scale, |
| 3062 | head_mask, |
| 3063 | m_i, |
| 3064 | l_i, |
| 3065 | acc_nope_0a, |
| 3066 | acc_nope_0b, |
| 3067 | acc_nope_1, |
| 3068 | acc_tail, |
| 3069 | EXTRA_BLOCK_SIZE, |
| 3070 | NOPE_DIM, |
| 3071 | BLOCK_K, |
| 3072 | IS_FNUZ_EXTRA, |
| 3073 | TRUST_EXTRA_CACHE_NAN_FREE, |
| 3074 | ) |
| 3075 | ( |
| 3076 | m_i, |
| 3077 | l_i, |
| 3078 | acc_nope_0a, |
| 3079 | acc_nope_0b, |
| 3080 | acc_nope_1, |
| 3081 | acc_tail, |
| 3082 | ) = _sparse_attn_decode_gfx950_partial_loaded_tile( |
| 3083 | q_combined, |
| 3084 | extra_cache_ptr, |
| 3085 | slot_hi, |
| 3086 | valid_hi, |
| 3087 | extra_cache_stride0, |
| 3088 | scale, |
| 3089 | head_mask, |
| 3090 | m_i, |
| 3091 | l_i, |
| 3092 | acc_nope_0a, |
| 3093 | acc_nope_0b, |
| 3094 | acc_nope_1, |
| 3095 | acc_tail, |
| 3096 | EXTRA_BLOCK_SIZE, |
| 3097 | NOPE_DIM, |
| 3098 | BLOCK_K, |
| 3099 | IS_FNUZ_EXTRA, |
| 3100 | TRUST_EXTRA_CACHE_NAN_FREE, |
| 3101 | ) |
| 3102 | for tail_idx in tl.static_range(2): |
| 3103 | tail_start = extra_hi_full + tail_idx * BLOCK_K |
| 3104 | if tail_start < extra_hi: |
| 3105 | k_pos = tail_start + k_offsets |
| 3106 | in_range = k_pos < extra_hi |
| 3107 | slot = tl.load( |
| 3108 | extra_indices_ptr + extra_start + k_pos, |
| 3109 | mask=in_range, |
| 3110 | other=-1, |
| 3111 | ) |
| 3112 | valid = in_range & (slot >= 0) & (slot < extra_num_rows) |
| 3113 | ( |
| 3114 | m_i, |
| 3115 | l_i, |
| 3116 | acc_nope_0a, |
| 3117 | acc_nope_0b, |
| 3118 | acc_nope_1, |
| 3119 | acc_tail, |
| 3120 | ) = _sparse_attn_decode_gfx950_partial_loaded_tile( |
| 3121 | q_combined, |
| 3122 | extra_cache_ptr, |
| 3123 | slot, |
| 3124 | valid, |
| 3125 | extra_cache_stride0, |
| 3126 | scale, |
| 3127 | head_mask, |
| 3128 | m_i, |
| 3129 | l_i, |
| 3130 | acc_nope_0a, |
| 3131 | acc_nope_0b, |
| 3132 | acc_nope_1, |
| 3133 | acc_tail, |
| 3134 | EXTRA_BLOCK_SIZE, |
| 3135 | NOPE_DIM, |
| 3136 | BLOCK_K, |
| 3137 | IS_FNUZ_EXTRA, |
| 3138 | TRUST_EXTRA_CACHE_NAN_FREE, |
| 3139 | ) |
| 3140 | |
| 3141 | pm_base = (query_idx * NUM_SPLITS + split_id) * num_heads + head_offsets |
| 3142 | m_store = tl.where(l_i > 0.0, m_i * 0.6931471805599453, neg_large) |
| 3143 | tl.store(part_m_ptr + pm_base, m_store, mask=head_mask) |
| 3144 | tl.store(part_l_ptr + pm_base, l_i, mask=head_mask) |
| 3145 | acc_base = part_acc_ptr + ( |
| 3146 | (query_idx * NUM_SPLITS + split_id) * num_heads + head_offsets[:, None] |
| 3147 | ) * (NOPE_DIM + ROPE_DIM) |
| 3148 | tl.store( |
| 3149 | acc_base + nope_offsets_0a[None, :], |
| 3150 | acc_nope_0a, |
| 3151 | mask=head_mask[:, None], |
| 3152 | ) |
| 3153 | tl.store( |
| 3154 | acc_base + nope_offsets_0b[None, :], |
| 3155 | acc_nope_0b, |
| 3156 | mask=head_mask[:, None], |
| 3157 | ) |
| 3158 | tl.store( |
| 3159 | acc_base + nope_offsets_1[None, :], |
| 3160 | acc_nope_1, |
| 3161 | mask=head_mask[:, None], |
| 3162 | ) |
| 3163 | tl.store( |
| 3164 | acc_base + tail_offsets_128[None, :], |
| 3165 | acc_tail, |
| 3166 | mask=head_mask[:, None], |
| 3167 | ) |
| 3168 | |
| 3169 | |
| 3170 | @triton.jit |
| 3171 | def _mxfp8_scale_bits(amax): |
| 3172 | """Biased E8M0 exponent that puts ``amax`` at the top of the e4m3 range. |
| 3173 | |
| 3174 | Same rounding as ``mxfp8_e4m3_quantize``, so the output is bit-identical to |
| 3175 | quantizing the tensor there. |
| 3176 | """ |
| 3177 | amax = tl.maximum(amax, 1.1754943508222875e-38) |
| 3178 | bits = tl.ceil(tl.log2(amax / 448.0)) + 127.0 |
| 3179 | return tl.minimum(tl.maximum(bits, 0.0), 254.0) |
| 3180 | |
| 3181 | |
| 3182 | @triton.jit |
| 3183 | def _mxfp8_quantize_rows(x, ROWS: tl.constexpr, COLS: tl.constexpr): |
| 3184 | """MXFP8-quantize ``x`` [ROWS, COLS] in registers, one scale per 32 lanes. |
| 3185 | |
| 3186 | Returns the rescaled fp32 values (to be cast to e4m3 on store) and the |
| 3187 | [ROWS, COLS // 32] biased E8M0 exponents. |
| 3188 | """ |
| 3189 | blocks = tl.reshape(x, (ROWS, COLS // 32, 32)) |
| 3190 | bits = _mxfp8_scale_bits(tl.max(tl.abs(blocks), axis=2)) |
| 3191 | # Multiply by the reciprocal: a divisor of 2**-127 would be subnormal and |
| 3192 | # flush to zero, turning an all-zero block into NaN. |
| 3193 | q = blocks * tl.exp2(127.0 - bits)[:, :, None] |
| 3194 | return tl.reshape(q, (ROWS, COLS)), bits |
| 3195 | |
| 3196 | |
| 3197 | @triton.jit |
| 3198 | def _sparse_attn_decode_reduce_kernel( |
| 3199 | part_m_ptr, |
| 3200 | part_l_ptr, |
| 3201 | part_acc_ptr, |
| 3202 | attn_sink_ptr, |
| 3203 | out_ptr, |
| 3204 | pos_ptr, |
| 3205 | cos_sin_ptr, |
| 3206 | out_scale_ptr, |
| 3207 | out_stride0, |
| 3208 | out_stride1, |
| 3209 | os_stride0, |
| 3210 | os_stride1, |
| 3211 | pm_stride0, |
| 3212 | pm_stride_s, |
| 3213 | pa_stride0, |
| 3214 | pa_stride_s, |
| 3215 | pa_stride_h, |
| 3216 | cs_stride, |
| 3217 | num_heads, |
| 3218 | HAS_ATTN_SINK: tl.constexpr, |
| 3219 | ADAPTIVE_SPLITS: tl.constexpr, |
| 3220 | COMB_DIM: tl.constexpr, |
| 3221 | BLOCK_H: tl.constexpr, |
| 3222 | NUM_SPLITS: tl.constexpr, |
| 3223 | SPLITS_PAD: tl.constexpr, |
| 3224 | FUSE_INV_ROPE: tl.constexpr, |
| 3225 | NOPE: tl.constexpr, |
| 3226 | HALF: tl.constexpr, |
| 3227 | QUANT_OUT: tl.constexpr, |
| 3228 | ): |
| 3229 | query_idx = tl.program_id(0) |
| 3230 | pid_h = tl.program_id(1) |
| 3231 | |
| 3232 | head_offsets = pid_h * BLOCK_H + tl.arange(0, BLOCK_H) |
| 3233 | head_mask = head_offsets < num_heads |
| 3234 | comb_offsets = tl.arange(0, COMB_DIM) |
| 3235 | # SPLITS_PAD is NUM_SPLITS rounded up to a power of two so the parallel |
| 3236 | # split-axis load is a legal arange for any split count; padding lanes are |
| 3237 | # masked off. |
| 3238 | split_offsets = tl.arange(0, SPLITS_PAD) |
| 3239 | split_mask = split_offsets < NUM_SPLITS |
| 3240 | |
| 3241 | neg_large = -3.4028234663852886e38 |
| 3242 | |
| 3243 | # Phase 1: load every split's running max/sum at once and reduce the max |
| 3244 | # in parallel (tl.max over the split axis) instead of walking the splits |
| 3245 | # serially. This breaks the long online-softmax dependency chain that made |
| 3246 | # the reduce latency-bound. |
| 3247 | load_mask = split_mask[:, None] & head_mask[None, :] |
| 3248 | pm_split = ( |
| 3249 | part_m_ptr |
| 3250 | + query_idx * pm_stride0 |
| 3251 | + split_offsets[:, None] * pm_stride_s |
| 3252 | + head_offsets[None, :] |
| 3253 | ) |
| 3254 | m_all = tl.load(pm_split, mask=load_mask, other=neg_large) # [S, H] |
| 3255 | l_all = tl.load( |
| 3256 | part_l_ptr |
| 3257 | + query_idx * pm_stride0 |
| 3258 | + split_offsets[:, None] * pm_stride_s |
| 3259 | + head_offsets[None, :], |
| 3260 | mask=load_mask, |
| 3261 | other=0.0, |
| 3262 | ) |
| 3263 | |
| 3264 | m_comb = tl.max(m_all, axis=0) # [H] |
| 3265 | if HAS_ATTN_SINK: |
| 3266 | sink = tl.load( |
| 3267 | attn_sink_ptr + head_offsets, mask=head_mask, other=neg_large |
| 3268 | ).to(tl.float32) |
| 3269 | m_final = tl.maximum(m_comb, sink) |
| 3270 | else: |
| 3271 | m_final = m_comb |
| 3272 | |
| 3273 | w_all = tl.exp(m_all - m_final[None, :]) # [S, H] |
| 3274 | w_all = tl.where(load_mask, w_all, 0.0) |
| 3275 | l_final = tl.sum(w_all * l_all, axis=0) # [H] |
| 3276 | if HAS_ATTN_SINK: |
| 3277 | l_final = l_final + tl.exp(sink - m_final) |
| 3278 | denom = tl.maximum(l_final, 1.0e-30) |
| 3279 | |
| 3280 | # Phase 2: weighted sum of the per-split accumulators. The combine weight |
| 3281 | # for each split only depends on the (already known) global max, so the |
| 3282 | # acc loads carry no cross-split dependency and the compiler can pipeline |
| 3283 | # them; only the cheap FMA into `acc` is loop-carried. |
| 3284 | acc = tl.zeros((BLOCK_H, COMB_DIM), dtype=tl.float32) |
| 3285 | for s in tl.static_range(NUM_SPLITS): |
| 3286 | m_s = tl.load( |
| 3287 | part_m_ptr + query_idx * pm_stride0 + s * pm_stride_s + head_offsets, |
| 3288 | mask=head_mask, |
| 3289 | other=neg_large, |
| 3290 | ) |
| 3291 | w_s = tl.exp(m_s - m_final) |
| 3292 | if ADAPTIVE_SPLITS: |
| 3293 | active_split = m_s > neg_large |
| 3294 | w_s = tl.where(head_mask & active_split, w_s, 0.0) |
| 3295 | acc_base = ( |
| 3296 | part_acc_ptr |
| 3297 | + query_idx * pa_stride0 |
| 3298 | + s * pa_stride_s |
| 3299 | + head_offsets[:, None] * pa_stride_h |
| 3300 | ) |
| 3301 | if ADAPTIVE_SPLITS: |
| 3302 | acc_s = tl.load( |
| 3303 | acc_base + comb_offsets[None, :], |
| 3304 | mask=head_mask[:, None] & active_split[:, None], |
| 3305 | other=0.0, |
| 3306 | ) |
| 3307 | else: |
| 3308 | acc_s = tl.load( |
| 3309 | acc_base + comb_offsets[None, :], |
| 3310 | mask=head_mask[:, None], |
| 3311 | other=0.0, |
| 3312 | ) |
| 3313 | acc += w_s[:, None] * acc_s |
| 3314 | |
| 3315 | out = tl.where(l_final[:, None] > 0.0, acc / denom[:, None], 0.0) |
| 3316 | |
| 3317 | if FUSE_INV_ROPE: |
| 3318 | # Inverse GPT-J RoPE on the trailing rope lanes, straight out of the |
| 3319 | # combine registers: out_even = a*cos + b*sin, out_odd = b*cos - a*sin. |
| 3320 | # NoPE lanes take cos=1/sin=0 so one expression covers the whole row |
| 3321 | # and the o_proj rotation pass disappears for these tokens. |
| 3322 | pos = tl.load(pos_ptr + query_idx).to(tl.int64) |
| 3323 | pair_idx = tl.arange(0, COMB_DIM // 2) - (NOPE // 2) |
| 3324 | is_rope = pair_idx >= 0 |
| 3325 | k = tl.where(is_rope, pair_idx, 0) |
| 3326 | cos = tl.where(is_rope, tl.load(cos_sin_ptr + pos * cs_stride + k), 1.0) |
| 3327 | sin = tl.where(is_rope, tl.load(cos_sin_ptr + pos * cs_stride + HALF + k), 0.0) |
| 3328 | even, odd = tl.split(tl.reshape(out, (BLOCK_H, COMB_DIM // 2, 2))) |
| 3329 | out = tl.reshape( |
| 3330 | tl.join( |
| 3331 | even * cos[None, :] + odd * sin[None, :], |
| 3332 | odd * cos[None, :] - even * sin[None, :], |
| 3333 | ), |
| 3334 | (BLOCK_H, COMB_DIM), |
| 3335 | ) |
| 3336 | |
| 3337 | out_row_ptr = ( |
| 3338 | out_ptr + query_idx * out_stride0 + head_offsets[:, None] * out_stride1 |
| 3339 | ) |
| 3340 | if QUANT_OUT: |
| 3341 | # Emit wo_a's MXFP8 input directly instead of a bf16 row that a |
| 3342 | # separate pass would read back to quantize. |
| 3343 | out, scale_bits = _mxfp8_quantize_rows(out, BLOCK_H, COMB_DIM) |
| 3344 | scale_offsets = tl.arange(0, COMB_DIM // 32) |
| 3345 | tl.store( |
| 3346 | out_scale_ptr |
| 3347 | + query_idx * os_stride0 |
| 3348 | + head_offsets[:, None] * os_stride1 |
| 3349 | + scale_offsets[None, :], |
| 3350 | scale_bits.to(tl.uint8), |
| 3351 | mask=head_mask[:, None], |
| 3352 | ) |
| 3353 | tl.store( |
| 3354 | out_row_ptr + comb_offsets[None, :], |
| 3355 | out.to(out_ptr.dtype.element_ty), |
| 3356 | mask=head_mask[:, None], |
| 3357 | ) |
| 3358 | |
| 3359 | |
| 3360 | def _rocm_sparse_attn_prefill_ragged_triton( |
| 3361 | q: torch.Tensor, |
| 3362 | kv: torch.Tensor, |
| 3363 | indices: torch.Tensor, |
| 3364 | indptr: torch.Tensor, |
| 3365 | scale: float, |
| 3366 | attn_sink: torch.Tensor | None, |
| 3367 | nope_head_dim: int, |
| 3368 | rope_head_dim: int, |
| 3369 | out: torch.Tensor | None = None, |
| 3370 | ) -> torch.Tensor: |
| 3371 | assert q.ndim == 3, f"expected q=[sq,h,d], got {q.shape}" |
| 3372 | assert kv.ndim == 2, f"expected kv=[skv,d], got {kv.shape}" |
| 3373 | assert indices.ndim == 1, f"expected indices=[nnz], got {indices.shape}" |
| 3374 | assert indptr.ndim == 1, f"expected indptr=[sq+1], got {indptr.shape}" |
| 3375 | assert not q.is_cpu and not kv.is_cpu and not indices.is_cpu and not indptr.is_cpu |
| 3376 | |
| 3377 | indices = _as_int32_contiguous_1d(indices) |
| 3378 | indptr = _as_int32_contiguous_1d(indptr) |
| 3379 | has_attn_sink = attn_sink is not None |
| 3380 | if attn_sink is None: |
| 3381 | attn_sink = torch.empty(1, device=q.device, dtype=torch.float32) |
| 3382 | else: |
| 3383 | attn_sink = attn_sink.contiguous() |
| 3384 | |
| 3385 | num_queries, num_heads, head_dim = q.shape |
| 3386 | assert indptr.numel() == num_queries + 1, ( |
| 3387 | f"expected indptr shape [{num_queries + 1}], got {indptr.shape}" |
| 3388 | ) |
| 3389 | _validate_sparse_dims( |
| 3390 | head_dim, |
| 3391 | nope_head_dim, |
| 3392 | rope_head_dim, |
| 3393 | "_rocm_sparse_attn_prefill_ragged_triton", |
| 3394 | ) |
| 3395 | |
| 3396 | block_h = 16 |
| 3397 | block_d = triton.next_power_of_2(head_dim) |
| 3398 | block_k = 16 if head_dim >= 256 else 32 |
| 3399 | num_warps = 4 |
| 3400 | if out is None: |
| 3401 | out = torch.empty_like(q) |
| 3402 | else: |
| 3403 | assert out.shape[:2] == q.shape[:2], ( |
| 3404 | f"expected out=[{num_queries},{num_heads},*], got {out.shape}" |
| 3405 | ) |
| 3406 | assert out.shape[-1] in (nope_head_dim, head_dim), ( |
| 3407 | f"expected out width {nope_head_dim} or {head_dim}, got {out.shape[-1]}" |
| 3408 | ) |
| 3409 | _sparse_attn_prefill_ragged_kernel[(num_queries, triton.cdiv(num_heads, block_h))]( |
| 3410 | q, |
| 3411 | kv, |
| 3412 | indices, |
| 3413 | indptr, |
| 3414 | attn_sink, |
| 3415 | out, |
| 3416 | q.stride(0), |
| 3417 | q.stride(1), |
| 3418 | q.stride(2), |
| 3419 | kv.stride(0), |
| 3420 | kv.stride(1), |
| 3421 | out.stride(0), |
| 3422 | out.stride(1), |
| 3423 | out.stride(2), |
| 3424 | num_heads, |
| 3425 | head_dim, |
| 3426 | kv.shape[0], |
| 3427 | float(scale), |
| 3428 | HAS_ATTN_SINK=has_attn_sink, |
| 3429 | OUT_DV=out.shape[-1], |
| 3430 | BLOCK_H=block_h, |
| 3431 | BLOCK_D=block_d, |
| 3432 | BLOCK_K=block_k, |
| 3433 | num_warps=num_warps, |
| 3434 | ) |
| 3435 | return out |
| 3436 | |
| 3437 | |
| 3438 | def _rocm_sparse_attn_prefill_triton( |
| 3439 | q: torch.Tensor, |
| 3440 | kv: torch.Tensor, |
| 3441 | indices: torch.Tensor, |
| 3442 | scale: float, |
| 3443 | attn_sink: torch.Tensor | None, |
| 3444 | nope_head_dim: int, |
| 3445 | rope_head_dim: int, |
| 3446 | topk_length: torch.Tensor | None = None, |
| 3447 | out: torch.Tensor | None = None, |
| 3448 | ) -> torch.Tensor: |
| 3449 | ragged_indices, ragged_indptr = build_ragged_indices_from_dense( |
| 3450 | indices, |
| 3451 | topk_length |
| 3452 | if topk_length is not None |
| 3453 | else (indices >= 0).sum(dim=-1, dtype=torch.int32), |
| 3454 | num_rows=kv.shape[0], |
| 3455 | ) |
| 3456 | return _rocm_sparse_attn_prefill_ragged_triton( |
| 3457 | q=q, |
| 3458 | kv=kv, |
| 3459 | indices=ragged_indices, |
| 3460 | indptr=ragged_indptr, |
| 3461 | scale=scale, |
| 3462 | attn_sink=attn_sink, |
| 3463 | nope_head_dim=nope_head_dim, |
| 3464 | rope_head_dim=rope_head_dim, |
| 3465 | out=out, |
| 3466 | ) |
| 3467 | |
| 3468 | |
| 3469 | def _can_use_aiter_sparse_prefill_opus( |
| 3470 | q: torch.Tensor, |
| 3471 | kv: torch.Tensor, |
| 3472 | attn_sink: torch.Tensor | None, |
| 3473 | output: torch.Tensor, |
| 3474 | on_gfx950: bool = _ON_GFX950, |
| 3475 | ) -> bool: |
| 3476 | return ( |
| 3477 | on_gfx950 |
| 3478 | and q.shape[0] >= _GFX950_AITER_SPARSE_PREFILL_OPUS_MIN_QUERIES |
| 3479 | and q.is_cuda |
| 3480 | and output.shape == q.shape |
| 3481 | and kv.shape[-1] == q.shape[-1] |
| 3482 | and q.dtype in (torch.bfloat16, torch.float16) |
| 3483 | and kv.dtype == q.dtype |
| 3484 | and output.dtype == q.dtype |
| 3485 | and kv.device == q.device |
| 3486 | and output.device == q.device |
| 3487 | and q.stride(-1) == 1 |
| 3488 | and kv.stride(-1) == 1 |
| 3489 | and output.stride() == q.stride() |
| 3490 | and attn_sink is not None |
| 3491 | and attn_sink.shape == (q.shape[1],) |
| 3492 | and attn_sink.dtype == torch.float32 |
| 3493 | and attn_sink.device == q.device |
| 3494 | ) |
| 3495 | |
| 3496 | |
| 3497 | def _rocm_sparse_attn_prefill_ragged_aiter_opus( |
| 3498 | q: torch.Tensor, |
| 3499 | kv: torch.Tensor, |
| 3500 | indices: torch.Tensor, |
| 3501 | indptr: torch.Tensor, |
| 3502 | scale: float, |
| 3503 | attn_sink: torch.Tensor, |
| 3504 | output: torch.Tensor, |
| 3505 | ) -> bool: |
| 3506 | pa_sparse_prefill_opus = _get_aiter_sparse_prefill_opus() |
| 3507 | if pa_sparse_prefill_opus is None: |
| 3508 | return False |
| 3509 | |
| 3510 | indices = _as_int32_contiguous_1d(indices) |
| 3511 | indptr = _as_int32_contiguous_1d(indptr) |
| 3512 | empty_indices = indices[:0] |
| 3513 | empty_indptr = torch.zeros_like(indptr) |
| 3514 | pa_sparse_prefill_opus( |
| 3515 | q, |
| 3516 | kv, |
| 3517 | indices, |
| 3518 | indptr, |
| 3519 | kv[:1], |
| 3520 | empty_indices, |
| 3521 | empty_indptr, |
| 3522 | attn_sink.contiguous(), |
| 3523 | float(scale), |
| 3524 | out=output, |
| 3525 | ) |
| 3526 | return True |
| 3527 | |
| 3528 | |
| 3529 | @functools.lru_cache |
| 3530 | def _decode_cu_count() -> int: |
| 3531 | try: |
| 3532 | return torch.cuda.get_device_properties(0).multi_processor_count |
| 3533 | except Exception: |
| 3534 | return 256 # For gfx950 arch, gated behind a fallback path for other archs. |
| 3535 | |
| 3536 | |
| 3537 | def _decode_partial_iters( |
| 3538 | avg_main_len: float, avg_extra_len: float, splits: int, block_k: int |
| 3539 | ) -> int: |
| 3540 | """BLOCK_K iterations one partial workgroup walks for ``splits`` splits. |
| 3541 | |
| 3542 | Each split processes ``ceil(seg_len / splits)`` tokens of a segment, walked |
| 3543 | ``BLOCK_K`` at a time, and the main/extra segments are handled separately. |
| 3544 | """ |
| 3545 | main_iters = ( |
| 3546 | math.ceil(math.ceil(avg_main_len / splits) / block_k) if avg_main_len > 0 else 0 |
| 3547 | ) |
| 3548 | extra_iters = ( |
| 3549 | math.ceil(math.ceil(avg_extra_len / splits) / block_k) |
| 3550 | if avg_extra_len > 0 |
| 3551 | else 0 |
| 3552 | ) |
| 3553 | return main_iters + extra_iters |
| 3554 | |
| 3555 | |
| 3556 | def _decode_num_splits( |
| 3557 | num_queries: int, |
| 3558 | heads_blocks: int, |
| 3559 | avg_main_len: float = 0.0, |
| 3560 | avg_extra_len: float = 0.0, |
| 3561 | block_k: int = 32, |
| 3562 | ) -> int: |
| 3563 | """Pick a flash-decode split count to keep the GPU busy across batch sizes. |
| 3564 | |
| 3565 | Decode launches only ``num_queries * heads_blocks`` workgroups otherwise, |
| 3566 | which severely under-fills the device for the low-concurrency regime that |
| 3567 | dominates latency. Splitting the KV sequence adds parallelism. |
| 3568 | |
| 3569 | We model the relative partial-kernel latency for a given split count ``s`` |
| 3570 | as ``waves * (1/s + mu)`` where ``waves = ceil(base * s / CU)`` and ``mu`` |
| 3571 | is a small per-wave overhead penalty: |
| 3572 | |
| 3573 | - ``waves / s`` captures the partial compute: each wave walks roughly |
| 3574 | ``total_tokens / s`` tokens and there are ``waves`` of them, so dividing |
| 3575 | by ``s`` makes more splits cheaper *until* they spill into extra waves. |
| 3576 | - ``mu * waves`` charges per-wave launch/tail overhead so we do not |
| 3577 | over-split into many mostly-idle waves (e.g. batch 224 on 256 CUs is |
| 3578 | best left at 1 split rather than 8 splits across 7 waves). |
| 3579 | |
| 3580 | The minimiser naturally prefers split counts that pack the device into full |
| 3581 | waves (``base * s`` near a multiple of ``CU``) and falls back to 1 split |
| 3582 | once the batch already fills the device. Ties favour the smaller split |
| 3583 | count (less reduce work). |
| 3584 | |
| 3585 | Finally we "snap down" the chosen split count to the smallest value that |
| 3586 | yields the same wave count *and* the same per-workgroup BLOCK_K iteration |
| 3587 | count. Because latency tracks iteration count (not raw token count), extra |
| 3588 | splits that do not lower the iteration count add only reduce/HBM overhead |
| 3589 | for no parallelism gain (e.g. batch 24: s8 and s10 both walk 4 extra iters |
| 3590 | in one wave, so s8 is strictly better). Snapping needs the average segment |
| 3591 | lengths, which the caller derives sync-free from the ragged index sizes. |
| 3592 | """ |
| 3593 | base = max(1, num_queries * heads_blocks) |
| 3594 | # Target ~1 workgroup per CU: enough to fill the device while keeping the |
| 3595 | # reduce cost (which grows with split count) small. Tuned on gfx950. |
| 3596 | cu = max(1, _decode_cu_count()) |
| 3597 | # Per-wave overhead penalty: higher values discourage split counts that |
| 3598 | # spill into extra GPU waves. Tuned on gfx950. |
| 3599 | mu = 0.04 |
| 3600 | best_splits = 1 |
| 3601 | best_cost = None |
| 3602 | # Search up to 16 splits; beyond that the reduce/HBM overhead dominates. |
| 3603 | for splits in range(1, 17): |
| 3604 | waves = (base * splits + cu - 1) // cu |
| 3605 | cost = waves * (1.0 / splits + mu) |
| 3606 | if best_cost is None or cost < best_cost - 1e-9: |
| 3607 | best_splits = splits |
| 3608 | best_cost = cost |
| 3609 | |
| 3610 | if best_splits > 1 and (avg_main_len > 0 or avg_extra_len > 0): |
| 3611 | target_waves = (base * best_splits + cu - 1) // cu |
| 3612 | target_iters = _decode_partial_iters( |
| 3613 | avg_main_len, avg_extra_len, best_splits, block_k |
| 3614 | ) |
| 3615 | for splits in range(1, best_splits): |
| 3616 | waves = (base * splits + cu - 1) // cu |
| 3617 | iters = _decode_partial_iters(avg_main_len, avg_extra_len, splits, block_k) |
| 3618 | if waves == target_waves and iters == target_iters: |
| 3619 | best_splits = splits |
| 3620 | break |
| 3621 | return best_splits |
| 3622 | |
| 3623 | |
| 3624 | def _decode_gfx950_num_splits( |
| 3625 | num_queries: int, |
| 3626 | heads_blocks: int, |
| 3627 | avg_main_len: float = 0.0, |
| 3628 | avg_extra_len: float = 0.0, |
| 3629 | block_k: int = 32, |
| 3630 | ) -> int: |
| 3631 | base = max(1, num_queries * heads_blocks) |
| 3632 | cu = max(1, _decode_cu_count()) |
| 3633 | target_workgroups = 2 * cu |
| 3634 | num_splits = min( |
| 3635 | 32, |
| 3636 | max( |
| 3637 | 1, |
| 3638 | math.ceil(target_workgroups / base), |
| 3639 | ), |
| 3640 | ) |
| 3641 | if ( |
| 3642 | base >= 16 |
| 3643 | and num_splits > 4 |
| 3644 | and _decode_partial_iters(avg_main_len, avg_extra_len, 4, block_k) <= 3 |
| 3645 | ): |
| 3646 | return 4 |
| 3647 | if 16 <= base < 64: |
| 3648 | one_wave_splits = max(1, cu // base) |
| 3649 | one_wave_iters = _decode_partial_iters( |
| 3650 | avg_main_len, avg_extra_len, one_wave_splits, block_k |
| 3651 | ) |
| 3652 | target_waves = 1 if one_wave_iters <= 9 else 2 |
| 3653 | num_splits = min(num_splits, max(1, target_waves * cu // base)) |
| 3654 | if base >= 16 and num_splits > 1: |
| 3655 | target_waves = (base * num_splits + cu - 1) // cu |
| 3656 | target_iters = _decode_partial_iters( |
| 3657 | avg_main_len, avg_extra_len, num_splits, block_k |
| 3658 | ) |
| 3659 | for splits in range(1, num_splits): |
| 3660 | waves = (base * splits + cu - 1) // cu |
| 3661 | iters = _decode_partial_iters(avg_main_len, avg_extra_len, splits, block_k) |
| 3662 | if waves == target_waves and iters == target_iters: |
| 3663 | return splits |
| 3664 | return num_splits |
| 3665 | |
| 3666 | |
| 3667 | def _rocm_sparse_attn_decode_ragged_triton( |
| 3668 | q: torch.Tensor, |
| 3669 | main_cache: torch.Tensor, |
| 3670 | main_indices: torch.Tensor, |
| 3671 | main_indptr: torch.Tensor, |
| 3672 | scale: float, |
| 3673 | attn_sink: torch.Tensor | None, |
| 3674 | nope_head_dim: int, |
| 3675 | rope_head_dim: int, |
| 3676 | extra_cache: torch.Tensor | None = None, |
| 3677 | extra_indices: torch.Tensor | None = None, |
| 3678 | extra_indptr: torch.Tensor | None = None, |
| 3679 | out: torch.Tensor | None = None, |
| 3680 | extra_cache_nan_free: bool = False, |
| 3681 | adaptive_splits: bool = False, |
| 3682 | inv_rope_positions: torch.Tensor | None = None, |
| 3683 | inv_rope_cos_sin_cache: torch.Tensor | None = None, |
| 3684 | out_mxfp8: tuple[torch.Tensor, torch.Tensor] | None = None, |
| 3685 | ) -> torch.Tensor: |
| 3686 | """Split-K sparse decode; returns the attention output. |
| 3687 | |
| 3688 | With ``out_mxfp8 = (data, scale)`` the reduce writes MXFP8 instead of |
| 3689 | bf16: ``data`` is [b, h * d] e4m3 and ``scale`` [b, h * d // 32] E8M0, and |
| 3690 | ``data`` viewed as [b, h, d] is returned. |
| 3691 | """ |
| 3692 | assert q.ndim == 3, f"expected q=[b,h,d], got {q.shape}" |
| 3693 | assert main_cache.ndim == 3, ( |
| 3694 | f"expected main_cache=[blocks,block,bytes], got {main_cache.shape}" |
| 3695 | ) |
| 3696 | assert main_indices.ndim == 1, ( |
| 3697 | f"expected main_indices=[nnz], got {main_indices.shape}" |
| 3698 | ) |
| 3699 | assert main_indptr.ndim == 1, f"expected main_indptr=[b+1], got {main_indptr.shape}" |
| 3700 | assert ( |
| 3701 | not q.is_cpu |
| 3702 | and not main_cache.is_cpu |
| 3703 | and not main_indices.is_cpu |
| 3704 | and not main_indptr.is_cpu |
| 3705 | ) |
| 3706 | |
| 3707 | main_indices = _as_int32_contiguous_1d(main_indices) |
| 3708 | main_indptr = _as_int32_contiguous_1d(main_indptr) |
| 3709 | has_attn_sink = attn_sink is not None |
| 3710 | if attn_sink is None: |
| 3711 | attn_sink = torch.empty(1, device=q.device, dtype=torch.float32) |
| 3712 | else: |
| 3713 | attn_sink = attn_sink.contiguous() |
| 3714 | |
| 3715 | num_queries, num_heads, head_dim = q.shape |
| 3716 | assert main_indptr.numel() == num_queries + 1, ( |
| 3717 | f"expected main_indptr shape [{num_queries + 1}], got {main_indptr.shape}" |
| 3718 | ) |
| 3719 | _validate_dsv4_sparse_dims( |
| 3720 | head_dim, |
| 3721 | nope_head_dim, |
| 3722 | rope_head_dim, |
| 3723 | "_rocm_sparse_attn_decode_ragged_triton", |
| 3724 | ) |
| 3725 | |
| 3726 | has_extra = ( |
| 3727 | extra_cache is not None |
| 3728 | and extra_indices is not None |
| 3729 | and extra_indptr is not None |
| 3730 | ) |
| 3731 | assert not extra_cache_nan_free or (_ON_GFX950 and has_extra), ( |
| 3732 | "extra_cache_nan_free requires a gfx950 compressed cache with trusted " |
| 3733 | "canonical-writer provenance" |
| 3734 | ) |
| 3735 | if has_extra: |
| 3736 | assert extra_cache is not None |
| 3737 | assert extra_indices is not None |
| 3738 | assert extra_indptr is not None |
| 3739 | assert extra_indices.ndim == 1, ( |
| 3740 | f"expected extra_indices=[nnz], got {extra_indices.shape}" |
| 3741 | ) |
| 3742 | assert extra_indptr.ndim == 1, ( |
| 3743 | f"expected extra_indptr=[b+1], got {extra_indptr.shape}" |
| 3744 | ) |
| 3745 | extra_indices = _as_int32_contiguous_1d(extra_indices) |
| 3746 | extra_indptr = _as_int32_contiguous_1d(extra_indptr) |
| 3747 | assert extra_indptr.numel() == num_queries + 1, ( |
| 3748 | f"expected extra_indptr shape [{num_queries + 1}], got {extra_indptr.shape}" |
| 3749 | ) |
| 3750 | else: |
| 3751 | extra_cache = main_cache |
| 3752 | extra_indices = torch.empty(0, device=q.device, dtype=torch.int32) |
| 3753 | extra_indptr = torch.zeros(num_queries + 1, device=q.device, dtype=torch.int32) |
| 3754 | |
| 3755 | block_h = 16 |
| 3756 | out_scale = None |
| 3757 | if out_mxfp8 is not None: |
| 3758 | assert out is None, "out and out_mxfp8 are mutually exclusive" |
| 3759 | assert _ON_GFX950, "the MXFP8 reduce epilogue is gfx950-only" |
| 3760 | assert inv_rope_positions is not None, ( |
| 3761 | "the MXFP8 output feeds wo_a, so it must be inverse-RoPE'd first" |
| 3762 | ) |
| 3763 | out_data, out_scale = out_mxfp8 |
| 3764 | assert out_data.dtype == torch.float8_e4m3fn and out_scale.dtype == ( |
| 3765 | torch.uint8 |
| 3766 | ), f"expected e4m3/uint8 MXFP8 buffers, got {out_data.dtype}/{out_scale.dtype}" |
| 3767 | assert out_data.shape == (num_queries, num_heads * head_dim), ( |
| 3768 | f"expected MXFP8 data [{num_queries}, {num_heads * head_dim}], " |
| 3769 | f"got {tuple(out_data.shape)}" |
| 3770 | ) |
| 3771 | assert out_scale.shape == (num_queries, num_heads * head_dim // 32), ( |
| 3772 | f"expected MXFP8 scale [{num_queries}, {num_heads * head_dim // 32}], " |
| 3773 | f"got {tuple(out_scale.shape)}" |
| 3774 | ) |
| 3775 | assert out_data.stride(-1) == 1 and out_scale.stride(-1) == 1 |
| 3776 | out = out_data.view(num_queries, num_heads, head_dim) |
| 3777 | elif out is None: |
| 3778 | out = torch.empty_like(q, dtype=torch.bfloat16) |
| 3779 | else: |
| 3780 | assert out.shape == q.shape, f"expected out shape {q.shape}, got {out.shape}" |
| 3781 | assert out.device == q.device, ( |
| 3782 | f"expected out on device {q.device}, got {out.device}" |
| 3783 | ) |
| 3784 | assert out.dtype == torch.bfloat16, ( |
| 3785 | f"expected out dtype {torch.bfloat16}, got {out.dtype}" |
| 3786 | ) |
| 3787 | heads_blocks = triton.cdiv(num_heads, block_h) |
| 3788 | nope_block = triton.next_power_of_2(nope_head_dim) |
| 3789 | comb_dim = nope_head_dim + rope_head_dim |
| 3790 | is_fnuz = current_platform.is_fp8_fnuz() |
| 3791 | |
| 3792 | if not (_ON_GFX942 or _ON_GFX950): # Fallback path for un-tuned architectures. |
| 3793 | block_k = 16 if head_dim >= 256 else 32 |
| 3794 | _sparse_attn_decode_ragged_kernel[(num_queries, heads_blocks)]( |
| 3795 | q, |
| 3796 | main_cache, |
| 3797 | main_indices, |
| 3798 | main_indptr, |
| 3799 | extra_cache, |
| 3800 | extra_indices, |
| 3801 | extra_indptr, |
| 3802 | attn_sink, |
| 3803 | out, |
| 3804 | q.stride(0), |
| 3805 | q.stride(1), |
| 3806 | out.stride(0), |
| 3807 | out.stride(1), |
| 3808 | main_cache.stride(0), |
| 3809 | extra_cache.stride(0), |
| 3810 | main_cache.shape[0] * main_cache.shape[1], |
| 3811 | extra_cache.shape[0] * extra_cache.shape[1], |
| 3812 | main_cache.shape[1], |
| 3813 | extra_cache.shape[1], |
| 3814 | scale, |
| 3815 | num_heads, |
| 3816 | HAS_ATTN_SINK=has_attn_sink, |
| 3817 | HAS_EXTRA=has_extra, |
| 3818 | NOPE_DIM=nope_head_dim, |
| 3819 | NOPE_BLOCK=nope_block, |
| 3820 | ROPE_DIM=rope_head_dim, |
| 3821 | IS_FNUZ_MAIN=is_fnuz, |
| 3822 | IS_FNUZ_EXTRA=False, |
| 3823 | BLOCK_H=block_h, |
| 3824 | BLOCK_K=block_k, |
| 3825 | num_warps=8, |
| 3826 | ) |
| 3827 | return out |
| 3828 | |
| 3829 | block_k = 32 # KV tokens walked per split-K iteration. Tuned on gfx950. |
| 3830 | if _ON_GFX950: |
| 3831 | inv_q = 1.0 / max(1, num_queries) |
| 3832 | avg_main_len = main_indices.numel() * inv_q |
| 3833 | avg_extra_len = (extra_indices.numel() * inv_q) if has_extra else 0.0 |
| 3834 | num_splits = _decode_gfx950_num_splits( |
| 3835 | num_queries, |
| 3836 | heads_blocks, |
| 3837 | avg_main_len, |
| 3838 | avg_extra_len, |
| 3839 | block_k, |
| 3840 | ) |
| 3841 | else: |
| 3842 | # Average per-query segment lengths, read sync-free from the ragged |
| 3843 | # index sizes, let the split heuristic avoid over-splitting. |
| 3844 | inv_q = 1.0 / max(1, num_queries) |
| 3845 | avg_main_len = main_indices.numel() * inv_q |
| 3846 | avg_extra_len = (extra_indices.numel() * inv_q) if has_extra else 0.0 |
| 3847 | num_splits = _decode_num_splits( |
| 3848 | num_queries, heads_blocks, avg_main_len, avg_extra_len, block_k |
| 3849 | ) |
| 3850 | |
| 3851 | base_workgroups = num_queries * heads_blocks |
| 3852 | adaptive_splits = ( |
| 3853 | _ON_GFX950 and adaptive_splits and base_workgroups >= 16 and num_splits > 4 |
| 3854 | ) |
| 3855 | one_wave_splits = ( |
| 3856 | max(1, _decode_cu_count() // base_workgroups) |
| 3857 | if adaptive_splits and 16 <= base_workgroups < 64 |
| 3858 | else num_splits |
| 3859 | ) |
| 3860 | |
| 3861 | part_m = torch.empty( |
| 3862 | (num_queries, num_splits, num_heads), dtype=torch.float32, device=q.device |
| 3863 | ) |
| 3864 | part_l = torch.empty_like(part_m) |
| 3865 | part_acc = torch.empty( |
| 3866 | (num_queries, num_splits, num_heads, comb_dim), |
| 3867 | dtype=torch.float32, |
| 3868 | device=q.device, |
| 3869 | ) |
| 3870 | |
| 3871 | if _ON_GFX950: |
| 3872 | _sparse_attn_decode_gfx950_partial_kernel[ |
| 3873 | (num_queries, num_splits, heads_blocks) |
| 3874 | ]( |
| 3875 | q, |
| 3876 | main_cache, |
| 3877 | main_indices, |
| 3878 | main_indptr, |
| 3879 | extra_cache, |
| 3880 | extra_indices, |
| 3881 | extra_indptr, |
| 3882 | part_m, |
| 3883 | part_l, |
| 3884 | part_acc, |
| 3885 | q.stride(0), |
| 3886 | q.stride(1), |
| 3887 | main_cache.stride(0), |
| 3888 | extra_cache.stride(0), |
| 3889 | main_cache.shape[0] * main_cache.shape[1], |
| 3890 | extra_cache.shape[0] * extra_cache.shape[1], |
| 3891 | main_cache.shape[1], |
| 3892 | extra_cache.shape[1], |
| 3893 | scale, |
| 3894 | num_heads, |
| 3895 | HAS_EXTRA=has_extra, |
| 3896 | NOPE_DIM=nope_head_dim, |
| 3897 | ROPE_DIM=rope_head_dim, |
| 3898 | IS_FNUZ_MAIN=is_fnuz, |
| 3899 | IS_FNUZ_EXTRA=False, |
| 3900 | TRUST_EXTRA_CACHE_NAN_FREE=extra_cache_nan_free, |
| 3901 | ADAPTIVE_SPLITS=adaptive_splits, |
| 3902 | ONE_WAVE_SPLITS=one_wave_splits, |
| 3903 | BLOCK_H=block_h, |
| 3904 | BLOCK_K=block_k, |
| 3905 | NUM_SPLITS=num_splits, |
| 3906 | NUM_STAGES=1, |
| 3907 | num_warps=4, |
| 3908 | waves_per_eu=0, |
| 3909 | ) |
| 3910 | else: |
| 3911 | _sparse_attn_decode_partial_kernel[(num_queries, num_splits, heads_blocks)]( |
| 3912 | q, |
| 3913 | main_cache, |
| 3914 | main_indices, |
| 3915 | main_indptr, |
| 3916 | extra_cache, |
| 3917 | extra_indices, |
| 3918 | extra_indptr, |
| 3919 | part_m, |
| 3920 | part_l, |
| 3921 | part_acc, |
| 3922 | q.stride(0), |
| 3923 | q.stride(1), |
| 3924 | main_cache.stride(0), |
| 3925 | extra_cache.stride(0), |
| 3926 | part_m.stride(0), |
| 3927 | part_m.stride(1), |
| 3928 | part_acc.stride(0), |
| 3929 | part_acc.stride(1), |
| 3930 | part_acc.stride(2), |
| 3931 | main_cache.shape[0] * main_cache.shape[1], |
| 3932 | extra_cache.shape[0] * extra_cache.shape[1], |
| 3933 | main_cache.shape[1], |
| 3934 | extra_cache.shape[1], |
| 3935 | scale, |
| 3936 | num_heads, |
| 3937 | HAS_EXTRA=has_extra, |
| 3938 | NOPE_DIM=nope_head_dim, |
| 3939 | NOPE_BLOCK=nope_block, |
| 3940 | ROPE_DIM=rope_head_dim, |
| 3941 | # main_cache = swa_k_cache (C++ encoder, FNUZ on gfx942 / OCP on gfx950). |
| 3942 | # extra_cache = compressed kv_cache (Triton encoder, OCP everywhere). |
| 3943 | # Reading both with a single IS_FNUZ would decode one of them with the |
| 3944 | # wrong FNUZ/OCP scale ratio (~1.87×). |
| 3945 | IS_FNUZ_MAIN=is_fnuz, |
| 3946 | IS_FNUZ_EXTRA=False, |
| 3947 | BLOCK_H=block_h, |
| 3948 | BLOCK_K=block_k, |
| 3949 | NUM_SPLITS=num_splits, |
| 3950 | NUM_STAGES=1, |
| 3951 | num_warps=4, |
| 3952 | ) |
| 3953 | |
| 3954 | fuse_inv_rope = inv_rope_positions is not None |
| 3955 | if fuse_inv_rope: |
| 3956 | assert inv_rope_cos_sin_cache is not None |
| 3957 | assert inv_rope_cos_sin_cache.shape[-1] == rope_head_dim, ( |
| 3958 | "fused inverse RoPE expects cos_sin_cache laid out as " |
| 3959 | f"[P, {rope_head_dim}] = cos | sin, got " |
| 3960 | f"{tuple(inv_rope_cos_sin_cache.shape)}" |
| 3961 | ) |
| 3962 | assert nope_head_dim % 2 == 0 and rope_head_dim % 2 == 0, ( |
| 3963 | "fused inverse RoPE pairs adjacent lanes, so both head dims must " |
| 3964 | f"be even, got nope={nope_head_dim} rope={rope_head_dim}" |
| 3965 | ) |
| 3966 | |
| 3967 | _sparse_attn_decode_reduce_kernel[(num_queries, num_heads)]( |
| 3968 | part_m, |
| 3969 | part_l, |
| 3970 | part_acc, |
| 3971 | attn_sink, |
| 3972 | out, |
| 3973 | inv_rope_positions, |
| 3974 | inv_rope_cos_sin_cache, |
| 3975 | out_scale, |
| 3976 | out.stride(0), |
| 3977 | out.stride(1), |
| 3978 | out_scale.stride(0) if out_scale is not None else 0, |
| 3979 | head_dim // 32, |
| 3980 | part_m.stride(0), |
| 3981 | part_m.stride(1), |
| 3982 | part_acc.stride(0), |
| 3983 | part_acc.stride(1), |
| 3984 | part_acc.stride(2), |
| 3985 | inv_rope_cos_sin_cache.stride(0) if inv_rope_cos_sin_cache is not None else 0, |
| 3986 | num_heads, |
| 3987 | HAS_ATTN_SINK=has_attn_sink, |
| 3988 | ADAPTIVE_SPLITS=adaptive_splits, |
| 3989 | COMB_DIM=comb_dim, |
| 3990 | BLOCK_H=1, |
| 3991 | NUM_SPLITS=num_splits, |
| 3992 | SPLITS_PAD=triton.next_power_of_2(num_splits), |
| 3993 | FUSE_INV_ROPE=fuse_inv_rope, |
| 3994 | NOPE=nope_head_dim, |
| 3995 | HALF=rope_head_dim // 2, |
| 3996 | QUANT_OUT=out_scale is not None, |
| 3997 | num_warps=4, |
| 3998 | ) |
| 3999 | return out |
| 4000 | |
| 4001 | |
| 4002 | def _rocm_sparse_attn_decode_triton( |
| 4003 | q: torch.Tensor, |
| 4004 | main_cache: torch.Tensor, |
| 4005 | main_indices: torch.Tensor, |
| 4006 | scale: float, |
| 4007 | attn_sink: torch.Tensor | None, |
| 4008 | nope_head_dim: int, |
| 4009 | rope_head_dim: int, |
| 4010 | extra_cache: torch.Tensor | None = None, |
| 4011 | extra_indices: torch.Tensor | None = None, |
| 4012 | main_lengths: torch.Tensor | None = None, |
| 4013 | extra_lengths: torch.Tensor | None = None, |
| 4014 | main_ragged_indices: torch.Tensor | None = None, |
| 4015 | main_ragged_indptr: torch.Tensor | None = None, |
| 4016 | extra_ragged_indices: torch.Tensor | None = None, |
| 4017 | extra_ragged_indptr: torch.Tensor | None = None, |
| 4018 | out: torch.Tensor | None = None, |
| 4019 | extra_cache_nan_free: bool = False, |
| 4020 | adaptive_splits: bool = False, |
| 4021 | inv_rope_positions: torch.Tensor | None = None, |
| 4022 | inv_rope_cos_sin_cache: torch.Tensor | None = None, |
| 4023 | out_mxfp8: tuple[torch.Tensor, torch.Tensor] | None = None, |
| 4024 | ) -> torch.Tensor: |
| 4025 | if main_ragged_indices is None or main_ragged_indptr is None: |
| 4026 | main_ragged_indices, main_ragged_indptr = build_ragged_indices_from_dense( |
| 4027 | main_indices, |
| 4028 | main_lengths |
| 4029 | if main_lengths is not None |
| 4030 | else (main_indices >= 0).sum(dim=-1, dtype=torch.int32), |
| 4031 | num_rows=main_cache.shape[0] * main_cache.shape[1], |
| 4032 | ) |
| 4033 | |
| 4034 | if ( |
| 4035 | (extra_ragged_indices is None or extra_ragged_indptr is None) |
| 4036 | and extra_cache is not None |
| 4037 | and extra_indices is not None |
| 4038 | ): |
| 4039 | extra_ragged_indices, extra_ragged_indptr = build_ragged_indices_from_dense( |
| 4040 | extra_indices, |
| 4041 | extra_lengths |
| 4042 | if extra_lengths is not None |
| 4043 | else (extra_indices >= 0).sum(dim=-1, dtype=torch.int32), |
| 4044 | num_rows=extra_cache.shape[0] * extra_cache.shape[1], |
| 4045 | ) |
| 4046 | |
| 4047 | return _rocm_sparse_attn_decode_ragged_triton( |
| 4048 | q=q, |
| 4049 | main_cache=main_cache, |
| 4050 | main_indices=main_ragged_indices, |
| 4051 | main_indptr=main_ragged_indptr, |
| 4052 | scale=scale, |
| 4053 | attn_sink=attn_sink, |
| 4054 | nope_head_dim=nope_head_dim, |
| 4055 | rope_head_dim=rope_head_dim, |
| 4056 | extra_cache=extra_cache, |
| 4057 | extra_indices=extra_ragged_indices, |
| 4058 | extra_indptr=extra_ragged_indptr, |
| 4059 | out=out, |
| 4060 | extra_cache_nan_free=extra_cache_nan_free, |
| 4061 | adaptive_splits=adaptive_splits, |
| 4062 | inv_rope_positions=inv_rope_positions, |
| 4063 | inv_rope_cos_sin_cache=inv_rope_cos_sin_cache, |
| 4064 | out_mxfp8=out_mxfp8, |
| 4065 | ) |
| 4066 | |
| 4067 | |
| 4068 | def rocm_sparse_attn_prefill( |
| 4069 | q: torch.Tensor, |
| 4070 | kv: torch.Tensor, |
| 4071 | indices: torch.Tensor | None, |
| 4072 | topk_length: torch.Tensor | None, |
| 4073 | scale: float, |
| 4074 | head_dim: int, |
| 4075 | nope_head_dim: int, |
| 4076 | rope_head_dim: int, |
| 4077 | attn_sink: torch.Tensor | None, |
| 4078 | output: torch.Tensor, |
| 4079 | ragged_indices: torch.Tensor | None = None, |
| 4080 | ragged_indptr: torch.Tensor | None = None, |
| 4081 | ) -> None: |
| 4082 | assert kv.ndim == 3 and kv.shape[1] == 1, ( |
| 4083 | f"ROCm Triton sparse prefill expects kv=[skv,1,d], got {kv.shape}" |
| 4084 | ) |
| 4085 | _validate_sparse_dims( |
| 4086 | head_dim, |
| 4087 | nope_head_dim, |
| 4088 | rope_head_dim, |
| 4089 | "rocm_sparse_attn_prefill", |
| 4090 | ) |
| 4091 | opus_attn_sink = None if attn_sink is None else attn_sink[: q.shape[1]] |
| 4092 | if ( |
| 4093 | _can_use_aiter_sparse_prefill_opus(q, kv.squeeze(1), opus_attn_sink, output) |
| 4094 | and _get_aiter_sparse_prefill_opus() is not None |
| 4095 | ): |
| 4096 | if ragged_indices is None or ragged_indptr is None: |
| 4097 | assert indices is not None |
| 4098 | indices_2d = indices.reshape(indices.shape[0], -1) |
| 4099 | ragged_indices, ragged_indptr = build_ragged_indices_from_dense( |
| 4100 | indices_2d, |
| 4101 | topk_length |
| 4102 | if topk_length is not None |
| 4103 | else (indices_2d >= 0).sum(dim=-1, dtype=torch.int32), |
| 4104 | num_rows=kv.shape[0], |
| 4105 | ) |
| 4106 | assert opus_attn_sink is not None |
| 4107 | if _rocm_sparse_attn_prefill_ragged_aiter_opus( |
| 4108 | q=q, |
| 4109 | kv=kv.squeeze(1), |
| 4110 | indices=ragged_indices, |
| 4111 | indptr=ragged_indptr, |
| 4112 | scale=scale, |
| 4113 | attn_sink=opus_attn_sink, |
| 4114 | output=output, |
| 4115 | ): |
| 4116 | return |
| 4117 | |
| 4118 | if ragged_indices is not None and ragged_indptr is not None: |
| 4119 | _rocm_sparse_attn_prefill_ragged_triton( |
| 4120 | q=q, |
| 4121 | kv=kv.squeeze(1), |
| 4122 | indices=ragged_indices, |
| 4123 | indptr=ragged_indptr, |
| 4124 | scale=scale, |
| 4125 | attn_sink=None if attn_sink is None else attn_sink[: q.shape[1]], |
| 4126 | nope_head_dim=nope_head_dim, |
| 4127 | rope_head_dim=rope_head_dim, |
| 4128 | out=output, |
| 4129 | ) |
| 4130 | else: |
| 4131 | assert indices is not None |
| 4132 | indices_2d = indices.reshape(indices.shape[0], -1) |
| 4133 | _rocm_sparse_attn_prefill_triton( |
| 4134 | q=q, |
| 4135 | kv=kv.squeeze(1), |
| 4136 | indices=indices_2d, |
| 4137 | scale=scale, |
| 4138 | attn_sink=None if attn_sink is None else attn_sink[: q.shape[1]], |
| 4139 | nope_head_dim=nope_head_dim, |
| 4140 | rope_head_dim=rope_head_dim, |
| 4141 | topk_length=topk_length, |
| 4142 | out=output, |
| 4143 | ) |
| 4144 | |
| 4145 | |
| 4146 | def rocm_sparse_attn_decode( |
| 4147 | q: torch.Tensor, |
| 4148 | kv_cache: torch.Tensor | None, |
| 4149 | swa_k_cache: torch.Tensor, |
| 4150 | swa_only: bool, |
| 4151 | topk_indices: torch.Tensor | None, |
| 4152 | topk_lens: torch.Tensor | None, |
| 4153 | swa_indices: torch.Tensor, |
| 4154 | swa_lens: torch.Tensor, |
| 4155 | swa_ragged_indices: torch.Tensor | None, |
| 4156 | swa_ragged_indptr: torch.Tensor | None, |
| 4157 | topk_ragged_indices: torch.Tensor | None, |
| 4158 | topk_ragged_indptr: torch.Tensor | None, |
| 4159 | attn_sink: torch.Tensor | None, |
| 4160 | scale: float, |
| 4161 | head_dim: int, |
| 4162 | nope_head_dim: int, |
| 4163 | rope_head_dim: int, |
| 4164 | output: torch.Tensor | None, |
| 4165 | extra_cache_nan_free: bool = False, |
| 4166 | adaptive_splits: bool = False, |
| 4167 | inv_rope_positions: torch.Tensor | None = None, |
| 4168 | inv_rope_cos_sin_cache: torch.Tensor | None = None, |
| 4169 | output_mxfp8: tuple[torch.Tensor, torch.Tensor] | None = None, |
| 4170 | ) -> int: |
| 4171 | """Run sparse MLA decode into ``output``. |
| 4172 | |
| 4173 | Passing ``inv_rope_positions`` folds the inverse RoPE into the reduce |
| 4174 | epilogue. Returns how many leading rows of ``output`` came back rotated, |
| 4175 | so a caller mixing in a decode path that does not fuse still knows what it |
| 4176 | owes the standalone pass. Read it from the eager attention segment only. |
| 4177 | |
| 4178 | ``output_mxfp8 = (data, scale)`` replaces ``output``: the reduce also |
| 4179 | MXFP8-quantizes the rotated rows for the FP8 wo_a (see |
| 4180 | ``_rocm_sparse_attn_decode_ragged_triton``). It needs gfx950 and the fused |
| 4181 | inverse RoPE, and always covers every row. |
| 4182 | """ |
| 4183 | assert swa_k_cache.dtype == torch.uint8, ( |
| 4184 | "ROCm Triton sparse decode expects uint8 fp8_ds_mla SWA cache, " |
| 4185 | f"got {swa_k_cache.dtype}" |
| 4186 | ) |
| 4187 | _validate_dsv4_sparse_dims( |
| 4188 | head_dim, |
| 4189 | nope_head_dim, |
| 4190 | rope_head_dim, |
| 4191 | "rocm_sparse_attn_decode", |
| 4192 | ) |
| 4193 | |
| 4194 | main_indices = swa_indices.reshape(swa_indices.shape[0], -1) |
| 4195 | |
| 4196 | extra_cache = None |
| 4197 | extra_indices = None |
| 4198 | if not swa_only: |
| 4199 | assert kv_cache is not None |
| 4200 | assert topk_indices is not None or ( |
| 4201 | topk_ragged_indices is not None and topk_ragged_indptr is not None |
| 4202 | ) |
| 4203 | assert kv_cache.dtype == torch.uint8, ( |
| 4204 | "ROCm Triton sparse decode expects uint8 fp8_ds_mla extra cache, " |
| 4205 | f"got {kv_cache.dtype}" |
| 4206 | ) |
| 4207 | extra_cache = kv_cache |
| 4208 | if topk_indices is not None: |
| 4209 | extra_indices = topk_indices.reshape(topk_indices.shape[0], -1) |
| 4210 | |
| 4211 | if output_mxfp8 is not None: |
| 4212 | assert output is None, "output and output_mxfp8 are mutually exclusive" |
| 4213 | direct_out = None |
| 4214 | else: |
| 4215 | assert output is not None |
| 4216 | direct_out = output if _ON_GFX950 and output.dtype == torch.bfloat16 else None |
| 4217 | attn_out = _rocm_sparse_attn_decode_triton( |
| 4218 | q=q, |
| 4219 | main_cache=swa_k_cache, |
| 4220 | main_indices=main_indices, |
| 4221 | scale=scale, |
| 4222 | attn_sink=None if attn_sink is None else attn_sink[: q.shape[1]], |
| 4223 | nope_head_dim=nope_head_dim, |
| 4224 | rope_head_dim=rope_head_dim, |
| 4225 | extra_cache=extra_cache, |
| 4226 | extra_indices=extra_indices, |
| 4227 | main_lengths=swa_lens, |
| 4228 | extra_lengths=topk_lens, |
| 4229 | main_ragged_indices=swa_ragged_indices, |
| 4230 | main_ragged_indptr=swa_ragged_indptr, |
| 4231 | extra_ragged_indices=topk_ragged_indices, |
| 4232 | extra_ragged_indptr=topk_ragged_indptr, |
| 4233 | out=direct_out, |
| 4234 | extra_cache_nan_free=extra_cache_nan_free, |
| 4235 | adaptive_splits=adaptive_splits, |
| 4236 | inv_rope_positions=inv_rope_positions, |
| 4237 | inv_rope_cos_sin_cache=inv_rope_cos_sin_cache, |
| 4238 | out_mxfp8=output_mxfp8, |
| 4239 | ) |
| 4240 | if output_mxfp8 is not None: |
| 4241 | return q.shape[0] |
| 4242 | assert output is not None |
| 4243 | if direct_out is None: |
| 4244 | output.copy_(attn_out.to(output.dtype)) |
| 4245 | return output.shape[0] if inv_rope_positions is not None else 0 |
| 4246 | |
| 4247 | |
| 4248 | @triton.jit(do_not_specialize=["token_offset"]) |
| 4249 | def _prefill_topk_global_slots_kernel( |
| 4250 | out_ptr, |
| 4251 | lens_ptr, |
| 4252 | topk_ptr, |
| 4253 | topk_stride, |
| 4254 | token_to_req_ptr, |
| 4255 | query_start_loc_ptr, |
| 4256 | seq_lens_ptr, |
| 4257 | is_valid_token_ptr, |
| 4258 | block_table_ptr, |
| 4259 | block_table_stride, |
| 4260 | token_offset, |
| 4261 | num_compressed, |
| 4262 | TOPK: tl.constexpr, |
| 4263 | COMPRESS_RATIO: tl.constexpr, |
| 4264 | BLOCK_SIZE: tl.constexpr, |
| 4265 | BLOCK_W: tl.constexpr, |
| 4266 | ): |
| 4267 | row = tl.program_id(0) |
| 4268 | token = row + token_offset |
| 4269 | req = tl.load(token_to_req_ptr + token) |
| 4270 | query_start = tl.load(query_start_loc_ptr + req) |
| 4271 | query_len = tl.load(query_start_loc_ptr + req + 1) - query_start |
| 4272 | pos = tl.load(seq_lens_ptr + req) - query_len + token - query_start |
| 4273 | # Only (pos + 1) // ratio compressed tokens exist yet; the rest of the row |
| 4274 | # can hold stale indices. |
| 4275 | row_len = tl.minimum((pos + 1) // COMPRESS_RATIO, TOPK) |
| 4276 | row_len = tl.where(tl.load(is_valid_token_ptr + token), row_len, 0) |
| 4277 | |
| 4278 | for start in range(0, TOPK, BLOCK_W): |
| 4279 | cols = start + tl.arange(0, BLOCK_W) |
| 4280 | live = cols < row_len |
| 4281 | local = tl.load(topk_ptr + row * topk_stride + cols, mask=live, other=-1) |
| 4282 | valid = live & (local >= 0) & (local < num_compressed) |
| 4283 | local = tl.where(valid, local, 0) |
| 4284 | block = tl.load( |
| 4285 | block_table_ptr + req * block_table_stride + local // BLOCK_SIZE, |
| 4286 | mask=valid, |
| 4287 | other=0, |
| 4288 | ) |
| 4289 | slot = tl.where(valid, block * BLOCK_SIZE + local % BLOCK_SIZE, -1) |
| 4290 | tl.store(out_ptr + row * TOPK + cols, slot, mask=cols < TOPK) |
| 4291 | tl.store(lens_ptr + row, row_len) |
| 4292 | |
| 4293 | |
| 4294 | def build_prefill_topk_ragged_indices( |
| 4295 | topk_indices: torch.Tensor, |
| 4296 | token_to_req_indices: torch.Tensor, |
| 4297 | query_start_loc: torch.Tensor, |
| 4298 | seq_lens: torch.Tensor, |
| 4299 | is_valid_token: torch.Tensor, |
| 4300 | block_table: torch.Tensor, |
| 4301 | block_size: int, |
| 4302 | compress_ratio: int, |
| 4303 | num_compressed: int, |
| 4304 | token_offset: int, |
| 4305 | num_rows: int = -1, |
| 4306 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 4307 | """Map prefill top-k rows to a ragged stream of compressed-cache slots. |
| 4308 | |
| 4309 | ``topk_indices`` holds local compressed positions for the prefill tokens, |
| 4310 | which sit at ``token_offset`` in the batch; ``token_to_req_indices``, |
| 4311 | ``query_start_loc``, ``seq_lens`` and ``block_table`` are batch-wide. |
| 4312 | ``block_size`` is the compressed cache's, i.e. already divided by the ratio. |
| 4313 | """ |
| 4314 | topk_indices = topk_indices.reshape(topk_indices.shape[0], -1) |
| 4315 | num_tokens, width = topk_indices.shape |
| 4316 | dense = torch.empty( |
| 4317 | (num_tokens, width), dtype=torch.int32, device=topk_indices.device |
| 4318 | ) |
| 4319 | lens = torch.empty(num_tokens, dtype=torch.int32, device=topk_indices.device) |
| 4320 | if num_tokens > 0 and width > 0: |
| 4321 | _prefill_topk_global_slots_kernel[(num_tokens,)]( |
| 4322 | dense, |
| 4323 | lens, |
| 4324 | topk_indices, |
| 4325 | topk_indices.stride(0), |
| 4326 | token_to_req_indices, |
| 4327 | query_start_loc, |
| 4328 | seq_lens, |
| 4329 | is_valid_token, |
| 4330 | block_table, |
| 4331 | block_table.stride(0), |
| 4332 | token_offset, |
| 4333 | num_compressed, |
| 4334 | TOPK=width, |
| 4335 | COMPRESS_RATIO=compress_ratio, |
| 4336 | BLOCK_SIZE=block_size, |
| 4337 | BLOCK_W=min(triton.next_power_of_2(width), 1024), |
| 4338 | ) |
| 4339 | else: |
| 4340 | lens.zero_() |
| 4341 | return build_ragged_indices_from_dense(dense, lens, num_rows=num_rows) |