| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | import functools |
| 5 | from dataclasses import dataclass |
| 6 | from enum import Enum |
| 7 | from pathlib import Path |
| 8 | from typing import TYPE_CHECKING, ClassVar, Final |
| 9 | |
| 10 | import torch |
| 11 | |
| 12 | from vllm import envs |
| 13 | from vllm._aiter_ops import rocm_aiter_ops |
| 14 | from vllm.config import VllmConfig |
| 15 | from vllm.config.cache import CacheDType |
| 16 | from vllm.distributed import get_dcp_group |
| 17 | from vllm.logger import init_logger |
| 18 | from vllm.model_executor.layers.attention.mla_attention import ( |
| 19 | MLACommonBackend, |
| 20 | MLACommonDecodeMetadata, |
| 21 | MLACommonImpl, |
| 22 | MLACommonMetadata, |
| 23 | MLACommonMetadataBuilder, |
| 24 | QueryLenSupport, |
| 25 | ) |
| 26 | from vllm.triton_utils import tl, triton |
| 27 | from vllm.utils.gpu_sync_debug import gpu_sync_allowed |
| 28 | from vllm.utils.math_utils import cdiv, largest_power_of_2_divisor |
| 29 | from vllm.v1.attention.backend import ( |
| 30 | AttentionCGSupport, |
| 31 | AttentionLayer, |
| 32 | CommonAttentionMetadata, |
| 33 | MultipleOf, |
| 34 | ) |
| 35 | from vllm.v1.attention.backends.utils import ( |
| 36 | get_dcp_local_seq_lens, |
| 37 | ) |
| 38 | from vllm.v1.attention.ops.rocm_aiter_mla_merge import ( |
| 39 | merge_mla_segments_triton, |
| 40 | ) |
| 41 | from vllm.v1.kv_cache_interface import AttentionSpec, is_quantized_kv_cache |
| 42 | |
| 43 | if TYPE_CHECKING: |
| 44 | from vllm.platforms.interface import DeviceCapability |
| 45 | |
| 46 | logger = init_logger(__name__) |
| 47 | |
| 48 | |
| 49 | def _segmented_mla_page_size(block_size: int) -> int: |
| 50 | """Largest supported power-of-two subpage dividing a physical KV block. |
| 51 | |
| 52 | The subpage size becomes the segmented kernel's ``TILE_SIZE``: the cache is |
| 53 | reinterpreted as ``(-1, page_size, 1, head_dim)`` before the call. It must |
| 54 | divide the physical block and be a power of two, since the kernel walks a |
| 55 | tile with ``tl.arange(0, TILE_SIZE)``. 128 is the largest tile that tiling |
| 56 | supports -- a tile holds ``TILE_SIZE x kv_lora_rank`` keys and its scores |
| 57 | are ``BLOCK_M x TILE_SIZE`` -- so a larger manager block is split into |
| 58 | several subpages instead of widening the tile. |
| 59 | """ |
| 60 | assert block_size > 0 |
| 61 | return min(128, largest_power_of_2_divisor(block_size)) |
| 62 | |
| 63 | |
| 64 | @functools.lru_cache(maxsize=1) |
| 65 | def _get_mla_gluon(): |
| 66 | """Load the small-head Gluon MLA entry point.""" |
| 67 | unified_module = "aiter.ops.triton.gluon.mla_gluon" |
| 68 | try: |
| 69 | from aiter.ops.triton.gluon.mla_gluon import mla_gluon |
| 70 | |
| 71 | return mla_gluon |
| 72 | except ModuleNotFoundError as unified_import_error: |
| 73 | if not unified_module.startswith(unified_import_error.name or ""): |
| 74 | raise |
| 75 | legacy_module = "aiter.ops.triton.gluon.mla_decode_gluon" |
| 76 | try: |
| 77 | from aiter.ops.triton.gluon.mla_decode_gluon import mla_decode_gluon |
| 78 | |
| 79 | return mla_decode_gluon |
| 80 | except ModuleNotFoundError as legacy_import_error: |
| 81 | if not legacy_module.startswith(legacy_import_error.name or ""): |
| 82 | raise |
| 83 | raise RuntimeError( |
| 84 | "ROCM_AITER_MLA requires an AITER build with the small-head " |
| 85 | "Gluon MLA kernel (mla_gluon or mla_decode_gluon) when decode " |
| 86 | "heads are fewer than 16." |
| 87 | ) from unified_import_error |
| 88 | |
| 89 | |
| 90 | @functools.lru_cache(maxsize=1) |
| 91 | def _fp8_mla_prefill_supported() -> bool: |
| 92 | """Auto-detect FP8 MLA prefill via mla_prefill_ps_asm_fwd + mla_reduce_v1. |
| 93 | |
| 94 | Requires gfx950 plus an AITER build that exports both kernels. When |
| 95 | either is missing we silently fall back to ``flash_attn_varlen_func``. |
| 96 | """ |
| 97 | try: |
| 98 | from vllm.platforms.rocm import on_gfx950 |
| 99 | except Exception: # noqa: BLE001 |
| 100 | return False |
| 101 | if not on_gfx950(): |
| 102 | return False |
| 103 | try: |
| 104 | from aiter import mla_prefill_ps_asm_fwd, mla_reduce_v1 # noqa: F401 |
| 105 | except Exception: # noqa: BLE001 |
| 106 | return False |
| 107 | return True |
| 108 | |
| 109 | |
| 110 | @functools.lru_cache(maxsize=1) |
| 111 | def _aiter_mla_native_h24_reducer_supported() -> bool: |
| 112 | """Whether AITER's JIT reducer supports the native H24/512 shape.""" |
| 113 | try: |
| 114 | from aiter.jit.core import AITER_CSRC_DIR |
| 115 | |
| 116 | reduce_source = Path(AITER_CSRC_DIR) / "kernels" / "mla" / "reduce.cu" |
| 117 | source = "".join(reduce_source.read_text(encoding="utf-8").split()) |
| 118 | except (ImportError, OSError): |
| 119 | return False |
| 120 | return "MLA_REDUCE_CASE_EF(NUM_HEAD,24,HEAD_DIM,512," in source |
| 121 | |
| 122 | |
| 123 | @functools.lru_cache(maxsize=1) |
| 124 | def _aiter_mla_native_h24_metadata_supported() -> bool: |
| 125 | """Whether AITER's fast MLA metadata planner accepts native H24. |
| 126 | |
| 127 | The reducer and metadata planner have independent shape dispatch. Checking |
| 128 | only the reducer can route H24 into a planner that rejects it before the |
| 129 | attention kernel launches. Until AITER exposes a capability API, inspect |
| 130 | the shipped JIT source for an explicit native-H24 planner branch. |
| 131 | """ |
| 132 | try: |
| 133 | from aiter.jit.core import AITER_CSRC_DIR |
| 134 | |
| 135 | metadata_source = ( |
| 136 | Path(AITER_CSRC_DIR) / "kernels" / "mla" / "metadata" / "v1_2_device.cuh" |
| 137 | ) |
| 138 | source = "".join(metadata_source.read_text(encoding="utf-8").split()) |
| 139 | except (ImportError, OSError): |
| 140 | return False |
| 141 | return "num_heads==24" in source |
| 142 | |
| 143 | |
| 144 | def _aiter_mla_native_h24_supported() -> bool: |
| 145 | """Whether the complete AITER decode path supports native H24.""" |
| 146 | return ( |
| 147 | _aiter_mla_native_h24_reducer_supported() |
| 148 | and _aiter_mla_native_h24_metadata_supported() |
| 149 | ) |
| 150 | |
| 151 | |
| 152 | def _aiter_mla_non_causal_asm_kernels() -> bool: |
| 153 | """Whether this arch ships non-causal MLA decode ASM kernels. |
| 154 | |
| 155 | The Python `causal=` probe cannot see the per-arch manifest. gfx950 has |
| 156 | the kernels; gfx942 and everything else must fall through to another |
| 157 | backend. |
| 158 | """ |
| 159 | try: |
| 160 | from vllm.platforms.rocm import on_gfx950 |
| 161 | except Exception: # noqa: BLE001 |
| 162 | return False |
| 163 | return bool(on_gfx950()) |
| 164 | |
| 165 | |
| 166 | @functools.lru_cache(maxsize=1) |
| 167 | def _gluon_mla_decode_supported() -> bool: |
| 168 | """The small-head Gluon MLA decode kernel only has a gfx950 (CDNA4) build. |
| 169 | |
| 170 | Its tiling needs ~160 KiB of LDS, which exceeds CDNA3's 64 KiB, so on |
| 171 | gfx942 there is no kernel to fall through to and selecting it asserts |
| 172 | (``mla_gluon requires gfx950``). Restrict Gluon decode to gfx950; other |
| 173 | archs use the asm persistent decode, which ``get_mla_padded_q`` makes |
| 174 | correct for any 1..15 heads. |
| 175 | """ |
| 176 | try: |
| 177 | from vllm.platforms.rocm import on_gfx950 |
| 178 | except Exception: # noqa: BLE001 |
| 179 | return False |
| 180 | return on_gfx950() |
| 181 | |
| 182 | |
| 183 | # Past 2 GiB, mla_gluon swaps its masked buffer_load for an unmasked |
| 184 | # global_load that reads outside the cache and aborts the process. Mirrors the |
| 185 | # kernel's own within_2gb bound, so the fallback engages exactly where it does. |
| 186 | _GLUON_MAX_KV_CACHE_BYTES = 1 << 31 |
| 187 | |
| 188 | |
| 189 | def _gluon_kv_cache_in_bounds(kv_cache_bytes: int | None) -> bool: |
| 190 | """Whether this layer's KV cache keeps Gluon on its bounds-checked path. |
| 191 | |
| 192 | The cache is one flat tensor per layer, so the bound disqualifies the layer |
| 193 | outright; no batch shape brings an oversized cache back in range. ``None`` |
| 194 | is the not-yet-sized case during profiling, before any kernel runs. |
| 195 | """ |
| 196 | if kv_cache_bytes is None or kv_cache_bytes <= _GLUON_MAX_KV_CACHE_BYTES: |
| 197 | return True |
| 198 | logger.warning_once( |
| 199 | "KV cache is %.1f GiB per layer, past the 2 GiB where the Gluon MLA " |
| 200 | "kernel drops its KV bounds mask; using the padded ASM decode instead. " |
| 201 | "Lower --gpu-memory-utilization or --max-model-len to get Gluon back.", |
| 202 | kv_cache_bytes / (1 << 30), |
| 203 | ) |
| 204 | return False |
| 205 | |
| 206 | |
| 207 | @functools.lru_cache(maxsize=1) |
| 208 | def _get_segmented_mla_decode(): |
| 209 | """Load AITER's segmented MLA decode with unreduced partial output.""" |
| 210 | from aiter.ops.triton.attention.mla import mla_decode_fwd |
| 211 | |
| 212 | return mla_decode_fwd |
| 213 | |
| 214 | |
| 215 | @functools.lru_cache(maxsize=1) |
| 216 | def _get_aiter_mla_decode(): |
| 217 | from aiter.mla import mla_decode_fwd |
| 218 | |
| 219 | return mla_decode_fwd |
| 220 | |
| 221 | |
| 222 | @functools.lru_cache(maxsize=1) |
| 223 | def _segmented_mla_decode_supported() -> bool: |
| 224 | """Whether AITER exposes the segmented MLA decode used by DCP verify.""" |
| 225 | try: |
| 226 | _get_segmented_mla_decode() |
| 227 | except Exception: # noqa: BLE001 |
| 228 | return False |
| 229 | return True |
| 230 | |
| 231 | |
| 232 | def _segmented_dcp_verify_supported(dcp_world_size: int, cp_interleave: int) -> bool: |
| 233 | """Whether this configuration can serve DCP verify on segmented MLA. |
| 234 | |
| 235 | Configuration only -- whether a given batch takes the route additionally |
| 236 | depends on its query length. Round-robin interleaving other than 1 is |
| 237 | excluded because the per-row causal window has not been validated there. |
| 238 | """ |
| 239 | return ( |
| 240 | dcp_world_size > 1 and cp_interleave == 1 and _segmented_mla_decode_supported() |
| 241 | ) |
| 242 | |
| 243 | |
| 244 | # DCP decode routing |
| 245 | # |
| 246 | # With decode context parallelism each rank holds a round-robin shard of the KV |
| 247 | # cache (token t on rank t % dcp_world_size; requires interleave size 1) and |
| 248 | # attends over it with all DCP-gathered query heads. Partial outputs are merged |
| 249 | # across ranks by LSE. Each decode batch takes one route: |
| 250 | # |
| 251 | # * plain: qlen == 1 or a non-causal block. Every query sees every local token. |
| 252 | # * cprr: causal qlen >= _MIN_CPRR_QLEN, gfx950, speculative decoding. AITER's |
| 253 | # round-robin ASM decode rebuilds global positions from g_kv_indptr, cp_rank |
| 254 | # and cp_world_size and applies the causal window in-kernel. Kernels exist only |
| 255 | # at _NATIVE_CPRR_HEADS; other gathered head counts are padded up. |
| 256 | # * segmented: causal qlen > 1 when cprr is not selected or qlen is below |
| 257 | # _MIN_CPRR_QLEN. Each verify token becomes a single-query row whose KV length |
| 258 | # encodes its causal window. |
| 259 | # |
| 260 | # The plain kernel must never serve a causal qlen > 1 block: it applies |
| 261 | # causality on local indices, which is wrong over a round-robin shard. |
| 262 | _NATIVE_CPRR_HEADS: Final = (16, 32, 64, 128) |
| 263 | _MIN_CPRR_QLEN: Final = 3 |
| 264 | |
| 265 | |
| 266 | class _DCPDecodeRoute(Enum): |
| 267 | PLAIN = "plain" |
| 268 | SEGMENTED = "segmented" |
| 269 | CPRR = "cprr" |
| 270 | |
| 271 | |
| 272 | def _asm_dcp_verify_heads(decode_num_heads: int) -> int: |
| 273 | """Head count the cprr asm decode runs at, or 0 if it cannot serve this shape.""" |
| 274 | if decode_num_heads in _NATIVE_CPRR_HEADS: |
| 275 | return decode_num_heads |
| 276 | for nat in _NATIVE_CPRR_HEADS: |
| 277 | if nat >= decode_num_heads: |
| 278 | return nat |
| 279 | return 0 |
| 280 | |
| 281 | |
| 282 | def _select_dcp_decode_route( |
| 283 | supports_segmented: bool, |
| 284 | causal: bool, |
| 285 | max_qo_len: int, |
| 286 | asm_selected: bool, |
| 287 | ) -> _DCPDecodeRoute: |
| 288 | """Select one DCP decode route for this batch; see DCP decode routing.""" |
| 289 | if not causal or max_qo_len <= 1: |
| 290 | return _DCPDecodeRoute.PLAIN |
| 291 | if asm_selected and max_qo_len >= _MIN_CPRR_QLEN: |
| 292 | return _DCPDecodeRoute.CPRR |
| 293 | if supports_segmented: |
| 294 | return _DCPDecodeRoute.SEGMENTED |
| 295 | raise RuntimeError( |
| 296 | "ROCM_AITER_MLA DCP multi-token verify requires either segmented " |
| 297 | "MLA or the round-robin asm decode." |
| 298 | ) |
| 299 | |
| 300 | |
| 301 | def _asm_dcp_verify_configured( |
| 302 | dcp_world_size: int, cp_interleave: int, multi_token_decode: bool |
| 303 | ) -> bool: |
| 304 | """Whether asm cprr DCP verify is reachable in this process. |
| 305 | |
| 306 | Requires DCP with interleave size 1, speculative decoding, and gfx950 (the |
| 307 | only arch AITER builds cprr kernels for). |
| 308 | """ |
| 309 | if not (dcp_world_size > 1 and cp_interleave == 1 and multi_token_decode): |
| 310 | return False |
| 311 | from vllm.platforms.rocm import on_gfx950 |
| 312 | |
| 313 | return bool(on_gfx950()) |
| 314 | |
| 315 | |
| 316 | def _aiter_mla_small_head_mode() -> str: |
| 317 | """Small-head (<16) MLA decode kernel selection. |
| 318 | |
| 319 | Controlled by ``VLLM_ROCM_AITER_MLA_ASM_PADDING``: |
| 320 | |
| 321 | - ``"auto"`` (default): let the arch decide -- divisor head counts keep the |
| 322 | Gluon decode where a build exists (gfx950), everything else (non-divisor |
| 323 | counts and all counts on gfx942) uses the padded persistent-scheduling |
| 324 | ASM decode. |
| 325 | - ``"gluon"``: prefer the Gluon path wherever a build exists. |
| 326 | - ``"asm"``: force the padded persistent-scheduling ASM decode. |
| 327 | |
| 328 | On gfx942 (no Gluon build) the ASM path is always used regardless of this |
| 329 | setting; ``"gluon"`` there falls back to ASM with a one-time warning. |
| 330 | """ |
| 331 | import vllm.envs as envs |
| 332 | |
| 333 | mode = (envs.VLLM_ROCM_AITER_MLA_ASM_PADDING or "auto").lower() |
| 334 | if mode == "gluon" and not _gluon_mla_decode_supported(): |
| 335 | logger.warning_once( |
| 336 | "VLLM_ROCM_AITER_MLA_ASM_PADDING=gluon requested, but this device " |
| 337 | "has no Gluon MLA decode build (Gluon requires gfx950); using the " |
| 338 | "padded persistent-scheduling ASM decode instead." |
| 339 | ) |
| 340 | return mode |
| 341 | |
| 342 | |
| 343 | class AiterMLABackend(MLACommonBackend): |
| 344 | supported_dtypes: ClassVar[list[torch.dtype]] = [torch.float16, torch.bfloat16] |
| 345 | supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [ |
| 346 | "auto", |
| 347 | "float16", |
| 348 | "bfloat16", |
| 349 | "fp8", |
| 350 | "fp8_e4m3", |
| 351 | "fp8_e5m2", |
| 352 | ] |
| 353 | |
| 354 | @classmethod |
| 355 | def get_supported_head_sizes(cls) -> list[int]: |
| 356 | return [] |
| 357 | |
| 358 | @staticmethod |
| 359 | def get_supported_kernel_block_sizes(kv_cache_spec=None) -> list[int | MultipleOf]: |
| 360 | # The aiter MLA decode kernel always operates with page_size=1 |
| 361 | # internally (the wrapper flattens kv_buffer via .view(-1, 1, 1, H)). |
| 362 | # We support any kernel_block_size by expanding block-level indices |
| 363 | # into per-token flat indices in the metadata builder. |
| 364 | return [MultipleOf(1)] |
| 365 | |
| 366 | @staticmethod |
| 367 | def get_name() -> str: |
| 368 | return "ROCM_AITER_MLA" |
| 369 | |
| 370 | @classmethod |
| 371 | def supports_non_causal(cls) -> bool: |
| 372 | # causal= on mla_decode_fwd is necessary but not sufficient: pinned |
| 373 | # AITER v0.1.21.post2 still exports that argument on gfx942, whose ASM |
| 374 | # manifest has no non-causal decode kernels. Selecting this backend |
| 375 | # there reaches `cannot get heuristic kernel!` instead of TRITON_MLA. |
| 376 | return bool( |
| 377 | rocm_aiter_ops.mla_decode_supports_non_causal() |
| 378 | and _aiter_mla_non_causal_asm_kernels() |
| 379 | ) |
| 380 | |
| 381 | @classmethod |
| 382 | def validate_configuration( |
| 383 | cls, |
| 384 | head_size: int, |
| 385 | dtype: torch.dtype, |
| 386 | kv_cache_dtype: CacheDType | None, |
| 387 | block_size: int | None, |
| 388 | use_mla: bool, |
| 389 | has_sink: bool, |
| 390 | use_sparse: bool, |
| 391 | use_mm_prefix: bool, |
| 392 | use_per_head_quant_scales: bool, |
| 393 | device_capability: "DeviceCapability", |
| 394 | attn_type: str, |
| 395 | has_sliding_window: bool = False, |
| 396 | use_non_causal: bool = False, |
| 397 | use_batch_invariant: bool = False, |
| 398 | use_kv_connector: bool = False, |
| 399 | use_pcp: bool = False, |
| 400 | use_adaptive_verification: bool = False, |
| 401 | use_dcp: bool = False, |
| 402 | use_rswa: bool = False, |
| 403 | ) -> list[str]: |
| 404 | invalid_reasons = super().validate_configuration( |
| 405 | head_size=head_size, |
| 406 | dtype=dtype, |
| 407 | kv_cache_dtype=kv_cache_dtype, |
| 408 | block_size=block_size, |
| 409 | use_mla=use_mla, |
| 410 | has_sink=has_sink, |
| 411 | use_sparse=use_sparse, |
| 412 | use_mm_prefix=use_mm_prefix, |
| 413 | use_per_head_quant_scales=use_per_head_quant_scales, |
| 414 | device_capability=device_capability, |
| 415 | attn_type=attn_type, |
| 416 | has_sliding_window=has_sliding_window, |
| 417 | use_non_causal=use_non_causal, |
| 418 | use_batch_invariant=use_batch_invariant, |
| 419 | use_kv_connector=use_kv_connector, |
| 420 | use_pcp=use_pcp, |
| 421 | use_adaptive_verification=use_adaptive_verification, |
| 422 | use_dcp=use_dcp, |
| 423 | use_rswa=use_rswa, |
| 424 | ) |
| 425 | # Arch/signature cannot express this: the backend still advertises |
| 426 | # fp16, so a DSpark draft with kv_cache_dtype="auto" would select |
| 427 | # AITER and abort on fp16 query. Causal fp16 is fine. |
| 428 | if ( |
| 429 | use_non_causal |
| 430 | and dtype == torch.float16 |
| 431 | and kv_cache_dtype in (None, "auto", "float16") |
| 432 | ): |
| 433 | invalid_reasons.append("non-causal fp16 MLA decode not supported") |
| 434 | return invalid_reasons |
| 435 | |
| 436 | @staticmethod |
| 437 | def get_impl_cls() -> type["AiterMLAImpl"]: |
| 438 | return AiterMLAImpl |
| 439 | |
| 440 | @staticmethod |
| 441 | def get_builder_cls() -> type["AiterMLAMetadataBuilder"]: |
| 442 | return AiterMLAMetadataBuilder |
| 443 | |
| 444 | |
| 445 | @dataclass |
| 446 | class AiterMLADCPVerifyMetadata: |
| 447 | """One paged-KV row per verify token, for segmented DCP verification. |
| 448 | |
| 449 | These are produced together or not at all, so they travel as one value: its |
| 450 | presence on the decode metadata *is* the routing decision the builder made, |
| 451 | and the impl does not re-derive it. |
| 452 | """ |
| 453 | |
| 454 | # Local KV length of each verify row, already causally bounded. |
| 455 | row_lens: torch.Tensor |
| 456 | # Subpage IDs each row reads, shape [num_rows, max_local_pages]. |
| 457 | block_table: torch.Tensor |
| 458 | # One query per row, so this is an arange. |
| 459 | qo_indptr: torch.Tensor |
| 460 | # Subpage size the block table is expressed in, and the segmented kernel's |
| 461 | # TILE_SIZE. Carried so the impl reinterprets the cache exactly the way the |
| 462 | # block table was built, instead of re-deriving it from the cache shape. |
| 463 | page_size: int |
| 464 | # Kernel-visible KV bound. Whenever full graphs are enabled this is the |
| 465 | # configuration's maximum for every batch, not just during capture, so the |
| 466 | # page table keeps one shape across replays. |
| 467 | max_kv_seq_len: int |
| 468 | |
| 469 | |
| 470 | @dataclass |
| 471 | class AiterMLADecodeMetadata(MLACommonDecodeMetadata): |
| 472 | # The indptr of the paged kv cache, shape: [batch_size + 1] |
| 473 | paged_kv_indptr: torch.Tensor | None = None |
| 474 | # The page indices of the paged kv cache |
| 475 | paged_kv_indices: torch.Tensor | None = None |
| 476 | # The number of entries in the last page of each request in |
| 477 | # the paged kv cache, shape: [batch_size] |
| 478 | paged_kv_last_page_len: torch.Tensor | None = None |
| 479 | # The query indptr, shape : [num_decode + 1] |
| 480 | qo_indptr: torch.Tensor | None = None |
| 481 | # The dtype of MLA out tensor |
| 482 | attn_out_dtype: torch.dtype = torch.bfloat16 |
| 483 | # The max query output length: int |
| 484 | max_qo_len: int | None = None |
| 485 | # Minimum KV length used by Gluon to choose a safe split count. |
| 486 | min_kv_seq_len: int = 1 |
| 487 | # Exactly one DCP decode route is selected by the metadata builder. |
| 488 | dcp_route: _DCPDecodeRoute = _DCPDecodeRoute.PLAIN |
| 489 | # Set exactly when this batch routes to segmented DCP verification. |
| 490 | dcp_verify: AiterMLADCPVerifyMetadata | None = None |
| 491 | # cprr only: global per-request page indptr and this rank's position. |
| 492 | g_kv_indptr: torch.Tensor | None = None |
| 493 | cp_world_size: int = 1 |
| 494 | cp_rank: int = 0 |
| 495 | # Padded head count cprr runs at; 0 when this batch is not on cprr. |
| 496 | asm_decode_num_heads: int = 0 |
| 497 | # Must equal the split cap the reduce scratch was sized with, or the kernel |
| 498 | # writes past it and faults. |
| 499 | mla_num_kv_splits: int = 0 |
| 500 | # Small-head decode uses Gluon (avoids padding to 16). |
| 501 | use_gluon_decode: bool = False |
| 502 | # Small-head non-DCP multi-token verify uses Gluon's native MTP entry. |
| 503 | use_gluon_verify: bool = False |
| 504 | # Whether persistent MLA metadata was computed |
| 505 | has_persistent_metadata: bool = False |
| 506 | |
| 507 | |
| 508 | @dataclass |
| 509 | class AiterMLAMetadata(MLACommonMetadata[AiterMLADecodeMetadata]): |
| 510 | work_meta_data: torch.Tensor | None = None |
| 511 | work_indptr: torch.Tensor | None = None |
| 512 | work_info_set: torch.Tensor | None = None |
| 513 | reduce_indptr: torch.Tensor | None = None |
| 514 | reduce_final_map: torch.Tensor | None = None |
| 515 | reduce_partial_map: torch.Tensor | None = None |
| 516 | |
| 517 | # FP8 ASM prefill persistent-scheduling (PS) metadata. Populated by |
| 518 | # AiterMLAMetadataBuilder._build_fp8_prefill_ps_metadata when prefill |
| 519 | # tokens are present and FP8 MLA prefill is supported on the device. |
| 520 | # Left as None on hosts/configs that fall back to flash_attn_varlen_func. |
| 521 | fp8_prefill_qo_indptr: torch.Tensor | None = None |
| 522 | fp8_prefill_kv_indptr: torch.Tensor | None = None |
| 523 | fp8_prefill_kv_indices: torch.Tensor | None = None |
| 524 | fp8_prefill_work_indptr: torch.Tensor | None = None |
| 525 | fp8_prefill_work_info_set: torch.Tensor | None = None |
| 526 | fp8_prefill_reduce_indptr: torch.Tensor | None = None |
| 527 | fp8_prefill_reduce_final_map: torch.Tensor | None = None |
| 528 | fp8_prefill_reduce_partial_map: torch.Tensor | None = None |
| 529 | fp8_prefill_max_q_len: int | None = None |
| 530 | fp8_prefill_num_partial_tiles: int | None = None |
| 531 | |
| 532 | |
| 533 | # Tile size used by the mla_prefill_ps_asm_fwd assembly kernel. |
| 534 | _FP8_PREFILL_TILE_Q = 256 |
| 535 | |
| 536 | |
| 537 | class AiterMLAMetadataBuilder(MLACommonMetadataBuilder[AiterMLAMetadata]): |
| 538 | # TODO(luka, lucas): audit this as part of: |
| 539 | # https://github.com/vllm-project/vllm/issues/22945 |
| 540 | _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH |
| 541 | query_len_support: ClassVar[QueryLenSupport] = QueryLenSupport.UNIFORM |
| 542 | # Served by passing the mask to the kernel; _build_decode turns away the |
| 543 | # shapes AITER has no non-causal kernel for. |
| 544 | supports_non_causal_multi_token_decode: ClassVar[bool] = True |
| 545 | # Set from the common metadata every build; a batch is causal unless the |
| 546 | # drafter says otherwise. |
| 547 | _decode_causal: bool = True |
| 548 | # A non-causal draft needs no cross-shard causal window: the ordinary |
| 549 | # mask0 decode returns per-row LSE for the DCP merge. |
| 550 | supports_non_causal_multi_token_dcp: ClassVar[bool] = True |
| 551 | |
| 552 | @staticmethod |
| 553 | def _uniform_padded_mtp_qo_len( |
| 554 | qo_len: torch.Tensor, |
| 555 | max_qo_len: int, |
| 556 | num_decode_tokens: int, |
| 557 | ) -> int: |
| 558 | num_reqs = qo_len.numel() |
| 559 | if num_reqs == 0 or num_decode_tokens <= 0: |
| 560 | return 0 |
| 561 | |
| 562 | # Full-CG pads q to a captured token count while leaving |
| 563 | # query_start_loc flat for dummy requests. Only synthesize dummy rows |
| 564 | # when every padded request maps to the same qlen and the q buffer has |
| 565 | # exactly that many rows. |
| 566 | if num_decode_tokens <= int(qo_len.sum().item()): |
| 567 | return 0 |
| 568 | if num_decode_tokens % num_reqs != 0: |
| 569 | return 0 |
| 570 | |
| 571 | uniform_qo_len = num_decode_tokens // num_reqs |
| 572 | if uniform_qo_len <= 1: |
| 573 | return 0 |
| 574 | |
| 575 | positive_qo_len = qo_len[qo_len > 0] |
| 576 | if positive_qo_len.numel() == qo_len.numel(): |
| 577 | return 0 |
| 578 | if positive_qo_len.numel() > 0: |
| 579 | if max_qo_len != uniform_qo_len: |
| 580 | return 0 |
| 581 | if not torch.all(positive_qo_len == uniform_qo_len): |
| 582 | return 0 |
| 583 | |
| 584 | zero_positions = torch.nonzero(qo_len == 0, as_tuple=False).flatten() |
| 585 | if zero_positions.numel() > 0: |
| 586 | first_zero = int(zero_positions[0].item()) |
| 587 | if torch.any(qo_len[first_zero:] > 0): |
| 588 | return 0 |
| 589 | |
| 590 | return uniform_qo_len |
| 591 | |
| 592 | def __init__( |
| 593 | self, |
| 594 | kv_cache_spec: AttentionSpec, |
| 595 | layer_names: list[str], |
| 596 | vllm_config: VllmConfig, |
| 597 | device: torch.device, |
| 598 | ): |
| 599 | parallel_config = vllm_config.parallel_config |
| 600 | supports_segmented_dcp_verify = _segmented_dcp_verify_supported( |
| 601 | parallel_config.decode_context_parallel_size, |
| 602 | parallel_config.cp_kv_cache_interleave_size, |
| 603 | ) |
| 604 | asm_dcp_verify_config = ( |
| 605 | _asm_dcp_verify_configured( |
| 606 | parallel_config.decode_context_parallel_size, |
| 607 | parallel_config.cp_kv_cache_interleave_size, |
| 608 | multi_token_decode=vllm_config.speculative_config is not None, |
| 609 | ) |
| 610 | and envs.VLLM_ROCM_AITER_MLA_DCP_VERIFY == "asm" |
| 611 | ) |
| 612 | super().__init__( |
| 613 | kv_cache_spec, |
| 614 | layer_names, |
| 615 | vllm_config, |
| 616 | device, |
| 617 | AiterMLAMetadata, |
| 618 | supports_dcp_with_varlen=( |
| 619 | supports_segmented_dcp_verify or asm_dcp_verify_config |
| 620 | ), |
| 621 | ) |
| 622 | self._asm_dcp_verify = False |
| 623 | self._asm_dcp_verify_heads = 0 |
| 624 | # The head-count half of the route decision needs self.num_heads, so only |
| 625 | # the config half is known before super().__init__(). |
| 626 | if asm_dcp_verify_config: |
| 627 | self._asm_dcp_verify_heads = _asm_dcp_verify_heads( |
| 628 | self.num_heads * self.dcp_world_size |
| 629 | ) |
| 630 | if not self._asm_dcp_verify_heads: |
| 631 | raise ValueError( |
| 632 | "VLLM_ROCM_AITER_MLA_DCP_VERIFY=asm, but the round-robin asm " |
| 633 | f"decode has no kernel for {self.num_heads * self.dcp_world_size} " |
| 634 | f"DCP-gathered heads (native counts: {_NATIVE_CPRR_HEADS}). " |
| 635 | "Set VLLM_ROCM_AITER_MLA_DCP_VERIFY=segmented." |
| 636 | ) |
| 637 | self._asm_dcp_verify = True |
| 638 | self._supports_segmented_dcp_verify = supports_segmented_dcp_verify |
| 639 | self._mla_max_split_per_batch = 0 |
| 640 | if self._asm_dcp_verify: |
| 641 | # Cap on KV splits per batch; at low batch the KV axis is the only |
| 642 | # parallelism. Buffer sizing and every runtime call must see this value. |
| 643 | self._mla_max_split_per_batch = torch.cuda.get_device_properties( |
| 644 | device |
| 645 | ).multi_processor_count |
| 646 | |
| 647 | self.compilation_config = vllm_config.compilation_config |
| 648 | self.decode_attn_out_dtype = vllm_config.model_config.dtype |
| 649 | |
| 650 | # Needed to place a verify row's causal window on this rank's KV shard. |
| 651 | self.dcp_rank = get_dcp_group().rank_in_group if self.dcp_world_size > 1 else 0 |
| 652 | |
| 653 | # reorder_batch_threshold is the largest query length decode can be |
| 654 | # handed, and already accounts for the drafting scheme. A method-name |
| 655 | # whitelist sizes unlisted drafters for qlen=1, which closes the |
| 656 | # persistent gate below and makes aiter raise a KeyError mid-run. |
| 657 | self._mtp_decode_qlen = self.reorder_batch_threshold or 1 |
| 658 | |
| 659 | # Store the kernel block size from the spec. When kernel_block_size=1 |
| 660 | # (no spec-dec), behavior is identical to the original. When > 1 |
| 661 | # (e.g. 16 with Eagle3), we expand block-level indices into per-token |
| 662 | # flat indices since the aiter kernel always uses page_size=1 internally. |
| 663 | self.kernel_block_size = kv_cache_spec.block_size |
| 664 | self._segmented_page_size = _segmented_mla_page_size(self.kernel_block_size) |
| 665 | |
| 666 | # In the flat view (.view(-1,1,1,H)), each token is its own page, |
| 667 | # so max_num_pages_per_req = max_model_len regardless of |
| 668 | # kernel_block_size. |
| 669 | max_num_pages_per_req = vllm_config.model_config.max_model_len |
| 670 | max_num_reqs = vllm_config.scheduler_config.max_num_seqs |
| 671 | max_num_pages = max_num_reqs * max_num_pages_per_req |
| 672 | |
| 673 | # Preparing persistent buffers |
| 674 | # TODO: we can disambiguate between decode and mixed-prefill decode here |
| 675 | # so we can only use the persistent buffer if a cudagraph is actually |
| 676 | # being used. |
| 677 | |
| 678 | # paged_kv_last_page_len is always 1s (the aiter kernel always sees |
| 679 | # page_size=1 after .view(-1,1,1,H) flattening), so we create it |
| 680 | # once and reuse slices in both eager and cudagraph modes. |
| 681 | self.paged_kv_last_page_len = torch.ones( |
| 682 | max_num_reqs, dtype=torch.int32, device=device |
| 683 | ) |
| 684 | |
| 685 | # Persistent buffer for paged_kv_indices to avoid blocking boolean mask |
| 686 | # indexing (block_table_tensor[mask]) which has data-dependent output size. |
| 687 | self.paged_kv_indices = torch.zeros( |
| 688 | max_num_pages, dtype=torch.int32, device=device |
| 689 | ) |
| 690 | |
| 691 | from aiter import dtypes, get_mla_metadata_info_v1 |
| 692 | |
| 693 | # Decode kernels consume the DCP-gathered query heads. |
| 694 | self._decode_num_heads = self.num_heads * self.dcp_world_size |
| 695 | # Keep metadata sizing consistent with the padded tensor shape passed |
| 696 | # to mla_decode_fwd, including native 24-head AITER builds. |
| 697 | self._num_attention_heads = AiterMLAHelper.get_actual_mla_num_heads( |
| 698 | self._decode_num_heads |
| 699 | ) |
| 700 | # the cprr kernel exists only at the native head |
| 701 | # counts, so run (and size the persistent schedule) at the padded count. |
| 702 | # 96 is 16-aligned, so get_actual_mla_num_heads leaves it alone and the |
| 703 | # pad has to be applied here explicitly. |
| 704 | if self._asm_dcp_verify: |
| 705 | self._num_attention_heads = self._asm_dcp_verify_heads |
| 706 | kv_cache_dtype_str = getattr(vllm_config.cache_config, "cache_dtype", "auto") |
| 707 | if kv_cache_dtype_str in ("fp8", "fp8_e4m3", "fp8_e5m2"): |
| 708 | kv_cache_dtype_str = "fp8" |
| 709 | kv_dtype = dtypes.fp8 |
| 710 | else: |
| 711 | kv_dtype = { |
| 712 | torch.float16: dtypes.fp16, |
| 713 | torch.bfloat16: dtypes.bf16, |
| 714 | }[kv_cache_spec.dtype] |
| 715 | # _build_decode needs the cache dtype to pick the decode kernel; keep |
| 716 | # the normalized string instead of dropping it at the end of __init__. |
| 717 | self._kv_cache_dtype_str = kv_cache_dtype_str |
| 718 | # Sized per layer, matching the flat KV tensor the kernel is handed. |
| 719 | num_gpu_blocks = vllm_config.cache_config.num_gpu_blocks |
| 720 | self._kv_cache_bytes = ( |
| 721 | None |
| 722 | if num_gpu_blocks is None |
| 723 | else num_gpu_blocks * kv_cache_spec.page_size_bytes |
| 724 | ) |
| 725 | # MLAAttention quantizes decode Q to FP8 before calling this backend |
| 726 | # whenever the KV cache is FP8 and supports_quant_query_input is true. |
| 727 | q_dtype = ( |
| 728 | dtypes.fp8 if kv_cache_dtype_str == "fp8" else self.decode_attn_out_dtype |
| 729 | ) |
| 730 | # Persist for get_mla_metadata_v1 (decode build): omitting these causes |
| 731 | # wrong split/reduce metadata for the gfx950 fp8 nhead=32 fold path. |
| 732 | self._mla_q_dtype = q_dtype |
| 733 | self._mla_kv_dtype = kv_dtype |
| 734 | ( |
| 735 | (work_meta_data_size, work_meta_data_type), |
| 736 | (work_indptr_size, work_indptr_type), |
| 737 | (work_info_set_size, work_info_set_type), |
| 738 | (reduce_indptr_size, reduce_indptr_type), |
| 739 | (reduce_final_map_size, reduce_final_map_type), |
| 740 | (reduce_partial_map_size, reduce_partial_map_type), |
| 741 | ) = get_mla_metadata_info_v1( |
| 742 | max_num_reqs, |
| 743 | self._mtp_decode_qlen, |
| 744 | self._num_attention_heads, |
| 745 | q_dtype, |
| 746 | kv_dtype, |
| 747 | is_sparse=False, |
| 748 | fast_mode=True, |
| 749 | # Must match max_split_per_batch in get_mla_metadata_v1; without it |
| 750 | # aiter sizes the reduce scratch from capacity (~9 GiB). |
| 751 | **( |
| 752 | dict(max_split_per_batch=self._mla_max_split_per_batch) |
| 753 | if self._asm_dcp_verify |
| 754 | else {} |
| 755 | ), |
| 756 | ) |
| 757 | self._mla_work_meta_data = torch.empty( |
| 758 | work_meta_data_size, dtype=work_meta_data_type, device=device |
| 759 | ) |
| 760 | self._mla_work_indptr = torch.empty( |
| 761 | work_indptr_size, dtype=work_indptr_type, device=device |
| 762 | ) |
| 763 | self._mla_work_info_set = torch.empty( |
| 764 | work_info_set_size, dtype=work_info_set_type, device=device |
| 765 | ) |
| 766 | self._mla_reduce_indptr = torch.empty( |
| 767 | reduce_indptr_size, dtype=reduce_indptr_type, device=device |
| 768 | ) |
| 769 | self._mla_reduce_final_map = torch.empty( |
| 770 | reduce_final_map_size, dtype=reduce_final_map_type, device=device |
| 771 | ) |
| 772 | self._mla_reduce_partial_map = torch.empty( |
| 773 | reduce_partial_map_size, |
| 774 | dtype=reduce_partial_map_type, |
| 775 | device=device, |
| 776 | ) |
| 777 | |
| 778 | # The assembly prefill requires FP8 KV, bf16 output, and 16-aligned |
| 779 | # heads. It writes bf16 through a raw output pointer, so fp16 must use |
| 780 | # the standard prefill path. Head counts that are not a multiple of 16 |
| 781 | # are replicate-padded up to one in _mla_fp8_prefill_attn, so the gate |
| 782 | # is the same head-count predicate the decode path uses. |
| 783 | self._fp8_prefill_enabled = _fp8_mla_prefill_supported() and ( |
| 784 | kv_cache_dtype_str == "fp8" |
| 785 | and vllm_config.model_config.dtype == torch.bfloat16 |
| 786 | and AiterMLAHelper.is_valid_num_heads(self.num_heads) |
| 787 | ) |
| 788 | if self._fp8_prefill_enabled: |
| 789 | max_prefill_qlen = min( |
| 790 | vllm_config.model_config.max_model_len, |
| 791 | vllm_config.scheduler_config.max_num_batched_tokens, |
| 792 | ) |
| 793 | self._init_fp8_prefill_ps_buffers( |
| 794 | max_num_reqs, |
| 795 | max_prefill_qlen, |
| 796 | vllm_config.scheduler_config.max_num_batched_tokens, |
| 797 | vllm_config.model_config.dtype, |
| 798 | device, |
| 799 | ) |
| 800 | |
| 801 | # Full cudagraphs need a stable address for the cprr global page indptr. |
| 802 | # Zero-initialized; element 0 is the indptr base and is never rewritten. |
| 803 | self._g_kv_indptr_buf: torch.Tensor | None = None |
| 804 | if self._asm_dcp_verify: |
| 805 | self._g_kv_indptr_buf = torch.zeros( |
| 806 | max_num_reqs + 1, dtype=torch.int32, device=device |
| 807 | ) |
| 808 | |
| 809 | # Persistent buffers for segmented DCP verification. Captured graphs |
| 810 | # require stable addresses while row lengths vary between replays. |
| 811 | self._dcp_verify_buffers: AiterMLADCPVerifyMetadata | None = None |
| 812 | self._graph_seq_lens: torch.Tensor | None = None |
| 813 | if self.compilation_config.cudagraph_mode.has_full_cudagraphs(): |
| 814 | self.paged_kv_indptr = torch.zeros( |
| 815 | max_num_reqs + 1, dtype=torch.int32, device=device |
| 816 | ) |
| 817 | |
| 818 | self.qo_indptr = torch.zeros( |
| 819 | max_num_reqs + 1, dtype=torch.int32, device=device |
| 820 | ) |
| 821 | |
| 822 | # Full graphs require a stable address after uniform-MTP padding. |
| 823 | self._graph_seq_lens = torch.zeros( |
| 824 | max_num_reqs, dtype=torch.int32, device=device |
| 825 | ) |
| 826 | |
| 827 | if self._supports_segmented_dcp_verify and self._mtp_decode_qlen > 1: |
| 828 | # Allocate even when CPRR is the preferred route: a later |
| 829 | # replay can fall below _MIN_CPRR_QLEN and needs these |
| 830 | # addresses. A DCP rank's shard of the longest sequence |
| 831 | # bounds every verify row, and full graphs need that bound |
| 832 | # to be constant. |
| 833 | num_dcp_partitions = ( |
| 834 | self.dcp_world_size * self.cp_kv_cache_interleave_size |
| 835 | ) |
| 836 | graph_max_kv_seq_len = ( |
| 837 | cdiv(vllm_config.model_config.max_model_len, num_dcp_partitions) |
| 838 | * self.cp_kv_cache_interleave_size |
| 839 | ) |
| 840 | max_verify_rows = max_num_reqs * self._mtp_decode_qlen |
| 841 | max_local_pages = cdiv(graph_max_kv_seq_len, self._segmented_page_size) |
| 842 | self._dcp_verify_buffers = AiterMLADCPVerifyMetadata( |
| 843 | row_lens=torch.zeros( |
| 844 | max_verify_rows, dtype=torch.int32, device=device |
| 845 | ), |
| 846 | block_table=torch.zeros( |
| 847 | (max_verify_rows, max_local_pages), |
| 848 | dtype=torch.int32, |
| 849 | device=device, |
| 850 | ), |
| 851 | qo_indptr=torch.arange( |
| 852 | max_verify_rows + 1, dtype=torch.int32, device=device |
| 853 | ), |
| 854 | page_size=self._segmented_page_size, |
| 855 | max_kv_seq_len=graph_max_kv_seq_len, |
| 856 | ) |
| 857 | |
| 858 | def _init_fp8_prefill_ps_buffers( |
| 859 | self, |
| 860 | max_num_reqs: int, |
| 861 | max_prefill_qlen: int, |
| 862 | max_num_batched_tokens: int, |
| 863 | attn_out_dtype: torch.dtype, |
| 864 | device: torch.device, |
| 865 | ) -> None: |
| 866 | """Pre-allocate persistent buffers for FP8 MLA prefill PS metadata. |
| 867 | |
| 868 | Uses ``get_ps_metadata_info_v1`` with max values so the buffers are |
| 869 | large enough for any batch. ``get_ps_metadata_v1`` fills them |
| 870 | per-batch in ``build()``. The FP8 prefill forward path also uses the |
| 871 | global workspace manager for per-call scratch, so reserve its maximum |
| 872 | shape here before the workspace manager is locked after warmup. |
| 873 | |
| 874 | Args: |
| 875 | max_num_reqs: Maximum number of concurrent requests. |
| 876 | max_prefill_qlen: Maximum Q-length for a single request in one |
| 877 | prefill batch. Should be ``min(max_model_len, |
| 878 | max_num_batched_tokens)`` — a single request never exceeds |
| 879 | ``max_model_len`` tokens, nor the per-batch token budget. |
| 880 | max_num_batched_tokens: Maximum number of tokens scheduled in one |
| 881 | batch. The ``final_lse`` scratch is sized by ``total_q`` (the |
| 882 | summed Q-length over all prefill requests in the batch), which |
| 883 | is bounded by this budget rather than by a single request's |
| 884 | ``max_prefill_qlen`` — concurrent requests can sum to more than |
| 885 | ``max_model_len`` when ``max_model_len < max_num_batched_tokens``. |
| 886 | attn_out_dtype: Dtype of the attention output buffer, used to size |
| 887 | the padded-head output scratch (small head counts only). |
| 888 | device: Target device for the buffers. |
| 889 | |
| 890 | """ |
| 891 | from aiter import get_ps_metadata_info_v1 |
| 892 | |
| 893 | # After kv_b_proj decompression, K has num_heads heads (same as Q). |
| 894 | # So gqa_ratio=1 and num_head_k=num_heads for the PS kernel. |
| 895 | # Head counts that are not a multiple of 16 (K3: 12/rank at TP8) are |
| 896 | # replicate-padded up to one in _mla_fp8_prefill_attn; build the PS |
| 897 | # metadata for that same padded count so the work/reduce maps and the |
| 898 | # scratch reservations describe the width the kernel is handed. |
| 899 | # |
| 900 | # This was previously max(16, num_heads), which agrees with the helper |
| 901 | # at every head count reachable today (12 and 16 both give 16) and |
| 902 | # differs only above 16: at 24 it leaves num_head_k=24 while the |
| 903 | # forward pads to 32. That is not a correctness bug -- the 24-wide work |
| 904 | # maps still cover head-tiles 0..23, which are the real heads -- but it |
| 905 | # is expensive, because a lower head alignment yields more partial |
| 906 | # tiles: gcd-driven, 24 heads -> 64 tiles vs 32 heads -> 16, i.e. |
| 907 | # 193.6 MiB of reservations instead of 68.6 MiB at batch=1/qlen=512 |
| 908 | # (measured on gfx950). Sizing both from one helper keeps the widths |
| 909 | # equal and takes the cheaper tiling. |
| 910 | num_head_k = AiterMLAHelper.get_fp8_prefill_num_heads(self.num_heads) |
| 911 | v_head_dim = self.mla_dims.v_head_dim |
| 912 | # gqa_ratio = 1 |
| 913 | # qlen_granularity = _FP8_PREFILL_TILE_Q // max(gqa_ratio, 1) |
| 914 | qlen_granularity = _FP8_PREFILL_TILE_Q |
| 915 | |
| 916 | ( |
| 917 | (work_metadata_size, work_metadata_dtype), |
| 918 | (work_indptr_size, work_indptr_dtype), |
| 919 | (work_info_size, work_info_dtype), |
| 920 | (reduce_indptr_size, reduce_indptr_dtype), |
| 921 | (reduce_final_map_size, reduce_final_map_dtype), |
| 922 | (reduce_partial_map_size, reduce_partial_map_dtype), |
| 923 | ) = get_ps_metadata_info_v1( |
| 924 | batch_size=max_num_reqs, |
| 925 | num_head_k=num_head_k, |
| 926 | max_qlen=max_prefill_qlen, |
| 927 | qlen_granularity=qlen_granularity, |
| 928 | ) |
| 929 | |
| 930 | self.fp8_ps_work_metadata = torch.empty( |
| 931 | work_metadata_size, dtype=work_metadata_dtype, device=device |
| 932 | ) |
| 933 | self.fp8_ps_work_indptr = torch.empty( |
| 934 | work_indptr_size, dtype=work_indptr_dtype, device=device |
| 935 | ) |
| 936 | self.fp8_ps_work_info = torch.empty( |
| 937 | *work_info_size, dtype=work_info_dtype, device=device |
| 938 | ) |
| 939 | self.fp8_ps_reduce_indptr = torch.empty( |
| 940 | reduce_indptr_size, dtype=reduce_indptr_dtype, device=device |
| 941 | ) |
| 942 | self.fp8_ps_reduce_final_map = torch.empty( |
| 943 | *reduce_final_map_size, dtype=reduce_final_map_dtype, device=device |
| 944 | ) |
| 945 | self.fp8_ps_reduce_partial_map = torch.empty( |
| 946 | reduce_partial_map_size, |
| 947 | dtype=reduce_partial_map_dtype, |
| 948 | device=device, |
| 949 | ) |
| 950 | |
| 951 | # get_ps_metadata_v1 builds the plan on the host and writes its outputs with |
| 952 | # blocking hipMemcpy that is not ordered on the current stream. Writing the |
| 953 | # device buffers directly lets step N+1's build (async scheduling) overwrite a |
| 954 | # plan that step N's prefill kernels are still reading. Build into pinned host |
| 955 | # staging instead and copy on the current stream. Two staging slots, each |
| 956 | # reused only after its previous H2D copy has completed. |
| 957 | self._fp8_ps_device_outputs = ( |
| 958 | self.fp8_ps_work_indptr, |
| 959 | self.fp8_ps_work_info, |
| 960 | self.fp8_ps_reduce_indptr, |
| 961 | self.fp8_ps_reduce_final_map, |
| 962 | self.fp8_ps_reduce_partial_map, |
| 963 | ) |
| 964 | self._fp8_ps_staging = [ |
| 965 | tuple( |
| 966 | torch.empty(t.shape, dtype=t.dtype, device="cpu", pin_memory=True) |
| 967 | for t in self._fp8_ps_device_outputs |
| 968 | ) |
| 969 | for _ in range(2) |
| 970 | ] |
| 971 | # One event per slot, recorded after that slot's H2D copy. synchronize() |
| 972 | # on an event that has not been recorded yet returns immediately. |
| 973 | self._fp8_ps_staging_free = [torch.cuda.Event(), torch.cuda.Event()] |
| 974 | self._fp8_ps_slot = 0 |
| 975 | |
| 976 | from vllm.platforms import current_platform |
| 977 | from vllm.v1.worker.workspace import current_workspace_manager |
| 978 | |
| 979 | # AITER metadata sizing assumes all requests can carry max_prefill_qlen tokens, |
| 980 | # which is a loose worst case in the number of QO tiles. Rather, the sum of |
| 981 | # qlens is bounded by max_num_batched_tokens. Manually compute the number of |
| 982 | # qo tiles to avoid OOM at startup. |
| 983 | # TODO: AITER should give us this budget constrained value |
| 984 | qo_tile_cnt = ( |
| 985 | cdiv(max_num_batched_tokens, _FP8_PREFILL_TILE_Q) + max_num_reqs - 1 |
| 986 | ) |
| 987 | max_num_partial_tiles = qo_tile_cnt + current_platform.num_compute_units() |
| 988 | reservations: list[tuple[tuple[int, ...], torch.dtype]] = [ |
| 989 | ( |
| 990 | (max_num_partial_tiles * _FP8_PREFILL_TILE_Q, num_head_k, v_head_dim), |
| 991 | torch.float32, |
| 992 | ), |
| 993 | ( |
| 994 | (max_num_partial_tiles * _FP8_PREFILL_TILE_Q, num_head_k), |
| 995 | torch.float32, |
| 996 | ), |
| 997 | ((max_num_batched_tokens, num_head_k), torch.float32), |
| 998 | ] |
| 999 | if self.num_heads < num_head_k: |
| 1000 | # Padded head counts also take their kernel output buffer from the |
| 1001 | # workspace: the caller's [total_q, num_heads * v_head_dim] output |
| 1002 | # cannot back a num_head_k-head view (see _mla_fp8_prefill_attn). |
| 1003 | reservations.append( |
| 1004 | ((max_num_batched_tokens, num_head_k, v_head_dim), attn_out_dtype) |
| 1005 | ) |
| 1006 | current_workspace_manager().get_simultaneous(*reservations) |
| 1007 | |
| 1008 | logger.info( |
| 1009 | "FP8 MLA prefill PS buffers allocated " |
| 1010 | "(max_batch=%d, max_qlen=%d, num_head_k=%d)", |
| 1011 | max_num_reqs, |
| 1012 | max_prefill_qlen, |
| 1013 | num_head_k, |
| 1014 | ) |
| 1015 | |
| 1016 | def _build_fp8_prefill_ps_metadata( |
| 1017 | self, |
| 1018 | metadata: AiterMLAMetadata, |
| 1019 | common_attn_metadata: CommonAttentionMetadata, |
| 1020 | ) -> None: |
| 1021 | """Build per-batch FP8 MLA prefill PS metadata and attach to *metadata*. |
| 1022 | |
| 1023 | Called from ``build()`` when prefill tokens are present and |
| 1024 | FP8 MLA prefill is enabled (auto-detected via |
| 1025 | ``_fp8_mla_prefill_supported()``). |
| 1026 | """ |
| 1027 | from aiter import get_ps_metadata_v1 |
| 1028 | |
| 1029 | prefill = metadata.prefill |
| 1030 | # Caller (build()) only invokes this when prefill tokens exist, so |
| 1031 | # metadata.prefill is guaranteed non-None. Assert to narrow for mypy. |
| 1032 | assert prefill is not None |
| 1033 | qo_indptr = prefill.query_start_loc |
| 1034 | kv_indptr = qo_indptr # new tokens: KV length == Q length |
| 1035 | |
| 1036 | # Reuse the existing CPU view of query_start_loc instead of forcing a |
| 1037 | # device->host copy. Prefill batches sit at the tail of the request |
| 1038 | # list, so we slice from num_decodes onwards and rebase to zero, the |
| 1039 | # same transform the parent build applies on device tensors. |
| 1040 | num_decodes = metadata.num_decodes |
| 1041 | qsl_cpu = common_attn_metadata.query_start_loc_cpu |
| 1042 | qo_indptr_cpu = (qsl_cpu[num_decodes:] - qsl_cpu[num_decodes]).to(torch.int32) |
| 1043 | kv_indptr_cpu = qo_indptr_cpu.clone() |
| 1044 | seq_lens_cpu = (qo_indptr_cpu[1:] - qo_indptr_cpu[:-1]).to(torch.int32) |
| 1045 | |
| 1046 | # Head counts that are not a multiple of 16 (K3: 12/rank at TP8) are |
| 1047 | # replicate-padded up to one in _mla_fp8_prefill_attn; build the PS |
| 1048 | # metadata for that same padded count so the work/reduce maps and the |
| 1049 | # scratch reservations describe the width the kernel is handed. |
| 1050 | # |
| 1051 | # This was previously max(16, num_heads), which agrees with the helper |
| 1052 | # at every head count reachable today (12 and 16 both give 16) and |
| 1053 | # differs only above 16: at 24 it leaves num_head_k=24 while the |
| 1054 | # forward pads to 32. That is not a correctness bug -- the 24-wide work |
| 1055 | # maps still cover head-tiles 0..23, which are the real heads -- but it |
| 1056 | # is expensive, because a lower head alignment yields more partial |
| 1057 | # tiles: gcd-driven, 24 heads -> 64 tiles vs 32 heads -> 16, i.e. |
| 1058 | # 193.6 MiB of reservations instead of 68.6 MiB at batch=1/qlen=512 |
| 1059 | # (measured on gfx950). Sizing both from one helper keeps the widths |
| 1060 | # equal and takes the cheaper tiling. |
| 1061 | num_head_k = AiterMLAHelper.get_fp8_prefill_num_heads(self.num_heads) |
| 1062 | # gqa_ratio = 1 |
| 1063 | # qhead_granularity = max(gqa_ratio, 1) |
| 1064 | # qlen_granularity = _FP8_PREFILL_TILE_Q // qhead_granularity |
| 1065 | gqa_ratio = 1 |
| 1066 | qhead_granularity = 1 |
| 1067 | qlen_granularity = _FP8_PREFILL_TILE_Q |
| 1068 | kvlen_granularity = 128 |
| 1069 | block_size = 1 # non-paged: each "page" is one token |
| 1070 | |
| 1071 | slot = self._fp8_ps_slot |
| 1072 | self._fp8_ps_slot ^= 1 |
| 1073 | staging_free = self._fp8_ps_staging_free[slot] |
| 1074 | with gpu_sync_allowed(): |
| 1075 | staging_free.synchronize() |
| 1076 | ( |
| 1077 | work_indptr_host, |
| 1078 | work_info_host, |
| 1079 | reduce_indptr_host, |
| 1080 | reduce_final_map_host, |
| 1081 | reduce_partial_map_host, |
| 1082 | ) = self._fp8_ps_staging[slot] |
| 1083 | |
| 1084 | # work_metadata is not written by the host planner; it stays on device. |
| 1085 | get_ps_metadata_v1( |
| 1086 | qo_indptr_cpu, |
| 1087 | kv_indptr_cpu, |
| 1088 | seq_lens_cpu, |
| 1089 | gqa_ratio, |
| 1090 | num_head_k, |
| 1091 | self.fp8_ps_work_metadata, |
| 1092 | work_indptr_host, |
| 1093 | work_info_host, |
| 1094 | reduce_indptr_host, |
| 1095 | reduce_final_map_host, |
| 1096 | reduce_partial_map_host, |
| 1097 | qhead_granularity=qhead_granularity, |
| 1098 | qlen_granularity=qlen_granularity, |
| 1099 | kvlen_granularity=kvlen_granularity, |
| 1100 | block_size=block_size, |
| 1101 | is_causal=True, |
| 1102 | ) |
| 1103 | for dst, src in zip(self._fp8_ps_device_outputs, self._fp8_ps_staging[slot]): |
| 1104 | dst.copy_(src, non_blocking=True) |
| 1105 | staging_free.record() |
| 1106 | |
| 1107 | total_prefill_tokens = int(qo_indptr_cpu[-1].item()) |
| 1108 | kv_indices = torch.arange( |
| 1109 | total_prefill_tokens, device=qo_indptr.device, dtype=torch.int32 |
| 1110 | ) |
| 1111 | |
| 1112 | # The actual number of active partial tiles for this batch is the |
| 1113 | # final value of reduce_indptr, read from the host plan (no GPU sync). |
| 1114 | num_partial_tiles = int(reduce_indptr_host[-1]) |
| 1115 | |
| 1116 | # Attach PS metadata to the metadata object so forward_mha can read it. |
| 1117 | metadata.fp8_prefill_qo_indptr = qo_indptr |
| 1118 | metadata.fp8_prefill_kv_indptr = kv_indptr |
| 1119 | metadata.fp8_prefill_kv_indices = kv_indices |
| 1120 | metadata.fp8_prefill_work_indptr = self.fp8_ps_work_indptr |
| 1121 | metadata.fp8_prefill_work_info_set = self.fp8_ps_work_info |
| 1122 | metadata.fp8_prefill_reduce_indptr = self.fp8_ps_reduce_indptr |
| 1123 | metadata.fp8_prefill_reduce_final_map = self.fp8_ps_reduce_final_map |
| 1124 | metadata.fp8_prefill_reduce_partial_map = self.fp8_ps_reduce_partial_map |
| 1125 | metadata.fp8_prefill_max_q_len = prefill.max_query_len |
| 1126 | metadata.fp8_prefill_num_partial_tiles = num_partial_tiles |
| 1127 | |
| 1128 | def _build_dcp_verify_row_view( |
| 1129 | self, |
| 1130 | qlen: int, |
| 1131 | block_table: torch.Tensor, |
| 1132 | dcp_tot_seq_lens: torch.Tensor, |
| 1133 | ) -> AiterMLADCPVerifyMetadata: |
| 1134 | """Build one paged-KV row per verify token for segmented DCP verification. |
| 1135 | |
| 1136 | Every row is a single query (``qo_indptr`` is an arange), so the whole |
| 1137 | causal structure is carried by ``dcp_local_verify_row_lens`` and the |
| 1138 | kernel applies no tail of its own. |
| 1139 | """ |
| 1140 | assert self.dcp_world_size > 1 |
| 1141 | num_reqs = dcp_tot_seq_lens.numel() |
| 1142 | row_lens = AiterMLAHelper.dcp_local_verify_row_lens( |
| 1143 | dcp_tot_seq_lens, |
| 1144 | qlen, |
| 1145 | self.dcp_world_size, |
| 1146 | self.dcp_rank, |
| 1147 | self.cp_kv_cache_interleave_size, |
| 1148 | ) |
| 1149 | num_rows = row_lens.numel() |
| 1150 | buffers = self._dcp_verify_buffers |
| 1151 | # Persistent buffers exist exactly when full graphs are on, and they |
| 1152 | # already carry the static bound they were sized for. |
| 1153 | max_kv_seq_len = ( |
| 1154 | buffers.max_kv_seq_len |
| 1155 | if buffers is not None |
| 1156 | else max(1, int(row_lens.max().item())) |
| 1157 | ) |
| 1158 | page_size = self._segmented_page_size |
| 1159 | max_local_pages = cdiv(max_kv_seq_len, page_size) |
| 1160 | if buffers is not None: |
| 1161 | row_block_table = buffers.block_table[:num_rows] |
| 1162 | buffers.row_lens[:num_rows].copy_(row_lens, non_blocking=True) |
| 1163 | row_lens = buffers.row_lens[:num_rows] |
| 1164 | qo_indptr = buffers.qo_indptr[: num_rows + 1] |
| 1165 | else: |
| 1166 | row_block_table = torch.empty( |
| 1167 | (num_rows, max_local_pages), |
| 1168 | dtype=torch.int32, |
| 1169 | device=block_table.device, |
| 1170 | ) |
| 1171 | qo_indptr = torch.arange( |
| 1172 | num_rows + 1, |
| 1173 | dtype=torch.int32, |
| 1174 | device=block_table.device, |
| 1175 | ) |
| 1176 | self._fill_dcp_verify_page_table(row_block_table, block_table, num_reqs, qlen) |
| 1177 | return AiterMLADCPVerifyMetadata( |
| 1178 | row_lens=row_lens, |
| 1179 | block_table=row_block_table, |
| 1180 | qo_indptr=qo_indptr, |
| 1181 | page_size=page_size, |
| 1182 | max_kv_seq_len=max_kv_seq_len, |
| 1183 | ) |
| 1184 | |
| 1185 | def _fill_dcp_verify_page_table( |
| 1186 | self, |
| 1187 | row_block_table: torch.Tensor, |
| 1188 | block_table: torch.Tensor, |
| 1189 | num_reqs: int, |
| 1190 | qlen: int, |
| 1191 | ) -> None: |
| 1192 | """Expand each request's blocks into the subpages one verify row reads. |
| 1193 | |
| 1194 | Every row of a request shares the request's shard, so the page list is |
| 1195 | built once per request and repeated; only ``row_lens`` distinguishes the |
| 1196 | rows. A DCP rank holds ``1/dcp_world_size`` of the sequence, so the |
| 1197 | request's block table is always wider than the pages a row can reach. |
| 1198 | """ |
| 1199 | kernel_block_size = self.kernel_block_size |
| 1200 | page_size = self._segmented_page_size |
| 1201 | assert kernel_block_size is not None |
| 1202 | assert page_size is not None |
| 1203 | pages_per_block = kernel_block_size // page_size |
| 1204 | max_local_pages = row_block_table.shape[1] |
| 1205 | max_local_blocks = cdiv(max_local_pages, pages_per_block) |
| 1206 | assert max_local_blocks <= block_table.shape[1], ( |
| 1207 | f"DCP verify needs {max_local_blocks} blocks per request but the " |
| 1208 | f"block table only has {block_table.shape[1]}" |
| 1209 | ) |
| 1210 | subpage_offsets = torch.arange( |
| 1211 | pages_per_block, |
| 1212 | dtype=block_table.dtype, |
| 1213 | device=block_table.device, |
| 1214 | ) |
| 1215 | per_req_page_table = ( |
| 1216 | block_table[:num_reqs, :max_local_blocks, None] * pages_per_block |
| 1217 | + subpage_offsets |
| 1218 | ).flatten(1)[:, :max_local_pages] |
| 1219 | # Broadcast the request's page list across its rows instead of |
| 1220 | # materializing a repeat_interleave copy. |
| 1221 | row_block_table.unflatten(0, (num_reqs, qlen)).copy_( |
| 1222 | per_req_page_table.unsqueeze(1) |
| 1223 | ) |
| 1224 | |
| 1225 | def _build_decode( |
| 1226 | self, |
| 1227 | block_table_tensor: torch.Tensor, |
| 1228 | seq_lens_device: torch.Tensor, |
| 1229 | max_seq_len: int, |
| 1230 | query_start_loc_cpu: torch.Tensor, |
| 1231 | query_start_loc_device: torch.Tensor, |
| 1232 | num_decode_tokens: int, |
| 1233 | max_query_len: int, |
| 1234 | dcp_tot_seq_lens_device: torch.Tensor | None, |
| 1235 | ) -> AiterMLADecodeMetadata: |
| 1236 | causal = self._decode_causal |
| 1237 | device = self.device |
| 1238 | num_reqs = seq_lens_device.size(0) |
| 1239 | qo_len = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1] |
| 1240 | max_qo_len = qo_len.max().item() |
| 1241 | padded_mtp_qo_len = self._uniform_padded_mtp_qo_len( |
| 1242 | qo_len, max_qo_len, num_decode_tokens |
| 1243 | ) |
| 1244 | if padded_mtp_qo_len > 0: |
| 1245 | max_qo_len = padded_mtp_qo_len |
| 1246 | pad_uniform_mtp = padded_mtp_qo_len > 0 |
| 1247 | |
| 1248 | seq_lens_for_kernel = seq_lens_device |
| 1249 | # the global lengths need the same dummy-row |
| 1250 | # treatment as the local ones below, but only for g_kv_indptr -- the |
| 1251 | # metadata field keeps the true values, which is what the segmented |
| 1252 | # route wants (it derives its own row lens and gives a dummy row |
| 1253 | # length 0, correctly). |
| 1254 | g_tot_seq_lens = dcp_tot_seq_lens_device |
| 1255 | num_kernel_reqs = num_reqs |
| 1256 | if pad_uniform_mtp: |
| 1257 | qo_lens_device = ( |
| 1258 | query_start_loc_device[1 : num_reqs + 1] |
| 1259 | - query_start_loc_device[:num_reqs] |
| 1260 | ).to(torch.int32) |
| 1261 | seq_lens_for_kernel = torch.where( |
| 1262 | qo_lens_device > 0, |
| 1263 | seq_lens_for_kernel, |
| 1264 | seq_lens_for_kernel.new_full((), max_qo_len), |
| 1265 | ) |
| 1266 | # A dummy row just got a LOCAL length of max_qo_len, so its GLOBAL |
| 1267 | # length has to say the same thing or the cprr mask is degenerate: |
| 1268 | # global_len 0 puts the row query positions at {-3,-2,-1} while its |
| 1269 | # local shard sits at {rank, W+rank, 2W+rank}, so every entry is |
| 1270 | # masked and the row softmaxes over nothing. max_qo_len * W is |
| 1271 | # exactly the global length whose round-robin shard is max_qo_len |
| 1272 | # on every rank. |
| 1273 | if g_tot_seq_lens is not None: |
| 1274 | g_tot_seq_lens = torch.where( |
| 1275 | qo_lens_device > 0, |
| 1276 | g_tot_seq_lens, |
| 1277 | g_tot_seq_lens.new_full((), max_qo_len * self.dcp_world_size), |
| 1278 | ) |
| 1279 | |
| 1280 | if self._graph_seq_lens is not None: |
| 1281 | self._graph_seq_lens[:num_kernel_reqs].copy_( |
| 1282 | seq_lens_for_kernel, non_blocking=True |
| 1283 | ) |
| 1284 | seq_lens_for_kernel = self._graph_seq_lens[:num_kernel_reqs] |
| 1285 | |
| 1286 | # The aiter kernel always operates with page_size=1 (the wrapper |
| 1287 | # flattens kv_buffer). last_page_len is always 1. |
| 1288 | paged_kv_last_page_len = self.paged_kv_last_page_len[:num_kernel_reqs] |
| 1289 | |
| 1290 | # indptr: cumsum of seq_lens (one page per token in the flat view) |
| 1291 | paged_kv_indptr = torch.cat( |
| 1292 | [ |
| 1293 | torch.zeros(1, dtype=torch.int32, device=device), |
| 1294 | seq_lens_for_kernel.cumsum(dim=0, dtype=torch.int32), |
| 1295 | ] |
| 1296 | ) |
| 1297 | use_gluon_decode = AiterMLAHelper.use_gluon_decode( |
| 1298 | self._decode_num_heads, |
| 1299 | int(max_qo_len), |
| 1300 | self._kv_cache_dtype_str, |
| 1301 | self._kv_cache_bytes, |
| 1302 | ) |
| 1303 | use_gluon_verify = AiterMLAHelper.use_gluon_verify( |
| 1304 | self._decode_num_heads, |
| 1305 | int(max_qo_len), |
| 1306 | self._kv_cache_dtype_str, |
| 1307 | self.dcp_world_size, |
| 1308 | causal, |
| 1309 | self._kv_cache_bytes, |
| 1310 | ) |
| 1311 | dcp_route = _DCPDecodeRoute.PLAIN |
| 1312 | if self.dcp_world_size > 1: |
| 1313 | dcp_route = _select_dcp_decode_route( |
| 1314 | self._supports_segmented_dcp_verify, |
| 1315 | causal, |
| 1316 | int(max_qo_len), |
| 1317 | self._asm_dcp_verify, |
| 1318 | ) |
| 1319 | |
| 1320 | # Segmented DCP verify carries its own per-row subpage table, so the |
| 1321 | # flat per-token view is dead work for it. Leave the buffer alone and |
| 1322 | # hand the metadata None, so a future reader cannot pick up whatever |
| 1323 | # the previous batch left behind. |
| 1324 | paged_kv_indices = None |
| 1325 | if dcp_route is not _DCPDecodeRoute.SEGMENTED: |
| 1326 | if self.compilation_config.cudagraph_mode.has_full_cudagraphs(): |
| 1327 | self.paged_kv_indices.fill_(-1) |
| 1328 | |
| 1329 | # Expand block_table entries into per-token flat indices. |
| 1330 | # When kernel_block_size=1, this degrades to a direct copy (identical |
| 1331 | # to the original _copy_page_indices_kernel). |
| 1332 | # When kernel_block_size=K>1, block_table entry b covering K tokens |
| 1333 | # gets expanded to flat indices b*K, b*K+1, ..., b*K+(K-1). |
| 1334 | # Chunk count comes from the block table width, an upper bound on |
| 1335 | # tokens per request that is available host-side, so this adds no |
| 1336 | # device synchronisation. Programs whose chunk lies past |
| 1337 | # num_tokens mask out entirely. |
| 1338 | max_tokens_per_req = block_table_tensor.shape[1] * self.kernel_block_size |
| 1339 | num_chunks = max(1, cdiv(max_tokens_per_req, 1024)) |
| 1340 | _expand_page_indices_kernel[(num_reqs, num_chunks)]( |
| 1341 | self.paged_kv_indices, |
| 1342 | block_table_tensor, |
| 1343 | block_table_tensor.stride(0), |
| 1344 | block_table_tensor.stride(1), |
| 1345 | paged_kv_indptr, |
| 1346 | KERNEL_BLOCK_SIZE=self.kernel_block_size, |
| 1347 | BLOCK_SIZE=1024, |
| 1348 | ) |
| 1349 | paged_kv_indices = self.paged_kv_indices |
| 1350 | |
| 1351 | if self.compilation_config.cudagraph_mode.has_full_cudagraphs(): |
| 1352 | self.paged_kv_indptr[: 1 + num_kernel_reqs].copy_( |
| 1353 | paged_kv_indptr, non_blocking=True |
| 1354 | ) |
| 1355 | self.paged_kv_indptr[1 + num_kernel_reqs :].fill_(paged_kv_indptr[-1]) |
| 1356 | paged_kv_indptr = self.paged_kv_indptr[: 1 + num_kernel_reqs] |
| 1357 | |
| 1358 | # paged_kv_last_page_len already uses the pre-initialized buffer slice |
| 1359 | # (set above), so no copy needed - buffer is always 1s. |
| 1360 | |
| 1361 | if pad_uniform_mtp: |
| 1362 | qo_indptr_src = AiterMLAHelper.qo_indptr_for_uniform_qlen( |
| 1363 | num_kernel_reqs, int(max_qo_len), device |
| 1364 | ) |
| 1365 | else: |
| 1366 | qo_indptr_src = query_start_loc_device[: 1 + num_kernel_reqs] |
| 1367 | self.qo_indptr[: 1 + num_kernel_reqs].copy_( |
| 1368 | qo_indptr_src, non_blocking=True |
| 1369 | ) |
| 1370 | self.qo_indptr[1 + num_kernel_reqs :] = qo_indptr_src[-1] |
| 1371 | qo_indptr = self.qo_indptr[: 1 + num_kernel_reqs] |
| 1372 | |
| 1373 | else: |
| 1374 | if max_qo_len == 1: |
| 1375 | qo_indptr = AiterMLAHelper.qo_indptr_for_uniform_qlen( |
| 1376 | num_kernel_reqs, 1, device |
| 1377 | ) |
| 1378 | else: |
| 1379 | if pad_uniform_mtp: |
| 1380 | qo_indptr = AiterMLAHelper.qo_indptr_for_uniform_qlen( |
| 1381 | num_kernel_reqs, int(max_qo_len), device |
| 1382 | ) |
| 1383 | else: |
| 1384 | qo_indptr = query_start_loc_device[: 1 + num_kernel_reqs] |
| 1385 | |
| 1386 | g_kv_indptr = None |
| 1387 | if dcp_route is _DCPDecodeRoute.CPRR and g_tot_seq_lens is not None: |
| 1388 | assert self._g_kv_indptr_buf is not None |
| 1389 | _ngk = g_tot_seq_lens.shape[0] |
| 1390 | g_kv_indptr = self._g_kv_indptr_buf[: _ngk + 1] |
| 1391 | g_kv_indptr[1:].copy_(g_tot_seq_lens.cumsum(dim=0, dtype=torch.int32)) |
| 1392 | # A FULL cudagraph replays at its capture-time request count, so a |
| 1393 | # smaller real batch leaves the tail holding the PREVIOUS step's |
| 1394 | # cumsum. Flatten it the way paged_kv_indptr is flattened below, so |
| 1395 | # padding rows read global_len == 0 and agree with their zero-length |
| 1396 | # local shard instead of a stale global length. |
| 1397 | self._g_kv_indptr_buf[_ngk + 1 :].fill_(g_kv_indptr[-1]) |
| 1398 | |
| 1399 | has_persistent_metadata = False |
| 1400 | # Only the asm decode consumes the schedule, so gate on the routing |
| 1401 | # rather than on num_heads >= 16, which denies it to a padded rank |
| 1402 | # running the same asm kernels. The predicates are disjoint -- decode |
| 1403 | # is qlen==1, verify is qlen>1 -- and cover both Gluon entries plus the |
| 1404 | # segmented DCP verify. |
| 1405 | use_persistent_metadata = ( |
| 1406 | not use_gluon_decode |
| 1407 | and not use_gluon_verify |
| 1408 | and dcp_route is not _DCPDecodeRoute.SEGMENTED |
| 1409 | # A padded rank has no bf16 persistent kernel past qlen 4 where the |
| 1410 | # gfx950 fold is absent; the non-persistent entry covers it. fp8 |
| 1411 | # keeps the schedule -- its fold rejects non-persistent outright. |
| 1412 | # A non-causal block keeps it too: what the fold drops past qlen 4 |
| 1413 | # is the block's causal staircase, which a non-causal block does |
| 1414 | # not have, and the schedule is the only thing carrying its mask. |
| 1415 | and ( |
| 1416 | not causal |
| 1417 | or self._decode_num_heads >= AiterMLAHelper._AITER_MIN_MLA_HEADS |
| 1418 | or max_qo_len <= AiterMLAHelper._ASM_PADDED_MAX_PS_QLEN |
| 1419 | or is_quantized_kv_cache(self._kv_cache_dtype_str) |
| 1420 | ) |
| 1421 | and max_qo_len >= 1 |
| 1422 | and max_qo_len <= self._mtp_decode_qlen |
| 1423 | ) |
| 1424 | if ( |
| 1425 | not causal |
| 1426 | and max_qo_len == 2 |
| 1427 | and is_quantized_kv_cache(self._kv_cache_dtype_str) |
| 1428 | and not AiterMLAHelper.has_fp8_non_causal_qlen2_kernel( |
| 1429 | self._decode_num_heads |
| 1430 | ) |
| 1431 | ): |
| 1432 | # AITER's fp8 dispatch folds (gqa 16, qlen 3 or 4) onto the |
| 1433 | # qseqlen-4 kernel but never lists 2. Only 32/64/96/128 heads |
| 1434 | # at qlen 2 fold onto that 16-head / 4-token non-causal kernel; |
| 1435 | # 16-head qlen 2, and padded 48/80/112, keep Q2 and abort the |
| 1436 | # process rather than raising. The bf16 fold has no such hole. |
| 1437 | raise ValueError( |
| 1438 | "AITER has no non-causal fp8 MLA kernel for this 2-token " |
| 1439 | "query block. Pin the draft to TRITON_MLA for this " |
| 1440 | "speculative config." |
| 1441 | ) |
| 1442 | if use_persistent_metadata: |
| 1443 | from aiter import get_mla_metadata_v1 |
| 1444 | |
| 1445 | uni_qo_len = ( |
| 1446 | max_qo_len if pad_uniform_mtp or torch.all(qo_len == max_qo_len) else -1 |
| 1447 | ) |
| 1448 | cprr_kwargs: dict = {} |
| 1449 | is_causal = causal |
| 1450 | if self._asm_dcp_verify: |
| 1451 | cprr_kwargs = dict( |
| 1452 | max_split_per_batch=self._mla_max_split_per_batch, |
| 1453 | intra_batch_mode=False, |
| 1454 | ) |
| 1455 | if dcp_route is _DCPDecodeRoute.CPRR: |
| 1456 | is_causal = False |
| 1457 | cprr_kwargs["is_cp_round_robin"] = True |
| 1458 | metadata_num_heads = ( |
| 1459 | self._asm_dcp_verify_heads |
| 1460 | if dcp_route is _DCPDecodeRoute.CPRR |
| 1461 | else AiterMLAHelper.get_actual_mla_num_heads(self._decode_num_heads) |
| 1462 | ) |
| 1463 | get_mla_metadata_v1( |
| 1464 | qo_indptr, |
| 1465 | paged_kv_indptr, |
| 1466 | paged_kv_last_page_len, |
| 1467 | metadata_num_heads, |
| 1468 | 1, |
| 1469 | is_causal, |
| 1470 | self._mla_work_meta_data, |
| 1471 | self._mla_work_info_set, |
| 1472 | self._mla_work_indptr, |
| 1473 | self._mla_reduce_indptr, |
| 1474 | self._mla_reduce_final_map, |
| 1475 | self._mla_reduce_partial_map, |
| 1476 | page_size=1, |
| 1477 | kv_granularity=16, |
| 1478 | max_seqlen_qo=max_qo_len, |
| 1479 | uni_seqlen_qo=uni_qo_len, |
| 1480 | fast_mode=True, |
| 1481 | dtype_q=self._mla_q_dtype, |
| 1482 | dtype_kv=self._mla_kv_dtype, |
| 1483 | **cprr_kwargs, |
| 1484 | ) |
| 1485 | has_persistent_metadata = True |
| 1486 | |
| 1487 | # Small-head multi-token verify uses mla_gluon's 4-D MTP entry over the |
| 1488 | # ordinary per-request paged-KV view, so there is no expanded per-token |
| 1489 | # buffer to build here. mla_gluon still wants a lower bound on the KV |
| 1490 | # length it is asked to split; on the verify path that bound is over |
| 1491 | # active requests, not cudagraph padding rows pinned to max_qo_len. |
| 1492 | # The .item() below runs in the builder, outside the captured region, |
| 1493 | # so it does not abort HIP graph capture the way the per-layer syncs |
| 1494 | # in forward_mqa did. DCP verify never reaches here -- it carries its |
| 1495 | # own per-row lengths instead of a single split bound. |
| 1496 | min_kv_seq_len = 1 |
| 1497 | if use_gluon_verify: |
| 1498 | per_req_len = paged_kv_indptr[1:] - paged_kv_indptr[:-1] |
| 1499 | if pad_uniform_mtp: |
| 1500 | active = qo_lens_device > 0 |
| 1501 | if active.any(): |
| 1502 | min_kv_seq_len = int(per_req_len[active].min().item()) |
| 1503 | else: |
| 1504 | min_kv_seq_len = int(per_req_len.min().item()) |
| 1505 | |
| 1506 | dcp_verify = None |
| 1507 | if dcp_route is _DCPDecodeRoute.SEGMENTED: |
| 1508 | assert dcp_tot_seq_lens_device is not None |
| 1509 | dcp_verify = self._build_dcp_verify_row_view( |
| 1510 | int(max_qo_len), |
| 1511 | block_table_tensor, |
| 1512 | dcp_tot_seq_lens_device, |
| 1513 | ) |
| 1514 | |
| 1515 | attn_metadata = AiterMLADecodeMetadata( |
| 1516 | block_table=block_table_tensor, |
| 1517 | seq_lens=seq_lens_for_kernel, |
| 1518 | paged_kv_indptr=paged_kv_indptr, |
| 1519 | paged_kv_indices=paged_kv_indices, |
| 1520 | paged_kv_last_page_len=paged_kv_last_page_len, |
| 1521 | qo_indptr=qo_indptr, |
| 1522 | dcp_tot_seq_lens=dcp_tot_seq_lens_device, |
| 1523 | max_qo_len=max_qo_len, |
| 1524 | min_kv_seq_len=min_kv_seq_len, |
| 1525 | dcp_route=dcp_route, |
| 1526 | dcp_verify=dcp_verify, |
| 1527 | g_kv_indptr=g_kv_indptr, |
| 1528 | cp_world_size=self.dcp_world_size, |
| 1529 | cp_rank=self.dcp_rank, |
| 1530 | asm_decode_num_heads=( |
| 1531 | self._asm_dcp_verify_heads if dcp_route is _DCPDecodeRoute.CPRR else 0 |
| 1532 | ), |
| 1533 | mla_num_kv_splits=( |
| 1534 | self._mla_max_split_per_batch |
| 1535 | if dcp_route is _DCPDecodeRoute.CPRR |
| 1536 | else 0 |
| 1537 | ), |
| 1538 | use_gluon_decode=use_gluon_decode, |
| 1539 | use_gluon_verify=use_gluon_verify, |
| 1540 | attn_out_dtype=self.decode_attn_out_dtype, |
| 1541 | has_persistent_metadata=has_persistent_metadata, |
| 1542 | ) |
| 1543 | |
| 1544 | return attn_metadata |
| 1545 | |
| 1546 | def build( |
| 1547 | self, |
| 1548 | common_prefix_len: int, |
| 1549 | common_attn_metadata: CommonAttentionMetadata, |
| 1550 | fast_build: bool = False, |
| 1551 | ) -> AiterMLAMetadata: |
| 1552 | # The common MLA builder owns the causal flag but its _build_decode |
| 1553 | # contract is shared with CUDA backends. Keep this ROCm-specific state |
| 1554 | # on the per-ubatch Aiter builder instead of changing that shared API; |
| 1555 | # super().build invokes _build_decode synchronously. |
| 1556 | self._decode_causal = common_attn_metadata.causal |
| 1557 | attn_metadata = super().build( |
| 1558 | common_prefix_len, common_attn_metadata, fast_build |
| 1559 | ) |
| 1560 | if ( |
| 1561 | attn_metadata.decode is not None |
| 1562 | and attn_metadata.decode.has_persistent_metadata |
| 1563 | ): |
| 1564 | attn_metadata.work_meta_data = self._mla_work_meta_data |
| 1565 | attn_metadata.work_indptr = self._mla_work_indptr |
| 1566 | attn_metadata.work_info_set = self._mla_work_info_set |
| 1567 | attn_metadata.reduce_indptr = self._mla_reduce_indptr |
| 1568 | attn_metadata.reduce_final_map = self._mla_reduce_final_map |
| 1569 | attn_metadata.reduce_partial_map = self._mla_reduce_partial_map |
| 1570 | if ( |
| 1571 | self._fp8_prefill_enabled |
| 1572 | and attn_metadata.prefill is not None |
| 1573 | and attn_metadata.prefill.chunked_context is None |
| 1574 | ): |
| 1575 | self._build_fp8_prefill_ps_metadata(attn_metadata, common_attn_metadata) |
| 1576 | return attn_metadata |
| 1577 | |
| 1578 | |
| 1579 | @triton.jit |
| 1580 | def _expand_page_indices_kernel( |
| 1581 | page_indices, |
| 1582 | block_table, |
| 1583 | block_table_stride_0, |
| 1584 | block_table_stride_1, |
| 1585 | cu_num_tokens, |
| 1586 | KERNEL_BLOCK_SIZE: tl.constexpr, |
| 1587 | BLOCK_SIZE: tl.constexpr, |
| 1588 | ): |
| 1589 | """Expand block table entries into per-token flat page indices. |
| 1590 | |
| 1591 | The aiter MLA kernel always operates with page_size=1 internally |
| 1592 | (kv_buffer is flattened via .view(-1, 1, 1, H)). This kernel converts |
| 1593 | block-level indices from the block table into individual token positions |
| 1594 | in the flattened KV buffer. |
| 1595 | |
| 1596 | When KERNEL_BLOCK_SIZE=1: block_idx=t, offset=0, flat=block_id |
| 1597 | (equivalent to a direct copy -- no regression from the original kernel). |
| 1598 | |
| 1599 | When KERNEL_BLOCK_SIZE=K: block table entry b (covering K tokens) |
| 1600 | is expanded to flat indices b*K, b*K+1, ..., b*K+(K-1). |
| 1601 | """ |
| 1602 | # One program per (request, token-chunk). Parallelising over requests |
| 1603 | # alone degenerates at low concurrency: with num_reqs == 1 a single |
| 1604 | # workgroup walked the whole sequence in a serial loop. |
| 1605 | req_idx = tl.program_id(0) |
| 1606 | chunk_idx = tl.program_id(1) |
| 1607 | row_ptr = block_table + req_idx * block_table_stride_0 |
| 1608 | start_idx = tl.load(cu_num_tokens + req_idx) |
| 1609 | num_tokens = tl.load(cu_num_tokens + req_idx + 1) - start_idx |
| 1610 | |
| 1611 | # The grid is sized from the block table width, an upper bound over all |
| 1612 | # requests, so a ragged batch launches chunks past a short request's end. |
| 1613 | # Returning here keeps those programs from issuing masked-out loads and |
| 1614 | # stores at all. |
| 1615 | chunk_start = chunk_idx * BLOCK_SIZE |
| 1616 | if chunk_start >= num_tokens: |
| 1617 | return |
| 1618 | |
| 1619 | token_offsets = chunk_start + tl.arange(0, BLOCK_SIZE) |
| 1620 | mask = token_offsets < num_tokens |
| 1621 | |
| 1622 | # Which block in the block table does this token belong to? |
| 1623 | block_idx = token_offsets // KERNEL_BLOCK_SIZE |
| 1624 | # Offset within that block |
| 1625 | offset_in_block = token_offsets % KERNEL_BLOCK_SIZE |
| 1626 | |
| 1627 | # Load the block ID from the block table |
| 1628 | # Both strides are taken from the caller: the block table is a view owned |
| 1629 | # elsewhere, so a unit column stride must not be assumed. |
| 1630 | block_ids = tl.load(row_ptr + block_idx * block_table_stride_1, mask=mask) |
| 1631 | |
| 1632 | # Compute flat index in the flattened kv_buffer |
| 1633 | flat_indices = block_ids * KERNEL_BLOCK_SIZE + offset_in_block |
| 1634 | |
| 1635 | tl.store( |
| 1636 | page_indices + start_idx + token_offsets, |
| 1637 | flat_indices, |
| 1638 | mask=mask, |
| 1639 | ) |
| 1640 | |
| 1641 | |
| 1642 | class AiterMLAHelper: |
| 1643 | """AITER MLA persistent (asm) decode requires a multiple of 16 heads. Unaligned |
| 1644 | head counts through 128 are padded to the next multiple of 16 by tiling the |
| 1645 | query heads and slicing to the padded size. Native H24 AITER builds bypass |
| 1646 | that padding. Small divisors of 16 retain the existing repeat_interleave and |
| 1647 | strided-unpad behavior. Native and aligned counts pass through without |
| 1648 | copies. |
| 1649 | """ |
| 1650 | |
| 1651 | _AITER_MIN_MLA_HEADS: Final = 16 |
| 1652 | _AITER_MAX_PADDED_MLA_HEADS: Final = 128 |
| 1653 | # Largest qlen the padded gqa=16 asm decode has a bf16 persistent kernel |
| 1654 | # for. Above it only the non-persistent qseqlen=8 entry exists, and the |
| 1655 | # fold that reaches a persistent one is gfx950-only. |
| 1656 | _ASM_PADDED_MAX_PS_QLEN: Final = 4 |
| 1657 | _AITER_UNSUPPORTED_HEADS: ClassVar[tuple[int, ...]] = () |
| 1658 | # Pinned AITER v0.1.21.post2 folds these fp8 qlen-2 counts onto the |
| 1659 | # non-causal 16-head / 4-token kernel. Other multiples of 16 (48/80/112) |
| 1660 | # fold to H16 while keeping Q2, which has no non-causal fp8 entry. |
| 1661 | _AITER_FP8_NON_CAUSAL_QLEN2_HEADS: ClassVar[tuple[int, ...]] = (32, 64, 96, 128) |
| 1662 | |
| 1663 | @staticmethod |
| 1664 | def qo_indptr_for_uniform_qlen( |
| 1665 | num_reqs: int, |
| 1666 | qlen: int, |
| 1667 | device: torch.device, |
| 1668 | dtype: torch.dtype = torch.int32, |
| 1669 | ) -> torch.Tensor: |
| 1670 | """Build ``[0, qlen, 2*qlen, ..., num_reqs*qlen]``.""" |
| 1671 | return torch.arange( |
| 1672 | 0, |
| 1673 | (num_reqs + 1) * qlen, |
| 1674 | step=qlen, |
| 1675 | dtype=dtype, |
| 1676 | device=device, |
| 1677 | ) |
| 1678 | |
| 1679 | @staticmethod |
| 1680 | def check_num_heads_validity(num_heads: int): |
| 1681 | assert AiterMLAHelper.is_valid_num_heads(num_heads), ( |
| 1682 | "ROCM AITER MLA requires a positive multiple of 16 heads, or an " |
| 1683 | "unaligned head count up to 128 (padded to the next multiple of " |
| 1684 | f"16), but got {num_heads}.\n" |
| 1685 | f"Try adjusting tensor_parallel_size value." |
| 1686 | ) |
| 1687 | |
| 1688 | @staticmethod |
| 1689 | def is_valid_num_heads(num_heads: int) -> bool: |
| 1690 | return ( |
| 1691 | num_heads > 0 |
| 1692 | and num_heads not in AiterMLAHelper._AITER_UNSUPPORTED_HEADS |
| 1693 | and ( |
| 1694 | num_heads <= AiterMLAHelper._AITER_MAX_PADDED_MLA_HEADS |
| 1695 | or num_heads % AiterMLAHelper._AITER_MIN_MLA_HEADS == 0 |
| 1696 | ) |
| 1697 | ) |
| 1698 | |
| 1699 | @staticmethod |
| 1700 | def has_fp8_non_causal_qlen2_kernel(num_heads: int) -> bool: |
| 1701 | """Whether fp8 (num_heads, qlen=2) folds onto a non-causal ASM kernel. |
| 1702 | |
| 1703 | Pinned AITER v0.1.21.post2 folds 32/64/96/128 heads at qlen 2 onto the |
| 1704 | non-causal 16-head / 4-token kernel. A 16-head (or padded-to-16) qlen-2 |
| 1705 | block has no matching entry, and padded 48/80/112 keep Q2 after the |
| 1706 | H16 fold. |
| 1707 | """ |
| 1708 | kernel_heads = AiterMLAHelper.get_actual_mla_num_heads(num_heads) |
| 1709 | return kernel_heads in AiterMLAHelper._AITER_FP8_NON_CAUSAL_QLEN2_HEADS |
| 1710 | |
| 1711 | @staticmethod |
| 1712 | def get_actual_mla_num_heads(num_heads: int) -> int: |
| 1713 | if num_heads == 24 and _aiter_mla_native_h24_supported(): |
| 1714 | return num_heads |
| 1715 | m = AiterMLAHelper._AITER_MIN_MLA_HEADS |
| 1716 | return -(-num_heads // m) * m |
| 1717 | |
| 1718 | @staticmethod |
| 1719 | def get_fp8_prefill_num_heads(num_heads: int) -> int: |
| 1720 | """Head count the FP8 PS asm prefill runs at: the next multiple of 16. |
| 1721 | |
| 1722 | Deliberately *not* ``get_actual_mla_num_heads``. That one carves out |
| 1723 | native H24 when ``_aiter_mla_native_h24_supported()``, which probes the |
| 1724 | asm *decode* reducer and metadata. The prefill is a different kernel |
| 1725 | pair (``mla_prefill_ps_asm_fwd`` + ``mla_reduce_v1``) with no such |
| 1726 | probe, so 24 heads pad to 32 here even on a native-H24 build. |
| 1727 | |
| 1728 | The PS metadata in ``_init_fp8_prefill_ps_buffers``/``build()`` must be |
| 1729 | sized with this same function, or the work/reduce maps describe a |
| 1730 | different head count than the kernel is handed. |
| 1731 | |
| 1732 | This function itself has no upper bound; the ceiling comes from the |
| 1733 | ``is_valid_num_heads`` gate, which rejects counts above |
| 1734 | ``_AITER_MAX_PADDED_MLA_HEADS`` (128) unless they are already |
| 1735 | 16-aligned. That bound was established for the asm *decode* padding, so |
| 1736 | an architecture with, say, 136 heads per rank would be refused the |
| 1737 | prefill here for a reason that was never measured against this kernel |
| 1738 | pair. Revisit the constant rather than special-casing prefill. |
| 1739 | """ |
| 1740 | m = AiterMLAHelper._AITER_MIN_MLA_HEADS |
| 1741 | return -(-num_heads // m) * m |
| 1742 | |
| 1743 | @staticmethod |
| 1744 | def get_mla_padded_q( |
| 1745 | num_heads: int, q: torch.Tensor, target_heads: int | None = None |
| 1746 | ) -> torch.Tensor: |
| 1747 | m = ( |
| 1748 | target_heads |
| 1749 | if target_heads is not None |
| 1750 | else AiterMLAHelper.get_actual_mla_num_heads(num_heads) |
| 1751 | ) |
| 1752 | if num_heads == m: |
| 1753 | return q |
| 1754 | if m % num_heads == 0: |
| 1755 | return q.repeat_interleave(m // num_heads, dim=1) |
| 1756 | # Non-divisor head counts cannot be padded by repeat_interleave. Tile |
| 1757 | # the query heads and slice to exactly m. MLA attention is independent |
| 1758 | # per query head over the shared KV, so padding heads cannot affect |
| 1759 | # heads [0:num_heads]; they are sliced back off the output. |
| 1760 | reps = -(-m // num_heads) # ceil(m / num_heads) |
| 1761 | # Slicing a tiled tensor yields a non-contiguous view. The asm decode |
| 1762 | # reads q as packed [tokens, m, head_dim], so materialize it. |
| 1763 | return q.repeat(1, reps, 1)[:, :m, :].contiguous() |
| 1764 | |
| 1765 | @staticmethod |
| 1766 | def get_mla_unpadded_o(num_heads: int, o: torch.Tensor) -> torch.Tensor: |
| 1767 | return AiterMLAHelper._get_mla_unpadded_heads(num_heads, o) |
| 1768 | |
| 1769 | @staticmethod |
| 1770 | def _get_mla_unpadded_heads(num_heads: int, tensor: torch.Tensor) -> torch.Tensor: |
| 1771 | m = AiterMLAHelper.get_actual_mla_num_heads(num_heads) |
| 1772 | if num_heads == m: |
| 1773 | return tensor |
| 1774 | if m % num_heads == 0: |
| 1775 | return tensor[:, :: m // num_heads, ...] |
| 1776 | # Undo the tile-padding from get_mla_padded_q: the real heads are the |
| 1777 | # first num_heads. |
| 1778 | return tensor[:, :num_heads, ...] |
| 1779 | |
| 1780 | @staticmethod |
| 1781 | def get_mla_unpadded_lse(num_heads: int, lse: torch.Tensor) -> torch.Tensor: |
| 1782 | return AiterMLAHelper._get_mla_unpadded_heads(num_heads, lse) |
| 1783 | |
| 1784 | @staticmethod |
| 1785 | def use_gluon_decode( |
| 1786 | num_heads: int, |
| 1787 | max_qo_len: int, |
| 1788 | kv_cache_dtype: str, |
| 1789 | kv_cache_bytes: int | None = None, |
| 1790 | ) -> bool: |
| 1791 | # Small-head (<16) single-token decode takes either the Gluon kernel or |
| 1792 | # the padded asm persistent decode, selected by |
| 1793 | # VLLM_ROCM_AITER_MLA_ASM_PADDING and the arch (Gluon is gfx950 only). |
| 1794 | m = AiterMLAHelper._AITER_MIN_MLA_HEADS |
| 1795 | if num_heads >= m or max_qo_len != 1: |
| 1796 | return False |
| 1797 | # Gluon's only fp8-KV regime, bh16bn128, is a bf16-query kernel with a |
| 1798 | # hardcoded scale that asserts batch_size == 1, so it cannot serve a |
| 1799 | # decode batch. Checked before the mode knob: an explicit "gluon" |
| 1800 | # request under fp8 would assert immediately. |
| 1801 | if is_quantized_kv_cache(kv_cache_dtype): |
| 1802 | return False |
| 1803 | mode = _aiter_mla_small_head_mode() |
| 1804 | if mode == "asm": |
| 1805 | return False |
| 1806 | if not _gluon_mla_decode_supported(): |
| 1807 | return False |
| 1808 | if mode != "gluon" and m % num_heads != 0: |
| 1809 | return False |
| 1810 | # Last, so the size warning only fires where Gluon would have run. |
| 1811 | return _gluon_kv_cache_in_bounds(kv_cache_bytes) |
| 1812 | |
| 1813 | @staticmethod |
| 1814 | def use_gluon_verify( |
| 1815 | num_heads: int, |
| 1816 | max_qo_len: int, |
| 1817 | kv_cache_dtype: str, |
| 1818 | dcp_world_size: int = 1, |
| 1819 | causal: bool = True, |
| 1820 | kv_cache_bytes: int | None = None, |
| 1821 | ) -> bool: |
| 1822 | """Whether a small-head multi-token verify uses native Gluon MTP. |
| 1823 | |
| 1824 | bf16 has no gqa<16, qseqlen>1 asm kernel, so verify goes through |
| 1825 | ``mla_gluon``'s 4-D MTP entry (``q`` shaped ``[batch, qlen, nhead, dim]``) |
| 1826 | with ``use_2d_view=False``. fp8 has one via the q-row fold and must not |
| 1827 | come here: the MTP path hands Gluon the batch size its fp8 regime |
| 1828 | asserts against. A predicate rather than inline in forward_mqa so the |
| 1829 | builder sees the same answer the impl acts on. |
| 1830 | |
| 1831 | DCP verify is excluded: its per-row causal windows are served by the |
| 1832 | segmented path, which Gluon's MTP entry cannot express. |
| 1833 | |
| 1834 | Gluon masks the block causally with no way to turn it off, so a |
| 1835 | non-causal block takes the padded asm decode whatever the head count |
| 1836 | -- padding is what gives it a kernel there. |
| 1837 | """ |
| 1838 | if not causal: |
| 1839 | return False |
| 1840 | if max_qo_len <= 1 or dcp_world_size > 1: |
| 1841 | return False |
| 1842 | if is_quantized_kv_cache(kv_cache_dtype): |
| 1843 | return False |
| 1844 | if num_heads >= AiterMLAHelper._AITER_MIN_MLA_HEADS: |
| 1845 | return False |
| 1846 | if not _gluon_mla_decode_supported(): |
| 1847 | return False |
| 1848 | # Same arch, mode and cache-size gating as use_gluon_decode. |
| 1849 | if _aiter_mla_small_head_mode() == "asm": |
| 1850 | return False |
| 1851 | return _gluon_kv_cache_in_bounds(kv_cache_bytes) |
| 1852 | |
| 1853 | @staticmethod |
| 1854 | def dcp_local_verify_row_lens( |
| 1855 | tot_seq_lens: torch.Tensor, |
| 1856 | qlen: int, |
| 1857 | dcp_world_size: int, |
| 1858 | dcp_rank: int, |
| 1859 | cp_interleave: int, |
| 1860 | ) -> torch.Tensor: |
| 1861 | """Local KV length of every verify row, in global-causal order. |
| 1862 | |
| 1863 | Row ``t`` of a qlen-token verify attends global positions |
| 1864 | ``[0, seq_len - qlen + t]``, its own token included, so its local length |
| 1865 | is the round-robin count evaluated at that bound. Truncating causally in |
| 1866 | global coordinates has to happen before the shard mapping: the tokens a |
| 1867 | row drops sit on ``qlen - 1 - t`` different ranks, while subtracting the |
| 1868 | offset from a request's local length takes one from every rank. |
| 1869 | """ |
| 1870 | offsets = torch.arange( |
| 1871 | 1, |
| 1872 | qlen + 1, |
| 1873 | device=tot_seq_lens.device, |
| 1874 | dtype=tot_seq_lens.dtype, |
| 1875 | ) |
| 1876 | visible = (tot_seq_lens.unsqueeze(1) - qlen + offsets).clamp_(min=0) |
| 1877 | return get_dcp_local_seq_lens( |
| 1878 | visible, |
| 1879 | dcp_world_size, |
| 1880 | dcp_rank, |
| 1881 | cp_interleave, |
| 1882 | ).flatten() |
| 1883 | |
| 1884 | |
| 1885 | class AiterMLAImpl(MLACommonImpl[AiterMLAMetadata]): |
| 1886 | # DCP decode paths return natural-log softmax LSE for the cross-rank merge. |
| 1887 | can_return_lse_for_decode: bool = True |
| 1888 | supports_dcp: bool = True |
| 1889 | # Measured on gfx950: aiter.mla.mla_decode_fwd(return_lse=True) matches |
| 1890 | # logsumexp to fp32 exactly, and merge_mla_segments_triton converts AITER's |
| 1891 | # base-2 segment statistics with LOGE2. Stated rather than inherited because |
| 1892 | # the DCP combine silently corrupts the softmax denominator if it disagrees. |
| 1893 | lse_base_on_e: bool = True |
| 1894 | |
| 1895 | @property |
| 1896 | def _decode_num_heads(self) -> int: |
| 1897 | """Return the query-head count after DCP gathering.""" |
| 1898 | return self.num_heads * self.dcp_world_size |
| 1899 | |
| 1900 | def __init__( |
| 1901 | self, |
| 1902 | num_heads: int, |
| 1903 | head_size: int, |
| 1904 | scale: float, |
| 1905 | num_kv_heads: int, |
| 1906 | alibi_slopes: list[float] | None, |
| 1907 | sliding_window: int | None, |
| 1908 | kv_cache_dtype: str, |
| 1909 | logits_soft_cap: float | None, |
| 1910 | attn_type: str, |
| 1911 | kv_sharing_target_layer_name: str | None, |
| 1912 | # MLA Specific Arguments |
| 1913 | **mla_args, |
| 1914 | ) -> None: |
| 1915 | super().__init__( |
| 1916 | num_heads, |
| 1917 | head_size, |
| 1918 | scale, |
| 1919 | num_kv_heads, |
| 1920 | alibi_slopes, |
| 1921 | sliding_window, |
| 1922 | kv_cache_dtype, |
| 1923 | logits_soft_cap, |
| 1924 | attn_type, |
| 1925 | kv_sharing_target_layer_name, |
| 1926 | **mla_args, |
| 1927 | ) |
| 1928 | AiterMLAHelper.check_num_heads_validity(num_heads) |
| 1929 | AiterMLAHelper.check_num_heads_validity(self._decode_num_heads) |
| 1930 | |
| 1931 | unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap] |
| 1932 | if any(unsupported_features): |
| 1933 | raise NotImplementedError( |
| 1934 | "Aiter MLA does not support one of the following: " |
| 1935 | "alibi_slopes, sliding_window, logits_soft_cap" |
| 1936 | ) |
| 1937 | |
| 1938 | from aiter import flash_attn_varlen_func |
| 1939 | |
| 1940 | self.flash_attn_varlen_func = flash_attn_varlen_func |
| 1941 | |
| 1942 | # FP8 MLA prefill kernel imports (lazy, only when enabled). |
| 1943 | # Auto-enabled on gfx950 when AITER ships the kernels. Only runs when the |
| 1944 | # KV cache is FP8. Head counts that are not a multiple of 16 are |
| 1945 | # replicate-padded up to one (see _mla_fp8_prefill_attn). |
| 1946 | from vllm.utils.torch_utils import is_quantized_kv_cache |
| 1947 | |
| 1948 | self._fp8_prefill_enabled = _fp8_mla_prefill_supported() and ( |
| 1949 | is_quantized_kv_cache(kv_cache_dtype) |
| 1950 | and AiterMLAHelper.is_valid_num_heads(self.num_heads) |
| 1951 | ) |
| 1952 | if self._fp8_prefill_enabled: |
| 1953 | from aiter import mla_prefill_ps_asm_fwd, mla_reduce_v1 |
| 1954 | |
| 1955 | self._mla_prefill_ps_asm_fwd = mla_prefill_ps_asm_fwd |
| 1956 | self._mla_reduce_v1 = mla_reduce_v1 |
| 1957 | |
| 1958 | def _flash_attn_varlen_diff_headdims( |
| 1959 | self, q, k, v, return_softmax_lse=False, softmax_scale=None, **kwargs |
| 1960 | ): |
| 1961 | output = self.flash_attn_varlen_func( # type: ignore[call-arg] |
| 1962 | q=q, |
| 1963 | k=k, |
| 1964 | v=v, |
| 1965 | softmax_scale=softmax_scale, |
| 1966 | return_lse=return_softmax_lse, |
| 1967 | **kwargs, |
| 1968 | ) |
| 1969 | |
| 1970 | return output |
| 1971 | |
| 1972 | def _mla_fp8_prefill_attn( |
| 1973 | self, |
| 1974 | q: torch.Tensor, |
| 1975 | k: torch.Tensor, |
| 1976 | v: torch.Tensor, |
| 1977 | attn_metadata: AiterMLAMetadata, |
| 1978 | out: torch.Tensor, |
| 1979 | ) -> None: |
| 1980 | """Run FP8 MLA prefill via mla_prefill_ps_asm_fwd + mla_reduce_v1. |
| 1981 | |
| 1982 | Q, K, V are already decompressed (post-kv_b_proj), so K and V have |
| 1983 | ``num_heads`` heads (same as Q) and gqa_ratio=1. Writes the |
| 1984 | result in-place to ``out``, which is the [total_q, nhead * v_head_dim] |
| 1985 | output buffer supplied by ``forward_mha``; no extra allocation or |
| 1986 | copy is required. |
| 1987 | """ |
| 1988 | from vllm.platforms import current_platform |
| 1989 | from vllm.v1.worker.workspace import current_workspace_manager |
| 1990 | |
| 1991 | fp8_dtype = current_platform.fp8_dtype() |
| 1992 | total_q = q.shape[0] |
| 1993 | # PS asm prefill + mla_reduce_v1 require 16-aligned heads, and the PS |
| 1994 | # metadata is built for get_fp8_prefill_num_heads(num_heads). For head |
| 1995 | # counts that are not a multiple of 16 (K3 = 12/rank at TP8) |
| 1996 | # replicate-pad q/k/v up to that count, then slice the output back to |
| 1997 | # the real head count. |
| 1998 | # |
| 1999 | # Counts above 16 that are not multiples of 16 (24, 40, ...) are |
| 2000 | # handled by the same code but are not reached by any current model |
| 2001 | # and TP that fits: 96 heads would need TP4, whose weights exceed a |
| 2002 | # 288 GiB GPU, and 128-head models land on 128/64/32/16/8. The path is |
| 2003 | # kept general for future architectures and its numerics are covered by |
| 2004 | # test_fp8_prefill_matches_reference[num_heads=24]. Note the cost is |
| 2005 | # (padded - real)/real extra FLOPs and q/k/v bytes, which is worst just |
| 2006 | # above a multiple of 16 (17 heads pad to 32); a future arch landing |
| 2007 | # there should measure against the flash_attn_varlen_func fallback |
| 2008 | # rather than assume the asm path wins. |
| 2009 | # |
| 2010 | # Exact, not approximate: after kv_b_proj gqa_ratio is 1, so q, k and v |
| 2011 | # all carry num_heads heads and attention is independent per head. |
| 2012 | # Padding all three identically makes padded head j a duplicate of real |
| 2013 | # head j % num_heads, so the real heads [0:num_heads] are bit-identical |
| 2014 | # to the unpadded result. Same argument as the decode path; only the |
| 2015 | # target width differs. |
| 2016 | _real_nhead = self.num_heads |
| 2017 | nhead = AiterMLAHelper.get_fp8_prefill_num_heads(_real_nhead) |
| 2018 | _pad = nhead != _real_nhead |
| 2019 | if _pad: |
| 2020 | q = AiterMLAHelper.get_mla_padded_q(_real_nhead, q, nhead) |
| 2021 | k = AiterMLAHelper.get_mla_padded_q(_real_nhead, k, nhead) |
| 2022 | v = AiterMLAHelper.get_mla_padded_q(_real_nhead, v, nhead) |
| 2023 | v_head_dim = self.v_head_dim |
| 2024 | tile_q = _FP8_PREFILL_TILE_Q |
| 2025 | |
| 2026 | # The FP8 ASM kernel expects FP8 inputs; the q_scale/k_scale/v_scale |
| 2027 | # parameters select per-tensor dequant scales. Q/K/V arrive as |
| 2028 | # bf16 from kv_b_proj, so cast here (one_scale=1.0 disables scaling). |
| 2029 | if q.dtype != fp8_dtype: |
| 2030 | q = q.to(fp8_dtype) |
| 2031 | if k.dtype != fp8_dtype: |
| 2032 | k = k.to(fp8_dtype) |
| 2033 | if v.dtype != fp8_dtype: |
| 2034 | v = v.to(fp8_dtype) |
| 2035 | |
| 2036 | one_scale = torch.ones((), dtype=torch.float32, device=q.device) |
| 2037 | |
| 2038 | # num_partial_tiles is resolved during metadata build to avoid an |
| 2039 | # in-forward .item() sync that would prevent CUDA Graph capture. |
| 2040 | # forward_mha gates the FP8 path on fp8_prefill_qo_indptr being set, |
| 2041 | # and the builder always sets every fp8_prefill_* field together, so |
| 2042 | # num_partial_tiles is non-None here. |
| 2043 | num_partial_tiles = attn_metadata.fp8_prefill_num_partial_tiles |
| 2044 | assert num_partial_tiles is not None |
| 2045 | |
| 2046 | # Per-call scratch is served from the workspace manager so allocator |
| 2047 | # churn in the prefill hot path is bounded after warmup, matching the |
| 2048 | # pattern in PR #41002. The builder reserves the maximum shape of every |
| 2049 | # tensor requested here before the workspace is locked. |
| 2050 | scratch: list[tuple[tuple[int, ...], torch.dtype]] = [ |
| 2051 | ((num_partial_tiles * tile_q, nhead, v_head_dim), torch.float32), |
| 2052 | ((num_partial_tiles * tile_q, nhead), torch.float32), |
| 2053 | ((total_q, nhead), torch.float32), |
| 2054 | ] |
| 2055 | if _pad: |
| 2056 | # The ASM and reduce kernels write a [total_q, nhead, v_head_dim] |
| 2057 | # buffer. With unpadded heads that aliases the caller's |
| 2058 | # [total_q, nhead * v_head_dim] output, so write straight into it; |
| 2059 | # padded heads do not fit that storage and need their own buffer. |
| 2060 | scratch.append(((total_q, nhead, v_head_dim), out.dtype)) |
| 2061 | |
| 2062 | workspace = current_workspace_manager() |
| 2063 | logits, attn_lse, final_lse, *pad_out = workspace.get_simultaneous(*scratch) |
| 2064 | out_3d = pad_out[0] if _pad else out.view(total_q, nhead, v_head_dim) |
| 2065 | |
| 2066 | # Phase 1: persistent-scheduling assembly prefill kernel. |
| 2067 | self._mla_prefill_ps_asm_fwd( |
| 2068 | q, |
| 2069 | k, |
| 2070 | v, |
| 2071 | attn_metadata.fp8_prefill_qo_indptr, |
| 2072 | attn_metadata.fp8_prefill_kv_indptr, |
| 2073 | attn_metadata.fp8_prefill_kv_indices, |
| 2074 | attn_metadata.fp8_prefill_work_indptr, |
| 2075 | attn_metadata.fp8_prefill_work_info_set, |
| 2076 | attn_metadata.fp8_prefill_max_q_len, |
| 2077 | self.scale, |
| 2078 | True, # is_causal |
| 2079 | logits, |
| 2080 | attn_lse, |
| 2081 | out_3d, |
| 2082 | one_scale, |
| 2083 | one_scale, |
| 2084 | one_scale, |
| 2085 | ) |
| 2086 | |
| 2087 | # Phase 2: reduction across KV splits. |
| 2088 | self._mla_reduce_v1( |
| 2089 | logits, |
| 2090 | attn_lse, |
| 2091 | attn_metadata.fp8_prefill_reduce_indptr, |
| 2092 | attn_metadata.fp8_prefill_reduce_final_map, |
| 2093 | attn_metadata.fp8_prefill_reduce_partial_map, |
| 2094 | tile_q, |
| 2095 | # num_kv_splits added by ROCm/aiter#3391; 0 selects the kernel |
| 2096 | # default max(cu_num, 0) == cu_num, matching pre-#3391 behavior. |
| 2097 | 0, |
| 2098 | out_3d, |
| 2099 | final_lse, |
| 2100 | ) |
| 2101 | |
| 2102 | if _pad: |
| 2103 | out.view(total_q, _real_nhead, v_head_dim).copy_(out_3d[:, :_real_nhead, :]) |
| 2104 | |
| 2105 | def forward_mha( |
| 2106 | self, |
| 2107 | q: torch.Tensor, |
| 2108 | kv_c_normed: torch.Tensor, |
| 2109 | k_pe: torch.Tensor, |
| 2110 | kv_c_and_k_pe_cache: torch.Tensor, |
| 2111 | attn_metadata: MLACommonMetadata, |
| 2112 | k_scale: torch.Tensor, |
| 2113 | output: torch.Tensor, |
| 2114 | output_scale: torch.Tensor | None = None, |
| 2115 | ) -> None: |
| 2116 | """Dispatch prefill to the FP8 ASM kernel when available. |
| 2117 | |
| 2118 | Falls back to the parent (``flash_attn_varlen_func``) when FP8 |
| 2119 | MLA prefill is disabled, PS metadata is missing, or chunked |
| 2120 | context requires two-pass merge. |
| 2121 | |
| 2122 | The annotation uses the base ``MLACommonMetadata`` to honour LSP |
| 2123 | with ``MLACommonImpl.forward_mha``; the AITER builder always |
| 2124 | produces ``AiterMLAMetadata`` instances at runtime, so we narrow |
| 2125 | with ``isinstance`` before reading the AITER-specific FP8 fields. |
| 2126 | """ |
| 2127 | if ( |
| 2128 | not self._fp8_prefill_enabled |
| 2129 | or not isinstance(attn_metadata, AiterMLAMetadata) |
| 2130 | or attn_metadata.fp8_prefill_qo_indptr is None |
| 2131 | ): |
| 2132 | return super().forward_mha( |
| 2133 | q, |
| 2134 | kv_c_normed, |
| 2135 | k_pe, |
| 2136 | kv_c_and_k_pe_cache, |
| 2137 | attn_metadata, |
| 2138 | k_scale, |
| 2139 | output, |
| 2140 | output_scale, |
| 2141 | ) |
| 2142 | |
| 2143 | assert attn_metadata.prefill is not None |
| 2144 | prefill_metadata = attn_metadata.prefill |
| 2145 | has_context = prefill_metadata.chunked_context is not None |
| 2146 | |
| 2147 | if has_context: |
| 2148 | return super().forward_mha( |
| 2149 | q, |
| 2150 | kv_c_normed, |
| 2151 | k_pe, |
| 2152 | kv_c_and_k_pe_cache, |
| 2153 | attn_metadata, |
| 2154 | k_scale, |
| 2155 | output, |
| 2156 | output_scale, |
| 2157 | ) |
| 2158 | |
| 2159 | assert output_scale is None, ( |
| 2160 | "fused FP8 output not supported by the AITER FP8 MLA prefill path" |
| 2161 | ) |
| 2162 | |
| 2163 | kv_nope = self.kv_b_proj(kv_c_normed)[0].view( |
| 2164 | -1, self.num_heads, self.qk_nope_head_dim + self.v_head_dim |
| 2165 | ) |
| 2166 | k_nope, v = kv_nope.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1) |
| 2167 | k = self._concat_k_nope_k_pe(k_nope, k_pe) |
| 2168 | |
| 2169 | self._mla_fp8_prefill_attn(q, k, v, attn_metadata, output) |
| 2170 | |
| 2171 | def _forward_segmented_dcp_verify( |
| 2172 | self, |
| 2173 | q_nope: torch.Tensor, |
| 2174 | q_pe: torch.Tensor, |
| 2175 | verify: AiterMLADCPVerifyMetadata, |
| 2176 | kv_c_and_k_pe_cache: torch.Tensor, |
| 2177 | layer: AttentionLayer, |
| 2178 | out_dtype: torch.dtype, |
| 2179 | ) -> tuple[torch.Tensor, torch.Tensor]: |
| 2180 | """Run segmented attention over this rank's shard of every verify row. |
| 2181 | |
| 2182 | Each row's length already covers the current tokens this rank holds, so |
| 2183 | the cross-rank LSE merge in the MLA layer completes the causal block. |
| 2184 | """ |
| 2185 | q_mla = torch.cat([q_nope, q_pe], dim=-1) |
| 2186 | # skip_reduce with NUM_SEGMENTS>1 returns the partials and never writes |
| 2187 | # `out`. Pass None rather than aliasing q_mla: if that ever stops |
| 2188 | # holding, this fails loudly instead of scribbling over the query. |
| 2189 | segment_partials = _get_segmented_mla_decode()( |
| 2190 | q_mla, |
| 2191 | kv_c_and_k_pe_cache.view( |
| 2192 | -1, |
| 2193 | verify.page_size, |
| 2194 | 1, |
| 2195 | kv_c_and_k_pe_cache.shape[-1], |
| 2196 | ), |
| 2197 | None, |
| 2198 | verify.qo_indptr, |
| 2199 | verify.row_lens, |
| 2200 | verify.max_kv_seq_len, |
| 2201 | verify.block_table, |
| 2202 | self.scale, |
| 2203 | self.kv_lora_rank, |
| 2204 | self.qk_rope_head_dim, |
| 2205 | causal=True, |
| 2206 | q_descale=None, |
| 2207 | kv_descale=layer._k_scale, |
| 2208 | skip_reduce=True, |
| 2209 | ) |
| 2210 | assert isinstance(segment_partials, tuple) and len(segment_partials) == 3, ( |
| 2211 | "AITER segmented MLA verify must return segment partials " |
| 2212 | "when skip_reduce=True." |
| 2213 | ) |
| 2214 | segm_output, segm_max, segm_expsum = segment_partials |
| 2215 | return merge_mla_segments_triton( |
| 2216 | segm_output, |
| 2217 | segm_max, |
| 2218 | segm_expsum, |
| 2219 | verify.row_lens, |
| 2220 | verify.page_size, |
| 2221 | out_dtype, |
| 2222 | ) |
| 2223 | |
| 2224 | def forward_mqa( |
| 2225 | self, |
| 2226 | q: torch.Tensor | tuple[torch.Tensor, torch.Tensor], |
| 2227 | kv_c_and_k_pe_cache: torch.Tensor, |
| 2228 | attn_metadata: AiterMLAMetadata, |
| 2229 | layer: AttentionLayer, |
| 2230 | ) -> tuple[torch.Tensor, torch.Tensor | None]: |
| 2231 | assert kv_c_and_k_pe_cache.numel() > 0 |
| 2232 | assert attn_metadata.decode is not None |
| 2233 | |
| 2234 | decode = attn_metadata.decode |
| 2235 | assert decode.max_qo_len is not None |
| 2236 | if decode.use_gluon_decode: |
| 2237 | assert decode.paged_kv_indptr is not None |
| 2238 | assert decode.paged_kv_indices is not None |
| 2239 | if type(q) is tuple: |
| 2240 | q_nope, q_pe = q |
| 2241 | else: |
| 2242 | q_nope, q_pe = torch.split( |
| 2243 | q, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1 |
| 2244 | ) |
| 2245 | B, num_q_heads, _ = q_nope.shape |
| 2246 | o = torch.empty( |
| 2247 | B, |
| 2248 | num_q_heads, |
| 2249 | self.kv_lora_rank, |
| 2250 | dtype=decode.attn_out_dtype, |
| 2251 | device=q_nope.device, |
| 2252 | ) |
| 2253 | kv_buffer = kv_c_and_k_pe_cache.reshape(-1, kv_c_and_k_pe_cache.shape[-1]) |
| 2254 | mla_gluon = _get_mla_gluon() |
| 2255 | mla_gluon( |
| 2256 | q_nope=q_nope, |
| 2257 | q_pe=q_pe, |
| 2258 | kv_c=kv_buffer, |
| 2259 | o=o, |
| 2260 | page_table=decode.paged_kv_indices, |
| 2261 | seq_info=decode.paged_kv_indptr, |
| 2262 | sm_scale=self.scale, |
| 2263 | k_pe=None, |
| 2264 | kv_pe_offset=self.kv_lora_rank, |
| 2265 | use_2d_view=False, |
| 2266 | kv_scale=1.0, |
| 2267 | min_kv_seq_len=decode.min_kv_seq_len, |
| 2268 | ) |
| 2269 | return o, None |
| 2270 | |
| 2271 | # 12-head (<16) multi-token verify (DSpark): the asm path has no |
| 2272 | # gqa<16, qseqlen>1 kernel, so the block goes to the gluon kernel's 4-D |
| 2273 | # MTP entry, which serves a whole (1 + num_spec) block in one launch. |
| 2274 | # The block is causal -- the target is checking draft tokens, so |
| 2275 | # position t must not see t+1 -- and the kernel bounds each query |
| 2276 | # position's scores itself. |
| 2277 | # Arch, mode and dtype gating all live in use_gluon_verify, which the |
| 2278 | # builder evaluates and records on the metadata, so this branch acts on |
| 2279 | # the same answer the builder used to decide whether the asm decode |
| 2280 | # would run. DCP verify is routed to the segmented path below instead. |
| 2281 | if decode.use_gluon_verify: |
| 2282 | if type(q) is tuple: |
| 2283 | q_nope, q_pe = q |
| 2284 | else: |
| 2285 | q_nope, q_pe = torch.split( |
| 2286 | q, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1 |
| 2287 | ) |
| 2288 | B, num_q_heads, _ = q_nope.shape |
| 2289 | o = torch.empty( |
| 2290 | B, |
| 2291 | num_q_heads, |
| 2292 | self.kv_lora_rank, |
| 2293 | dtype=decode.attn_out_dtype, |
| 2294 | device=q_nope.device, |
| 2295 | ) |
| 2296 | kv_buffer = kv_c_and_k_pe_cache.reshape(-1, kv_c_and_k_pe_cache.shape[-1]) |
| 2297 | assert attn_metadata.causal, ( |
| 2298 | "AITER MLA small-head verify MTP is causal-only" |
| 2299 | ) |
| 2300 | # Hand mla_gluon its 4-D MTP entry instead of an expanded |
| 2301 | # per-verify-token paged-KV view. The flat query layout is already |
| 2302 | # row-major (r * qlen + t), so unflatten is a free view. mla_gluon |
| 2303 | # applies the per-position causal bound |
| 2304 | # score_end = min(split_kv_end, seq_len - qlen + q_pos + 1) |
| 2305 | # in-kernel, and seq_lens already spans the verify block, so that |
| 2306 | # bound is context_r + q_pos + 1 -- the same window the expanded |
| 2307 | # view supplied by truncating each row's page list. |
| 2308 | qlen = int(decode.max_qo_len) |
| 2309 | num_reqs = B // qlen |
| 2310 | if num_reqs * qlen != B: |
| 2311 | raise ValueError( |
| 2312 | f"verify block {B} rows is not a multiple of qlen {qlen}" |
| 2313 | ) |
| 2314 | assert decode.paged_kv_indptr is not None |
| 2315 | assert decode.paged_kv_indices is not None |
| 2316 | mla_gluon = _get_mla_gluon() |
| 2317 | mla_gluon( |
| 2318 | q_nope=q_nope.unflatten(0, (num_reqs, qlen)), |
| 2319 | q_pe=q_pe.unflatten(0, (num_reqs, qlen)), |
| 2320 | kv_c=kv_buffer, |
| 2321 | o=o.unflatten(0, (num_reqs, qlen)), |
| 2322 | page_table=decode.paged_kv_indices, |
| 2323 | seq_info=decode.paged_kv_indptr, |
| 2324 | sm_scale=self.scale, |
| 2325 | k_pe=None, |
| 2326 | kv_pe_offset=self.kv_lora_rank, |
| 2327 | use_2d_view=False, |
| 2328 | kv_scale=1.0, |
| 2329 | min_kv_seq_len=decode.min_kv_seq_len, |
| 2330 | ) |
| 2331 | return o, None |
| 2332 | |
| 2333 | dcp_route = decode.dcp_route |
| 2334 | if dcp_route is _DCPDecodeRoute.SEGMENTED: |
| 2335 | verify = decode.dcp_verify |
| 2336 | assert verify is not None |
| 2337 | if type(q) is tuple: |
| 2338 | q_nope, q_pe = q |
| 2339 | else: |
| 2340 | q_nope, q_pe = torch.split( |
| 2341 | q, [self.kv_lora_rank, self.qk_rope_head_dim], dim=-1 |
| 2342 | ) |
| 2343 | if ( |
| 2344 | is_quantized_kv_cache(self.kv_cache_dtype) |
| 2345 | and q_nope.dtype != torch.bfloat16 |
| 2346 | ): |
| 2347 | q_nope = q_nope.to(torch.bfloat16) * layer._q_scale |
| 2348 | q_pe = q_pe.to(torch.bfloat16) * layer._q_scale |
| 2349 | return self._forward_segmented_dcp_verify( |
| 2350 | q_nope, |
| 2351 | q_pe, |
| 2352 | verify, |
| 2353 | kv_c_and_k_pe_cache, |
| 2354 | layer, |
| 2355 | decode.attn_out_dtype, |
| 2356 | ) |
| 2357 | |
| 2358 | if type(q) is tuple: |
| 2359 | q = torch.cat(q, dim=-1) |
| 2360 | |
| 2361 | assert isinstance(q, torch.Tensor) |
| 2362 | assert decode.paged_kv_indptr is not None |
| 2363 | assert decode.paged_kv_indices is not None |
| 2364 | B = q.shape[0] |
| 2365 | |
| 2366 | assert q.shape[1] == self._decode_num_heads, ( |
| 2367 | "ROCM_AITER_MLA decode expected the DCP-gathered query head count " |
| 2368 | f"{self._decode_num_heads}, got {q.shape[1]}" |
| 2369 | ) |
| 2370 | # cprr has kernels only at native head counts, so pad q up (e.g. 96 -> |
| 2371 | # 128). Heads are independent in MLA; the extras are sliced off below. |
| 2372 | asm_dcp_heads = decode.asm_decode_num_heads |
| 2373 | if asm_dcp_heads > self._decode_num_heads: |
| 2374 | mla_num_heads = asm_dcp_heads |
| 2375 | mla_padded_q = AiterMLAHelper.get_mla_padded_q( |
| 2376 | self._decode_num_heads, q, target_heads=mla_num_heads |
| 2377 | ) |
| 2378 | else: |
| 2379 | mla_padded_q = AiterMLAHelper.get_mla_padded_q(self._decode_num_heads, q) |
| 2380 | mla_num_heads = AiterMLAHelper.get_actual_mla_num_heads( |
| 2381 | self._decode_num_heads |
| 2382 | ) |
| 2383 | o = torch.empty( |
| 2384 | B, |
| 2385 | mla_num_heads, |
| 2386 | self.kv_lora_rank, |
| 2387 | dtype=attn_metadata.decode.attn_out_dtype, |
| 2388 | device=q.device, |
| 2389 | ) |
| 2390 | if decode.max_qo_len > 1 and not decode.has_persistent_metadata: |
| 2391 | # MTP verification can call the AITER MLA decode kernel with |
| 2392 | # qlen > 1. If that path is running without persistent metadata, |
| 2393 | # zero-fill so unwritten lanes cannot leak into logits. |
| 2394 | o.zero_() |
| 2395 | |
| 2396 | kv_buffer = kv_c_and_k_pe_cache.unsqueeze(2) |
| 2397 | |
| 2398 | # Build kwargs for mla_decode_fwd. Pass persistent metadata only |
| 2399 | # when it was successfully computed. |
| 2400 | mla_kwargs = dict( |
| 2401 | q_scale=layer._q_scale, |
| 2402 | kv_scale=layer._k_scale, |
| 2403 | ) |
| 2404 | if attn_metadata.work_meta_data is not None: |
| 2405 | mla_kwargs.update( |
| 2406 | work_meta_data=attn_metadata.work_meta_data, |
| 2407 | work_indptr=attn_metadata.work_indptr, |
| 2408 | work_info_set=attn_metadata.work_info_set, |
| 2409 | reduce_indptr=attn_metadata.reduce_indptr, |
| 2410 | reduce_final_map=attn_metadata.reduce_final_map, |
| 2411 | reduce_partial_map=attn_metadata.reduce_partial_map, |
| 2412 | ) |
| 2413 | |
| 2414 | lse = None |
| 2415 | if self.dcp_world_size > 1: |
| 2416 | if dcp_route is _DCPDecodeRoute.CPRR: |
| 2417 | # cprr only: global positions for the in-kernel causal window. |
| 2418 | assert decode.g_kv_indptr is not None |
| 2419 | mla_kwargs["g_kv_indptr"] = decode.g_kv_indptr |
| 2420 | mla_kwargs["cp_world_size"] = decode.cp_world_size |
| 2421 | mla_kwargs["cp_rank"] = decode.cp_rank |
| 2422 | # Must match the schedule get_mla_metadata_v1 was built with. |
| 2423 | assert decode.mla_num_kv_splits > 0 |
| 2424 | mla_kwargs["num_kv_splits"] = decode.mla_num_kv_splits |
| 2425 | mla_kwargs["intra_batch_mode"] = False |
| 2426 | # The vLLM custom-op wrapper exposes only the in-place output and |
| 2427 | # drops aiter's final LSE, which the cross-shard merge needs, so go |
| 2428 | # through aiter's native entry point on the DCP path. |
| 2429 | _, lse = _get_aiter_mla_decode()( |
| 2430 | mla_padded_q, |
| 2431 | kv_buffer.view(-1, 1, 1, mla_padded_q.shape[-1]), |
| 2432 | o, |
| 2433 | decode.qo_indptr, |
| 2434 | decode.paged_kv_indptr, |
| 2435 | decode.paged_kv_indices, |
| 2436 | decode.paged_kv_last_page_len, |
| 2437 | decode.max_qo_len, |
| 2438 | sm_scale=self.scale, |
| 2439 | return_lse=True, |
| 2440 | causal=attn_metadata.causal, |
| 2441 | **mla_kwargs, |
| 2442 | ) |
| 2443 | assert lse is not None, ( |
| 2444 | "aiter mla_decode_fwd(return_lse=True) returned no LSE; upgrade " |
| 2445 | "aiter to a build with decode LSE support." |
| 2446 | ) |
| 2447 | else: |
| 2448 | rocm_aiter_ops.mla_decode_fwd( |
| 2449 | mla_padded_q, |
| 2450 | kv_buffer, |
| 2451 | o, |
| 2452 | self.scale, |
| 2453 | decode.qo_indptr, |
| 2454 | decode.max_qo_len, |
| 2455 | decode.paged_kv_indptr, |
| 2456 | decode.paged_kv_indices, |
| 2457 | decode.paged_kv_last_page_len, |
| 2458 | causal=attn_metadata.causal, |
| 2459 | **mla_kwargs, |
| 2460 | ) |
| 2461 | |
| 2462 | if asm_dcp_heads > self._decode_num_heads: |
| 2463 | # Keep a view: a post-kernel copy races the symm-mem a2a combine. |
| 2464 | output = o[:, : self._decode_num_heads] |
| 2465 | if lse is not None: |
| 2466 | lse = lse[:, : self._decode_num_heads, ...] |
| 2467 | else: |
| 2468 | output = AiterMLAHelper.get_mla_unpadded_o(self._decode_num_heads, o) |
| 2469 | if lse is not None: |
| 2470 | lse = AiterMLAHelper.get_mla_unpadded_lse(self._decode_num_heads, lse) |
| 2471 | return output, lse |