main
py 629 lines 22 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Callable
5
6 import torch
7 from torch._higher_order_ops.auto_functionalize import auto_functionalized
8
9 from vllm.config import VllmConfig, get_layers_from_vllm_config
10 from vllm.logger import init_logger
11 from vllm.model_executor.layers.attention.mla_attention import MLAAttention
12 from vllm.model_executor.layers.quantization.utils.quant_utils import (
13 QuantKey,
14 kFp8Dynamic64Sym,
15 kFp8Dynamic128Sym,
16 kFp8StaticTensorSym,
17 kNvfp4Dynamic,
18 )
19 from vllm.platforms import current_platform
20 from vllm.utils.math_utils import round_up
21 from vllm.utils.torch_utils import _USE_LAYERNAME, _encode_layer_name
22
23 from ..vllm_inductor_pass import VllmFusionPatternMatcherPass, VllmPatternReplacement
24 from .matcher_utils import MatcherQuantFP8
25 from .rms_quant_fusion import QUANT_OPS
26
27 logger = init_logger(__name__)
28
29 FP8_DTYPE = current_platform.fp8_dtype()
30 FP4_DTYPE = torch.uint8
31
32 MLA_ATTN_OP = torch.ops.vllm.unified_mla_attention_with_output.default
33
34
35 class MLAAttnFp8StaticQuantPattern(VllmPatternReplacement[..., torch.Tensor]):
36 """Fusion for MLA Attention+Fp8StaticQuant.
37
38 Matches the pattern: MLA attention -> static FP8 quant, and replaces
39 it with MLA attention(output_scale=scale, output=fp8_buffer).
40 """
41
42 def __init__(self, layer: MLAAttention, dtype: torch.dtype) -> None:
43 self._layer_name = layer.layer_name
44 self._num_heads = layer.num_heads
45 self._v_head_dim = layer.v_head_dim
46 self._kv_lora_rank = layer.kv_lora_rank
47 self._qk_rope_head_dim = layer.qk_rope_head_dim
48 self._qk_head_dim = layer.qk_nope_head_dim + layer.qk_rope_head_dim
49 self._output_dim = layer.num_heads * layer.v_head_dim
50 self._dtype = dtype
51 self._quant_matcher = MatcherQuantFP8(kFp8StaticTensorSym)
52
53 @property
54 def pattern(self) -> Callable[..., torch.Tensor]:
55 _ln = _encode_layer_name(self._layer_name)
56
57 if _USE_LAYERNAME:
58
59 def _pattern_with_ln( # type: ignore[misc]
60 q,
61 kv_c_normed,
62 k_pe,
63 output_attn,
64 scale,
65 kv_cache_dummy_dep,
66 layer_name,
67 ):
68 at1 = auto_functionalized(
69 MLA_ATTN_OP,
70 q=q,
71 kv_c_normed=kv_c_normed,
72 k_pe=k_pe,
73 output=output_attn,
74 layer_name=layer_name,
75 output_scale=None,
76 output_block_scale=None,
77 kv_cache_dummy_dep=kv_cache_dummy_dep,
78 )
79 # MLA output is already 2D (T, N*V), no reshape needed
80 return self._quant_matcher(at1[1], scale)[0]
81
82 return _pattern_with_ln
83
84 def _pattern(q, kv_c_normed, k_pe, output_attn, scale, kv_cache_dummy_dep):
85 at1 = auto_functionalized(
86 MLA_ATTN_OP,
87 q=q,
88 kv_c_normed=kv_c_normed,
89 k_pe=k_pe,
90 output=output_attn,
91 layer_name=_ln,
92 output_scale=None,
93 output_block_scale=None,
94 kv_cache_dummy_dep=kv_cache_dummy_dep,
95 )
96 # MLA output is already 2D (T, N*V), no reshape needed
97 return self._quant_matcher(at1[1], scale)[0]
98
99 return _pattern
100
101 @property
102 def replacement(self) -> Callable[..., torch.Tensor]:
103 _ln = _encode_layer_name(self._layer_name)
104
105 if _USE_LAYERNAME:
106
107 def _replacement_with_ln( # type: ignore[misc]
108 q,
109 kv_c_normed,
110 k_pe,
111 output_attn,
112 scale,
113 kv_cache_dummy_dep,
114 layer_name,
115 ):
116 # MLA output in quant_dtype
117 output_attn = torch.empty(
118 [q.shape[0], self._output_dim],
119 dtype=FP8_DTYPE,
120 device=q.device,
121 )
122 at1 = auto_functionalized(
123 MLA_ATTN_OP,
124 q=q,
125 kv_c_normed=kv_c_normed,
126 k_pe=k_pe,
127 output=output_attn,
128 layer_name=layer_name,
129 output_scale=scale,
130 output_block_scale=None,
131 kv_cache_dummy_dep=kv_cache_dummy_dep,
132 )
133 return at1[1]
134
135 return _replacement_with_ln
136
137 def _replacement(q, kv_c_normed, k_pe, output_attn, scale, kv_cache_dummy_dep):
138 # MLA output in quant_dtype
139 output_attn = torch.empty(
140 [q.shape[0], self._output_dim],
141 dtype=FP8_DTYPE,
142 device=q.device,
143 )
144 at1 = auto_functionalized(
145 MLA_ATTN_OP,
146 q=q,
147 kv_c_normed=kv_c_normed,
148 k_pe=k_pe,
149 output=output_attn,
150 layer_name=_ln,
151 output_scale=scale,
152 output_block_scale=None,
153 kv_cache_dummy_dep=kv_cache_dummy_dep,
154 )
155 return at1[1]
156
157 return _replacement
158
159 def get_inputs(self) -> list[torch.Tensor]:
160 inputs: list = [
161 self.empty(5, self._num_heads, self._qk_head_dim, dtype=self._dtype),
162 self.empty(5, self._kv_lora_rank, dtype=self._dtype),
163 self.empty(5, 1, self._qk_rope_head_dim, dtype=self._dtype),
164 self.empty(5, self._output_dim, dtype=self._dtype),
165 self.empty_fp32(1, 1),
166 self.empty(0, dtype=self._dtype),
167 ]
168 if _USE_LAYERNAME:
169 inputs.append(_encode_layer_name(self._layer_name))
170 return inputs
171
172
173 class MLAAttnNvfp4QuantPattern(
174 VllmPatternReplacement[..., tuple[torch.Tensor, torch.Tensor]]
175 ):
176 """Fusion for MLA Attention+Nvfp4Quant.
177
178 Matches the pattern: MLA attention -> NVFP4 quant, and replaces
179 it with MLA attention(output_scale=scale, output_block_scale=block_scale,
180 output=fp4_buffer).
181 """
182
183 def __init__(self, layer: MLAAttention, dtype: torch.dtype) -> None:
184 self._layer_name = layer.layer_name
185 self._num_heads = layer.num_heads
186 self._v_head_dim = layer.v_head_dim
187 self._kv_lora_rank = layer.kv_lora_rank
188 self._qk_rope_head_dim = layer.qk_rope_head_dim
189 self._qk_head_dim = layer.qk_nope_head_dim + layer.qk_rope_head_dim
190 self._output_dim = layer.num_heads * layer.v_head_dim
191 self._dtype = dtype
192 self._QUANT_OP = QUANT_OPS[kNvfp4Dynamic]
193
194 @property
195 def pattern(
196 self,
197 ) -> Callable[..., tuple[torch.Tensor, torch.Tensor]]:
198 _ln = _encode_layer_name(self._layer_name)
199
200 if _USE_LAYERNAME:
201
202 def _pattern_with_ln( # type: ignore[misc]
203 q,
204 kv_c_normed,
205 k_pe,
206 output_attn,
207 output_quant,
208 output_scale,
209 input_scale,
210 kv_cache_dummy_dep,
211 layer_name,
212 ):
213 at1 = auto_functionalized(
214 MLA_ATTN_OP,
215 q=q,
216 kv_c_normed=kv_c_normed,
217 k_pe=k_pe,
218 output=output_attn,
219 layer_name=layer_name,
220 output_scale=None,
221 output_block_scale=None,
222 kv_cache_dummy_dep=kv_cache_dummy_dep,
223 )
224 at2 = auto_functionalized(
225 self._QUANT_OP,
226 input=at1[1],
227 input_scale=input_scale,
228 is_sf_swizzled_layout=True,
229 output=output_quant,
230 output_scale=output_scale,
231 )
232 output_scale_view = torch.ops.aten.view.dtype(at2[2], FP8_DTYPE)
233 return at2[1], output_scale_view
234
235 return _pattern_with_ln
236
237 def _pattern(
238 q,
239 kv_c_normed,
240 k_pe,
241 output_attn,
242 output_quant,
243 output_scale,
244 input_scale,
245 kv_cache_dummy_dep,
246 ):
247 at1 = auto_functionalized(
248 MLA_ATTN_OP,
249 q=q,
250 kv_c_normed=kv_c_normed,
251 k_pe=k_pe,
252 output=output_attn,
253 layer_name=_ln,
254 output_scale=None,
255 output_block_scale=None,
256 kv_cache_dummy_dep=kv_cache_dummy_dep,
257 )
258 at2 = auto_functionalized(
259 self._QUANT_OP,
260 input=at1[1],
261 input_scale=input_scale,
262 is_sf_swizzled_layout=True,
263 output=output_quant,
264 output_scale=output_scale,
265 )
266 output_scale_view = torch.ops.aten.view.dtype(at2[2], FP8_DTYPE)
267 return at2[1], output_scale_view
268
269 return _pattern
270
271 @property
272 def replacement(
273 self,
274 ) -> Callable[..., tuple[torch.Tensor, torch.Tensor]]:
275 _ln = _encode_layer_name(self._layer_name)
276
277 if _USE_LAYERNAME:
278
279 def _replacement_with_ln( # type: ignore[misc]
280 q,
281 kv_c_normed,
282 k_pe,
283 output_attn,
284 _output_quant,
285 output_scale,
286 input_scale,
287 kv_cache_dummy_dep,
288 layer_name,
289 ):
290 # MLA output in quant_dtype (FP4 packed as uint8)
291 output_attn = torch.empty(
292 [q.shape[0], self._output_dim // 2],
293 dtype=FP4_DTYPE,
294 device=q.device,
295 )
296 output_scale_view = torch.ops.aten.view.dtype(output_scale, FP8_DTYPE)
297 at2 = auto_functionalized(
298 MLA_ATTN_OP,
299 q=q,
300 kv_c_normed=kv_c_normed,
301 k_pe=k_pe,
302 output=output_attn,
303 layer_name=layer_name,
304 output_scale=input_scale,
305 output_block_scale=output_scale_view,
306 kv_cache_dummy_dep=kv_cache_dummy_dep,
307 )
308 return at2[1], at2[2]
309
310 return _replacement_with_ln
311
312 def _replacement(
313 q,
314 kv_c_normed,
315 k_pe,
316 output_attn,
317 _output_quant,
318 output_scale,
319 input_scale,
320 kv_cache_dummy_dep,
321 ):
322 # MLA output in quant_dtype (FP4 packed as uint8)
323 output_attn = torch.empty(
324 [q.shape[0], self._output_dim // 2],
325 dtype=FP4_DTYPE,
326 device=q.device,
327 )
328 output_scale_view = torch.ops.aten.view.dtype(output_scale, FP8_DTYPE)
329 at2 = auto_functionalized(
330 MLA_ATTN_OP,
331 q=q,
332 kv_c_normed=kv_c_normed,
333 k_pe=k_pe,
334 output=output_attn,
335 layer_name=_ln,
336 output_scale=input_scale,
337 output_block_scale=output_scale_view,
338 kv_cache_dummy_dep=kv_cache_dummy_dep,
339 )
340 return at2[1], at2[2]
341
342 return _replacement
343
344 def get_inputs(self) -> list[torch.Tensor]:
345 inputs: list = [
346 self.empty(5, self._num_heads, self._qk_head_dim, dtype=self._dtype),
347 self.empty(5, self._kv_lora_rank, dtype=self._dtype),
348 self.empty(5, 1, self._qk_rope_head_dim, dtype=self._dtype),
349 self.empty(5, self._output_dim, dtype=self._dtype),
350 self.empty(5, self._output_dim // 2, dtype=FP4_DTYPE),
351 self.empty_i32(128, round_up(self._output_dim // 16, 4)),
352 self.empty_fp32(1, 1),
353 self.empty(0, dtype=self._dtype),
354 ]
355 if _USE_LAYERNAME:
356 inputs.append(_encode_layer_name(self._layer_name))
357 return inputs
358
359
360 class MLAAttnFp8GroupQuantPattern(
361 VllmPatternReplacement[..., tuple[torch.Tensor, torch.Tensor]]
362 ):
363 """Fusion for MLA Attention+Fp8GroupQuant (per-group dynamic FP8).
364
365 Matches the pattern: MLA attention -> per_token_group_fp8_quant, and
366 replaces it with MLA attention(output_block_scale=group_scale_buffer).
367 Used by models with block FP8 quantization (e.g. DeepSeek V3).
368 """
369
370 def __init__(
371 self,
372 layer: MLAAttention,
373 dtype: torch.dtype,
374 quant_key: QuantKey,
375 has_col_major_scales: bool,
376 is_e8m0: bool,
377 is_tma_aligned: bool,
378 ) -> None:
379 self._layer_name = layer.layer_name
380 self._num_heads = layer.num_heads
381 self._v_head_dim = layer.v_head_dim
382 self._kv_lora_rank = layer.kv_lora_rank
383 self._qk_rope_head_dim = layer.qk_rope_head_dim
384 self._qk_head_dim = layer.qk_nope_head_dim + layer.qk_rope_head_dim
385 self._output_dim = layer.num_heads * layer.v_head_dim
386 self._dtype = dtype
387 self._layer = layer
388 self._group_size = quant_key.scale.group_shape[1]
389 self._has_col_major_scales = has_col_major_scales
390 self._is_e8m0 = is_e8m0
391 self._is_tma_aligned = is_tma_aligned
392
393 self._quant_matcher = MatcherQuantFP8(
394 quant_key,
395 has_col_major_scales=has_col_major_scales,
396 is_e8m0=is_e8m0,
397 is_tma_aligned=is_tma_aligned,
398 )
399
400 @property
401 def pattern(
402 self,
403 ) -> Callable[..., tuple[torch.Tensor, torch.Tensor]]:
404 _ln = _encode_layer_name(self._layer_name)
405
406 if _USE_LAYERNAME:
407
408 def _pattern_with_ln( # type: ignore[misc]
409 q,
410 kv_c_normed,
411 k_pe,
412 output_attn,
413 kv_cache_dummy_dep,
414 scale,
415 layer_name,
416 ):
417 at1 = auto_functionalized(
418 MLA_ATTN_OP,
419 q=q,
420 kv_c_normed=kv_c_normed,
421 k_pe=k_pe,
422 output=output_attn,
423 layer_name=layer_name,
424 output_scale=None,
425 output_block_scale=None,
426 kv_cache_dummy_dep=kv_cache_dummy_dep,
427 )
428 attn_out = at1[1]
429 result = torch.empty(
430 attn_out.shape, device=attn_out.device, dtype=FP8_DTYPE
431 )
432 finfo = torch.finfo(FP8_DTYPE)
433 _, result, scale = auto_functionalized(
434 self._quant_matcher.QUANT_OP,
435 input=attn_out,
436 output_q=result,
437 output_s=scale,
438 group_size=self._group_size,
439 eps=1e-10,
440 fp8_min=finfo.min,
441 fp8_max=finfo.max,
442 scale_ue8m0=self._is_e8m0,
443 dummy_is_scale_transposed=self._has_col_major_scales,
444 dummy_is_tma_aligned=self._is_tma_aligned,
445 )
446 return result, scale
447
448 return _pattern_with_ln
449
450 def _pattern(
451 q: torch.Tensor,
452 kv_c_normed: torch.Tensor,
453 k_pe: torch.Tensor,
454 output_attn: torch.Tensor,
455 kv_cache_dummy_dep: torch.Tensor,
456 scale: torch.Tensor,
457 ) -> tuple[torch.Tensor, torch.Tensor]:
458 at1 = auto_functionalized(
459 MLA_ATTN_OP,
460 q=q,
461 kv_c_normed=kv_c_normed,
462 k_pe=k_pe,
463 output=output_attn,
464 layer_name=_ln,
465 output_scale=None,
466 output_block_scale=None,
467 kv_cache_dummy_dep=kv_cache_dummy_dep,
468 )
469 attn_out = at1[1]
470 result = torch.empty(
471 attn_out.shape, device=attn_out.device, dtype=FP8_DTYPE
472 )
473 finfo = torch.finfo(FP8_DTYPE)
474 _, result, scale = auto_functionalized(
475 self._quant_matcher.QUANT_OP,
476 input=attn_out,
477 output_q=result,
478 output_s=scale,
479 group_size=self._group_size,
480 eps=1e-10,
481 fp8_min=finfo.min,
482 fp8_max=finfo.max,
483 scale_ue8m0=self._is_e8m0,
484 dummy_is_scale_transposed=self._has_col_major_scales,
485 dummy_is_tma_aligned=self._is_tma_aligned,
486 )
487 return result, scale
488
489 return _pattern
490
491 @property
492 def replacement(
493 self,
494 ) -> Callable[..., tuple[torch.Tensor, torch.Tensor]]:
495 _ln = _encode_layer_name(self._layer_name)
496
497 if _USE_LAYERNAME:
498
499 def _replacement_with_ln( # type: ignore[misc]
500 q,
501 kv_c_normed,
502 k_pe,
503 output_attn,
504 kv_cache_dummy_dep,
505 scale,
506 layer_name,
507 ):
508 output_attn = torch.empty(
509 [q.shape[0], self._output_dim],
510 dtype=FP8_DTYPE,
511 device=q.device,
512 )
513 at1 = auto_functionalized(
514 MLA_ATTN_OP,
515 q=q,
516 kv_c_normed=kv_c_normed,
517 k_pe=k_pe,
518 output=output_attn,
519 layer_name=layer_name,
520 output_scale=None,
521 output_block_scale=scale,
522 kv_cache_dummy_dep=kv_cache_dummy_dep,
523 quant_group_size=self._group_size,
524 quant_scale_ue8m0=self._is_e8m0,
525 quant_col_major=self._has_col_major_scales,
526 quant_tma_aligned=self._is_tma_aligned,
527 )
528 return at1[1], at1[2]
529
530 return _replacement_with_ln
531
532 def _replacement(q, kv_c_normed, k_pe, output_attn, kv_cache_dummy_dep, scale):
533 output_attn = torch.empty(
534 [q.shape[0], self._output_dim],
535 dtype=FP8_DTYPE,
536 device=q.device,
537 )
538 at1 = auto_functionalized(
539 MLA_ATTN_OP,
540 q=q,
541 kv_c_normed=kv_c_normed,
542 k_pe=k_pe,
543 output=output_attn,
544 layer_name=_ln,
545 output_scale=None,
546 output_block_scale=scale,
547 kv_cache_dummy_dep=kv_cache_dummy_dep,
548 quant_group_size=self._group_size,
549 quant_scale_ue8m0=self._is_e8m0,
550 quant_col_major=self._has_col_major_scales,
551 quant_tma_aligned=self._is_tma_aligned,
552 )
553 return at1[1], at1[2]
554
555 return _replacement
556
557 def get_inputs(self) -> list[torch.Tensor]:
558 inputs: list = [
559 self.empty(5, self._num_heads, self._qk_head_dim, dtype=self._dtype),
560 self.empty(5, self._kv_lora_rank, dtype=self._dtype),
561 self.empty(5, 1, self._qk_rope_head_dim, dtype=self._dtype),
562 self.empty(5, self._output_dim, dtype=self._dtype),
563 self.empty(0, dtype=self._dtype),
564 self._quant_matcher.empty_f32(1, 1),
565 ]
566 if _USE_LAYERNAME:
567 inputs.append(_encode_layer_name(self._layer_name))
568 return inputs
569
570
571 class MLAAttnQuantFusionPass(VllmFusionPatternMatcherPass):
572 """This pass fuses post-attention quantization onto MLA attention if supported.
573
574 It uses the pattern matcher and matches each MLA layer manually, as strings
575 cannot be wildcarded. This also lets us check support on attention layers
576 upon registration instead of during pattern matching.
577 """
578
579 def __init__(self, config: VllmConfig) -> None:
580 super().__init__(config, "mla_attn_quant_fusion")
581
582 dtype = config.model_config.dtype
583 layers = list(get_layers_from_vllm_config(config, MLAAttention).values())
584
585 if len(layers) == 0:
586 logger.warning(
587 "MLA attention + quant fusion is enabled, but no MLA "
588 "attention layers were found in "
589 "CompilationConfig.static_forward_context "
590 "so no fusion patterns were registered."
591 )
592
593 # When _USE_LAYERNAME is enabled, layer_name is a wildcard so all
594 # layers produce the same pattern — register once then break.
595 for layer in layers:
596 if layer.impl.fused_output_quant_supported(kFp8StaticTensorSym):
597 self.register(MLAAttnFp8StaticQuantPattern(layer, dtype))
598 if _USE_LAYERNAME:
599 break
600
601 if current_platform.is_cuda() and hasattr(torch.ops._C, "scaled_fp4_quant"):
602 for layer in layers:
603 if layer.impl.fused_output_quant_supported(kNvfp4Dynamic):
604 self.register(MLAAttnNvfp4QuantPattern(layer, dtype))
605 if _USE_LAYERNAME:
606 break
607
608 # Per-group FP8 (block quant) — register all flag combinations.
609 if current_platform.is_cuda():
610 for quant_key in [kFp8Dynamic128Sym, kFp8Dynamic64Sym]:
611 for col_major in [True, False]:
612 for is_e8m0 in [True, False]:
613 for tma_aligned in [False, True]:
614 for layer in layers:
615 if layer.impl.fused_output_quant_supported(quant_key):
616 self.register(
617 MLAAttnFp8GroupQuantPattern(
618 layer,
619 dtype,
620 quant_key,
621 col_major,
622 is_e8m0,
623 tma_aligned,
624 )
625 )
626 if _USE_LAYERNAME:
627 break
628
629 self.dump_patterns(config, self.pm_pass)