main
py 1,086 lines 39.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Shared MHA implementation and metadata builder for sparse MLA backends."""
4
5 import math
6 from dataclasses import dataclass
7 from shutil import which
8 from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar
9
10 import numpy as np
11 import torch
12
13 from vllm import _custom_ops as ops
14 from vllm.config import get_current_vllm_config
15 from vllm.distributed import (
16 get_dcp_group,
17 get_tensor_model_parallel_world_size,
18 )
19 from vllm.logger import init_logger
20 from vllm.model_executor.layers.attention.mla_attention import (
21 MLACommonBaseImpl,
22 MLACommonDecodeMetadata,
23 MLACommonMetadata,
24 MLACommonPrefillMetadata,
25 accumulate_mla_context_chunk,
26 align_mla_chunked_context_workspace_size,
27 build_mla_chunked_context_metadata,
28 get_mla_dims,
29 init_mla_context_partial,
30 )
31 from vllm.platforms import current_platform
32 from vllm.triton_utils import tl, triton
33 from vllm.utils.flashinfer import has_flashinfer
34 from vllm.utils.torch_utils import (
35 PIN_MEMORY,
36 is_quantized_kv_cache,
37 np_to_pinned_tensor,
38 )
39 from vllm.v1.attention.backend import AttentionMetadataBuilder
40 from vllm.v1.attention.backends.fa_utils import get_flash_attn_version
41 from vllm.v1.attention.backends.mla.index_group import (
42 SparseMLAIndexGroup,
43 SparseMLAIndexGroupBuilder,
44 )
45 from vllm.v1.attention.backends.utils import split_decodes_and_prefills
46 from vllm.v1.attention.ops.dcp import MLADCPManager
47 from vllm.v1.attention.ops.merge_attn_states import merge_attn_states
48 from vllm.v1.hisparse.runtime import (
49 HiSparsePrefillStagingPlan,
50 build_hisparse_prefill_staging_plan,
51 )
52
53 if TYPE_CHECKING:
54 from vllm.config import VllmConfig
55 from vllm.model_executor.layers.linear import ColumnParallelLinear
56 from vllm.v1.attention.backend import CommonAttentionMetadata
57 from vllm.v1.kv_cache_interface import AttentionSpec
58
59 logger = init_logger(__name__)
60
61 GLOBAL_TOPK_MASK_MAX_BYTES = 128 * 1024 * 1024 # 128 MiB
62
63
64 def _topk_mask_shape(
65 batch_size: int,
66 max_query_len: int,
67 max_key_len: int,
68 reserve_key_starts_word: bool = False,
69 ) -> tuple[int, int, int]:
70 """Shape of a bit-packed top-k mask, shared by every site that builds one."""
71 tile_m = 128 if max_query_len <= 128 else 256
72 padded_q_len = triton.cdiv(max_query_len, tile_m) * tile_m
73 num_words = triton.cdiv(max_key_len, 32) + int(reserve_key_starts_word)
74 num_words = triton.cdiv(num_words, 4) * 4
75 return batch_size, padded_q_len, num_words
76
77
78 def _masked_mha_workspace_fits(
79 batch_size: int,
80 max_query_len: int,
81 max_context_chunk_seq_len: int,
82 workspace_numel: int,
83 ) -> bool:
84 """Return whether the suffix and per-context-chunk masks fit the workspace.
85
86 The global mask is excluded: it always needs more, and has its own check.
87 """
88 max_key_len = max(max_query_len, max_context_chunk_seq_len)
89 needed = math.prod(_topk_mask_shape(batch_size, max_query_len, max_key_len))
90 return needed <= workspace_numel
91
92
93 def _is_masked_mha_available(
94 num_heads_total: int,
95 kv_lora_rank: int,
96 qk_nope_head_dim: int,
97 qk_rope_head_dim: int,
98 v_head_dim: int,
99 kv_cache_dtype: str,
100 ) -> bool:
101 """Check if masked MHA can ever fire for this model configuration."""
102 if not current_platform.is_device_capability_family(100):
103 return False
104 model_dims = (
105 num_heads_total,
106 kv_lora_rank,
107 qk_nope_head_dim,
108 qk_rope_head_dim,
109 v_head_dim,
110 )
111 if model_dims not in (
112 (128, 512, 128, 64, 128),
113 (64, 512, 192, 64, 256),
114 # GLM-5.3-Flash: NoPE, qk_head_dim 256 == the (192, 64, 256) kernel.
115 (64, 512, 256, 0, 256),
116 ):
117 return False
118 qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
119 if qk_head_dim == 256 and v_head_dim == 256:
120 # This path uses contiguous K/V, so it does not need the paged-KV
121 # features that keep head-dim 256 disabled in the general FA selector.
122 fa_version = get_flash_attn_version()
123 else:
124 fa_version = get_flash_attn_version(
125 head_size=qk_head_dim, head_size_v=v_head_dim
126 )
127 return fa_version == 4 and not is_quantized_kv_cache(kv_cache_dtype)
128
129
130 def _use_dense_mha_prefill(
131 vllm_config: "VllmConfig", prefill_max_seq_len: int, topk_tokens: int
132 ) -> bool:
133 attention_config = vllm_config.attention_config
134 return (
135 attention_config.hisparse_config is None
136 and not attention_config.sparse_mla_force_mqa
137 and prefill_max_seq_len <= topk_tokens
138 )
139
140
141 @dataclass
142 class SparseMLAPrefillMetadata(MLACommonPrefillMetadata):
143 host_staging_plan: HiSparsePrefillStagingPlan | None = None
144
145
146 @dataclass(kw_only=True)
147 class SparseMLACommonMetadata(MLACommonMetadata[MLACommonDecodeMetadata]):
148 block_table: torch.Tensor
149 req_id_per_token: torch.Tensor
150 seq_lens: torch.Tensor | None = None
151 block_size: int = 64
152 topk_tokens: int = 2048
153 num_decodes: int = 0
154 num_prefills: int = 0
155 num_decode_tokens: int = 0
156 decode_max_query_len: int = 0
157 prefill_max_seq_len: int = 0
158 prefill: SparseMLAPrefillMetadata | None = None
159 cp_kv_cache_interleave_size: int = 1
160
161
162 T = TypeVar("T", bound=SparseMLACommonMetadata)
163
164
165 class SparseMLACommonMetadataBuilder(AttentionMetadataBuilder[T]):
166 metadata_cls: type[T]
167 require_uniform_decodes: ClassVar[bool] = False
168
169 def __init__(
170 self,
171 kv_cache_spec: "AttentionSpec",
172 layer_names: list[str],
173 vllm_config: "VllmConfig",
174 device: torch.device,
175 ) -> None:
176 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
177 self.vllm_config = vllm_config
178 self.device = device
179 self.model_config = vllm_config.model_config
180 self.mla_dims = get_mla_dims(self.model_config)
181 self.topk_tokens: int = vllm_config.model_config.hf_text_config.index_topk
182 self.req_id_per_token_buffer = torch.empty(
183 (vllm_config.scheduler_config.max_num_batched_tokens,),
184 dtype=torch.int32,
185 device=device,
186 )
187 self._context_lens_cpu = torch.empty(
188 vllm_config.scheduler_config.max_num_seqs,
189 dtype=torch.int32,
190 device="cpu",
191 pin_memory=PIN_MEMORY,
192 )
193 parallel_config = vllm_config.parallel_config
194 self.pcp_world_size = parallel_config.prefill_context_parallel_size
195 self.use_pcp = self.pcp_world_size > 1
196 try:
197 self.dcp_world_size = get_dcp_group().world_size
198 except AssertionError:
199 # DCP might not be initialized in testing
200 self.dcp_world_size = 1
201 self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
202 self.dcp_local_block_size = self.cp_kv_cache_interleave_size
203 self.dcp_virtual_block_size = self.dcp_local_block_size * self.dcp_world_size
204
205 self.chunked_prefill_workspace_size = (
206 self.determine_chunked_prefill_workspace_size(vllm_config)
207 )
208 workspace_head_size = (
209 self.mla_dims.kv_lora_rank + self.mla_dims.qk_rope_head_dim
210 )
211 workspace_rows = self.chunked_prefill_workspace_size
212 if self.dcp_world_size > 1:
213 # DCP gathers each rank's local KV shard into the workspace, so it
214 # needs an extra 1/DCP rows beyond the TP allocation.
215 assert self.chunked_prefill_workspace_size % self.dcp_world_size == 0
216 workspace_rows += self.chunked_prefill_workspace_size // self.dcp_world_size
217 self.chunked_prefill_workspace = torch.empty(
218 (workspace_rows, workspace_head_size),
219 dtype=self.model_config.dtype,
220 device=device,
221 )
222 self.topk_mask_workspace: torch.Tensor | None = None
223 if _is_masked_mha_available(
224 self.model_config.model_arch_config.total_num_attention_heads,
225 self.mla_dims.kv_lora_rank,
226 self.mla_dims.qk_nope_head_dim,
227 self.mla_dims.qk_rope_head_dim,
228 self.mla_dims.v_head_dim,
229 vllm_config.cache_config.cache_dtype,
230 ):
231 self.topk_mask_workspace = torch.zeros(
232 GLOBAL_TOPK_MASK_MAX_BYTES // torch.int32.itemsize,
233 dtype=torch.int32,
234 device=device,
235 )
236 attention_layer = vllm_config.compilation_config.static_forward_context[
237 layer_names[0]
238 ]
239 layer_prefill_backend = attention_layer.prefill_backend
240 self.dcp_manager: MLADCPManager | None = None
241 if self.dcp_world_size > 1:
242 self.dcp_manager = getattr(attention_layer, "dcp_manager", None)
243 assert isinstance(self.dcp_manager, MLADCPManager)
244 if layer_prefill_backend is not None:
245 self.dcp_manager.init_kv_gather(
246 self.chunked_prefill_workspace,
247 self.chunked_prefill_workspace_size,
248 )
249 self._prefill_backend = (
250 layer_prefill_backend.clone() if layer_prefill_backend is not None else None
251 )
252
253 def _init_reorder_batch_threshold(
254 self,
255 reorder_batch_threshold: int | None = 1,
256 supports_spec_as_decode: bool = False,
257 supports_dcp_with_varlen: bool = False,
258 ) -> None:
259 if self.vllm_config.attention_config.hisparse_config is not None:
260 reorder_batch_threshold = 1
261 super()._init_reorder_batch_threshold(
262 reorder_batch_threshold,
263 supports_spec_as_decode,
264 supports_dcp_with_varlen,
265 )
266
267 @staticmethod
268 def determine_chunked_prefill_workspace_size(vllm_config: "VllmConfig") -> int:
269 scheduler_config = vllm_config.scheduler_config
270 cache_config = vllm_config.cache_config
271 model_config = vllm_config.model_config
272 topk_tokens = model_config.hf_text_config.index_topk
273
274 workspace_size = min(
275 max(
276 8 * model_config.max_model_len,
277 4 * scheduler_config.max_num_seqs * cache_config.block_size,
278 ),
279 64 * 1024,
280 scheduler_config.max_num_seqs * topk_tokens,
281 )
282 workspace_size = max(workspace_size, cache_config.block_size)
283 if vllm_config.parallel_config.decode_context_parallel_size > 1:
284 return align_mla_chunked_context_workspace_size(vllm_config, workspace_size)
285 return workspace_size
286
287 def _build_req_id_per_token(
288 self,
289 common_attn_metadata: "CommonAttentionMetadata",
290 ) -> torch.Tensor:
291 return common_attn_metadata.token_to_req_indices(self.req_id_per_token_buffer)
292
293 def _build_chunked_context_fields(
294 self,
295 common_attn_metadata: "CommonAttentionMetadata",
296 num_decodes: int,
297 num_prefills: int,
298 prefill_query_lens_cpu: torch.Tensor | None,
299 ) -> "MLACommonPrefillMetadata.ChunkedContextMetadata | None":
300 if num_prefills == 0 or prefill_query_lens_cpu is None:
301 return None
302
303 seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
304 assert seq_lens_cpu is not None
305 context_lens_cpu = self._context_lens_cpu[:num_prefills]
306 torch.subtract(
307 seq_lens_cpu[num_decodes : num_decodes + num_prefills],
308 prefill_query_lens_cpu,
309 out=context_lens_cpu,
310 )
311 qsl_cpu = common_attn_metadata.query_start_loc_cpu
312 prefill_query_start_loc_cpu = qsl_cpu[num_decodes:] - qsl_cpu[num_decodes]
313
314 return build_mla_chunked_context_metadata(
315 context_lens_cpu=context_lens_cpu,
316 prefill_query_start_loc_cpu=prefill_query_start_loc_cpu,
317 chunked_prefill_workspace=self.chunked_prefill_workspace,
318 chunked_prefill_workspace_size=self.chunked_prefill_workspace_size,
319 block_size=self.kv_cache_spec.block_size,
320 align_chunk_to_block=current_platform.is_cuda(),
321 device=self.device,
322 dcp_world_size=self.dcp_world_size,
323 dcp_local_block_size=self.dcp_local_block_size,
324 dcp_virtual_block_size=self.dcp_virtual_block_size,
325 dcp_manager=self.dcp_manager,
326 )
327
328 def build(
329 self,
330 common_prefix_len: int,
331 common_attn_metadata: "CommonAttentionMetadata",
332 fast_build: bool = False,
333 ) -> T:
334 req_id_per_token = self._build_req_id_per_token(common_attn_metadata)
335
336 num_decodes, num_prefills, num_decode_tokens, _ = split_decodes_and_prefills(
337 common_attn_metadata,
338 decode_threshold=self.reorder_batch_threshold or 1,
339 treat_short_extends_as_decodes=not self.use_pcp,
340 require_uniform=self.require_uniform_decodes,
341 )
342 (
343 prefill_query_start_loc,
344 prefill_max_query_len,
345 prefill_query_lens_cpu,
346 ) = self._build_prefill_fields(common_attn_metadata, num_decodes, num_prefills)
347 decode_max_query_len = 0
348 if num_decodes:
349 if common_attn_metadata.max_logits_per_req is not None:
350 decode_max_query_len = common_attn_metadata.max_logits_per_req
351 elif num_prefills == 0:
352 decode_max_query_len = common_attn_metadata.max_query_len
353 else:
354 decode_query_lens_cpu = torch.diff(
355 common_attn_metadata.query_start_loc_cpu[: num_decodes + 1]
356 )
357 decode_max_query_len = int(decode_query_lens_cpu.max().item())
358
359 prefill_max_seq_len = 0
360 prefill: SparseMLAPrefillMetadata | None = None
361 if num_prefills > 0 and self._prefill_backend is not None:
362 seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
363 assert seq_lens_cpu is not None
364 prefill_max_seq_len = int(
365 seq_lens_cpu[num_decodes : num_decodes + num_prefills].max().item()
366 )
367 block_table = common_attn_metadata.block_table_tensor[num_decodes:, ...]
368 chunked_context = self._build_chunked_context_fields(
369 common_attn_metadata,
370 num_decodes,
371 num_prefills,
372 prefill_query_lens_cpu,
373 )
374 staging_plan = None
375 if self.vllm_config.attention_config.hisparse_config is not None:
376 prefill_seq_lens_cpu = seq_lens_cpu[
377 num_decodes : num_decodes + num_prefills
378 ]
379 staging_block_capacity = int(
380 (
381 (prefill_seq_lens_cpu + self.kv_cache_spec.block_size - 1)
382 // self.kv_cache_spec.block_size
383 ).sum()
384 )
385 staging_plan = build_hisparse_prefill_staging_plan(
386 block_table,
387 common_attn_metadata.seq_lens[num_decodes:],
388 self.kv_cache_spec.block_size,
389 staging_block_capacity,
390 )
391 if chunked_context is not None:
392 block_table = staging_plan.block_table
393 prefill = SparseMLAPrefillMetadata(
394 block_table=block_table,
395 query_start_loc=prefill_query_start_loc,
396 max_query_len=prefill_max_query_len,
397 query_lens_cpu=prefill_query_lens_cpu,
398 chunked_context=chunked_context,
399 q_data_type=self.model_config.dtype,
400 output_dtype=self.model_config.dtype,
401 prefill_backend=self._prefill_backend,
402 use_dense_mha=(
403 _use_dense_mha_prefill(
404 self.vllm_config, prefill_max_seq_len, self.topk_tokens
405 )
406 # Dense MHA cannot be used under PCP+DCP.
407 and not (self.use_pcp and self.dcp_world_size > 1)
408 ),
409 topk_mask_workspace=self.topk_mask_workspace,
410 host_staging_plan=staging_plan,
411 )
412 self._prefill_backend.prepare_metadata(prefill)
413
414 return self.metadata_cls( # type: ignore[call-arg]
415 num_reqs=common_attn_metadata.num_reqs,
416 max_query_len=common_attn_metadata.max_query_len,
417 max_seq_len=common_attn_metadata.max_seq_len,
418 num_actual_tokens=common_attn_metadata.num_actual_tokens,
419 query_start_loc=common_attn_metadata.query_start_loc,
420 slot_mapping=common_attn_metadata.slot_mapping,
421 block_table=common_attn_metadata.block_table_tensor,
422 req_id_per_token=req_id_per_token,
423 seq_lens=common_attn_metadata.seq_lens,
424 block_size=self.kv_cache_spec.block_size,
425 topk_tokens=self.topk_tokens,
426 num_decodes=num_decodes,
427 num_prefills=num_prefills,
428 num_decode_tokens=num_decode_tokens,
429 decode_max_query_len=decode_max_query_len,
430 prefill_max_seq_len=prefill_max_seq_len,
431 prefill=prefill,
432 cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
433 )
434
435 @staticmethod
436 def _build_prefill_fields(
437 common_attn_metadata: "CommonAttentionMetadata",
438 num_decodes: int,
439 num_prefills: int,
440 ) -> tuple[
441 torch.Tensor | None, # prefill_query_start_loc
442 int, # prefill_max_query_len
443 torch.Tensor | None, # prefill_query_lens_cpu
444 ]:
445 if num_prefills == 0:
446 return None, 0, None
447
448 offset = common_attn_metadata.query_start_loc[num_decodes]
449 prefill_query_start_loc = (
450 common_attn_metadata.query_start_loc[num_decodes:] - offset
451 )
452
453 qsl_cpu = common_attn_metadata.query_start_loc_cpu
454 prefill_qsl_cpu = qsl_cpu[num_decodes:] - qsl_cpu[num_decodes]
455 prefill_query_lens = prefill_qsl_cpu[1:] - prefill_qsl_cpu[:-1]
456 prefill_max_query_len = int(prefill_query_lens.max().item())
457
458 return (
459 prefill_query_start_loc,
460 prefill_max_query_len,
461 prefill_query_lens,
462 )
463
464
465 @triton.jit
466 def _scatter_topk_kernel(
467 mask_ptr,
468 topk_ptr,
469 cu_q_lens_ptr,
470 num_words: tl.constexpr,
471 mask_row_stride: tl.constexpr,
472 num_topk: tl.constexpr,
473 topk_stride: tl.constexpr,
474 max_q_len: tl.constexpr,
475 BLOCK_TOPK: tl.constexpr,
476 BLOCK_WORDS: tl.constexpr,
477 ):
478 row_idx = tl.program_id(0)
479 row_ptr = mask_ptr + row_idx * mask_row_stride
480 word_offsets = tl.arange(0, BLOCK_WORDS)
481 tl.store(
482 row_ptr + word_offsets,
483 tl.zeros([BLOCK_WORDS], dtype=tl.int32),
484 mask=word_offsets < num_words,
485 )
486
487 req_idx = row_idx // max_q_len
488 q_local = row_idx % max_q_len
489 q_start = tl.load(cu_q_lens_ptr + req_idx)
490 q_len = tl.load(cu_q_lens_ptr + req_idx + 1) - q_start
491 if q_local < q_len:
492 src_row = q_start + q_local
493 offsets = tl.arange(0, BLOCK_TOPK)
494 in_range = offsets < num_topk
495 indices = tl.load(
496 topk_ptr + src_row * topk_stride + offsets,
497 mask=in_range,
498 other=-1,
499 )
500 valid = in_range & (indices >= 0)
501 word_indices = indices >> 5
502 bits = (1 << (indices & 31)).to(tl.int32)
503 tl.atomic_or(row_ptr + word_indices, bits, mask=valid)
504
505
506 @triton.jit
507 def _scatter_topk_single_req_kernel(
508 mask_ptr,
509 topk_ptr,
510 num_words: tl.constexpr,
511 mask_row_stride: tl.constexpr,
512 num_topk: tl.constexpr,
513 topk_stride: tl.constexpr,
514 total_q: tl.constexpr,
515 BLOCK_TOPK: tl.constexpr,
516 BLOCK_WORDS: tl.constexpr,
517 ):
518 row_idx = tl.program_id(0)
519 row_ptr = mask_ptr + row_idx * mask_row_stride
520 word_offsets = tl.arange(0, BLOCK_WORDS)
521 tl.store(
522 row_ptr + word_offsets,
523 tl.zeros([BLOCK_WORDS], dtype=tl.int32),
524 mask=word_offsets < num_words,
525 )
526 if row_idx < total_q:
527 offsets = tl.arange(0, BLOCK_TOPK)
528 in_range = offsets < num_topk
529 indices = tl.load(
530 topk_ptr + row_idx * topk_stride + offsets,
531 mask=in_range,
532 other=-1,
533 )
534 valid = in_range & (indices >= 0)
535 word_indices = indices >> 5
536 bits = (1 << (indices & 31)).to(tl.int32)
537 tl.atomic_or(row_ptr + word_indices, bits, mask=valid)
538
539
540 def _build_topk_mask(
541 topk_indices_per_req: list[torch.Tensor],
542 q_lens: list[int],
543 max_q_len: int,
544 max_seq_len: int,
545 out: torch.Tensor,
546 ) -> torch.Tensor:
547 """Build a bit-packed top-k mask while preserving padded row storage."""
548 batch_size = len(q_lens)
549 num_words = (max_seq_len + 31) // 32
550 total_rows = batch_size * max_q_len
551 if total_rows == 0:
552 return out[:batch_size, :max_q_len]
553
554 total_q = sum(q_lens)
555 mask_row_stride = out.stride(-2)
556 block_words = triton.next_power_of_2(num_words)
557
558 if batch_size == 1:
559 topk_packed = topk_indices_per_req[0]
560 num_topk = topk_packed.shape[1]
561 _scatter_topk_single_req_kernel[(max_q_len,)](
562 out,
563 topk_packed,
564 num_words=num_words,
565 mask_row_stride=mask_row_stride,
566 num_topk=num_topk,
567 topk_stride=topk_packed.stride(0),
568 total_q=total_q,
569 BLOCK_TOPK=triton.next_power_of_2(num_topk),
570 BLOCK_WORDS=block_words,
571 )
572 return out[:1, :max_q_len]
573
574 topk_packed = torch.cat(topk_indices_per_req, dim=0)
575 num_topk = topk_packed.shape[1]
576 q_lens_tensor = np_to_pinned_tensor(np.asarray(q_lens, dtype=np.int32)).to(
577 out.device, non_blocking=True
578 )
579 cu_q_lens = out.new_zeros(batch_size + 1)
580 torch.cumsum(q_lens_tensor, dim=0, out=cu_q_lens[1:])
581 _scatter_topk_kernel[(total_rows,)](
582 out,
583 topk_packed,
584 cu_q_lens,
585 num_words=num_words,
586 mask_row_stride=mask_row_stride,
587 num_topk=num_topk,
588 topk_stride=topk_packed.stride(0),
589 max_q_len=max_q_len,
590 BLOCK_TOPK=triton.next_power_of_2(num_topk),
591 BLOCK_WORDS=block_words,
592 )
593 return out[:batch_size, :max_q_len]
594
595
596 class SharedTopkIndicesBuffer:
597 """Resolves the shared top-k index buffer for sparse MLA implementations.
598
599 The indexer owns the buffer, but `LLMBaseProposer.load_model` repoints the
600 draft's indexers at the target's buffer after the impls are constructed, so
601 it must be resolved per read rather than snapshotted. Backbone skip-topk
602 layers have no indexer and pass the buffer explicitly.
603 """
604
605 _indexer: object | None = None
606 _topk_indices_buffer: torch.Tensor | None = None
607
608 def init_topk_indices_buffer(
609 self,
610 indexer: object | None,
611 topk_indices_buffer: torch.Tensor | None,
612 ) -> None:
613 self._indexer = indexer
614 self._topk_indices_buffer = topk_indices_buffer
615
616 @property
617 def topk_indices_buffer(self) -> torch.Tensor | None:
618 if self._indexer is not None:
619 return self._indexer.topk_indices_buffer # type: ignore[attr-defined]
620 return self._topk_indices_buffer
621
622 @topk_indices_buffer.setter
623 def topk_indices_buffer(self, buffer: torch.Tensor | None) -> None:
624 # An explicit assignment supersedes the indexer.
625 self._indexer = None
626 self._topk_indices_buffer = buffer
627
628
629 class SparseMLACommonImpl(MLACommonBaseImpl[T], SharedTopkIndicesBuffer, Generic[T]):
630 """Sparse MLA base with dense and masked-MHA prefill paths."""
631
632 is_sparse = True
633
634 def __init__(
635 self,
636 num_heads: int,
637 head_size: int,
638 scale: float,
639 num_kv_heads: int,
640 alibi_slopes: list[float] | None,
641 sliding_window: int | None,
642 kv_cache_dtype: str,
643 logits_soft_cap: float | None,
644 attn_type: str,
645 kv_sharing_target_layer_name: str | None,
646 q_lora_rank: int | None,
647 kv_lora_rank: int,
648 qk_nope_head_dim: int,
649 qk_rope_head_dim: int,
650 qk_head_dim: int,
651 v_head_dim: int,
652 kv_b_proj: "ColumnParallelLinear",
653 indexer: object | None = None,
654 topk_indices_buffer: torch.Tensor | None = None,
655 index_group_builder: SparseMLAIndexGroupBuilder | None = None,
656 q_pad_num_heads: int | None = None,
657 ) -> None:
658 super().__init__(
659 num_heads,
660 head_size,
661 scale,
662 num_kv_heads,
663 kv_cache_dtype,
664 kv_lora_rank,
665 qk_nope_head_dim,
666 qk_rope_head_dim,
667 qk_head_dim,
668 v_head_dim,
669 kv_b_proj,
670 )
671
672 self.init_topk_indices_buffer(indexer, topk_indices_buffer)
673 self.index_group: SparseMLAIndexGroup | None = None
674 self.index_group_index = 0
675 if index_group_builder is None and self.topk_indices_buffer is not None:
676 index_group_builder = SparseMLAIndexGroupBuilder(self.topk_indices_buffer)
677 if index_group_builder is not None:
678 vllm_config = get_current_vllm_config()
679 self.index_group, self.index_group_index = (
680 index_group_builder.register_layer(
681 indexer is not None,
682 vllm_config,
683 head_size=head_size,
684 kv_cache_dtype=kv_cache_dtype,
685 )
686 )
687
688 self._use_flashinfer_concat_mla_k = (
689 has_flashinfer()
690 and which("ninja") is not None
691 and (self.num_heads == 128)
692 and (self.qk_nope_head_dim == 128)
693 and (self.qk_rope_head_dim == 64)
694 )
695 self.masked_mha_available = _is_masked_mha_available(
696 num_heads_total=num_heads * get_tensor_model_parallel_world_size(),
697 kv_lora_rank=kv_lora_rank,
698 qk_nope_head_dim=qk_nope_head_dim,
699 qk_rope_head_dim=qk_rope_head_dim,
700 v_head_dim=v_head_dim,
701 kv_cache_dtype=kv_cache_dtype,
702 )
703
704 def record_logical_topk_ready(self) -> None:
705 if self.index_group is not None:
706 self.index_group.set_logical_topk_ready(self.index_group_index)
707
708 def prepare_for_batch(self, attn_metadata: T | None) -> None:
709 if self.index_group is not None:
710 self.index_group.prepare_for_batch(self.index_group_index, attn_metadata)
711
712 def _convert_logical_to_physical_topk(
713 self,
714 logical_topk_indices: torch.Tensor,
715 attn_metadata: Any,
716 *,
717 block_stride_rows: int | None,
718 return_valid_counts: bool,
719 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
720 group = self.index_group
721 assert group is not None
722 assert self.dcp_world_size == 1
723 return group.convert_logical_to_physical_topk(
724 self.index_group_index,
725 logical_topk_indices,
726 attn_metadata,
727 block_stride_rows=block_stride_rows,
728 return_valid_counts=return_valid_counts,
729 )
730
731 @staticmethod
732 def masked_mha_workspace_fits(prefill: MLACommonPrefillMetadata) -> bool:
733 """Whether this prefill batch's top-k masks fit the workspace."""
734 workspace = prefill.topk_mask_workspace
735 if workspace is None or prefill.query_lens_cpu is None:
736 return False
737 max_context_chunk_seq_len = 0
738 if prefill.chunked_context is not None:
739 max_context_chunk_seq_len = max(
740 chunk.max_seq_len for chunk in prefill.chunked_context.chunks
741 )
742 fits = _masked_mha_workspace_fits(
743 batch_size=len(prefill.query_lens_cpu),
744 max_query_len=prefill.max_query_len,
745 max_context_chunk_seq_len=max_context_chunk_seq_len,
746 workspace_numel=workspace.numel(),
747 )
748 if not fits:
749 logger.warning_once(
750 "Sparse MLA top-k mask workspace (%d MiB) is too small for some "
751 "prefill batches; those fall back to slower sparse MQA.",
752 workspace.numel() * torch.int32.itemsize // (1024 * 1024),
753 )
754 return fits
755
756 @staticmethod
757 def _slice_topk_per_req(
758 topk_all: torch.Tensor,
759 q_lens: list[int],
760 ) -> list[torch.Tensor]:
761 topk_per_req = []
762 offset = 0
763 for q_len in q_lens:
764 topk_per_req.append(topk_all[offset : offset + q_len])
765 offset += q_len
766 return topk_per_req
767
768 @staticmethod
769 def _remap_topk_to_ranges(
770 topk_per_req: list[torch.Tensor],
771 range_starts: list[int] | torch.Tensor,
772 range_lens: list[int],
773 ) -> list[torch.Tensor]:
774 remapped = []
775 for topk, start, length in zip(topk_per_req, range_starts, range_lens):
776 valid = (topk >= start) & (topk < start + length)
777 remapped.append(torch.where(valid, topk - start, -1))
778 return remapped
779
780 def _project_kv(
781 self, kv_c_normed: torch.Tensor, k_pe: torch.Tensor
782 ) -> tuple[torch.Tensor, torch.Tensor]:
783 kv_nope = self.kv_b_proj(kv_c_normed)[0].view(
784 -1,
785 self.num_heads,
786 self.qk_nope_head_dim + self.v_head_dim,
787 )
788 k_nope, v = kv_nope.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1)
789 return self._concat_k_nope_k_pe(k_nope, k_pe), v
790
791 @staticmethod
792 def _try_build_global_mask(
793 topk_per_req: list[torch.Tensor],
794 q_lens: list[int],
795 max_query_len: int,
796 max_seq_len: int,
797 topk_mask_workspace: torch.Tensor,
798 ) -> torch.Tensor | None:
799 """Build a full-sequence top-k mask if it fits within the budget.
800
801 When the mask fits, it is reused across the suffix and all context
802 chunks, avoiding per-chunk mask rebuilds. Returns None when the
803 mask is too large, signalling the caller to fall back to per-chunk
804 index remapping.
805 """
806 batch_size, padded_q_len, num_words_padded = _topk_mask_shape(
807 len(q_lens),
808 max_query_len,
809 max_seq_len,
810 reserve_key_starts_word=True,
811 )
812 needed = batch_size * padded_q_len * num_words_padded
813 if needed > topk_mask_workspace.numel():
814 return None
815
816 mask = topk_mask_workspace[:needed].view(
817 batch_size, padded_q_len, num_words_padded
818 )
819 _build_topk_mask(
820 topk_per_req,
821 q_lens,
822 padded_q_len,
823 max_seq_len,
824 mask,
825 )
826 return mask
827
828 def _run_masked_mha(
829 self,
830 q: torch.Tensor,
831 k: torch.Tensor,
832 v: torch.Tensor,
833 cu_seqlens_q: torch.Tensor,
834 cu_seqlens_k: torch.Tensor,
835 max_seqlen_q: int,
836 max_seqlen_k: int,
837 topk_per_req: list[torch.Tensor],
838 q_lens: list[int],
839 causal: bool,
840 return_softmax_lse: bool = False,
841 dense_mask: torch.Tensor | None = None,
842 key_starts: torch.Tensor | None = None,
843 topk_mask_workspace: torch.Tensor | None = None,
844 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
845 from vllm.model_executor.layers.attention.sparse_mla_mask import (
846 dense_mask_mod,
847 offset_dense_mask_mod,
848 )
849 from vllm.vllm_flash_attn import flash_attn_varlen_func
850
851 if dense_mask is None:
852 assert topk_mask_workspace is not None
853 batch_size, padded_q_len, num_words = _topk_mask_shape(
854 len(q_lens), max_seqlen_q, max_seqlen_k
855 )
856 words_needed = batch_size * padded_q_len * num_words
857 if words_needed > topk_mask_workspace.numel():
858 raise ValueError(
859 f"Sparse MLA top-k mask needs {words_needed} int32 words (batch="
860 f"{len(q_lens)}, q={max_seqlen_q}, k={max_seqlen_k}) but the "
861 f"workspace holds {topk_mask_workspace.numel()}."
862 )
863 workspace_3d = topk_mask_workspace[:words_needed].view(
864 batch_size, padded_q_len, num_words
865 )
866 dense_mask = _build_topk_mask(
867 topk_per_req,
868 q_lens,
869 padded_q_len,
870 max_seqlen_k,
871 workspace_3d,
872 )
873 if key_starts is not None:
874 dense_mask[:, 0, -1].copy_(key_starts)
875 kwargs = {
876 "q": q,
877 "k": k,
878 "v": v,
879 "cu_seqlens_q": cu_seqlens_q,
880 "cu_seqlens_k": cu_seqlens_k,
881 "max_seqlen_q": max_seqlen_q,
882 "max_seqlen_k": max_seqlen_k,
883 "softmax_scale": self.scale,
884 "return_softmax_lse": return_softmax_lse,
885 "fa_version": 4,
886 "mask_mod": dense_mask_mod if key_starts is None else offset_dense_mask_mod,
887 "aux_tensors": [dense_mask],
888 "aux_tensor_leading_dims": [2],
889 "causal": causal,
890 }
891
892 return flash_attn_varlen_func(**kwargs)
893
894 def _compute_context_mha(
895 self,
896 q: torch.Tensor,
897 kv_c_and_k_pe_cache: torch.Tensor,
898 prefill_metadata: MLACommonPrefillMetadata,
899 k_scale: torch.Tensor,
900 q_lens: list[int],
901 topk_per_req: list[torch.Tensor],
902 dense_mask: torch.Tensor | None = None,
903 ) -> tuple[torch.Tensor, torch.Tensor]:
904 if self.dcp_world_size > 1:
905 raise NotImplementedError(
906 "Masked MHA with context does not yet support decode context "
907 "parallelism"
908 )
909
910 chunked_context = prefill_metadata.chunked_context
911 assert chunked_context is not None
912 output: torch.Tensor | None = None
913 output_lse: torch.Tensor | None = None
914 workspace = chunked_context.workspace
915
916 for chunk in chunked_context.chunks:
917 toks = chunk.num_context_tokens
918 requests = chunk.request_slice
919 ops.gather_and_maybe_dequant_cache(
920 src_cache=kv_c_and_k_pe_cache,
921 dst=workspace,
922 block_table=prefill_metadata.block_table[requests],
923 cu_seq_lens=chunk.cu_seq_lens,
924 token_to_seq=chunk.token_to_seq,
925 num_tokens=toks,
926 kv_cache_dtype=self.kv_cache_dtype,
927 scale=k_scale,
928 seq_starts=chunk.starts,
929 )
930
931 chunk_kv_c = workspace[:toks, : self.kv_lora_rank]
932 chunk_k_pe = workspace[:toks, self.kv_lora_rank :].unsqueeze(1)
933 k, v = self._project_kv(chunk_kv_c, chunk_k_pe)
934 if dense_mask is not None:
935 chunk_mask: torch.Tensor | None = dense_mask[requests]
936 chunk_topk = topk_per_req[requests]
937 key_starts: torch.Tensor | None = chunk.starts
938 else:
939 chunk_mask = None
940 chunk_topk = self._remap_topk_to_ranges(
941 topk_per_req[requests],
942 chunk.starts,
943 chunk.seq_lens.tolist(),
944 )
945 key_starts = None
946 attn_out, lse = self._run_masked_mha(
947 q=q[chunk.token_slice],
948 k=k,
949 v=v,
950 cu_seqlens_q=chunk.query_start_loc,
951 cu_seqlens_k=chunk.cu_seq_lens,
952 max_seqlen_q=chunk.max_query_len,
953 max_seqlen_k=chunk.max_seq_len,
954 topk_per_req=chunk_topk,
955 q_lens=q_lens[requests],
956 causal=False,
957 return_softmax_lse=True,
958 dense_mask=chunk_mask,
959 key_starts=key_starts,
960 topk_mask_workspace=prefill_metadata.topk_mask_workspace,
961 )
962
963 if output is None:
964 if (
965 len(chunked_context.chunks) == 1
966 and not chunked_context.empty_token_slices
967 ):
968 return attn_out, lse
969 output, output_lse = init_mla_context_partial(
970 chunked_context,
971 attn_out,
972 lse,
973 num_tokens=q.shape[0],
974 )
975 accumulate_mla_context_chunk(chunk, attn_out, lse, output, output_lse)
976
977 assert output is not None and output_lse is not None
978 return output, output_lse
979
980 def forward_mha( # type: ignore[override]
981 self,
982 q: torch.Tensor,
983 kv_c_normed: torch.Tensor,
984 k_pe: torch.Tensor,
985 kv_c_and_k_pe_cache: torch.Tensor,
986 attn_metadata: T,
987 k_scale: torch.Tensor,
988 output: torch.Tensor,
989 output_scale: torch.Tensor | None = None,
990 ) -> None:
991 prefill_metadata = attn_metadata.prefill
992 assert prefill_metadata is not None
993 prefill_max_seq_len = attn_metadata.prefill_max_seq_len
994 topk_tokens = attn_metadata.topk_tokens
995 force_dense = getattr(self, "_sparse_mla_force_dense_mha", False)
996 force_masked = getattr(self, "_sparse_mla_force_masked_mha", False)
997 if force_dense or (prefill_max_seq_len <= topk_tokens and not force_masked):
998 return super().forward_mha(
999 q,
1000 kv_c_normed,
1001 k_pe,
1002 kv_c_and_k_pe_cache,
1003 attn_metadata,
1004 k_scale,
1005 output,
1006 output_scale,
1007 )
1008
1009 assert output_scale is None
1010 assert self.masked_mha_available
1011 assert prefill_metadata.query_lens_cpu is not None
1012 assert self.topk_indices_buffer is not None
1013
1014 q_lens = prefill_metadata.query_lens_cpu.tolist()
1015 num_decode_tokens = attn_metadata.num_decode_tokens
1016 topk_all = self.topk_indices_buffer[
1017 num_decode_tokens : num_decode_tokens + q.shape[0]
1018 ]
1019 topk_per_req = self._slice_topk_per_req(topk_all, q_lens)
1020
1021 k, v = self._project_kv(kv_c_normed, k_pe)
1022 chunked_context = prefill_metadata.chunked_context
1023 if chunked_context is None:
1024 attn_out = self._run_masked_mha(
1025 q=q,
1026 k=k,
1027 v=v,
1028 cu_seqlens_q=prefill_metadata.query_start_loc,
1029 cu_seqlens_k=prefill_metadata.query_start_loc,
1030 max_seqlen_q=prefill_metadata.max_query_len,
1031 max_seqlen_k=prefill_metadata.max_query_len,
1032 topk_per_req=topk_per_req,
1033 q_lens=q_lens,
1034 causal=True,
1035 topk_mask_workspace=prefill_metadata.topk_mask_workspace,
1036 )
1037 assert isinstance(attn_out, torch.Tensor)
1038 output.copy_(attn_out[..., : self.v_head_dim].flatten(start_dim=-2))
1039 return
1040
1041 context_lens = chunked_context.context_lens_list
1042 dense_mask = self._try_build_global_mask(
1043 topk_per_req,
1044 q_lens,
1045 prefill_metadata.max_query_len,
1046 prefill_max_seq_len,
1047 prefill_metadata.topk_mask_workspace,
1048 )
1049 if dense_mask is not None:
1050 suffix_topk = topk_per_req
1051 else:
1052 suffix_topk = self._remap_topk_to_ranges(topk_per_req, context_lens, q_lens)
1053 suffix_output, suffix_lse = self._run_masked_mha(
1054 q=q,
1055 k=k,
1056 v=v,
1057 cu_seqlens_q=prefill_metadata.query_start_loc,
1058 cu_seqlens_k=prefill_metadata.query_start_loc,
1059 max_seqlen_q=prefill_metadata.max_query_len,
1060 max_seqlen_k=prefill_metadata.max_query_len,
1061 topk_per_req=suffix_topk,
1062 q_lens=q_lens,
1063 causal=True,
1064 return_softmax_lse=True,
1065 dense_mask=dense_mask,
1066 key_starts=(
1067 chunked_context.context_lens if dense_mask is not None else None
1068 ),
1069 topk_mask_workspace=prefill_metadata.topk_mask_workspace,
1070 )
1071 context_output, context_lse = self._compute_context_mha(
1072 q=q,
1073 kv_c_and_k_pe_cache=kv_c_and_k_pe_cache,
1074 prefill_metadata=prefill_metadata,
1075 k_scale=k_scale,
1076 q_lens=q_lens,
1077 topk_per_req=topk_per_req,
1078 dense_mask=dense_mask,
1079 )
1080 merge_attn_states(
1081 output=output.view(-1, self.num_heads, self.v_head_dim),
1082 prefix_output=context_output[..., : self.v_head_dim],
1083 prefix_lse=context_lse,
1084 suffix_output=suffix_output[..., : self.v_head_dim],
1085 suffix_lse=suffix_lse,
1086 )