main
py 447 lines 15.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5 from collections.abc import Callable
6
7 import torch
8 from torch._higher_order_ops.auto_functionalize import auto_functionalized
9
10 from vllm._aiter_ops import rocm_aiter_ops
11 from vllm.config import VllmConfig, get_layers_from_vllm_config
12 from vllm.logger import init_logger
13 from vllm.model_executor.layers.attention import Attention
14 from vllm.model_executor.layers.quantization.utils.quant_utils import (
15 QuantKey,
16 kNvfp4Dynamic,
17 kStaticTensorScale,
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 ATTN_OP = torch.ops.vllm.unified_attention_with_output.default
33 RESHAPE_OP = torch.ops.aten.reshape.default
34
35
36 _FP8_QUANT_KEY = QuantKey(dtype=FP8_DTYPE, scale=kStaticTensorScale, symmetric=True)
37
38
39 class AttnFp8StaticQuantPattern(
40 VllmPatternReplacement[..., torch.Tensor | tuple[torch.Tensor, torch.Tensor]]
41 ):
42 """Fusion for Attention+Fp8StaticQuant.
43
44 Only triggers when the attention implementation returns True in
45 `fused_output_quant_supported()`. If the pattern is found, the
46 Fp8StaticQuant op will be removed from the graph, and its scale
47 will be passed into Attention op as the `output_scale` argument.
48 """
49
50 def __init__(self, layer: Attention, dtype: torch.dtype):
51 self._layer_name = layer.layer_name
52 self._num_heads = layer.num_heads
53 self._head_size = layer.head_size
54 self._dtype = dtype
55 self._quant_matcher = MatcherQuantFP8(_FP8_QUANT_KEY)
56
57 def _quantize(
58 self, value: torch.Tensor, scale: torch.Tensor
59 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
60 return self._quant_matcher(value, scale)[0]
61
62 def _output(
63 self, value: torch.Tensor, scale: torch.Tensor
64 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
65 return value
66
67 @property
68 def pattern(
69 self,
70 ) -> Callable[..., torch.Tensor | tuple[torch.Tensor, torch.Tensor]]:
71 # When _USE_LAYERNAME is enabled (torch >= 2.11), layer_name is
72 # passed as an explicit pattern input so the pattern matcher
73 # treats it as a wildcard matching hoisted LayerName placeholders.
74 # Otherwise it stays as a closure constant (original behavior).
75 _ln = _encode_layer_name(self._layer_name)
76
77 if _USE_LAYERNAME:
78
79 def _pattern_with_ln( # type: ignore[misc]
80 q, k, v, output_attn, scale, kv_cache_dummy_dep, layer_name
81 ):
82 at1 = auto_functionalized(
83 ATTN_OP,
84 query=q,
85 key=k,
86 value=v,
87 output=output_attn,
88 layer_name=layer_name,
89 output_scale=None,
90 output_block_scale=None,
91 kv_cache_dummy_dep=kv_cache_dummy_dep,
92 )
93 attn_out_view = RESHAPE_OP(
94 at1[1], [q.shape[0], self._num_heads * self._head_size]
95 )
96 return self._quantize(attn_out_view, scale)
97
98 return _pattern_with_ln
99
100 def _pattern(q, k, v, output_attn, scale, kv_cache_dummy_dep):
101 at1 = auto_functionalized(
102 ATTN_OP,
103 query=q,
104 key=k,
105 value=v,
106 output=output_attn,
107 layer_name=_ln,
108 output_scale=None,
109 output_block_scale=None,
110 kv_cache_dummy_dep=kv_cache_dummy_dep,
111 )
112 attn_out_view = RESHAPE_OP(
113 at1[1], [q.shape[0], self._num_heads * self._head_size]
114 )
115 return self._quantize(attn_out_view, scale)
116
117 return _pattern
118
119 @property
120 def replacement(
121 self,
122 ) -> Callable[..., torch.Tensor | tuple[torch.Tensor, torch.Tensor]]:
123 _ln = _encode_layer_name(self._layer_name)
124
125 if _USE_LAYERNAME:
126
127 def _replacement_with_ln( # type: ignore[misc]
128 q, k, v, output_attn, scale, kv_cache_dummy_dep, layer_name
129 ):
130 output_attn = torch.empty(
131 [q.shape[0], self._num_heads, self._head_size],
132 dtype=FP8_DTYPE,
133 device=q.device,
134 )
135 at1 = auto_functionalized(
136 ATTN_OP,
137 query=q,
138 key=k,
139 value=v,
140 output=output_attn,
141 layer_name=layer_name,
142 output_scale=scale,
143 output_block_scale=None,
144 kv_cache_dummy_dep=kv_cache_dummy_dep,
145 )
146 return self._output(
147 RESHAPE_OP(at1[1], [-1, self._num_heads * self._head_size]), scale
148 )
149
150 return _replacement_with_ln
151
152 def _replacement(q, k, v, output_attn, scale, kv_cache_dummy_dep):
153 output_attn = torch.empty(
154 [q.shape[0], self._num_heads, self._head_size],
155 dtype=FP8_DTYPE,
156 device=q.device,
157 )
158 at1 = auto_functionalized(
159 ATTN_OP,
160 query=q,
161 key=k,
162 value=v,
163 output=output_attn,
164 layer_name=_ln,
165 output_scale=scale,
166 output_block_scale=None,
167 kv_cache_dummy_dep=kv_cache_dummy_dep,
168 )
169 return self._output(
170 RESHAPE_OP(at1[1], [-1, self._num_heads * self._head_size]), scale
171 )
172
173 return _replacement
174
175 def get_inputs(self):
176 dtype = self._dtype
177 num_heads = self._num_heads
178 head_size = self._head_size
179 inputs: list = [
180 self.empty(5, num_heads, head_size, dtype=dtype), # q
181 self.empty(5, num_heads, head_size, dtype=dtype), # k
182 self.empty(5, num_heads, head_size, dtype=dtype), # v
183 self.empty(5, num_heads, head_size, dtype=dtype), # attn_output
184 self.empty_fp32(1, 1), # scale
185 self.empty(0, dtype=dtype), # kv_cache_dummy_dep
186 ]
187 if _USE_LAYERNAME:
188 inputs.append(_encode_layer_name(self._layer_name))
189 return inputs
190
191
192 class RocmAttnFp8StaticQuantPattern(AttnFp8StaticQuantPattern):
193 """Fuse AITER static quantization while preserving its scale output."""
194
195 def _quantize(
196 self, value: torch.Tensor, scale: torch.Tensor
197 ) -> tuple[torch.Tensor, torch.Tensor]:
198 out = torch.empty_like(value, dtype=FP8_DTYPE)
199 _, out, scale = auto_functionalized(
200 torch.ops.vllm.rocm_aiter_per_tensor_quant.default,
201 out=out,
202 x=value,
203 scale=scale,
204 is_dynamic=False,
205 )
206 # The shared AITER op declares scale mutable for its dynamic variant.
207 # Static quant preserves it; keep this graph output when replacing it.
208 return out, scale
209
210 def _output(
211 self, value: torch.Tensor, scale: torch.Tensor
212 ) -> tuple[torch.Tensor, torch.Tensor]:
213 return value, scale
214
215
216 class AttnNvfp4QuantPattern(
217 VllmPatternReplacement[..., tuple[torch.Tensor, torch.Tensor]]
218 ):
219 """Fusion for Attention+Nvfp4Quant.
220
221 Only triggers when the attention implementation returns True in
222 `fused_output_quant_supported()`. If the pattern is found, the
223 Nvfp4Quant op will be removed from the graph, and its scale
224 will be passed into Attention op as the `output_scale` argument.
225 """
226
227 def __init__(self, layer: Attention, dtype: torch.dtype):
228 self._layer_name = layer.layer_name
229 self._num_heads = layer.num_heads
230 self._head_size = layer.head_size
231 self._dtype = dtype
232 self._QUANT_OP = QUANT_OPS[kNvfp4Dynamic]
233
234 @property
235 def pattern(self) -> Callable[..., tuple[torch.Tensor, torch.Tensor]]:
236 _ln = _encode_layer_name(self._layer_name)
237
238 if _USE_LAYERNAME:
239
240 def _pattern_with_ln( # type: ignore[misc]
241 q,
242 k,
243 v,
244 output_attn,
245 output_quant,
246 output_scale,
247 input_scale,
248 kv_cache_dummy_dep,
249 layer_name,
250 ):
251 at1 = auto_functionalized(
252 ATTN_OP,
253 query=q,
254 key=k,
255 value=v,
256 output=output_attn,
257 layer_name=layer_name,
258 output_scale=None,
259 output_block_scale=None,
260 kv_cache_dummy_dep=kv_cache_dummy_dep,
261 )
262 attn_out_view = RESHAPE_OP(
263 at1[1], [q.shape[0], self._num_heads * self._head_size]
264 )
265 at2 = auto_functionalized(
266 self._QUANT_OP,
267 input=attn_out_view,
268 input_scale=input_scale,
269 is_sf_swizzled_layout=True,
270 output=output_quant,
271 output_scale=output_scale,
272 )
273 return at2[1], torch.ops.aten.view.dtype(at2[2], FP8_DTYPE)
274
275 return _pattern_with_ln
276
277 def _pattern(
278 q,
279 k,
280 v,
281 output_attn,
282 output_quant,
283 output_scale,
284 input_scale,
285 kv_cache_dummy_dep,
286 ):
287 at1 = auto_functionalized(
288 ATTN_OP,
289 query=q,
290 key=k,
291 value=v,
292 output=output_attn,
293 layer_name=_ln,
294 output_scale=None,
295 output_block_scale=None,
296 kv_cache_dummy_dep=kv_cache_dummy_dep,
297 )
298 attn_out_view = RESHAPE_OP(
299 at1[1], [q.shape[0], self._num_heads * self._head_size]
300 )
301 at2 = auto_functionalized(
302 self._QUANT_OP,
303 input=attn_out_view,
304 input_scale=input_scale,
305 is_sf_swizzled_layout=True,
306 output=output_quant,
307 output_scale=output_scale,
308 )
309 return at2[1], torch.ops.aten.view.dtype(at2[2], FP8_DTYPE)
310
311 return _pattern
312
313 @property
314 def replacement(self) -> Callable[..., tuple[torch.Tensor, torch.Tensor]]:
315 _ln = _encode_layer_name(self._layer_name)
316
317 if _USE_LAYERNAME:
318
319 def _replacement_with_ln( # type: ignore[misc]
320 q,
321 k,
322 v,
323 output_attn,
324 _output_quant,
325 output_scale,
326 input_scale,
327 kv_cache_dummy_dep,
328 layer_name,
329 ):
330 output_attn = torch.empty(
331 [q.shape[0], self._num_heads, self._head_size // 2],
332 dtype=FP4_DTYPE,
333 device=q.device,
334 )
335 osv = torch.ops.aten.view.dtype(output_scale, FP8_DTYPE)
336 at2 = auto_functionalized(
337 ATTN_OP,
338 query=q,
339 key=k,
340 value=v,
341 output=output_attn,
342 layer_name=layer_name,
343 output_scale=input_scale,
344 output_block_scale=osv,
345 kv_cache_dummy_dep=kv_cache_dummy_dep,
346 )
347 return RESHAPE_OP(
348 at2[1], [-1, self._num_heads * self._head_size // 2]
349 ), at2[2]
350
351 return _replacement_with_ln
352
353 def _replacement(
354 q,
355 k,
356 v,
357 output_attn,
358 _output_quant,
359 output_scale,
360 input_scale,
361 kv_cache_dummy_dep,
362 ):
363 output_attn = torch.empty(
364 [q.shape[0], self._num_heads, self._head_size // 2],
365 dtype=FP4_DTYPE,
366 device=q.device,
367 )
368 osv = torch.ops.aten.view.dtype(output_scale, FP8_DTYPE)
369 at2 = auto_functionalized(
370 ATTN_OP,
371 query=q,
372 key=k,
373 value=v,
374 output=output_attn,
375 layer_name=_ln,
376 output_scale=input_scale,
377 output_block_scale=osv,
378 kv_cache_dummy_dep=kv_cache_dummy_dep,
379 )
380 return RESHAPE_OP(
381 at2[1], [-1, self._num_heads * self._head_size // 2]
382 ), at2[2]
383
384 return _replacement
385
386 def get_inputs(self):
387 dtype = self._dtype
388 num_heads = self._num_heads
389 head_size = self._head_size
390 inputs: list = [
391 self.empty_bf16(5, num_heads, head_size), # q
392 self.empty_bf16(5, num_heads, head_size), # k
393 self.empty_bf16(5, num_heads, head_size), # v
394 self.empty_bf16(5, num_heads, head_size), # output_attn
395 self.empty(5, num_heads * head_size // 2, dtype=FP4_DTYPE),
396 self.empty_i32(128, round_up(num_heads * head_size // 16, 4)),
397 self.empty_fp32(1, 1), # input_scale
398 self.empty(0, dtype=dtype), # kv_cache_dummy_dep
399 ]
400 if _USE_LAYERNAME:
401 inputs.append(_encode_layer_name(self._layer_name))
402 return inputs
403
404
405 class AttnQuantFusionPass(VllmFusionPatternMatcherPass):
406 """This pass fuses post-attention quantization onto attention if supported.
407
408 It uses the pattern matcher and matches each layer manually, as strings
409 cannot be wildcarded. This also lets us check support on attention layers
410 upon registration instead of during pattern matching.
411
412 Currently, only static fp8 quant is supported, but patterns could easily be
413 added for other quant schemes and dtypes. The bigger hurdle for wider
414 support are attention kernels, which need to support fusing output quant.
415 """
416
417 def __init__(self, config: VllmConfig) -> None:
418 super().__init__(config, "attn_quant_fusion")
419
420 dtype = config.model_config.dtype
421 layers = list(get_layers_from_vllm_config(config, Attention).values())
422
423 if len(layers) == 0:
424 logger.warning(
425 "Attention + quant fusion is enabled, but no attention layers "
426 "were found in CompilationConfig.static_forward_context "
427 "so no fusion patterns were registered."
428 )
429
430 # When _USE_LAYERNAME is enabled, layer_name is a wildcard so all
431 # layers produce the same pattern — register once then break.
432 for layer in layers:
433 if layer.impl.fused_output_quant_supported(_FP8_QUANT_KEY):
434 self.register(AttnFp8StaticQuantPattern(layer, dtype))
435 if current_platform.is_rocm() and rocm_aiter_ops.is_enabled():
436 self.register(RocmAttnFp8StaticQuantPattern(layer, dtype))
437 if _USE_LAYERNAME:
438 break
439
440 if current_platform.is_cuda() and hasattr(torch.ops._C, "scaled_fp4_quant"):
441 for layer in layers:
442 if layer.impl.fused_output_quant_supported(kNvfp4Dynamic):
443 self.register(AttnNvfp4QuantPattern(layer, dtype))
444 if _USE_LAYERNAME:
445 break
446
447 self.dump_patterns(config, self.pm_pass)