main
py 406 lines 15 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """MSA (SM100/Blackwell) block-sparse attention for MiniMax M3.
4
5 Prefill attends with ``fmha_sm100`` (``build_k2q_csr`` + ``sparse_atten_func``).
6 Decode uses Triton split-K by default, with an opt-in CUTLASS ``fmha_sm100``
7 path for regular decode and speculative verification. NVFP4 KV caches read the
8 vLLM packed pages directly in both prefill and CUTLASS decode.
9 """
10
11 from dataclasses import dataclass, replace
12 from typing import ClassVar
13
14 import torch
15
16 from vllm.config import VllmConfig
17 from vllm.config.attention import MiniMaxM3MSADecodeBackend
18 from vllm.config.cache import CacheDType
19 from vllm.forward_context import get_forward_context
20 from vllm.logger import init_logger
21 from vllm.models.minimax_m3.common.ops.sparse_attn import (
22 SPARSE_BLOCK_SIZE,
23 minimax_m3_sparse_attn_decode,
24 )
25 from vllm.models.minimax_m3.common.sparse_attention import (
26 MiniMaxM3SparseBackend,
27 MiniMaxM3SparseDecodeMetadata,
28 MiniMaxM3SparseImpl,
29 MiniMaxM3SparseMetadata,
30 MiniMaxM3SparseMetadataBuilder,
31 )
32 from vllm.models.minimax_m3.nvidia.msa_cutlass_sparse_decode import (
33 MSACutlassDecodeMetadata,
34 MSACutlassDecodePlanCache,
35 is_nvfp4_kv_cache,
36 msa_cutlass_sparse_decode,
37 nvfp4_kv_cache_views,
38 prepare_decode_metadata,
39 should_prepare_decode_metadata,
40 supports_cutlass_sparse_decode,
41 )
42 from vllm.utils.torch_utils import get_dtype_size, nvfp4_kv_cache_full_dim
43 from vllm.v1.attention.backend import (
44 AttentionLayer,
45 CommonAttentionMetadata,
46 )
47 from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheLayout
48
49 logger = init_logger(__name__)
50
51
52 def _dequantize_query(
53 query_fp8: torch.Tensor, q_scale: float, out: torch.Tensor
54 ) -> torch.Tensor:
55 """Write ``query_fp8 * q_scale`` into ``out`` (E4M3 is exact in BF16)."""
56 out.copy_(query_fp8)
57 if q_scale != 1.0:
58 out.mul_(q_scale)
59 return out
60
61
62 class MiniMaxM3SparseMSABackend(MiniMaxM3SparseBackend):
63 """MiniMax M3 backend with NVIDIA MSA-specific decode metadata."""
64
65 @staticmethod
66 def get_builder_cls() -> type["MiniMaxM3SparseMSAMetadataBuilder"]:
67 return MiniMaxM3SparseMSAMetadataBuilder
68
69
70 class MiniMaxM3SparseMSANvfp4Backend(MiniMaxM3SparseMSABackend):
71 """MSA backend over HND NVFP4 KV cache pages in per-head K/V slots."""
72
73 supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = ["nvfp4"]
74
75 @classmethod
76 def customize_spec(cls, spec: AttentionSpec) -> AttentionSpec:
77 """Store each head's K and V as two slots of packed data + block scales
78 (see ``nvfp4_kv_cache_views``)."""
79 if spec.state_content_bytes is not None or not spec.kv_quant_mode.is_nvfp4:
80 return spec
81 return replace(
82 spec,
83 num_head_slots=2 * spec.num_kv_heads,
84 state_content_bytes=nvfp4_kv_cache_full_dim(spec.head_size)
85 * get_dtype_size(spec.dtype),
86 )
87
88 @classmethod
89 def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...] | None:
90 # Every head's K/V slots are one contiguous run of the page.
91 return (KVCacheLayout.LBHNC,)
92
93
94 class MiniMaxM3SparseCutlassBackend(MiniMaxM3SparseMSABackend):
95 """Attention-backend alias selecting CUTLASS MSA sparse decode."""
96
97 @staticmethod
98 def get_name() -> str:
99 return "CUTLASS_MSA"
100
101
102 class MiniMaxM3SparseTritonBackend(MiniMaxM3SparseMSABackend):
103 """Attention-backend alias selecting Triton MSA sparse decode."""
104
105 @staticmethod
106 def get_name() -> str:
107 return "TRITON_MSA"
108
109
110 @dataclass
111 class MiniMaxM3SparseMSADecodeMetadata(MiniMaxM3SparseDecodeMetadata):
112 msa_cutlass: MSACutlassDecodeMetadata | None = None
113
114
115 class MiniMaxM3SparseMSAMetadataBuilder(MiniMaxM3SparseMetadataBuilder):
116 """Prepare MSA plans only for decode shapes supported by ``fmha_sm100``."""
117
118 def __init__(
119 self,
120 kv_cache_spec: AttentionSpec,
121 layer_names: list[str],
122 vllm_config: VllmConfig,
123 device: torch.device,
124 ) -> None:
125 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
126 config = vllm_config.model_config.hf_text_config
127 tp_size = vllm_config.parallel_config.tensor_parallel_size
128 self.num_q_heads = config.num_attention_heads // tp_size
129 self.num_kv_heads = kv_cache_spec.num_kv_heads
130 self.topk_blocks = config.sparse_attention_config["sparse_topk_blocks"]
131 # AttentionSpec stores every FP8 mode as uint8, so retain the configured
132 # format to distinguish E4M3 (supported) from E5M2 before planning.
133 self.kv_cache_dtype = vllm_config.cache_config.cache_dtype
134 self.decode_backend = vllm_config.attention_config.minimax_m3_msa_decode_backend
135 self.msa_cutlass_plan_cache = MSACutlassDecodePlanCache()
136
137 def build(
138 self,
139 common_prefix_len: int,
140 common_attn_metadata: CommonAttentionMetadata,
141 fast_build: bool = False,
142 ) -> MiniMaxM3SparseMetadata:
143 metadata = super().build(
144 common_prefix_len,
145 common_attn_metadata,
146 fast_build,
147 )
148 decode = metadata.decode
149 if decode is None:
150 return metadata
151
152 msa_cutlass = None
153 if should_prepare_decode_metadata(
154 metadata.num_decodes,
155 decode.decode_query_len,
156 decode_backend=self.decode_backend,
157 num_q_heads=self.num_q_heads,
158 num_kv_heads=self.num_kv_heads,
159 kv_cache_dtype=self.kv_cache_dtype,
160 page_size=SPARSE_BLOCK_SIZE,
161 topk_blocks=self.topk_blocks,
162 ):
163 seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
164 assert seq_lens_cpu is not None
165 msa_cutlass = prepare_decode_metadata(
166 decode.block_table,
167 decode.seq_lens,
168 seq_lens_cpu[: metadata.num_decodes],
169 decode.decode_query_len,
170 num_q_heads=self.num_q_heads,
171 num_kv_heads=self.num_kv_heads,
172 page_size=SPARSE_BLOCK_SIZE,
173 topk_blocks=self.topk_blocks,
174 plan_cache=self.msa_cutlass_plan_cache,
175 )
176 metadata.decode = MiniMaxM3SparseMSADecodeMetadata(
177 seq_lens=decode.seq_lens,
178 block_table=decode.block_table,
179 decode_query_len=decode.decode_query_len,
180 msa_cutlass=msa_cutlass,
181 )
182 return metadata
183
184
185 class MiniMaxM3SparseMSAImpl(MiniMaxM3SparseImpl):
186 """MSA block-sparse attention with guarded CUTLASS sparse decode."""
187
188 def __init__(
189 self,
190 num_heads: int,
191 head_size: int,
192 scale: float,
193 num_kv_heads: int | None = None,
194 kv_cache_dtype: str = "auto",
195 *,
196 topk_blocks: int,
197 sparse_block_size: int,
198 msa_decode_backend: MiniMaxM3MSADecodeBackend = "triton",
199 ) -> None:
200 super().__init__(
201 num_heads,
202 head_size,
203 scale,
204 num_kv_heads,
205 kv_cache_dtype,
206 topk_blocks=topk_blocks,
207 sparse_block_size=sparse_block_size,
208 )
209 self.use_nvfp4_kv = is_nvfp4_kv_cache(kv_cache_dtype)
210 self.use_cutlass_decode = supports_cutlass_sparse_decode(
211 decode_backend=msa_decode_backend,
212 num_q_heads=self.num_heads,
213 num_kv_heads=self.num_kv_heads,
214 kv_cache_dtype=self.kv_cache_dtype,
215 page_size=self.block_size,
216 topk_blocks=self.topk_blocks,
217 )
218 if self.use_nvfp4_kv and not self.use_cutlass_decode:
219 raise ValueError(
220 "MiniMax M3 NVFP4 KV cache requires CUTLASS MSA sparse decode "
221 f"(num_heads={self.num_heads}, num_kv_heads={self.num_kv_heads}, "
222 f"block_size={self.block_size}, topk_blocks={self.topk_blocks})"
223 )
224 logger.info_once(
225 "MiniMax M3 MSA sparse decode selected %s",
226 "CUTLASS" if self.use_cutlass_decode else "Triton",
227 )
228
229 def should_use_msa_decode(self, layer_name: str) -> bool:
230 if not self.use_cutlass_decode:
231 return False
232 attn_metadata = get_forward_context().attn_metadata
233 if not isinstance(attn_metadata, dict):
234 return False
235 main_md = attn_metadata[layer_name]
236 if not isinstance(main_md, MiniMaxM3SparseMetadata):
237 return False
238 decode = main_md.decode
239 return (
240 isinstance(decode, MiniMaxM3SparseMSADecodeMetadata)
241 and decode.msa_cutlass is not None
242 )
243
244 def forward(
245 self,
246 layer: AttentionLayer,
247 query: torch.Tensor,
248 kv_cache: torch.Tensor,
249 output: torch.Tensor,
250 *,
251 query_fp8: torch.Tensor | None = None,
252 ) -> torch.Tensor:
253 attn_metadata = get_forward_context().attn_metadata
254 if not isinstance(attn_metadata, dict):
255 return output # profiling run; caches unbound
256 main_md = attn_metadata[layer.layer_name] # type: ignore[attr-defined]
257 assert isinstance(main_md, MiniMaxM3SparseMetadata)
258
259 nd = main_md.num_decode_tokens
260 num_tokens = main_md.num_actual_tokens
261 # Indexer top-k from the shared token-major buffer [total_q, H, MK]; the
262 # kernels want [H, tokens, MK], so slice tokens on dim 0 then transpose.
263 topk = layer.topk_indices_buffer # type: ignore[attr-defined]
264 assert topk is not None
265 hd = self.head_size
266 q = query[:num_tokens].view(-1, self.num_heads, hd)
267 out = output[:num_tokens].view(-1, self.num_heads, hd)
268 # Given query_fp8, it is the only q the fused insert wrote: CUTLASS
269 # decode reads it directly, and the bf16-query kernels read it
270 # dequantized into ``query``.
271 q_fp8 = (
272 None
273 if query_fp8 is None
274 else query_fp8[:num_tokens].view(-1, self.num_heads, hd)
275 )
276 q_scale_float = getattr(layer, "_q_scale_float", 1.0)
277 if self.use_fp8_kv and not self.use_nvfp4_kv:
278 kv_cache = kv_cache.view(self.kv_cache_fp8_dtype)
279 k_scale = getattr(layer, "_k_scale", None) if self.use_fp8_kv else None
280 v_scale = getattr(layer, "_v_scale", None) if self.use_fp8_kv else None
281
282 # Decode [:nd]: CUTLASS for planned shapes, otherwise Triton.
283 if main_md.num_decodes > 0:
284 d = main_md.decode
285 assert d is not None
286 msa_metadata = (
287 d.msa_cutlass
288 if isinstance(d, MiniMaxM3SparseMSADecodeMetadata)
289 else None
290 )
291 if self.use_cutlass_decode and msa_metadata is not None:
292 assert q_fp8 is not None
293 msa_cutlass_sparse_decode(
294 q_fp8[:nd],
295 kv_cache,
296 topk[:nd],
297 out[:nd],
298 msa_metadata,
299 scale=self.scale,
300 q_scale_float=q_scale_float,
301 k_scale_float=getattr(layer, "_k_scale_float", 1.0),
302 v_scale_float=getattr(layer, "_v_scale_float", 1.0),
303 k_scale=k_scale if self.use_nvfp4_kv else None,
304 v_scale=v_scale if self.use_nvfp4_kv else None,
305 )
306 elif self.use_nvfp4_kv:
307 raise RuntimeError(
308 "MiniMax M3 NVFP4 KV cache has no Triton decode fallback; "
309 f"CUTLASS decode was not planned for {main_md.num_decodes} "
310 f"requests with query length {d.decode_query_len}"
311 )
312 else:
313 minimax_m3_sparse_attn_decode(
314 q[:nd]
315 if q_fp8 is None
316 else _dequantize_query(q_fp8[:nd], q_scale_float, q[:nd]),
317 kv_cache,
318 topk[:nd].transpose(0, 1),
319 d.block_table,
320 d.seq_lens,
321 self.num_kv_heads,
322 self.scale,
323 out[:nd],
324 d.decode_query_len,
325 k_scale=k_scale,
326 v_scale=v_scale,
327 )
328
329 # Prefill [nd:]: MSA sparse FMHA over the selected blocks.
330 if main_md.num_prefills > 0:
331 from vllm.third_party.fmha_sm100.sparse import (
332 build_k2q_csr,
333 sparse_atten_func,
334 sparse_atten_nvfp4_kv_func,
335 )
336
337 p = main_md.prefill
338 assert p is not None
339 # [H, prefill, MK] transposed view; build_k2q_csr consumes the
340 # strided view directly (topK stays innermost-contiguous).
341 prefill_topk = topk[nd:num_tokens].transpose(0, 1)
342 qp = (
343 q[nd:]
344 if q_fp8 is None
345 else _dequantize_query(q_fp8[nd:], q_scale_float, q[nd:])
346 )
347 k2q_row_ptr, k2q_q_indices, schedule = build_k2q_csr(
348 prefill_topk,
349 p.cu_seqlens_q,
350 p.cu_seqlens_k,
351 SPARSE_BLOCK_SIZE,
352 total_k=0,
353 max_seqlen_k=p.max_seq_len,
354 max_seqlen_q=p.max_query_len,
355 total_rows=p.total_kv_blocks,
356 qhead_per_kv=qp.shape[1] // self.num_kv_heads,
357 return_schedule=True,
358 )
359 if self.use_nvfp4_kv:
360 k_data, k_sf, v_data, v_sf = nvfp4_kv_cache_views(kv_cache)
361 sparse_atten_nvfp4_kv_func(
362 qp,
363 k_data,
364 v_data,
365 k_sf,
366 v_sf,
367 k_scale,
368 v_scale,
369 k2q_row_ptr,
370 k2q_q_indices,
371 self.topk_blocks,
372 cu_seqlens_q=p.cu_seqlens_q,
373 cu_seqlens_k=p.cu_seqlens_k,
374 max_seqlen_q=p.max_query_len,
375 max_seqlen_k=p.max_seq_len,
376 blk_kv=SPARSE_BLOCK_SIZE,
377 causal=True,
378 softmax_scale=self.scale,
379 page_table=p.block_table,
380 seqused_k=p.seq_lens,
381 schedule=schedule,
382 out=out[nd:],
383 kv_layout="vllm",
384 )
385 return output
386 k_cache, v_cache = kv_cache.split(self.head_size, dim=-1)
387 sparse_atten_func(
388 qp,
389 k_cache,
390 v_cache,
391 k2q_row_ptr,
392 k2q_q_indices,
393 topK=self.topk_blocks,
394 blk_kv=SPARSE_BLOCK_SIZE,
395 causal=True,
396 softmax_scale=self.scale,
397 cu_seqlens_q=p.cu_seqlens_q,
398 cu_seqlens_k=p.cu_seqlens_k,
399 max_seqlen_q=p.max_query_len,
400 max_seqlen_k=p.max_seq_len,
401 page_table=p.block_table,
402 seqused_k=p.seq_lens,
403 schedule=schedule,
404 out=out[nd:],
405 )
406 return output