A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 11, 12:30 UTC
[Bugfix] Fix fusion for VL models (#30244)
Signed-off-by: ElizaWszola <ewszola@redhat.com>
ElizaWszola committed
Dec 14, 2025 at 14:22 UTC
994acec0cc9d6348268b5f371c66239fe75f928d
4 files changed
+143
-72
tests/compile/distributed/test_fusions_e2e.py
+78
@@ -27,6 +27,7 @@ is_blackwell = lambda: current_platform.is_device_capability_family(100)
27
class Matches(NamedTuple):
28
attention_fusion: int = 0
29
allreduce_fusion: int = 0
30
+ rms_quant_norm_fusion: int = 0
31
sequence_parallel: int = 0
32
async_tp: int = 0
33
@@ -40,6 +41,7 @@ class ModelBackendTestCase(NamedTuple):
41
42
MODELS_FP8: list[ModelBackendTestCase] = []
43
MODELS_FP4: list[ModelBackendTestCase] = []
44
+MODELS_GROUP_FP8: list[ModelBackendTestCase] = []
45
MODELS: list[ModelBackendTestCase] = [] # tp-only
46
47
if current_platform.is_cuda():
@@ -498,3 +500,79 @@ def run_model(compile_config: int | CompilationConfig, model: str, **model_kwarg
500
compilation_config.compile_ranges_split_points = (
501
llm.llm_engine.vllm_config.compilation_config.compile_ranges_split_points
502
)
503
+
504
+
505
+if current_platform.is_cuda():
506
+ MODELS_GROUP_FP8 = [
507
+ ModelBackendTestCase(
508
+ model_name="Qwen/Qwen3-30B-A3B-FP8",
509
+ model_kwargs=dict(max_model_len=1024, kv_cache_dtype="fp8"),
510
+ backend=AttentionBackendEnum.TRITON_ATTN,
511
+ matches=Matches(
512
+ rms_quant_norm_fusion=48,
513
+ ),
514
+ ),
515
+ ]
516
+
517
+CUSTOM_OPS_QUANT_RMS_NORM = ["+quant_fp8,+rms_norm"]
518
+
519
+
520
+@pytest.mark.parametrize(
521
+ "model_name, model_kwargs, backend, matches, custom_ops",
522
+ # Test rms norm+group quant_fp8 fusion
523
+ list[tuple[Any, ...]](flat_product(MODELS_GROUP_FP8, CUSTOM_OPS_QUANT_RMS_NORM)),
524
+)
525
+@pytest.mark.parametrize("inductor_graph_partition", [True, False])
526
+def test_rms_group_quant(
527
+ model_name: str,
528
+ model_kwargs: dict[str, Any],
529
+ backend: AttentionBackendEnum,
530
+ matches: Matches,
531
+ custom_ops: str,
532
+ inductor_graph_partition: bool,
533
+ caplog_mp_spawn,
534
+ monkeypatch,
535
+):
536
+ if inductor_graph_partition and not is_torch_equal_or_newer("2.9.0.dev"):
537
+ pytest.skip("Inductor graph partition requires torch>=2.9")
538
+
539
+ custom_ops_list = custom_ops.split(",") if custom_ops else []
540
+
541
+ if inductor_graph_partition:
542
+ mode = CUDAGraphMode.FULL_AND_PIECEWISE
543
+ splitting_ops: list[str] | None = None
544
+ else:
545
+ mode = CUDAGraphMode.FULL_DECODE_ONLY
546
+ splitting_ops = []
547
+
548
+ # Disable, compile cache to make sure custom passes run.
549
+ # Otherwise, we can't verify fusion happened through the logs.
550
+ monkeypatch.setenv("VLLM_DISABLE_COMPILE_CACHE", "1")
551
+
552
+ # To capture subprocess logs, we need to know whether spawn or fork is used.
553
+ # Force spawn as it is more general.
554
+ monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
555
+ monkeypatch.setenv("VLLM_ATTENTION_BACKEND", backend.name)
556
+
557
+ compilation_config = CompilationConfig(
558
+ # Testing properties
559
+ custom_ops=custom_ops_list,
560
+ use_inductor_graph_partition=inductor_graph_partition,
561
+ cudagraph_mode=mode,
562
+ splitting_ops=splitting_ops,
563
+ # Common
564
+ mode=CompilationMode.VLLM_COMPILE,
565
+ pass_config=PassConfig(eliminate_noops=True, enable_fusion=True),
566
+ # Inductor caches custom passes by default as well via uuid
567
+ inductor_compile_config={"force_disable_caches": True},
568
+ )
569
+
570
+ with caplog_mp_spawn(logging.DEBUG) as log_holder:
571
+ run_model(compilation_config, model_name, **model_kwargs)
572
+
573
+ log_matches = re.findall(
574
+ r"\[fusion.py:\d+] Replaced (\d+) patterns",
575
+ log_holder.text,
576
+ )
577
+ assert len(log_matches) == 1, log_holder.text
578
+ assert int(log_matches[0]) == matches.rms_quant_norm_fusion
vllm/compilation/fusion.py
+52
-48
@@ -23,17 +23,14 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
23
kNvfp4Quant,
24
kStaticTensorScale,
25
)
26
-from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
27
- cutlass_block_fp8_supported,
28
-)
26
from vllm.platforms import current_platform
30
-from vllm.utils.deep_gemm import (
31
- is_deep_gemm_e8m0_used,
32
- should_use_deepgemm_for_fp8_linear_for_nk,
33
-)
27
28
from .inductor_pass import enable_fake_mode
36
-from .matcher_utils import MatcherFusedAddRMSNorm, MatcherQuantFP8, MatcherRMSNorm
29
+from .matcher_utils import (
30
+ MatcherFusedAddRMSNorm,
31
+ MatcherQuantFP8,
32
+ MatcherRMSNorm,
33
+)
34
from .vllm_inductor_pass import VllmInductorPass, VllmPatternMatcherPass
35
36
logger = init_logger(__name__)
@@ -118,21 +115,18 @@ FUSED_OPS: dict[FusedRMSQuantKey, OpOverload] = {
115
116
117
class RMSNormQuantPattern:
121
- def __init__(self, epsilon: float, key: FusedRMSQuantKey):
118
+ def __init__(
119
+ self,
120
+ epsilon: float,
121
+ key: FusedRMSQuantKey,
122
+ has_col_major_scales: bool = False,
123
+ is_e8m0: bool = False,
124
+ ):
125
self.epsilon = epsilon
126
self.quant_dtype = key.quant.dtype
127
config = get_current_vllm_config()
128
self.model_dtype = config.model_config.dtype if config.model_config else None
129
127
- # groupwise FP8 linear uses col major scales if deepgemm and cutlass
128
- using_deepgemm = should_use_deepgemm_for_fp8_linear_for_nk(
129
- self.model_dtype,
130
- config.model_config.hf_config.intermediate_size,
131
- config.model_config.hf_config.hidden_size,
132
- )
133
- use_col_major_scales = using_deepgemm or cutlass_block_fp8_supported()
134
- use_e8m0 = is_deep_gemm_e8m0_used() if using_deepgemm else False
135
-
130
assert key in FUSED_OPS, f"unsupported fused rmsnorm+quant op for {key}"
131
self.FUSED_OP = FUSED_OPS[key]
132
@@ -142,7 +136,7 @@ class RMSNormQuantPattern:
136
else MatcherFusedAddRMSNorm(epsilon)
137
)
138
self.quant_matcher = MatcherQuantFP8(
145
- key.quant, use_col_major_scales=use_col_major_scales, use_e8m0=use_e8m0
139
+ key.quant, has_col_major_scales=has_col_major_scales, is_e8m0=is_e8m0
140
)
141
142
@@ -260,6 +254,8 @@ class FusedAddRMSNormGroupQuantPattern(RMSNormQuantPattern):
254
quant_dtype: torch.dtype,
255
group_shape: GroupShape,
256
symmetric=True,
257
+ has_col_major_scales: bool = False,
258
+ is_e8m0: bool = False,
259
):
260
scale = ScaleDesc(torch.float32, False, group_shape)
261
key = FusedRMSQuantKey(
@@ -267,7 +263,11 @@ class FusedAddRMSNormGroupQuantPattern(RMSNormQuantPattern):
263
quant=QuantKey(dtype=quant_dtype, scale=scale, symmetric=symmetric),
264
)
265
self.group_shape = group_shape
270
- super().__init__(epsilon, key)
266
+ self.has_col_major_scales = has_col_major_scales
267
+ self.is_e8m0 = is_e8m0
268
+ super().__init__(
269
+ epsilon, key, has_col_major_scales=has_col_major_scales, is_e8m0=is_e8m0
270
+ )
271
272
def register(self, pm_pass: PatternMatcherPass):
273
def pattern(input: torch.Tensor, weight: torch.Tensor, residual: torch.Tensor):
@@ -283,9 +283,7 @@ class FusedAddRMSNormGroupQuantPattern(RMSNormQuantPattern):
283
input = input.to(dtype=self.model_dtype)
284
285
result = torch.empty_like(input, dtype=self.quant_dtype)
286
- scale = self.quant_matcher.make_scale(
287
- input, transposed=self.quant_matcher.use_col_major_scales
288
- )
286
+ scale = self.quant_matcher.make_scale(input, self.has_col_major_scales)
287
at = auto_functionalized(
288
self.FUSED_OP,
289
result=result,
@@ -296,7 +294,7 @@ class FusedAddRMSNormGroupQuantPattern(RMSNormQuantPattern):
294
scale_ub=None,
295
residual=residual,
296
group_size=self.group_shape[1],
299
- is_scale_transposed=self.quant_matcher.use_col_major_scales,
297
+ is_scale_transposed=self.has_col_major_scales,
298
)
299
300
# result, residual, scale
@@ -318,6 +316,8 @@ class RMSNormGroupQuantPattern(RMSNormQuantPattern):
316
quant_dtype: torch.dtype,
317
group_shape: GroupShape,
318
symmetric=True,
319
+ has_col_major_scales: bool = False,
320
+ is_e8m0: bool = False,
321
):
322
scale = ScaleDesc(torch.float32, False, group_shape)
323
key = FusedRMSQuantKey(
@@ -325,7 +325,9 @@ class RMSNormGroupQuantPattern(RMSNormQuantPattern):
325
quant=QuantKey(dtype=quant_dtype, scale=scale, symmetric=symmetric),
326
)
327
self.group_shape = group_shape
328
- super().__init__(epsilon, key)
328
+ super().__init__(
329
+ epsilon, key, has_col_major_scales=has_col_major_scales, is_e8m0=is_e8m0
330
+ )
331
332
def register(self, pm_pass: PatternMatcherPass):
333
def pattern(input: torch.Tensor, weight: torch.Tensor):
@@ -340,7 +342,7 @@ class RMSNormGroupQuantPattern(RMSNormQuantPattern):
342
343
result = torch.empty_like(input, dtype=self.quant_dtype)
344
scale = self.quant_matcher.make_scale(
343
- input, transposed=self.quant_matcher.use_col_major_scales
345
+ input, transposed=self.quant_matcher.has_col_major_scales
346
)
347
at = auto_functionalized(
348
self.FUSED_OP,
@@ -352,7 +354,7 @@ class RMSNormGroupQuantPattern(RMSNormQuantPattern):
354
scale_ub=None,
355
residual=None,
356
group_size=self.group_shape[1],
355
- is_scale_transposed=self.quant_matcher.use_col_major_scales,
357
+ is_scale_transposed=self.quant_matcher.has_col_major_scales,
358
)
359
360
# result, scale
@@ -489,27 +491,6 @@ class RMSNormQuantFusionPass(VllmPatternMatcherPass):
491
# Make sure fused add patterns are before simple rms norm,
492
# as the latter is a subset of the former in torch ops
493
for epsilon in [1e-5, 1e-6]:
492
- # Fuse fused_add_rms_norm + fp8 group quant
493
- # Only register group quant patterns on CUDA where the C++ op exists
494
- if current_platform.is_cuda():
495
- FusedAddRMSNormGroupQuantPattern(
496
- epsilon, FP8_DTYPE, group_shape=GroupShape(1, 128)
497
- ).register(self.patterns)
498
-
499
- # Fuse rms_norm + fp8 group quant
500
- RMSNormGroupQuantPattern(
501
- epsilon, FP8_DTYPE, group_shape=GroupShape(1, 128)
502
- ).register(self.patterns)
503
-
504
- FusedAddRMSNormGroupQuantPattern(
505
- epsilon, FP8_DTYPE, group_shape=GroupShape(1, 64)
506
- ).register(self.patterns)
507
-
508
- # Fuse rms_norm + fp8 group quant
509
- RMSNormGroupQuantPattern(
510
- epsilon, FP8_DTYPE, group_shape=GroupShape(1, 64)
511
- ).register(self.patterns)
512
-
494
# Fuse fused_add_rms_norm + static fp8 quant
495
FusedAddRMSNormStaticQuantPattern(epsilon, FP8_DTYPE).register(
496
self.patterns
@@ -526,6 +507,29 @@ class RMSNormQuantFusionPass(VllmPatternMatcherPass):
507
# Fuse rms_norm + dynamic per-token fp8 quant
508
RMSNormDynamicQuantPattern(epsilon, FP8_DTYPE).register(self.patterns)
509
510
+ # Only register group quant patterns on CUDA where the C++ op exists
511
+ if current_platform.is_cuda():
512
+ for group_shape in [GroupShape(1, 128), GroupShape(1, 64)]:
513
+ for has_col_major_scales in [True, False]:
514
+ for is_e8m0 in [True, False]:
515
+ # Fuse fused_add_rms_norm + fp8 group quant
516
+ FusedAddRMSNormGroupQuantPattern(
517
+ epsilon,
518
+ FP8_DTYPE,
519
+ group_shape=group_shape,
520
+ has_col_major_scales=has_col_major_scales,
521
+ is_e8m0=is_e8m0,
522
+ ).register(self.patterns)
523
+
524
+ # Fuse rms_norm + fp8 group quant
525
+ RMSNormGroupQuantPattern(
526
+ epsilon,
527
+ FP8_DTYPE,
528
+ group_shape=group_shape,
529
+ has_col_major_scales=has_col_major_scales,
530
+ is_e8m0=is_e8m0,
531
+ ).register(self.patterns)
532
+
533
self.dump_patterns(config, self.patterns)
534
535
@VllmInductorPass.time_and_log
vllm/compilation/matcher_utils.py
+13
-7
@@ -234,24 +234,30 @@ class MatcherQuantFP8(MatcherCustomOp):
234
self,
235
quant_key: QuantKey,
236
enabled: bool | None = None,
237
- use_col_major_scales: bool = False,
238
- use_e8m0: bool = False,
237
+ has_col_major_scales: bool = False,
238
+ is_e8m0: bool = False,
239
):
240
if enabled is None:
241
enabled = QuantFP8.enabled()
242
243
super().__init__(enabled)
244
self.quant_key = quant_key
245
- self.use_col_major_scales = use_col_major_scales
246
- self.use_e8m0 = use_e8m0
245
assert quant_key in QUANT_OPS, f"unsupported quantization scheme {quant_key}"
246
self.QUANT_OP = QUANT_OPS[quant_key]
247
248
+ self.has_col_major_scales = has_col_major_scales
249
+ self.is_e8m0 = is_e8m0
250
+
251
assert quant_key.dtype == current_platform.fp8_dtype(), (
252
"Only QuantFP8 supported by"
253
)
254
assert quant_key.scale2 is None
254
- self.quant_fp8 = QuantFP8(quant_key.scale.static, quant_key.scale.group_shape)
255
+ self.quant_fp8 = QuantFP8(
256
+ quant_key.scale.static,
257
+ quant_key.scale.group_shape,
258
+ column_major_scales=has_col_major_scales,
259
+ use_ue8m0=is_e8m0,
260
+ )
261
262
def forward_custom(
263
self,
@@ -264,7 +270,7 @@ class MatcherQuantFP8(MatcherCustomOp):
270
271
if self.quant_key.scale.group_shape.is_per_group():
272
assert scale is None
267
- scale = self.make_scale(input, transposed=self.use_col_major_scales)
273
+ scale = self.make_scale(input, transposed=self.has_col_major_scales)
274
275
finfo = torch.finfo(self.quant_key.dtype)
276
fp8_min = finfo.min
@@ -279,7 +285,7 @@ class MatcherQuantFP8(MatcherCustomOp):
285
eps=1e-10,
286
fp8_min=fp8_min,
287
fp8_max=fp8_max,
282
- scale_ue8m0=self.use_e8m0,
288
+ scale_ue8m0=self.is_e8m0,
289
)
290
return result, scale
291
vllm/utils/deep_gemm.py
-17
@@ -381,22 +381,6 @@ def should_use_deepgemm_for_fp8_linear(
381
)
382
383
384
-def should_use_deepgemm_for_fp8_linear_for_nk(
385
- output_dtype: torch.dtype,
386
- shape0: int,
387
- shape1: int,
388
- supports_deep_gemm: bool | None = None,
389
-):
390
- if supports_deep_gemm is None:
391
- supports_deep_gemm = is_deep_gemm_supported()
392
- return (
393
- supports_deep_gemm
394
- and output_dtype == torch.bfloat16
395
- and shape0 % 128 == 0
396
- and shape1 % 128 == 0
397
- )
398
-
399
-
384
__all__ = [
385
"calc_diff",
386
"DeepGemmQuantScaleFMT",
@@ -411,7 +395,6 @@ __all__ = [
395
"is_deep_gemm_supported",
396
"get_num_sms",
397
"should_use_deepgemm_for_fp8_linear",
414
- "should_use_deepgemm_for_fp8_linear_for_nk",
398
"get_col_major_tma_aligned_tensor",
399
"get_mk_alignment_for_contiguous_layout",
400
]