main
py 510 lines 17.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """FlashAttention backend for MLA prefill."""
4
5 import functools
6 from collections.abc import Callable
7 from dataclasses import dataclass
8 from typing import TYPE_CHECKING, Any
9
10 import torch
11
12 import vllm.envs as envs
13 from vllm.model_executor.layers.quantization.utils.quant_utils import (
14 kFp8StaticTensorSym,
15 )
16 from vllm.model_executor.warmup.jit_warmup import VllmJitKernel, zip_inputs
17 from vllm.platforms import current_platform
18 from vllm.v1.attention.backends.fa_utils import (
19 compile_flash_attn_varlen_func_from_specs,
20 get_flash_attn_version,
21 is_flash_attn_varlen_func_available,
22 )
23 from vllm.v1.attention.backends.mla.prefill.base import (
24 MLADimensions,
25 MLAPrefillBackend,
26 )
27
28 if TYPE_CHECKING:
29 from vllm.config import VllmConfig
30 from vllm.model_executor.layers.attention.mla_attention import (
31 MLACommonPrefillMetadata,
32 MLADims,
33 )
34 from vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
35
36 if is_flash_attn_varlen_func_available():
37 from vllm.v1.attention.backends.fa_utils import flash_attn_varlen_func
38 else:
39 flash_attn_varlen_func = None # type: ignore[assignment]
40
41
42 FA4_STANDARD_DTYPES = (torch.bfloat16, torch.float16)
43
44 # Current vLLM MLA prefill expands K/V to num_heads before FA4, so this plan
45 # covers qhead_per_kvhead=1. Batch is not a current FA4 MLA-prefill key field.
46 # Use b1 for compile-only specs because it is the conservative case for
47 # Split-KV shape heuristics.
48 # TODO(roberto): FA4 also has direct-GQA and qv/top-k absorbed-MLA paths, but
49 # vLLM does not use them in this backend yet; they need a separate
50 # num_kv_heads/qv/top-k-aware warmup plan if wired in later.
51 FA4_MLA_PREFILL_COMPILE_BATCH_SIZE = 1
52 FA4_MLA_PREFILL_Q_TILE = 128
53 FA4_MLA_PREFILL_K_TILE = 128
54 FA4_MLA_PREFILL_LONG_K_BLOCKS = 32
55 FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS = 64
56 FA4_MLA_PREFILL_CAUSAL_OPTIONS = (False, True)
57 FA4_MLA_PREFILL_LSE_OPTIONS = (False, True)
58
59
60 class FA4MLAPrefillKernel(VllmJitKernel["FA4MLAPrefillKernel.CompileKey"]):
61 """FA4 MLA prefill compile-key wrapper used by generic JIT warmup."""
62
63 @dataclass(frozen=True)
64 class CompileKey:
65 """High-level FA4 compile-only key used by vLLM warmup.
66
67 This is not the CuTeDSL cache key. FA4 owns the selector that maps these
68 serving inputs to the actual compile-static fields: tile sizes, q_stage,
69 Split-KV, scheduler choice, layout-presence booleans, dtype/head dims,
70 arch, and related fields.
71 """
72
73 q_shape: tuple[int, ...]
74 k_shape: tuple[int, ...]
75 v_shape: tuple[int, ...]
76 q_dtype: torch.dtype
77 max_seqlen_q: int
78 max_seqlen_k: int
79 softmax_scale: float
80 causal: bool
81 fa_version: int
82 v_stride: tuple[int, ...] | None = None
83 cu_seqlens_q_shape: tuple[int, ...] | None = None
84 cu_seqlens_k_shape: tuple[int, ...] | None = None
85 window_size: tuple[int, int] | None = None
86 return_softmax_lse: bool = False
87 num_splits: int = 0
88
89 @staticmethod
90 def kernel(
91 *args: Any,
92 runtime_kernel: Callable[..., Any] | None = None,
93 **kwargs: Any,
94 ) -> Any:
95 kernel = flash_attn_varlen_func if runtime_kernel is None else runtime_kernel
96 assert kernel is not None
97 return kernel(*args, **kwargs)
98
99 def dispatch( # type: ignore[override]
100 self,
101 *,
102 batch_size: int,
103 dtype: torch.dtype,
104 num_heads: int,
105 mla_dims: "MLADims",
106 max_seqlen_q: int,
107 max_seqlen_k: int,
108 requires_v_padding: bool,
109 causal: bool,
110 fa_version: int,
111 window_size: tuple[int, int] | None = None,
112 return_lse: bool = False,
113 num_splits: int = 0,
114 ) -> CompileKey:
115 return self.CompileKey(
116 q_shape=(
117 batch_size * max_seqlen_q,
118 num_heads,
119 mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim,
120 ),
121 k_shape=(
122 batch_size * max_seqlen_k,
123 num_heads,
124 mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim,
125 ),
126 v_shape=(
127 batch_size * max_seqlen_k,
128 num_heads,
129 (
130 mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim
131 if requires_v_padding
132 else mla_dims.v_head_dim
133 ),
134 ),
135 q_dtype=dtype,
136 max_seqlen_q=max_seqlen_q,
137 max_seqlen_k=max_seqlen_k,
138 softmax_scale=(mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim)
139 ** -0.5,
140 causal=causal,
141 fa_version=fa_version,
142 v_stride=(
143 None
144 if requires_v_padding
145 else (
146 num_heads * (mla_dims.qk_nope_head_dim + mla_dims.v_head_dim),
147 mla_dims.qk_nope_head_dim + mla_dims.v_head_dim,
148 1,
149 )
150 ),
151 cu_seqlens_q_shape=(batch_size + 1,),
152 cu_seqlens_k_shape=(batch_size + 1,),
153 window_size=window_size,
154 return_softmax_lse=return_lse,
155 num_splits=num_splits,
156 )
157
158 def _is_valid_warmup_shape_probe(
159 self,
160 *,
161 max_seqlen_k: int,
162 num_splits: int,
163 is_sm90: bool,
164 qk_head_dim: int,
165 effective_v_head_dim: int,
166 ) -> bool:
167 long_k = FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE
168 very_long_k = FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE
169 return (
170 max_seqlen_k < long_k
171 or (num_splits != 1 and max_seqlen_k == long_k)
172 or (
173 num_splits != 1
174 and max_seqlen_k == very_long_k
175 and not is_sm90
176 and qk_head_dim != effective_v_head_dim
177 )
178 )
179
180 def get_warmup_keys(self, vllm_config: "VllmConfig") -> list[CompileKey]:
181 from vllm.model_executor.layers.attention.mla_attention import (
182 get_mla_dims,
183 )
184
185 mla_dims = get_mla_dims(vllm_config.model_config)
186 qk_head_dim = mla_dims.qk_nope_head_dim + mla_dims.qk_rope_head_dim
187 fa_version = get_flash_attn_version(head_size=qk_head_dim)
188 if fa_version != 4:
189 return []
190
191 dtype = vllm_config.model_config.dtype
192 if dtype not in FA4_STANDARD_DTYPES:
193 dtype = torch.bfloat16
194
195 is_sm90 = current_platform.is_device_capability(90)
196 is_sm100_family = current_platform.is_device_capability_family(100)
197 is_sm120 = current_platform.is_device_capability_family(120)
198 if not (is_sm90 or is_sm100_family or is_sm120):
199 return []
200
201 num_splits = 1 if envs.VLLM_BATCH_INVARIANT else 0
202 if is_sm120 and num_splits != 1:
203 return []
204
205 num_heads = vllm_config.model_config.get_num_attention_heads(
206 vllm_config.parallel_config
207 )
208 if num_heads <= 0:
209 return []
210
211 requires_v_padding = False
212 effective_v_head_dim = (
213 qk_head_dim if requires_v_padding else mla_dims.v_head_dim
214 )
215 shape_probes = (
216 dict(max_seqlen_q=1, max_seqlen_k=FA4_MLA_PREFILL_K_TILE),
217 dict(
218 max_seqlen_q=FA4_MLA_PREFILL_Q_TILE + 1,
219 max_seqlen_k=4 * FA4_MLA_PREFILL_K_TILE,
220 ),
221 dict(
222 max_seqlen_q=1,
223 max_seqlen_k=FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
224 ),
225 dict(
226 max_seqlen_q=FA4_MLA_PREFILL_Q_TILE + 1,
227 max_seqlen_k=FA4_MLA_PREFILL_LONG_K_BLOCKS * FA4_MLA_PREFILL_K_TILE,
228 ),
229 dict(
230 max_seqlen_q=1,
231 max_seqlen_k=FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS
232 * FA4_MLA_PREFILL_K_TILE,
233 ),
234 dict(
235 max_seqlen_q=FA4_MLA_PREFILL_Q_TILE + 1,
236 max_seqlen_k=FA4_MLA_PREFILL_VERY_LONG_K_BLOCKS
237 * FA4_MLA_PREFILL_K_TILE,
238 ),
239 )
240
241 return self._trace_dispatch(self.dispatch)(
242 zip_inputs(*shape_probes),
243 batch_size=FA4_MLA_PREFILL_COMPILE_BATCH_SIZE,
244 dtype=dtype,
245 num_heads=num_heads,
246 mla_dims=mla_dims,
247 requires_v_padding=requires_v_padding,
248 is_sm90=is_sm90,
249 qk_head_dim=qk_head_dim,
250 effective_v_head_dim=effective_v_head_dim,
251 causal=FA4_MLA_PREFILL_CAUSAL_OPTIONS,
252 return_lse=FA4_MLA_PREFILL_LSE_OPTIONS,
253 num_splits=num_splits,
254 fa_version=fa_version,
255 _when=self._is_valid_warmup_shape_probe,
256 )
257
258 def compile(self, compile_key: CompileKey) -> None:
259 assert compile_flash_attn_varlen_func_from_specs is not None
260 window_size = (
261 list(compile_key.window_size)
262 if compile_key.window_size is not None
263 else None
264 )
265 compile_flash_attn_varlen_func_from_specs(
266 q_shape=compile_key.q_shape,
267 k_shape=compile_key.k_shape,
268 v_shape=compile_key.v_shape,
269 q_dtype=compile_key.q_dtype,
270 v_stride=compile_key.v_stride,
271 cu_seqlens_q_shape=compile_key.cu_seqlens_q_shape,
272 cu_seqlens_k_shape=compile_key.cu_seqlens_k_shape,
273 max_seqlen_q=compile_key.max_seqlen_q,
274 max_seqlen_k=compile_key.max_seqlen_k,
275 softmax_scale=compile_key.softmax_scale,
276 causal=compile_key.causal,
277 window_size=window_size,
278 return_softmax_lse=compile_key.return_softmax_lse,
279 fa_version=compile_key.fa_version,
280 num_splits=compile_key.num_splits,
281 )
282
283 def __call__(
284 self,
285 *,
286 q: torch.Tensor,
287 k: torch.Tensor,
288 v: torch.Tensor,
289 runtime_kernel: Callable[..., Any] | None = None,
290 **kwargs: Any,
291 ) -> Any:
292 return self.kernel(
293 q=q,
294 k=k,
295 v=v,
296 runtime_kernel=runtime_kernel,
297 **kwargs,
298 )
299
300
301 class FlashAttnPrefillBackend(MLAPrefillBackend):
302 """FlashAttention backend for MLA prefill."""
303
304 @staticmethod
305 def get_name() -> str:
306 return "FLASH_ATTN"
307
308 @classmethod
309 def is_available(cls) -> bool:
310 return is_flash_attn_varlen_func_available()
311
312 @classmethod
313 def supports_mla_dimensions(cls, mla_dimensions: MLADimensions) -> bool:
314 return mla_dimensions in [
315 MLADimensions(
316 qk_nope_head_dim=128,
317 qk_rope_head_dim=64,
318 v_head_dim=128,
319 ),
320 MLADimensions(
321 qk_nope_head_dim=192,
322 qk_rope_head_dim=64,
323 v_head_dim=256,
324 ),
325 MLADimensions(
326 qk_nope_head_dim=64,
327 qk_rope_head_dim=64,
328 v_head_dim=128,
329 ),
330 # GLM5Next NoPE layout: qk_head_dim 256 + 0 and v_head_dim 256 run
331 # the same kernels as the (192, 64, 256) DeepSeek-V3.2 layout.
332 MLADimensions(
333 qk_nope_head_dim=256,
334 qk_rope_head_dim=0,
335 v_head_dim=256,
336 ),
337 ]
338
339 def __init__(
340 self,
341 num_heads: int,
342 scale: float,
343 kv_lora_rank: int,
344 qk_nope_head_dim: int,
345 qk_rope_head_dim: int,
346 v_head_dim: int,
347 vllm_config: "VllmConfig",
348 ) -> None:
349 super().__init__(
350 num_heads=num_heads,
351 scale=scale,
352 kv_lora_rank=kv_lora_rank,
353 qk_nope_head_dim=qk_nope_head_dim,
354 qk_rope_head_dim=qk_rope_head_dim,
355 v_head_dim=v_head_dim,
356 vllm_config=vllm_config,
357 )
358
359 # Handle the differences between the flash_attn_varlen from
360 # flash_attn and the one from vllm_flash_attn
361 assert flash_attn_varlen_func is not None, (
362 "FlashAttnPrefillBackend requires flash_attn_varlen_func. "
363 "Ensure FlashAttnPrefillBackend.is_available() is checked first."
364 )
365 qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
366 self.flash_attn_varlen_func = flash_attn_varlen_func
367 self.vllm_flash_attn_version = get_flash_attn_version(
368 head_size=qk_head_dim, head_size_v=v_head_dim
369 )
370 if self.vllm_flash_attn_version is not None:
371 self.flash_attn_varlen_func = functools.partial(
372 flash_attn_varlen_func, fa_version=self.vllm_flash_attn_version
373 )
374
375 if (
376 vllm_config.kernel_config.enable_jit_warmup
377 and self.vllm_flash_attn_version == 4
378 ):
379 _FA4_MLA_PREFILL_KERNEL.register_warmup()
380
381 # Determine if we need to pad V
382 # For MLA the v head dim is smaller than qk head dim so we pad out
383 # v with 0s to match the qk head dim for attention backends that do
384 # not support different headdims.
385 # FA3 on Hopper (SM90) and FA4 natively handle diff headdims.
386 device_capability = current_platform.get_device_capability()
387 self.requires_v_padding = self.vllm_flash_attn_version is None or not (
388 (
389 self.vllm_flash_attn_version == 3
390 and device_capability is not None
391 and device_capability[0] == 9
392 )
393 or self.vllm_flash_attn_version == 4
394 )
395
396 # Track whether we're using vllm's FA or upstream (for ROCm)
397 self._is_vllm_fa = current_platform.is_cuda() or current_platform.is_xpu()
398
399 def supports_quant_output(self, quant_key: "QuantKey") -> bool:
400 device_capability = current_platform.get_device_capability()
401 return (
402 self.vllm_flash_attn_version == 4
403 and self._is_vllm_fa
404 and device_capability is not None
405 and device_capability[0] in (10, 11)
406 and quant_key == kFp8StaticTensorSym
407 )
408
409 def _flash_attn_varlen_diff_headdims(
410 self,
411 q: torch.Tensor,
412 k: torch.Tensor,
413 v: torch.Tensor,
414 return_softmax_lse: bool = False,
415 softmax_scale: float | None = None,
416 out: torch.Tensor | None = None,
417 output_scale: torch.Tensor | None = None,
418 **kwargs,
419 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
420 maybe_padded_v = v
421 if self.requires_v_padding:
422 maybe_padded_v = torch.nn.functional.pad(
423 v, [0, q.shape[-1] - v.shape[-1]], value=0
424 )
425
426 if self._is_vllm_fa:
427 kwargs["return_softmax_lse"] = return_softmax_lse
428 kwargs["out"] = out
429 kwargs["output_scale"] = output_scale
430 else:
431 # ROCm leverages the upstream flash_attn, which takes a parameter
432 # called "return_attn_probs" instead of return_softmax_lse
433 kwargs["return_attn_probs"] = return_softmax_lse
434 assert out is None and output_scale is None
435 if envs.VLLM_BATCH_INVARIANT:
436 kwargs["num_splits"] = 1
437
438 attn_out = _FA4_MLA_PREFILL_KERNEL(
439 q=q,
440 k=k,
441 v=maybe_padded_v,
442 runtime_kernel=self.flash_attn_varlen_func,
443 softmax_scale=softmax_scale,
444 **kwargs,
445 )
446
447 # Unpack the output if there are multiple results
448 lse = None
449 if isinstance(attn_out, tuple):
450 attn_out, lse = attn_out[0], attn_out[1]
451
452 # Remain consistent with old `flash_attn_varlen_func` where there
453 # is only one output tensor if `return_softmax_lse` is False.
454 if return_softmax_lse:
455 return attn_out, lse
456 return attn_out
457
458 def supports_out(self) -> bool:
459 # A padded V produces a qk_head_dim output that cannot be written into
460 # a v_head_dim `out`; only the unpadded path honors `out`.
461 return not self.requires_v_padding
462
463 def run_prefill_new_tokens(
464 self,
465 q: torch.Tensor,
466 k: torch.Tensor,
467 v: torch.Tensor,
468 return_softmax_lse: bool,
469 out: torch.Tensor | None = None,
470 output_scale: torch.Tensor | None = None,
471 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
472 return self._flash_attn_varlen_diff_headdims(
473 q=q,
474 k=k,
475 v=v,
476 cu_seqlens_q=self._prefill_metadata.query_start_loc,
477 cu_seqlens_k=self._prefill_metadata.query_start_loc,
478 max_seqlen_q=self._prefill_metadata.max_query_len,
479 max_seqlen_k=self._prefill_metadata.max_query_len,
480 softmax_scale=self.scale,
481 causal=True,
482 return_softmax_lse=return_softmax_lse,
483 out=out,
484 output_scale=output_scale,
485 )
486
487 def run_prefill_context_chunk(
488 self,
489 chunk: "MLACommonPrefillMetadata.ContextChunk",
490 q: torch.Tensor,
491 k: torch.Tensor,
492 v: torch.Tensor,
493 out: torch.Tensor | None = None,
494 ) -> tuple[torch.Tensor, torch.Tensor]:
495 return self._flash_attn_varlen_diff_headdims(
496 q=q,
497 k=k,
498 v=v,
499 cu_seqlens_q=chunk.query_start_loc,
500 cu_seqlens_k=chunk.cu_seq_lens,
501 max_seqlen_q=chunk.max_query_len,
502 max_seqlen_k=chunk.max_seq_len,
503 softmax_scale=self.scale,
504 causal=False, # Context is unmasked
505 return_softmax_lse=True,
506 out=out,
507 )
508
509
510 _FA4_MLA_PREFILL_KERNEL = FA4MLAPrefillKernel()