@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / bd7157a071
Fork 0

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 14, 05:30 UTC

[torch.compile] Enable attention and allreduce fusion without custom ops enabled (#24604)

Signed-off-by: Luka Govedič <lgovedic@redhat.com> Signed-off-by: Luka Govedič <ProExpertProg@users.noreply.github.com>

Luka Govedič committed Oct 17, 2025 at 10:10 UTC bd7157a071e28521979255e21526d887a87fc1a7
28 files changed +1499 -701
.buildkite/test-pipeline.yaml
+30 -12
@@ -416,8 +416,8 @@ steps:
416 - pytest -v -s compile/test_basic_correctness.py
417 - pytest -v -s compile/piecewise/
418
419 -- label: PyTorch Fullgraph Test # 20min
420 - timeout_in_minutes: 30
419 +- label: PyTorch Fullgraph Test # 22min
420 + timeout_in_minutes: 35
421 mirror_hardwares: [amdexperimental]
422 torch_nightly: true
423 source_file_dependencies:
@@ -425,6 +425,7 @@ steps:
425 - tests/compile
426 commands:
427 - pytest -v -s compile/test_full_graph.py
428 + - pytest -v -s compile/test_fusions_e2e.py
429
430 - label: Kernels Core Operation Test # 48min
431 timeout_in_minutes: 75
@@ -807,8 +808,8 @@ steps:
808 # Whisper needs spawn method to avoid deadlock
809 - VLLM_WORKER_MULTIPROC_METHOD=spawn python3 examples/offline_inference/audio_language.py --model-type whisper
810
810 -- label: Blackwell Test # 38 min
811 - timeout_in_minutes: 60
811 +- label: Blackwell Test # 21 min
812 + timeout_in_minutes: 30
813 working_dir: "/vllm-workspace/"
814 gpu: b200
815 # optional: true
@@ -821,8 +822,6 @@ steps:
822 - vllm/model_executor/layers/fused_moe/flashinfer_cutlass_prepare_finalize.py
823 - vllm/model_executor/layers/quantization/utils/flashinfer_utils.py
824 - vllm/v1/attention/backends/flashinfer.py
824 - - vllm/compilation/fusion.py
825 - - vllm/compilation/fusion_attn.py
825 commands:
826 - nvidia-smi
827 - python3 examples/offline_inference/basic/chat.py
@@ -839,15 +838,32 @@ steps:
838 - pytest -v -s tests/kernels/quantization/test_nvfp4_scaled_mm.py
839 - pytest -v -s tests/kernels/quantization/test_flashinfer_scaled_mm.py
840 - pytest -v -s tests/kernels/quantization/test_flashinfer_nvfp4_scaled_mm.py
841 + - pytest -v -s tests/kernels/quantization/test_nvfp4_qutlass.py
842 + - pytest -v -s tests/kernels/quantization/test_mxfp4_qutlass.py
843 - pytest -v -s tests/kernels/moe/test_nvfp4_moe.py
844 - pytest -v -s tests/kernels/moe/test_ocp_mx_moe.py
844 - # Fusion
845 - - pytest -v -s tests/compile/test_fusion_all_reduce.py
846 - - pytest -v -s tests/compile/test_fusion_attn.py::test_attention_quant_pattern
845 - pytest -v -s tests/kernels/moe/test_flashinfer.py
846 +
847 +- label: Blackwell Fusion Tests # 30 min
848 + timeout_in_minutes: 40
849 + working_dir: "/vllm-workspace/"
850 + gpu: b200
851 + source_file_dependencies:
852 + - csrc/quantization/fp4/
853 + - vllm/model_executor/layers/quantization/utils/flashinfer_utils.py
854 + - vllm/v1/attention/backends/flashinfer.py
855 + - vllm/compilation/
856 + # can affect pattern matching
857 + - vllm/model_executor/layers/layernorm.py
858 + - vllm/model_executor/layers/activation.py
859 + - vllm/model_executor/layers/quantization/input_quant_fp8.py
860 + commands:
861 + - nvidia-smi
862 + - pytest -v -s tests/compile/test_fusion_attn.py
863 - pytest -v -s tests/compile/test_silu_mul_quant_fusion.py
849 - - pytest -v -s tests/kernels/quantization/test_nvfp4_qutlass.py
850 - - pytest -v -s tests/kernels/quantization/test_mxfp4_qutlass.py
864 + # this runner has 2 GPUs available even though num_gpus=2 is not set
865 + - pytest -v -s tests/compile/test_fusion_all_reduce.py
866 + - pytest -v -s tests/compile/test_fusions_e2e.py
867
868 - label: Blackwell GPT-OSS Eval
869 timeout_in_minutes: 60
@@ -1100,7 +1116,7 @@ steps:
1116 - pytest -s -v test_lm_eval_correctness.py --config-list-file=configs/models-large.txt --tp-size=4
1117
1118 ##### H200 test #####
1103 -- label: Distrubted Tests (H200) # optional
1119 +- label: Distributed Tests (H200) # optional
1120 gpu: h200
1121 optional: true
1122 working_dir: "/vllm-workspace/"
@@ -1108,6 +1124,8 @@ steps:
1124 commands:
1125 - pytest -v -s tests/compile/test_async_tp.py
1126 - pytest -v -s tests/compile/test_sequence_parallelism.py
1127 + - pytest -v -s tests/compile/test_fusion_all_reduce.py
1128 + - pytest -v -s tests/compile/test_fusions_e2e.py::test_tp2_attn_quant_allreduce_rmsnorm
1129 - pytest -v -s tests/distributed/test_context_parallel.py
1130 - CUDA_VISIBLE_DEVICES=1,2 VLLM_ALL2ALL_BACKEND=deepep_high_throughput VLLM_USE_DEEP_GEMM=1 VLLM_LOGGING_LEVEL=DEBUG python3 examples/offline_inference/data_parallel.py --model Qwen/Qwen1.5-MoE-A2.7B --tp-size=1 --dp-size=2 --max-model-len 2048
1131
csrc/layernorm_kernels.cu
+2
@@ -392,6 +392,8 @@ void fused_add_rms_norm(torch::Tensor& input, // [..., hidden_size]
392 torch::Tensor& residual, // [..., hidden_size]
393 torch::Tensor& weight, // [hidden_size]
394 double epsilon) {
395 + TORCH_CHECK(weight.scalar_type() == input.scalar_type());
396 + TORCH_CHECK(input.scalar_type() == residual.scalar_type());
397 TORCH_CHECK(residual.is_contiguous());
398 TORCH_CHECK(weight.is_contiguous());
399 int hidden_size = input.size(-1);
csrc/layernorm_quant_kernels.cu
+2
@@ -229,6 +229,8 @@ void fused_add_rms_norm_static_fp8_quant(
229 double epsilon) {
230 TORCH_CHECK(out.is_contiguous());
231 TORCH_CHECK(residual.is_contiguous());
232 + TORCH_CHECK(residual.scalar_type() == input.scalar_type());
233 + TORCH_CHECK(weight.scalar_type() == input.scalar_type());
234 int hidden_size = input.size(-1);
235 int input_stride = input.stride(-2);
236 int num_tokens = input.numel() / hidden_size;
csrc/quantization/fused_kernels/fused_layernorm_dynamic_per_token_quant.cu
+4
@@ -145,7 +145,11 @@ void rms_norm_dynamic_per_token_quant(
145 if (scale_ub.has_value()) {
146 TORCH_CHECK(out.dtype() == kFp8Type);
147 }
148 + TORCH_CHECK(weight.dtype() == input.dtype());
149 TORCH_CHECK(scales.dtype() == torch::kFloat32);
150 + if (residual) {
151 + TORCH_CHECK(residual->scalar_type() == input.scalar_type());
152 + }
153
154 VLLM_DISPATCH_FLOATING_TYPES(
155 input.scalar_type(), "rms_norm_dynamic_per_token_quant_dispatch", [&] {
tests/compile/backend.py
+22 -3
@@ -3,16 +3,22 @@
3
4 import weakref
5 from collections.abc import Callable, Sequence
6 +from contextlib import nullcontext
7 from copy import deepcopy
8
9 +import depyf
10 from torch import fx
11 from torch._ops import OpOverload
12 +from torch.fx._utils import lazy_format_graph_code
13
14 from vllm.compilation.fx_utils import find_op_nodes
15 from vllm.compilation.inductor_pass import InductorPass
16 from vllm.compilation.pass_manager import with_pattern_match_debug
17 from vllm.compilation.vllm_inductor_pass import VllmInductorPass
18 from vllm.config import VllmConfig, get_current_vllm_config
19 +from vllm.logger import init_logger
20 +
21 +logger = init_logger("vllm.tests.compile.backend")
22
23
24 class LazyInitPass(InductorPass):
@@ -45,20 +51,32 @@ class TestBackend:
51
52 def __init__(self, *passes: InductorPass | Callable[[fx.Graph], None]):
53 self.custom_passes = list(passes)
48 - compile_config = get_current_vllm_config().compilation_config
49 - self.inductor_config = compile_config.inductor_compile_config
54 + vllm_config = get_current_vllm_config()
55 + compile_config = vllm_config.compilation_config
56 + # Deepcopy to allow multiple TestBackend instances to use the same VllmConfig
57 + self.inductor_config = deepcopy(compile_config.inductor_compile_config)
58 self.inductor_config["force_disable_caches"] = True
59 self.inductor_config["post_grad_custom_post_pass"] = self.post_pass
60
61 + if debug_dump_path := vllm_config.compile_debug_dump_path():
62 + logger.debug("Dumping depyf output to %s", debug_dump_path)
63 + self.debug_ctx = depyf.prepare_debug(debug_dump_path.as_posix())
64 + else:
65 + self.debug_ctx = nullcontext()
66 +
67 def __call__(self, graph: fx.GraphModule, example_inputs):
68 self.graph_pre_compile = deepcopy(graph)
69 from torch._inductor.compile_fx import compile_fx
70
57 - return compile_fx(graph, example_inputs, config_patches=self.inductor_config)
71 + with self.debug_ctx:
72 + return compile_fx(
73 + graph, example_inputs, config_patches=self.inductor_config
74 + )
75
76 @with_pattern_match_debug
77 def post_pass(self, graph: fx.Graph):
78 self.graph_pre_pass = deepcopy(graph)
79 + lazy_format_graph_code("graph_pre_pass", graph.owning_module)
80
81 VllmInductorPass.dump_prefix = 0
82 for pass_ in self.custom_passes:
@@ -68,6 +86,7 @@ class TestBackend:
86 VllmInductorPass.dump_prefix = None
87
88 self.graph_post_pass = deepcopy(graph)
89 + lazy_format_graph_code("graph_post_pass", graph.owning_module)
90 # assign by reference, will reflect the final state of the graph
91 self.final_graph = graph
92
tests/compile/test_full_graph.py
+59 -73
@@ -1,8 +1,8 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -import logging
4 import tempfile
5 +from pathlib import Path
6 from typing import Any
7
8 import pytest
@@ -10,8 +10,6 @@ import torch
10
11 from tests.quantization.utils import is_quant_method_supported
12 from vllm import LLM, SamplingParams
13 -from vllm.attention.backends.registry import _Backend
14 -from vllm.attention.selector import global_force_attn_backend_context_manager
13 from vllm.config import CompilationConfig, CompilationMode, CUDAGraphMode, PassConfig
14 from vllm.platforms import current_platform
15 from vllm.utils import is_torch_equal_or_newer
@@ -22,23 +20,24 @@ from ..utils import create_new_process_for_each_test
20 def models_list(*, all: bool = True, keywords: list[str] | None = None):
21 TEST_MODELS: list[tuple[str, dict[str, Any]]] = [
22 ("facebook/opt-125m", {}),
25 - (
26 - "nm-testing/tinyllama-oneshot-w8w8-test-static-shape-change",
27 - {
28 - "dtype": torch.float16,
29 - },
30 - ),
23 (
24 "neuralmagic/Llama-3.2-1B-Instruct-FP8-dynamic",
33 - {
34 - "dtype": torch.float16,
35 - },
25 + {"dtype": torch.float16},
26 ),
37 - ("neuralmagic/Llama-3.2-1B-Instruct-quantized.w8a8", {}),
27 ("meta-llama/Llama-3.2-1B-Instruct", {}),
28 ]
29
30 if all:
31 + TEST_MODELS.extend(
32 + [
33 + ("neuralmagic/Llama-3.2-1B-Instruct-quantized.w8a8", {}),
34 + (
35 + "nm-testing/tinyllama-oneshot-w8w8-test-static-shape-change",
36 + {"dtype": torch.float16},
37 + ),
38 + ]
39 + )
40 +
41 # TODO: figure out why this fails.
42 if False and is_quant_method_supported("gguf"): # noqa: SIM223
43 TEST_MODELS.append(
@@ -83,31 +82,38 @@ def models_list(*, all: bool = True, keywords: list[str] | None = None):
82 "compilation_mode",
83 [CompilationMode.DYNAMO_TRACE_ONCE, CompilationMode.VLLM_COMPILE],
84 )
86 -@pytest.mark.parametrize("model_info", models_list(all=True))
85 +@pytest.mark.parametrize("model, model_kwargs", models_list(all=True))
86 @create_new_process_for_each_test()
87 def test_full_graph(
88 monkeypatch: pytest.MonkeyPatch,
90 - model_info: tuple[str, dict[str, Any]],
89 + model: str,
90 + model_kwargs: dict[str, Any],
91 compilation_mode: int,
92 ):
93 - model, model_kwargs = model_info
93 + if (
94 + "w8a8" in model
95 + or "w8w8" in model
96 + and current_platform.has_device_capability((10, 0))
97 + ):
98 + # int8 removed on Blackwell:
99 + pytest.skip("int8 support removed on Blackwell")
100
101 with monkeypatch.context():
102 print(f"MODEL={model}")
103
98 - run_model(compilation_mode, model, model_kwargs)
104 + run_model(compilation_mode, model, **model_kwargs)
105
106
107 # TODO(luka) add other supported compilation config scenarios here
108 @pytest.mark.parametrize(
103 - "compilation_config, model_info",
109 + "compilation_config, model, model_kwargs",
110 [
111 # additional compile sizes, only some of the models
112 (
113 CompilationConfig(mode=CompilationMode.VLLM_COMPILE, compile_sizes=[1, 2]),
108 - model,
114 + *model_info,
115 )
110 - for model in models_list(all=False)
116 + for model_info in models_list(all=False)
117 ]
118 + [
119 # RMSNorm + quant fusion, only 8-bit quant models
@@ -117,18 +123,19 @@ def test_full_graph(
123 custom_ops=["+rms_norm"],
124 pass_config=PassConfig(enable_fusion=True, enable_noop=True),
125 ),
120 - model,
126 + *model_info,
127 )
122 - for model in models_list(keywords=["FP8-dynamic", "quantized.w8a8"])
128 + for model_info in models_list(keywords=["FP8-dynamic", "quantized.w8a8"])
129 ]
130 + [
131 # Test depyf integration works
132 (
133 CompilationConfig(
134 mode=CompilationMode.VLLM_COMPILE,
129 - debug_dump_path=tempfile.gettempdir(),
135 + debug_dump_path=Path(tempfile.gettempdir()),
136 ),
131 - ("facebook/opt-125m", {}),
137 + "facebook/opt-125m",
138 + {},
139 ),
140 ]
141 + [
@@ -142,9 +149,9 @@ def test_full_graph(
149 cudagraph_mode=CUDAGraphMode.PIECEWISE,
150 compile_sizes=[1, 2],
151 ),
145 - model,
152 + *model_info,
153 )
147 - for model in models_list(all=False)
154 + for model_info in models_list(all=False)
155 if is_torch_equal_or_newer("2.9.0.dev")
156 ],
157 )
@@ -152,16 +159,24 @@ def test_full_graph(
159 @create_new_process_for_each_test()
160 def test_custom_compile_config(
161 compilation_config: CompilationConfig,
155 - model_info: tuple[str, dict[str, Any]],
162 + model: str,
163 + model_kwargs: dict[str, Any],
164 ):
165 + if (
166 + "w8a8" in model
167 + or "w8w8" in model
168 + and current_platform.has_device_capability((10, 0))
169 + ):
170 + # int8 removed on Blackwell:
171 + pytest.skip("int8 support removed on Blackwell")
172 +
173 if compilation_config.use_inductor_graph_partition and not is_torch_equal_or_newer(
174 "2.9.0.dev"
175 ):
176 pytest.skip("inductor graph partition is only available in PyTorch 2.9+")
177
162 - model, model_kwargs = model_info
178 print(f"MODEL={model}")
164 - run_model(compilation_config, model, model_kwargs)
179 + run_model(compilation_config, model, **model_kwargs)
180
181
182 @pytest.mark.parametrize(
@@ -176,50 +191,16 @@ def test_fp8_kv_scale_compile(compilation_mode: int):
191 "calculate_kv_scales": True,
192 "max_model_len": 512,
193 }
179 - run_model(compilation_mode, model, model_kwargs)
194 + run_model(compilation_mode, model, **model_kwargs)
195
196
182 -def test_inductor_graph_partition_attn_fusion(caplog_vllm):
183 - if not is_torch_equal_or_newer("2.9.0.dev"):
184 - pytest.skip("inductor graph partition is only available in PyTorch 2.9+")
185 -
186 - model = "nvidia/Llama-4-Scout-17B-16E-Instruct-FP8"
187 - compilation_config = CompilationConfig(
188 - mode=CompilationMode.VLLM_COMPILE,
189 - use_inductor_graph_partition=True,
190 - cudagraph_mode=CUDAGraphMode.PIECEWISE,
191 - custom_ops=["+quant_fp8"],
192 - pass_config=PassConfig(enable_attn_fusion=True, enable_noop=True),
197 +def run_model(compile_config: int | CompilationConfig, model: str, **model_kwargs):
198 + compilation_config = (
199 + compile_config
200 + if isinstance(compile_config, CompilationConfig)
201 + else CompilationConfig(level=compile_config)
202 )
194 - model_kwargs = {
195 - "kv_cache_dtype": "fp8",
196 - "max_model_len": 1024,
197 - }
198 - with (
199 - caplog_vllm.at_level(logging.DEBUG),
200 - global_force_attn_backend_context_manager(_Backend.FLASHINFER),
201 - ):
202 - run_model(compilation_config, model, model_kwargs)
203
204 - try:
205 - assert "Fused quantization onto 48 attention nodes" in caplog_vllm.text, (
206 - caplog_vllm.text
207 - )
208 - except AssertionError:
209 - # Note: this message is only triggered when the compilation goes
210 - # through the custom pass. Due to multiple layers of cache on
211 - # PyTorch side, the compilation of a graph may be cached such
212 - # that custom pass directly goes through cache. In this case,
213 - # we go through this branch and assert that the pass is not
214 - # triggered.
215 - assert "Fused quantization" not in caplog_vllm.text
216 -
217 -
218 -def run_model(
219 - compile_config: int | CompilationConfig,
220 - model: str,
221 - model_kwargs: dict[str, Any],
222 -):
204 prompts = [
205 "Hello, my name is",
206 "The president of the United States is",
@@ -227,12 +208,17 @@ def run_model(
208 "The future of AI is",
209 ]
210 sampling_params = SamplingParams(temperature=0)
211 + # Allow override from model_kwargs
212 + model_kwargs = {"tensor_parallel_size": 1, **model_kwargs}
213 + model_kwargs = {"disable_custom_all_reduce": True, **model_kwargs}
214 +
215 + # No cudagraphs by default
216 + if compilation_config.cudagraph_mode is None:
217 + compilation_config.cudagraph_mode = CUDAGraphMode.NONE
218 +
219 llm = LLM(
220 model=model,
232 - enforce_eager=True,
233 - tensor_parallel_size=1,
234 - disable_custom_all_reduce=True,
235 - compilation_config=compile_config,
221 + compilation_config=compilation_config,
222 **model_kwargs,
223 )
224 outputs = llm.generate(prompts, sampling_params)
tests/compile/test_functionalization.py
+61 -53
@@ -11,7 +11,13 @@ from vllm.compilation.fusion import RMSNormQuantFusionPass
11 from vllm.compilation.fx_utils import find_auto_fn, find_auto_fn_maybe, is_func
12 from vllm.compilation.noop_elimination import NoOpEliminationPass
13 from vllm.compilation.post_cleanup import PostCleanupPass
14 -from vllm.config import CompilationConfig, PassConfig, VllmConfig
14 +from vllm.config import (
15 + CompilationConfig,
16 + ModelConfig,
17 + PassConfig,
18 + VllmConfig,
19 + set_current_vllm_config,
20 +)
21 from vllm.model_executor.layers.activation import SiluAndMul
22 from vllm.model_executor.layers.layernorm import RMSNorm
23 from vllm.model_executor.layers.quantization.utils.quant_utils import GroupShape
@@ -48,8 +54,7 @@ class TestSiluMul(torch.nn.Module):
54 return y
55
56 def example_inputs(self, num_tokens=32, hidden_size=128):
51 - dtype = torch.float16 if TEST_FP8 else torch.float32
52 - return (torch.rand(num_tokens, hidden_size * 2, dtype=dtype),)
57 + return (torch.rand(num_tokens, hidden_size * 2),)
58
59 def ops_in_model(self, do_fusion):
60 if TEST_FP8 and do_fusion:
@@ -67,15 +72,11 @@ class TestFusedAddRMSNorm(torch.nn.Module):
72 self.hidden_size = hidden_size
73 self.intermediate_size = intermediate_size
74
70 - dtype = torch.float16 if TEST_FP8 else torch.float32
71 -
75 self.gate_proj = torch.nn.Parameter(
73 - torch.empty((intermediate_size, hidden_size), dtype=dtype)
76 + torch.empty((intermediate_size, hidden_size))
77 )
78 self.norm = RMSNorm(intermediate_size, 1e-05)
76 - self.norm.weight = torch.nn.Parameter(
77 - torch.ones(intermediate_size, dtype=dtype)
78 - )
79 + self.norm.weight = torch.nn.Parameter(torch.ones(intermediate_size))
80
81 torch.nn.init.normal_(self.gate_proj, std=0.02)
82
@@ -112,9 +113,8 @@ class TestFusedAddRMSNorm(torch.nn.Module):
113 return norm_output, residual_output
114
115 def example_inputs(self, batch_size=8, hidden_size=16, seq_len=16):
115 - dtype = torch.float16 if TEST_FP8 else torch.float32
116 - hidden_states = torch.randn((batch_size * seq_len, hidden_size), dtype=dtype)
117 - residual = torch.randn((batch_size * seq_len, hidden_size), dtype=dtype)
116 + hidden_states = torch.randn((batch_size * seq_len, hidden_size))
117 + residual = torch.randn((batch_size * seq_len, hidden_size))
118 return (hidden_states, residual)
119
120 def ops_in_model(self, do_fusion):
@@ -145,10 +145,9 @@ class TestRotaryEmbedding(torch.nn.Module):
145 return q_rotated, k_rotated
146
147 def example_inputs(self, num_tokens=32, head_dim=64):
148 - dtype = torch.float16
148 positions = torch.arange(num_tokens, dtype=torch.long)
150 - q = torch.randn(num_tokens, head_dim, dtype=dtype)
151 - k = torch.randn(num_tokens, head_dim, dtype=dtype)
149 + q = torch.randn(num_tokens, head_dim)
150 + k = torch.randn(num_tokens, head_dim)
151 return (positions, q, k)
152
153 def ops_in_model(self, do_fusion):
@@ -166,7 +165,7 @@ class TestRotaryEmbeddingSliceScatter(torch.nn.Module):
165 self.hidden_size = head_dim * num_heads
166
167 self.qkv_proj = torch.nn.Linear(
169 - self.hidden_size, self.hidden_size * 3, bias=False, dtype=torch.float16
168 + self.hidden_size, self.hidden_size * 3, bias=False
169 )
170
171 self.rotary_emb = get_rope(
@@ -190,10 +189,9 @@ class TestRotaryEmbeddingSliceScatter(torch.nn.Module):
189 return qkv_updated
190
191 def example_inputs(self, num_tokens=32, head_dim=64, num_heads=4):
193 - dtype = torch.float16
192 hidden_size = head_dim * num_heads
193 positions = torch.arange(num_tokens, dtype=torch.long)
196 - hidden_states = torch.randn(num_tokens, hidden_size, dtype=dtype)
194 + hidden_states = torch.randn(num_tokens, hidden_size)
195 return (positions, hidden_states)
196
197 def ops_in_model(self, do_fusion):
@@ -211,48 +209,58 @@ MODELS = [
209 ]
210
211
212 +@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
213 @pytest.mark.parametrize("model_class", MODELS)
214 @pytest.mark.parametrize("do_fusion", [True, False])
215 @pytest.mark.skipif(envs.VLLM_TARGET_DEVICE != "cuda", reason="Only test on CUDA")
217 -def test_fix_functionalization(model_class: torch.nn.Module, do_fusion: bool):
216 +def test_fix_functionalization(
217 + model_class: torch.nn.Module, do_fusion: bool, dtype: torch.dtype
218 +):
219 torch.set_default_device("cuda")
219 -
220 - vllm_config = VllmConfig()
221 - vllm_config.compilation_config = CompilationConfig(
222 - pass_config=PassConfig(enable_fusion=do_fusion, enable_noop=True)
220 + torch.set_default_dtype(dtype)
221 +
222 + vllm_config = VllmConfig(
223 + model_config=ModelConfig(dtype=dtype),
224 + compilation_config=CompilationConfig(
225 + custom_ops=["all"],
226 + pass_config=PassConfig(enable_fusion=do_fusion, enable_noop=True),
227 + ),
228 )
224 - noop_pass = NoOpEliminationPass(vllm_config)
225 - fusion_pass = RMSNormQuantFusionPass(vllm_config)
226 - cleanup_pass = PostCleanupPass(vllm_config)
227 - act_quant_fusion_pass = ActivationQuantFusionPass(vllm_config)
228 -
229 - passes = (
230 - [noop_pass, fusion_pass, act_quant_fusion_pass, cleanup_pass]
231 - if do_fusion
232 - else [noop_pass, cleanup_pass]
233 - )
234 - func_pass = FixFunctionalizationPass(vllm_config)
229
236 - backend_func = TestBackend(*passes, func_pass)
237 - backend_no_func = TestBackend(*passes)
230 + with set_current_vllm_config(vllm_config):
231 + assert RMSNorm.enabled()
232 + noop_pass = NoOpEliminationPass(vllm_config)
233 + fusion_pass = RMSNormQuantFusionPass(vllm_config)
234 + cleanup_pass = PostCleanupPass(vllm_config)
235 + act_quant_fusion_pass = ActivationQuantFusionPass(vllm_config)
236 +
237 + passes = (
238 + [noop_pass, fusion_pass, act_quant_fusion_pass, cleanup_pass]
239 + if do_fusion
240 + else [noop_pass, cleanup_pass]
241 + )
242 + func_pass = FixFunctionalizationPass(vllm_config)
243
239 - model = model_class()
240 - torch.compile(model, backend=backend_func)(*model.example_inputs())
241 - torch.compile(model, backend=backend_no_func)(*model.example_inputs())
244 + backend_func = TestBackend(*passes, func_pass)
245 + backend_no_func = TestBackend(*passes)
246
243 - # check if the functionalization pass is applied
244 - for op in model.ops_in_model(do_fusion):
245 - find_auto_fn(backend_no_func.graph_post_pass.nodes, op)
246 - assert find_auto_fn_maybe(backend_func.graph_post_pass.nodes, op) is None
247 + model = model_class()
248 + torch.compile(model, backend=backend_func)(*model.example_inputs())
249 + torch.compile(model, backend=backend_no_func)(*model.example_inputs())
250
248 - # make sure the ops were all de-functionalized
249 - found = dict()
250 - for node in backend_func.graph_post_pass.nodes:
251 + # check if the functionalization pass is applied
252 for op in model.ops_in_model(do_fusion):
252 - if is_func(node, op):
253 - found[op] = True
254 - for op in model.ops_not_in_model():
255 - if is_func(node, op):
256 - found[op] = True
257 - assert all(found[op] for op in model.ops_in_model(do_fusion))
258 - assert all(not found.get(op) for op in model.ops_not_in_model())
253 + find_auto_fn(backend_no_func.graph_post_pass.nodes, op)
254 + assert find_auto_fn_maybe(backend_func.graph_post_pass.nodes, op) is None
255 +
256 + # make sure the ops were all de-functionalized
257 + found = dict()
258 + for node in backend_func.graph_post_pass.nodes:
259 + for op in model.ops_in_model(do_fusion):
260 + if is_func(node, op):
261 + found[op] = True
262 + for op in model.ops_not_in_model():
263 + if is_func(node, op):
264 + found[op] = True
265 + assert all(found[op] for op in model.ops_in_model(do_fusion))
266 + assert all(not found.get(op) for op in model.ops_not_in_model())
tests/compile/test_fusion.py
+86 -35
@@ -5,15 +5,18 @@ import pytest
5 import torch
6
7 import vllm.plugins
8 -from vllm.compilation.fusion import (
9 - FUSED_OPS,
10 - QUANT_OPS,
11 - FusedRMSQuantKey,
12 - RMSNormQuantFusionPass,
13 -)
8 +from vllm.compilation.fusion import FUSED_OPS, FusedRMSQuantKey, RMSNormQuantFusionPass
9 +from vllm.compilation.fx_utils import find_op_nodes
10 +from vllm.compilation.matcher_utils import QUANT_OPS
11 from vllm.compilation.noop_elimination import NoOpEliminationPass
12 from vllm.compilation.post_cleanup import PostCleanupPass
16 -from vllm.config import CompilationConfig, CompilationMode, PassConfig, VllmConfig
13 +from vllm.config import (
14 + CompilationConfig,
15 + CompilationMode,
16 + ModelConfig,
17 + PassConfig,
18 + VllmConfig,
19 +)
20 from vllm.model_executor.layers.layernorm import RMSNorm
21 from vllm.model_executor.layers.quantization.utils.quant_utils import (
22 GroupShape,
@@ -32,6 +35,9 @@ from .backend import TestBackend
35
36 FP8_DTYPE = current_platform.fp8_dtype()
37
38 +RMS_OP = torch.ops._C.rms_norm.default
39 +RMS_ADD_OP = torch.ops._C.fused_add_rms_norm.default
40 +
41
42 class TestModel(torch.nn.Module):
43 def __init__(
@@ -45,18 +51,18 @@ class TestModel(torch.nn.Module):
51 ):
52 super().__init__(*args, **kwargs)
53 self.cuda_force_torch = cuda_force_torch
48 - self.norm = [RMSNorm(hidden_size, eps) for _ in range(3)]
49 - self.wscale = [torch.rand(1, dtype=torch.float32) for _ in range(2)]
54 + self.norm = [RMSNorm(hidden_size, eps) for _ in range(4)]
55 + self.wscale = [torch.rand(1, dtype=torch.float32) for _ in range(3)]
56 group_shape = GroupShape.PER_TENSOR if static else GroupShape.PER_TOKEN
57 quant_scale = ScaleDesc(torch.float32, static, group_shape)
52 - self.key = QuantKey(dtype=FP8_DTYPE, scale=quant_scale, symmetric=True)
58 + self.quant_key = QuantKey(dtype=FP8_DTYPE, scale=quant_scale, symmetric=True)
59 if static:
54 - self.scale = [torch.rand(1, dtype=torch.float32) for _ in range(2)]
60 + self.scale = [torch.rand(1, dtype=torch.float32) for _ in range(3)]
61 else:
56 - self.scale = [None for _ in range(2)]
62 + self.scale = [None for _ in range(3)]
63 self.w = [
64 torch.rand(hidden_size, hidden_size).to(dtype=FP8_DTYPE).t()
59 - for _ in range(2)
65 + for _ in range(3)
66 ]
67
68 with override_cutlass_fp8_supported(not cuda_force_torch):
@@ -65,8 +71,12 @@ class TestModel(torch.nn.Module):
71 act_quant_group_shape=group_shape,
72 )
73
74 + self.enable_rms_norm_custom_op = self.norm[0].enabled()
75 + self.enable_quant_fp8_custom_op = self.fp8_linear.quant_fp8.enabled()
76 +
77 def forward(self, x):
69 - resid = torch.sqrt(x)
78 + # avoid having graph input be an arg to a pattern directly
79 + x = resid = torch.relu(x)
80 y = self.norm[0](x)
81
82 x2 = self.fp8_linear.apply(
@@ -78,24 +88,44 @@ class TestModel(torch.nn.Module):
88 x3 = self.fp8_linear.apply(
89 y2, self.w[1], self.wscale[1], input_scale=self.scale[1]
90 )
91 +
92 y3, resid = self.norm[2](x3, resid) # use resid here
82 - return y3
93
84 - def ops_in_model_before(self):
85 - return [QUANT_OPS[self.key]]
94 + x4 = self.fp8_linear.apply(
95 + y3, self.w[2], self.wscale[2], input_scale=self.scale[2]
96 + )
97 +
98 + y4, resid = self.norm[3](x4, resid) # use resid here
99 + return y4
100
101 def ops_in_model_after(self):
102 return [
89 - FUSED_OPS[FusedRMSQuantKey(self.key, False)],
90 - FUSED_OPS[FusedRMSQuantKey(self.key, True)],
103 + FUSED_OPS[FusedRMSQuantKey(self.quant_key, True)],
104 + FUSED_OPS[FusedRMSQuantKey(self.quant_key, False)],
105 ]
106
107 + def ops_in_model_before(self):
108 + return (
109 + [QUANT_OPS[self.quant_key]]
110 + if self.enable_quant_fp8_custom_op
111 + else [torch.ops.aten.reciprocal]
112 + )
113 +
114 + def ops_in_model_before_partial(self):
115 + return (
116 + [RMS_OP, RMS_ADD_OP]
117 + if self.enable_rms_norm_custom_op
118 + else [torch.ops.aten.rsqrt]
119 + )
120 +
121
122 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
123 @pytest.mark.parametrize("hidden_size", [64])
124 @pytest.mark.parametrize("num_tokens", [257])
125 @pytest.mark.parametrize("eps", [1e-5, 1e-6])
126 @pytest.mark.parametrize("static", [True, False])
127 +@pytest.mark.parametrize("enable_rms_norm_custom_op", [True, False])
128 +@pytest.mark.parametrize("enable_quant_fp8_custom_op", [True, False])
129 # cuda_force_torch used to test torch code path on platforms that
130 # cutlass_fp8_supported() == True.
131 @pytest.mark.parametrize(
@@ -105,19 +135,32 @@ class TestModel(torch.nn.Module):
135 not current_platform.is_cuda_alike(), reason="Only test on CUDA and ROCm"
136 )
137 def test_fusion_rmsnorm_quant(
108 - dtype, hidden_size, num_tokens, eps, static, cuda_force_torch
138 + dtype,
139 + hidden_size,
140 + num_tokens,
141 + eps,
142 + static,
143 + enable_rms_norm_custom_op,
144 + enable_quant_fp8_custom_op,
145 + cuda_force_torch,
146 ):
147 torch.set_default_device("cuda")
148 torch.set_default_dtype(dtype)
149 torch.manual_seed(1)
150 maybe_create_device_identity() # needed for certain non-cutlass fp8 paths
151
152 + custom_ops = []
153 + if enable_rms_norm_custom_op:
154 + custom_ops.append("+rms_norm")
155 + if enable_quant_fp8_custom_op:
156 + custom_ops.append("+quant_fp8")
157 vllm_config = VllmConfig(
158 + model_config=ModelConfig(dtype=dtype),
159 compilation_config=CompilationConfig(
160 mode=CompilationMode.VLLM_COMPILE,
118 - custom_ops=["+rms_norm", "+quant_fp8"],
161 + custom_ops=custom_ops,
162 pass_config=PassConfig(enable_fusion=True, enable_noop=True),
120 - )
163 + ),
164 )
165 with vllm.config.set_current_vllm_config(vllm_config):
166 # Reshape pass is needed for the fusion pass to work
@@ -126,31 +169,39 @@ def test_fusion_rmsnorm_quant(
169 cleanup_pass = PostCleanupPass(vllm_config)
170
171 backend = TestBackend(noop_pass, fusion_pass, cleanup_pass)
172 + backend2 = TestBackend(noop_pass, cleanup_pass)
173 model = TestModel(hidden_size, eps, static, cuda_force_torch)
174
175 # First dimension dynamic
176 x = torch.rand(num_tokens, hidden_size)
177 torch._dynamo.mark_dynamic(x, 0)
178
135 - result = model(x)
179 + model_fused = torch.compile(model, backend=backend)
180 + result_fused = model_fused(x)
181
137 - model2 = torch.compile(model, backend=backend)
138 - result2 = model2(x)
182 + model_unfused = torch.compile(model, backend=backend2)
183 + result_unfused = model_unfused(x)
184
140 - # Higher tol for dynamic, even higher for bfloat16
141 - if static:
142 - ATOL, RTOL = (1e-3, 1e-3)
143 - elif dtype == torch.float16:
185 + if dtype == torch.float16:
186 ATOL, RTOL = (2e-3, 2e-3)
187 else:
188 ATOL, RTOL = (1e-2, 1e-2)
189
148 - torch.testing.assert_close(result, result2, atol=ATOL, rtol=RTOL)
190 + torch.testing.assert_close(result_fused, result_unfused, atol=ATOL, rtol=RTOL)
191
150 - assert fusion_pass.matched_count == 2
151 -
152 - # In pre-nodes, fp8 quant should be there and fused kernels should not
192 + assert fusion_pass.matched_count == 3
193 backend.check_before_ops(model.ops_in_model_before())
154 -
155 - # In post-nodes, fused kernels should be there and fp8 quant should not
194 + backend.check_before_ops(
195 + model.ops_in_model_before_partial(), fully_replaced=False
196 + )
197 backend.check_after_ops(model.ops_in_model_after())
198 +
199 + # If RMSNorm custom op is disabled (native/torch impl used),
200 + # there's a risk that the fused add doesn't get included in the
201 + # replacement and only the rms part gets fused with quant.
202 + # Hence, we check only 2 add nodes are left (final fused rmsnorm add).
203 + if not enable_rms_norm_custom_op:
204 + n_add_nodes = lambda g: sum(1 for _ in find_op_nodes(torch.ops.aten.add, g))
205 + # 7 = 1 (RMS) + 3x2 (3xRMS_ADD, 2 each)
206 + assert n_add_nodes(backend.graph_pre_pass) == 7
207 + assert n_add_nodes(backend.graph_post_pass) == 2
tests/compile/test_fusion_all_reduce.py
+156 -80
@@ -6,6 +6,7 @@ import pytest
6 import torch
7
8 import vllm.envs as envs
9 +from vllm._custom_ops import cutlass_scaled_fp4_mm, scaled_fp4_quant
10 from vllm.compilation.collective_fusion import AllReduceFusionPass
11 from vllm.compilation.fix_functionalization import FixFunctionalizationPass
12 from vllm.compilation.noop_elimination import NoOpEliminationPass
@@ -17,6 +18,7 @@ from vllm.config import (
18 ModelConfig,
19 PassConfig,
20 VllmConfig,
21 + set_current_vllm_config,
22 )
23 from vllm.distributed import tensor_model_parallel_all_reduce
24 from vllm.distributed.parallel_state import (
@@ -25,8 +27,8 @@ from vllm.distributed.parallel_state import (
27 )
28 from vllm.model_executor.layers.layernorm import RMSNorm
29 from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
30 + Fp8LinearOp,
31 GroupShape,
29 - QuantFP8,
32 )
33 from vllm.platforms import current_platform
34 from vllm.utils import update_environment_variables
@@ -40,33 +42,30 @@ class TestAllReduceRMSNormModel(torch.nn.Module):
42 super().__init__()
43 self.hidden_size = hidden_size
44 self.eps = eps
43 - self.norm = RMSNorm(hidden_size, eps)
45 + self.norm = [RMSNorm(hidden_size, eps) for i in range(4)]
46 + self.w = [torch.rand(hidden_size, hidden_size) for _ in range(3)]
47
45 - def forward(self, hidden_states, residual):
46 - view = hidden_states.reshape(-1, self.hidden_size)
47 - all_reduce = tensor_model_parallel_all_reduce(view)
48 - norm = self.norm(all_reduce)
49 - return norm
48 + def forward(self, x):
49 + # avoid having graph input be an arg to a pattern directly
50 + z = torch.relu(x)
51 + x = resid = tensor_model_parallel_all_reduce(z)
52 + y = self.norm[0](x)
53
51 - def ops_in_model_before(self):
52 - return [torch.ops.vllm.all_reduce.default]
54 + z2 = torch.mm(y, self.w[0])
55 + x2 = tensor_model_parallel_all_reduce(z2)
56
54 - def ops_in_model_after(self):
55 - return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
57 + y2, resid = self.norm[1](x2, resid)
58
59 + z3 = torch.mm(y2, self.w[1])
60 + x3 = tensor_model_parallel_all_reduce(z3)
61
58 -class TestAllReduceFusedAddRMSNormModel(torch.nn.Module):
59 - def __init__(self, hidden_size=16, token_num=16, eps=1e-6):
60 - super().__init__()
61 - self.hidden_size = hidden_size
62 - self.eps = eps
63 - self.norm = RMSNorm(hidden_size, eps)
62 + y3, resid = self.norm[2](x3, resid)
63 +
64 + z4 = torch.mm(y3, self.w[2])
65 + x4 = tensor_model_parallel_all_reduce(z4)
66
65 - def forward(self, hidden_states, residual):
66 - view = hidden_states.reshape(-1, self.hidden_size)
67 - all_reduce = tensor_model_parallel_all_reduce(view)
68 - norm, _ = self.norm(all_reduce, residual)
69 - return norm
67 + y4, resid = self.norm[3](x4, resid)
68 + return y4
69
70 def ops_in_model_before(self):
71 return [torch.ops.vllm.all_reduce.default]
@@ -75,24 +74,53 @@ class TestAllReduceFusedAddRMSNormModel(torch.nn.Module):
74 return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
75
76
78 -class TestAllReduceFusedAddRMSNormStaticQuantFP8Model(torch.nn.Module):
77 +class TestAllReduceRMSNormStaticQuantFP8Model(torch.nn.Module):
78 def __init__(self, hidden_size=16, token_num=16, eps=1e-6):
79 super().__init__()
80 self.hidden_size = hidden_size
81 self.eps = eps
83 - self.norm = RMSNorm(hidden_size, eps)
84 - self.quant_fp8 = QuantFP8(static=True, group_shape=GroupShape.PER_TENSOR)
85 - self.scale = torch.rand(1, dtype=torch.float32)
86 - self.output = torch.empty((token_num, hidden_size), dtype=torch.float32)
87 -
88 - def forward(self, hidden_states, residual):
89 - view = hidden_states.reshape(-1, self.hidden_size)
90 - all_reduce = tensor_model_parallel_all_reduce(view)
91 - norm_output, residual_output = self.norm(all_reduce, residual)
92 - torch.ops._C.static_scaled_fp8_quant(
93 - self.output, norm_output.contiguous(), self.scale
82 + self.norm = [RMSNorm(hidden_size, eps) for i in range(4)]
83 + self.wscale = [torch.rand(1, dtype=torch.float32) for _ in range(3)]
84 + self.w = [
85 + torch.rand(hidden_size, hidden_size)
86 + .to(dtype=current_platform.fp8_dtype())
87 + .t()
88 + for _ in range(3)
89 + ]
90 +
91 + self.fp8_linear = Fp8LinearOp(
92 + act_quant_static=True,
93 + act_quant_group_shape=GroupShape.PER_TENSOR,
94 + )
95 +
96 + self.scale = [torch.rand(1, dtype=torch.float32) for _ in range(3)]
97 +
98 + def forward(self, hidden_states):
99 + # avoid having graph input be an arg to a pattern directly
100 + z = torch.relu(hidden_states)
101 + x = resid = tensor_model_parallel_all_reduce(z)
102 + y = self.norm[0](x)
103 +
104 + z2 = self.fp8_linear.apply(
105 + y, self.w[0], self.wscale[0], input_scale=self.scale[0]
106 + )
107 +
108 + x2 = tensor_model_parallel_all_reduce(z2)
109 + y2, resid = self.norm[1](x2, resid)
110 +
111 + z3 = self.fp8_linear.apply(
112 + y2, self.w[1], self.wscale[1], input_scale=self.scale[1]
113 )
95 - return self.output, residual_output
114 +
115 + x3 = tensor_model_parallel_all_reduce(z3)
116 + y3, resid = self.norm[2](x3, resid) # use resid here
117 +
118 + z4 = self.fp8_linear.apply(
119 + y3, self.w[2], self.wscale[2], input_scale=self.scale[2]
120 + )
121 + x4 = tensor_model_parallel_all_reduce(z4)
122 + y4, resid = self.norm[3](x4, resid) # use resid here
123 + return y4
124
125 def ops_in_model_after(self):
126 return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
@@ -100,7 +128,9 @@ class TestAllReduceFusedAddRMSNormStaticQuantFP8Model(torch.nn.Module):
128 def ops_in_model_before(self):
129 return [
130 torch.ops.vllm.all_reduce.default,
103 - torch.ops._C.static_scaled_fp8_quant.default,
131 + torch.ops._C.static_scaled_fp8_quant.default
132 + if self.fp8_linear.quant_fp8.enabled()
133 + else torch.ops.aten.reciprocal.default,
134 ]
135
136
@@ -109,25 +139,48 @@ class TestAllReduceFusedAddRMSNormStaticQuantFP4Model(torch.nn.Module):
139 super().__init__()
140 self.hidden_size = hidden_size
141 self.eps = eps
112 - self.norm = RMSNorm(hidden_size, eps)
113 - self.scale = torch.rand(1, dtype=torch.float32)
114 - self.output = torch.empty((token_num, hidden_size), dtype=torch.float32)
115 -
116 - round_up = lambda x, y: (x + y - 1) // y * y
117 - rounded_m = round_up(token_num, 128)
118 - scale_n = hidden_size // 16
119 - rounded_n = round_up(scale_n, 4)
120 - self.output_scale = torch.empty((rounded_m, rounded_n // 4), dtype=torch.int32)
121 -
122 - def forward(self, hidden_states, residual):
123 - view = hidden_states.reshape(-1, self.hidden_size)
124 - all_reduce = tensor_model_parallel_all_reduce(view)
125 - norm_output, residual_output = self.norm(all_reduce, residual)
126 - norm_output = norm_output.reshape(-1, norm_output.shape[-1])
127 - torch.ops._C.scaled_fp4_quant(
128 - self.output, norm_output, self.output_scale, self.scale
142 + self.norm = [RMSNorm(hidden_size, eps) for i in range(4)]
143 +
144 + self.w = [torch.rand(hidden_size, hidden_size) for _ in range(3)]
145 + self.agscale = [torch.rand(1, dtype=torch.float32) for _ in range(3)]
146 + wgscale = [torch.rand(1, dtype=torch.float32) for _ in range(3)]
147 + self.alpha = [1 / (w * a) for w, a in zip(wgscale, self.agscale)]
148 +
149 + wq_gen, wscale_gen = zip(
150 + *(scaled_fp4_quant(w, wg) for w, wg in zip(self.w, wgscale))
151 + )
152 + self.wq, self.wscale = list(wq_gen), list(wscale_gen)
153 + print(f"{self.wq=}, {self.wscale=}")
154 +
155 + def forward(self, hidden_states):
156 + # avoid having graph input be an arg to a pattern directly
157 + z = torch.relu(hidden_states)
158 + x = resid = tensor_model_parallel_all_reduce(z)
159 + y = self.norm[0](x)
160 +
161 + yq, y_scale = scaled_fp4_quant(y, self.agscale[0])
162 + z2 = cutlass_scaled_fp4_mm(
163 + yq, self.wq[0], y_scale, self.wscale[0], self.alpha[0], out_dtype=y.dtype
164 + )
165 +
166 + x2 = tensor_model_parallel_all_reduce(z2)
167 + y2, resid = self.norm[1](x2, resid)
168 +
169 + yq2, y_scale2 = scaled_fp4_quant(y2, self.agscale[1])
170 + z3 = cutlass_scaled_fp4_mm(
171 + yq2, self.wq[1], y_scale2, self.wscale[1], self.alpha[1], out_dtype=y2.dtype
172 )
130 - return self.output, residual_output, self.output_scale
173 +
174 + x3 = tensor_model_parallel_all_reduce(z3)
175 + y3, resid = self.norm[2](x3, resid) # use resid here
176 +
177 + yq3, y_scale3 = scaled_fp4_quant(y3, self.agscale[2])
178 + z4 = cutlass_scaled_fp4_mm(
179 + yq3, self.wq[2], y_scale3, self.wscale[2], self.alpha[2], out_dtype=y3.dtype
180 + )
181 + x4 = tensor_model_parallel_all_reduce(z4)
182 + y4, resid = self.norm[3](x4, resid) # use resid here
183 + return y4
184
185 def ops_in_model_after(self):
186 return [torch.ops.vllm.flashinfer_trtllm_fused_allreduce_norm.default]
@@ -141,19 +194,19 @@ class TestAllReduceFusedAddRMSNormStaticQuantFP4Model(torch.nn.Module):
194
195 @multi_gpu_test(num_gpus=2)
196 @pytest.mark.parametrize(
144 - "test_model",
197 + "test_model, enable_quant_fp8_custom_op",
198 [
146 - TestAllReduceRMSNormModel,
147 - TestAllReduceFusedAddRMSNormModel,
148 - TestAllReduceFusedAddRMSNormStaticQuantFP8Model,
149 - # TODO: Enable with torch==2.8.0
150 - # TestAllReduceFusedAddRMSNormStaticQuantFP4Model,
199 + (TestAllReduceRMSNormModel, False),
200 + (TestAllReduceRMSNormStaticQuantFP8Model, True),
201 + (TestAllReduceRMSNormStaticQuantFP8Model, False),
202 + (TestAllReduceFusedAddRMSNormStaticQuantFP4Model, False),
203 ],
204 )
205 @pytest.mark.parametrize("batch_size", [8])
206 @pytest.mark.parametrize("seq_len", [8])
155 -@pytest.mark.parametrize("hidden_size", [16])
207 +@pytest.mark.parametrize("hidden_size", [64])
208 @pytest.mark.parametrize("dtype", [torch.bfloat16])
209 +@pytest.mark.parametrize("enable_rms_norm_custom_op", [True, False])
210 @pytest.mark.skipif(envs.VLLM_TARGET_DEVICE not in ["cuda"], reason="Only test on CUDA")
211 @pytest.mark.skipif(
212 not find_spec("flashinfer")
@@ -167,6 +220,8 @@ def test_all_reduce_fusion_pass_replace(
220 seq_len: int,
221 hidden_size: int,
222 dtype: torch.dtype,
223 + enable_rms_norm_custom_op,
224 + enable_quant_fp8_custom_op,
225 ):
226 num_processes = 2
227 if (
@@ -181,7 +236,16 @@ def test_all_reduce_fusion_pass_replace(
236 def run_torch_spawn(fn, nprocs):
237 torch.multiprocessing.spawn(
238 fn,
184 - args=(num_processes, test_model, batch_size, seq_len, hidden_size, dtype),
239 + args=(
240 + num_processes,
241 + test_model,
242 + batch_size,
243 + seq_len,
244 + hidden_size,
245 + dtype,
246 + enable_rms_norm_custom_op,
247 + enable_quant_fp8_custom_op,
248 + ),
249 nprocs=nprocs,
250 )
251
@@ -196,6 +260,8 @@ def all_reduce_fusion_pass_on_test_model(
260 seq_len: int,
261 hidden_size: int,
262 dtype: torch.dtype,
263 + enable_rms_norm_custom_op,
264 + enable_quant_fp8_custom_op,
265 ):
266 current_platform.seed_everything(0)
267
@@ -217,15 +283,22 @@ def all_reduce_fusion_pass_on_test_model(
283 init_distributed_environment()
284 initialize_model_parallel(tensor_model_parallel_size=world_size)
285
286 + custom_ops = []
287 + if enable_rms_norm_custom_op:
288 + custom_ops.append("+rms_norm")
289 + if enable_quant_fp8_custom_op:
290 + custom_ops.append("+quant_fp8")
291 +
292 vllm_config = VllmConfig(
293 compilation_config=CompilationConfig(
222 - mode=CompilationMode.VLLM_COMPILE, custom_ops=["+rms_norm", "+quant_fp8"]
294 + mode=CompilationMode.VLLM_COMPILE, custom_ops=custom_ops
295 )
296 )
297 vllm_config.compilation_config.pass_config = PassConfig(
298 enable_fi_allreduce_fusion=True, enable_noop=True
299 )
300 vllm_config.device_config = DeviceConfig(device=torch.device("cuda"))
301 + vllm_config.parallel_config.rank = local_rank # Setup rank for debug path
302
303 # this is a fake model name to construct the model config
304 # in the vllm_config, it's not really used.
@@ -233,24 +306,27 @@ def all_reduce_fusion_pass_on_test_model(
306 vllm_config.model_config = ModelConfig(
307 model=model_name, trust_remote_code=True, dtype=dtype, seed=42
308 )
309 + with set_current_vllm_config(vllm_config):
310 + all_reduce_fusion_pass = AllReduceFusionPass(vllm_config)
311 + noop_pass = NoOpEliminationPass(vllm_config)
312 + func_pass = FixFunctionalizationPass(vllm_config)
313 + cleanup_pass = PostCleanupPass(vllm_config)
314 +
315 + backend = TestBackend(
316 + noop_pass, all_reduce_fusion_pass, func_pass, cleanup_pass
317 + )
318
237 - all_reduce_fusion_pass = AllReduceFusionPass(vllm_config)
238 - noop_pass = NoOpEliminationPass(vllm_config)
239 - func_pass = FixFunctionalizationPass(vllm_config)
240 - cleanup_pass = PostCleanupPass(vllm_config)
241 -
242 - backend = TestBackend(all_reduce_fusion_pass, noop_pass, func_pass, cleanup_pass)
243 -
244 - token_num = batch_size * seq_len
245 - model = test_model_cls(hidden_size, token_num)
319 + token_num = batch_size * seq_len
320 + model = test_model_cls(hidden_size, token_num)
321
247 - hidden_states = torch.randn((token_num, hidden_size), requires_grad=False)
248 - residual = torch.randn((token_num, hidden_size), requires_grad=False)
322 + hidden_states = torch.randn((token_num, hidden_size), requires_grad=False)
323
250 - compiled_model = torch.compile(model, backend=backend)
251 - compiled_model(hidden_states, residual)
324 + compiled_model = torch.compile(model, backend=backend)
325 + compiled_model(hidden_states)
326
253 - assert all_reduce_fusion_pass.matched_count == 1
254 - backend.check_before_ops(model.ops_in_model_before(), fully_replaced=False)
255 - backend.check_after_ops(model.ops_in_model_after())
256 - del all_reduce_fusion_pass
327 + assert all_reduce_fusion_pass.matched_count == 4, (
328 + f"{all_reduce_fusion_pass.matched_count=}"
329 + )
330 + backend.check_before_ops(model.ops_in_model_before(), fully_replaced=False)
331 + backend.check_after_ops(model.ops_in_model_after())
332 + del all_reduce_fusion_pass
tests/compile/test_fusion_attn.py
+58 -46
@@ -6,14 +6,15 @@ import pytest
6 import torch._dynamo
7
8 from tests.compile.backend import LazyInitPass, TestBackend
9 +from tests.utils import flat_product
10 from tests.v1.attention.utils import BatchSpec, create_common_attn_metadata
11 from vllm._custom_ops import cutlass_scaled_fp4_mm, scaled_fp4_quant
12 from vllm.attention import Attention, AttentionMetadata
13 from vllm.attention.backends.registry import _Backend
14 from vllm.attention.selector import global_force_attn_backend_context_manager
14 -from vllm.compilation.fusion import QUANT_OPS
15 from vllm.compilation.fusion_attn import ATTN_OP, AttnFusionPass
16 from vllm.compilation.fx_utils import find_op_nodes
17 +from vllm.compilation.matcher_utils import QUANT_OPS
18 from vllm.compilation.noop_elimination import NoOpEliminationPass
19 from vllm.compilation.post_cleanup import PostCleanupPass
20 from vllm.config import (
@@ -28,21 +29,18 @@ from vllm.config import (
29 )
30 from vllm.forward_context import get_forward_context, set_forward_context
31 from vllm.model_executor.layers.quantization.utils.quant_utils import (
32 + QuantKey,
33 kFp8StaticTensorSym,
34 kNvfp4Quant,
35 )
36 from vllm.model_executor.layers.quantization.utils.w8a8_utils import Fp8LinearOp
37 from vllm.platforms import current_platform
36 -from vllm.utils import is_torch_equal_or_newer
38 +from vllm.utils.flashinfer import has_flashinfer
39 from vllm.v1.kv_cache_interface import AttentionSpec
40
41 FP8_DTYPE = current_platform.fp8_dtype()
42 FP4_DTYPE = torch.uint8
43
42 -# globals needed for string-import custom Dynamo backend field
43 -backend: TestBackend | None = None
44 -backend_unfused: TestBackend | None = None
45 -
44
45 class AttentionQuantPatternModel(torch.nn.Module):
46 """Base model for AttentionQuantPattern fusion."""
@@ -104,6 +102,7 @@ class AttentionQuantPatternModel(torch.nn.Module):
102 num_blocks = batch_size * max_blocks
103 backend = self.attn.backend
104
105 + # TODO(luka) use get_kv_cache_stride_order
106 # Create dummy KV cache for the selected backend
107 if backend == _Backend.ROCM_ATTN:
108 # k/v as 1st dimention
@@ -241,26 +240,40 @@ class TestAttentionNvfp4QuantPatternModel(AttentionQuantPatternModel):
240 )
241
242
243 +MODELS_FP8: list[tuple[str, type]] = []
244 +MODELS_FP4: list[tuple[str, type]] = []
245 +HEADS: list[tuple[int, int]] = []
246 +SPLIT_ATTENTION: list[bool] = []
247 +BACKENDS_FP8: list[_Backend] = []
248 +BACKENDS_FP4: list[_Backend] = []
249 +
250 if current_platform.is_cuda():
245 - MODELS = [
251 + HEADS = [(64, 8), (40, 8)]
252 + MODELS_FP8 = [
253 (
254 "nvidia/Llama-4-Scout-17B-16E-Instruct-FP8",
255 TestAttentionFp8StaticQuantPatternModel,
249 - ),
256 + )
257 + ]
258 + MODELS_FP4 = [
259 (
260 "nvidia/Llama-4-Scout-17B-16E-Instruct-FP4",
261 TestAttentionNvfp4QuantPatternModel,
253 - ),
262 + )
263 ]
255 - HEADS = [(64, 8), (40, 8)]
264 + BACKENDS_FP8 = [_Backend.TRITON_ATTN, _Backend.FLASHINFER]
265 + BACKENDS_FP4 = [_Backend.FLASHINFER]
266 +
267 elif current_platform.is_rocm():
257 - MODELS = [
268 + HEADS = [(32, 8), (40, 8)]
269 + MODELS_FP8 = [
270 ("amd/Llama-3.1-8B-Instruct-FP8-KV", TestAttentionFp8StaticQuantPatternModel)
271 ]
260 - HEADS = [(32, 8), (40, 8)]
261 -else:
262 - MODELS = []
263 - HEADS = []
272 + BACKENDS = [
273 + _Backend.ROCM_AITER_UNIFIED_ATTN,
274 + _Backend.ROCM_ATTN,
275 + _Backend.TRITON_ATTN,
276 + ]
277
278
279 @pytest.mark.parametrize("num_qo_heads, num_kv_heads", HEADS)
@@ -269,46 +282,36 @@ else:
282 "batch_size", [7, 256, 533] if current_platform.is_cuda() else [8]
283 )
284 @pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
272 -@pytest.mark.parametrize("model_name, model_class", MODELS)
285 @pytest.mark.parametrize(
274 - "backend",
275 - [_Backend.FLASHINFER]
276 - if current_platform.is_cuda()
277 - else [_Backend.ROCM_AITER_UNIFIED_ATTN, _Backend.ROCM_ATTN, _Backend.TRITON_ATTN],
278 -)
279 -# TODO(boyuan): test inductor graph partition on rocm
280 -@pytest.mark.parametrize(
281 - "use_inductor_graph_partition",
282 - [False] if current_platform.is_rocm() else [False, True],
286 + "backend, model_name, model_class, custom_ops",
287 + # Test attention+quant_fp8 fusion with custom and torch impls of QuantFP8
288 + list(flat_product(BACKENDS_FP8, MODELS_FP8, ["+quant_fp8", "-quant_fp8"]))
289 + # quant_fp4 only has the custom impl
290 + + list(flat_product(BACKENDS_FP4, MODELS_FP4, [""])),
291 )
292 @pytest.mark.skipif(
293 not current_platform.is_cuda_alike(), reason="Only test ROCm or CUDA"
294 )
295 @pytest.mark.skipif(not current_platform.supports_fp8(), reason="Need FP8")
288 -@pytest.mark.skipif(
289 - current_platform.is_cuda() and not current_platform.is_device_capability((10, 0)),
290 - reason="On CUDA only test on SM100(Blackwell)",
291 -)
292 -@pytest.mark.skipif(
293 - not current_platform.is_cuda_alike(), reason="Only test ROCm or CUDA"
294 -)
296 def test_attention_quant_pattern(
297 num_qo_heads: int,
298 num_kv_heads: int,
299 head_size: int,
300 batch_size: int,
301 dtype: torch.dtype,
302 + custom_ops: str,
303 model_name: str,
304 model_class: type[AttentionQuantPatternModel],
305 backend: _Backend,
304 - use_inductor_graph_partition: bool,
306 dist_init,
306 - caplog_vllm,
307 ):
308 """Test AttentionStaticQuantPattern fusion pass"""
309 + if backend == _Backend.FLASHINFER and (
310 + not current_platform.is_device_capability((10, 0)) or not has_flashinfer()
311 + ):
312 + pytest.skip("FlashInfer attn fusion requires Blackwell and flashinfer")
313
310 - if use_inductor_graph_partition and not is_torch_equal_or_newer("2.9.0.dev"):
311 - pytest.skip("inductor graph partition is only available in PyTorch 2.9+")
314 + custom_ops_list = custom_ops.split(",") if custom_ops else []
315
316 device = torch.device("cuda:0")
317 torch.manual_seed(42)
@@ -322,8 +325,7 @@ def test_attention_quant_pattern(
325 scheduler_config=SchedulerConfig(max_num_seqs=1024),
326 compilation_config=CompilationConfig(
327 mode=CompilationMode.VLLM_COMPILE,
325 - custom_ops=["+quant_fp8"],
326 - use_inductor_graph_partition=use_inductor_graph_partition,
328 + custom_ops=custom_ops_list,
329 ),
330 cache_config=CacheConfig(cache_dtype="fp8"),
331 )
@@ -358,8 +360,9 @@ def test_attention_quant_pattern(
360 forward_ctx = get_forward_context()
361 forward_ctx.attn_metadata = model_unfused.build_attn_metadata(batch_size)
362
361 - # Run model directly without compilation and fusion
362 - result_unfused = model_unfused(q, k, v)
363 + # Run model directly without fusion
364 + # Still compile so query QuantFP8 has closer numerics
365 + result_unfused = torch.compile(model_unfused, fullgraph=True)(q, k, v)
366
367 # Run model with attn fusion enabled
368 vllm_config.compilation_config.pass_config = PassConfig(
@@ -414,16 +417,25 @@ def test_attention_quant_pattern(
417 )
418
419 # Check attn fusion support
417 - quant_key = model_class.quant_key
420 + quant_key: QuantKey = model_class.quant_key
421 attn_fusion_supported = [
422 layer.impl.fused_output_quant_supported(quant_key)
423 for key, layer in vllm_config.compilation_config.static_forward_context.items()
424 ]
422 - if any(attn_fusion_supported):
423 - # Check quantization ops in the graph before and after fusion
424 - # Note: fully_replaced=False because query quant ops remain in graph.
425 - # Only output quant ops are fused into attention.
426 - test_backend.check_before_ops([QUANT_OPS[quant_key]], fully_replaced=False)
425 + assert sum(attn_fusion_supported) == len(attn_fusion_supported), (
426 + "All layers should support attention fusion"
427 + )
428 +
429 + # Check quantization ops in the graph before and after fusion
430 + quant_op = (
431 + torch.ops.aten.reciprocal
432 + if "-quant_fp8" in custom_ops_list
433 + else QUANT_OPS[quant_key]
434 + )
435 +
436 + # Note: for fp8, fully_replaced=False because query quant ops remain in graph.
437 + # Only output quant ops are fused into attention.
438 + test_backend.check_before_ops([quant_op], fully_replaced=quant_key is kNvfp4Quant)
439
440 # access the underlying `AttnFusionPass` on the `LazyInitPass`
441 assert attn_pass.pass_.matched_count == sum(attn_fusion_supported)
tests/compile/test_fusions_e2e.py new
+305
@@ -0,0 +1,305 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from __future__ import annotations
5 +
6 +import itertools
7 +import logging
8 +from collections.abc import Iterable
9 +from typing import Any, NamedTuple
10 +
11 +import pytest
12 +import regex as re
13 +
14 +from tests.v1.attention.utils import _Backend
15 +from vllm import LLM, SamplingParams
16 +from vllm.config import CompilationConfig, CompilationMode, CUDAGraphMode, PassConfig
17 +from vllm.platforms import current_platform
18 +from vllm.utils import is_torch_equal_or_newer
19 +from vllm.utils.flashinfer import has_flashinfer
20 +
21 +from ..utils import flat_product, multi_gpu_test
22 +
23 +
24 +class ModelBackendTestCase(NamedTuple):
25 + model_name: str
26 + model_kwargs: dict[str, Any]
27 + backend: _Backend
28 + attention_fusions: int
29 + allreduce_fusions: int | None = None
30 +
31 +
32 +MODELS_FP8: list[ModelBackendTestCase] = []
33 +MODELS_FP4: list[ModelBackendTestCase] = []
34 +MODELS: list[ModelBackendTestCase] = [] # tp-only
35 +
36 +if current_platform.is_cuda():
37 + MODELS_FP8 = [
38 + ModelBackendTestCase(
39 + # Use smaller model for L40s in CI
40 + model_name="RedHatAI/Meta-Llama-3.1-8B-Instruct-FP8",
41 + model_kwargs=dict(max_model_len=1024),
42 + backend=_Backend.TRITON_ATTN,
43 + attention_fusions=32,
44 + allreduce_fusions=65,
45 + ),
46 + ModelBackendTestCase(
47 + model_name="nvidia/Llama-4-Scout-17B-16E-Instruct-FP8",
48 + model_kwargs=dict(max_model_len=1024, kv_cache_dtype="fp8"),
49 + backend=_Backend.FLASHINFER,
50 + attention_fusions=48,
51 + allreduce_fusions=96,
52 + ),
53 + ]
54 +
55 + MODELS_FP4 = [
56 + ModelBackendTestCase(
57 + model_name="nvidia/Llama-4-Scout-17B-16E-Instruct-FP4",
58 + model_kwargs=dict(max_model_len=1024, kv_cache_dtype="fp8"),
59 + backend=_Backend.FLASHINFER,
60 + attention_fusions=48,
61 + allreduce_fusions=96,
62 + ),
63 + ]
64 +
65 + # TP only
66 + MODELS = [
67 + ModelBackendTestCase(
68 + model_name="meta-llama/Llama-3.1-8B-Instruct",
69 + model_kwargs=dict(max_model_len=1024),
70 + backend=_Backend.TRITON_ATTN,
71 + attention_fusions=0,
72 + allreduce_fusions=65,
73 + ),
74 + ]
75 +
76 +elif current_platform.is_rocm():
77 + MODELS_FP8 = [
78 + ModelBackendTestCase(
79 + model_name="amd/Llama-3.1-8B-Instruct-FP8-KV",
80 + model_kwargs=dict(max_model_len=1024),
81 + backend=_Backend.TRITON_ATTN,
82 + attention_fusions=32,
83 + ),
84 + ModelBackendTestCase(
85 + model_name="amd/Llama-3.1-8B-Instruct-FP8-KV",
86 + model_kwargs=dict(max_model_len=1024),
87 + backend=_Backend.ROCM_ATTN,
88 + attention_fusions=32,
89 + ),
90 + ModelBackendTestCase(
91 + model_name="amd/Llama-3.1-8B-Instruct-FP8-KV",
92 + model_kwargs=dict(max_model_len=1024),
93 + backend=_Backend.ROCM_AITER_UNIFIED_ATTN,
94 + attention_fusions=32,
95 + ),
96 + ]
97 +
98 +# TODO(luka) test both in nightly
99 +CUSTOM_OPS_FP8 = ["-quant_fp8"] # , "+quant_fp8"]
100 +
101 +
102 +@pytest.mark.parametrize(
103 + "model_name, model_kwargs, backend, "
104 + "attention_fusions, allreduce_fusions, custom_ops",
105 + # Test attention+quant_fp8 fusion with custom and torch impls of QuantFP8
106 + list(flat_product(MODELS_FP8, CUSTOM_OPS_FP8))
107 + # quant_fp4 only has the custom impl
108 + + list(flat_product(MODELS_FP4, [""])),
109 +)
110 +@pytest.mark.parametrize("inductor_graph_partition", [True, False])
111 +def test_attn_quant(
112 + model_name: str,
113 + model_kwargs: dict[str, Any],
114 + backend: _Backend,
115 + attention_fusions: int,
116 + allreduce_fusions: int,
117 + custom_ops: str,
118 + inductor_graph_partition: bool,
119 + caplog_mp_spawn,
120 + monkeypatch,
121 +):
122 + if backend == _Backend.FLASHINFER and (
123 + not current_platform.is_device_capability((10, 0)) or not has_flashinfer()
124 + ):
125 + pytest.skip("FlashInfer attn fusion requires Blackwell and flashinfer")
126 + if inductor_graph_partition and not is_torch_equal_or_newer("2.9.0.dev"):
127 + pytest.skip("Inductor graph partition requires torch>=2.9")
128 +
129 + custom_ops_list = custom_ops.split(",") if custom_ops else []
130 +
131 + if inductor_graph_partition:
132 + mode = CUDAGraphMode.FULL_AND_PIECEWISE
133 + splitting_ops: list[str] | None = None
134 + else:
135 + mode = CUDAGraphMode.FULL_DECODE_ONLY
136 + splitting_ops = []
137 +
138 + # Disable, compile cache to make sure custom passes run.
139 + # Otherwise, we can't verify fusion happened through the logs.
140 + monkeypatch.setenv("VLLM_DISABLE_COMPILE_CACHE", "1")
141 +
142 + # To capture subprocess logs, we need to know whether spawn or fork is used.
143 + # Force spawn as it is more general.
144 + monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
145 + monkeypatch.setenv("VLLM_ATTENTION_BACKEND", backend.name)
146 +
147 + compilation_config = CompilationConfig(
148 + # Testing properties
149 + custom_ops=custom_ops_list,
150 + use_inductor_graph_partition=inductor_graph_partition,
151 + cudagraph_mode=mode,
152 + splitting_ops=splitting_ops,
153 + # Common
154 + level=CompilationMode.VLLM_COMPILE,
155 + pass_config=PassConfig(enable_attn_fusion=True, enable_noop=True),
156 + # Inductor caches custom passes by default as well via uuid
157 + inductor_compile_config={"force_disable_caches": True},
158 + )
159 +
160 + with caplog_mp_spawn(logging.DEBUG) as log_holder:
161 + run_model(compilation_config, model_name, **model_kwargs)
162 +
163 + matches = re.findall(
164 + r"fusion_attn.py:\d+] Fused quant onto (\d+) attention nodes",
165 + log_holder.text,
166 + )
167 + assert len(matches) == 1, log_holder.text
168 + assert int(matches[0]) == attention_fusions
169 +
170 +
171 +# TODO(luka) test both in nightly
172 +CUSTOM_OPS_RMS_NORM = ["-rms_norm"] # , "+rms_norm"]
173 +
174 +
175 +def custom_ops_product(*custom_ops_lists: list[str]) -> Iterable[str]:
176 + for op_list in itertools.product(*custom_ops_lists):
177 + yield ",".join(op_list)
178 +
179 +
180 +@multi_gpu_test(num_gpus=2)
181 +@pytest.mark.parametrize(
182 + "model_name, model_kwargs, backend, "
183 + "attention_fusions, allreduce_fusions, custom_ops",
184 + # Toggle RMSNorm and QuantFP8 for FP8 models
185 + list(
186 + flat_product(
187 + MODELS_FP8, custom_ops_product(CUSTOM_OPS_FP8, CUSTOM_OPS_RMS_NORM)
188 + )
189 + )
190 + # Toggle RMSNorm for FP4 models and unquant models
191 + + list(flat_product(MODELS_FP4 + MODELS, CUSTOM_OPS_RMS_NORM)),
192 +)
193 +@pytest.mark.parametrize("inductor_graph_partition", [True, False])
194 +@pytest.mark.skipif(
195 + not current_platform.is_cuda()
196 + or not has_flashinfer()
197 + or not current_platform.has_device_capability(90),
198 + reason="allreduce+rmsnorm fusion requires flashinfer",
199 +)
200 +def test_tp2_attn_quant_allreduce_rmsnorm(
201 + model_name: str,
202 + model_kwargs: dict,
203 + backend: _Backend,
204 + attention_fusions: int,
205 + allreduce_fusions: int,
206 + custom_ops: str,
207 + inductor_graph_partition: bool,
208 + caplog_mp_spawn,
209 + monkeypatch,
210 +):
211 + if inductor_graph_partition and not is_torch_equal_or_newer("2.9.0.dev"):
212 + pytest.skip("Inductor graph partition requires torch>=2.9")
213 +
214 + custom_ops_list = custom_ops.split(",") if custom_ops else []
215 +
216 + if inductor_graph_partition:
217 + mode = CUDAGraphMode.FULL_AND_PIECEWISE
218 + splitting_ops: list[str] | None = None
219 + else:
220 + mode = CUDAGraphMode.FULL_DECODE_ONLY
221 + splitting_ops = []
222 +
223 + # Disable, compile cache to make sure custom passes run.
224 + # Otherwise, we can't verify fusion happened through the logs.
225 + monkeypatch.setenv("VLLM_DISABLE_COMPILE_CACHE", "1")
226 +
227 + # To capture subprocess logs, we need to know whether spawn or fork is used.
228 + # Force spawn as it is more general.
229 + monkeypatch.setenv("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
230 + monkeypatch.setenv("VLLM_ATTENTION_BACKEND", backend.name)
231 +
232 + compilation_config = CompilationConfig(
233 + # Testing properties
234 + use_inductor_graph_partition=inductor_graph_partition,
235 + cudagraph_mode=mode,
236 + custom_ops=custom_ops_list,
237 + splitting_ops=splitting_ops,
238 + # Common
239 + level=CompilationMode.VLLM_COMPILE,
240 + pass_config=PassConfig(
241 + enable_attn_fusion=True,
242 + enable_noop=True,
243 + enable_fi_allreduce_fusion=True,
244 + ),
245 + # Inductor caches custom passes by default as well via uuid
246 + inductor_compile_config={"force_disable_caches": True},
247 + )
248 +
249 + with caplog_mp_spawn(logging.DEBUG) as log_holder:
250 + run_model(
251 + compilation_config, model_name, tensor_parallel_size=2, **model_kwargs
252 + )
253 + matches = re.findall(
254 + r"fusion_attn.py:\d+] Fused quant onto (\d+) attention nodes",
255 + log_holder.text,
256 + )
257 + assert len(matches) == 2, log_holder.text
258 +
259 + assert int(matches[0]) == attention_fusions
260 + assert int(matches[1]) == attention_fusions
261 +
262 + matches = re.findall(
263 + r"collective_fusion.py:\d+] Replaced (\d+) patterns",
264 + log_holder.text,
265 + )
266 + assert len(matches) == 2, log_holder.text
267 +
268 + assert int(matches[0]) == allreduce_fusions
269 + assert int(matches[1]) == allreduce_fusions
270 +
271 +
272 +def run_model(compile_config: int | CompilationConfig, model: str, **model_kwargs):
273 + compilation_config = (
274 + compile_config
275 + if isinstance(compile_config, CompilationConfig)
276 + else CompilationConfig(level=compile_config)
277 + )
278 +
279 + prompts = [
280 + "Hello, my name is",
281 + "The president of the United States is",
282 + "The capital of France is",
283 + "The future of AI is",
284 + ]
285 + sampling_params = SamplingParams(temperature=0)
286 + # Allow override from model_kwargs
287 + model_kwargs = {"tensor_parallel_size": 1, **model_kwargs}
288 + model_kwargs = {"disable_custom_all_reduce": True, **model_kwargs}
289 +
290 + # No cudagraphs by default
291 + if compilation_config.cudagraph_mode is None:
292 + compilation_config.cudagraph_mode = CUDAGraphMode.NONE
293 +
294 + llm = LLM(
295 + model=model,
296 + compilation_config=compilation_config,
297 + **model_kwargs,
298 + )
299 + outputs = llm.generate(prompts, sampling_params)
300 +
301 + # Print the outputs.
302 + for output in outputs:
303 + prompt = output.prompt
304 + generated_text = output.outputs[0].text
305 + print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
tests/compile/test_pass_manager.py
+3 -2
@@ -7,7 +7,7 @@ import torch
7
8 from vllm.compilation.inductor_pass import CallableInductorPass, InductorPass
9 from vllm.compilation.pass_manager import PostGradPassManager
10 -from vllm.config import VllmConfig
10 +from vllm.config import ModelConfig, VllmConfig
11
12
13 # dummy custom pass that doesn't inherit
@@ -42,7 +42,8 @@ class ProperPass(InductorPass):
42 ],
43 )
44 def test_pass_manager_uuid(callable):
45 - config = VllmConfig()
45 + # Some passes need dtype to be set
46 + config = VllmConfig(model_config=ModelConfig(dtype=torch.bfloat16))
47
48 pass_manager = PostGradPassManager()
49 pass_manager.configure(config)
tests/compile/test_sequence_parallelism.py
+60 -54
@@ -18,6 +18,8 @@ from vllm.config import (
18 ModelConfig,
19 PassConfig,
20 VllmConfig,
21 + get_current_vllm_config,
22 + set_current_vllm_config,
23 )
24 from vllm.distributed import tensor_model_parallel_all_reduce
25 from vllm.distributed.parallel_state import (
@@ -42,9 +44,7 @@ prompts = [
44
45
46 class TestModel(torch.nn.Module):
45 - def __init__(
46 - self, hidden_size=16, intermediate_size=32, vllm_config: VllmConfig = None
47 - ):
47 + def __init__(self, hidden_size=16, intermediate_size=32):
48 super().__init__()
49 self.hidden_size = hidden_size
50 self.intermediate_size = intermediate_size
@@ -95,13 +95,11 @@ class TestModel(torch.nn.Module):
95
96
97 class TestQuantModel(torch.nn.Module):
98 - def __init__(
99 - self, hidden_size=16, intermediate_size=32, vllm_config: VllmConfig = None
100 - ):
98 + def __init__(self, hidden_size=16, intermediate_size=32):
99 super().__init__()
100 self.hidden_size = hidden_size
101 self.intermediate_size = intermediate_size
104 - self.vllm_config = vllm_config
102 + self.vllm_config = get_current_vllm_config()
103 self.gate_proj = torch.nn.Parameter(
104 torch.empty((intermediate_size, hidden_size)), requires_grad=False
105 )
@@ -266,76 +264,84 @@ def sequence_parallelism_pass_on_test_model(
264 initialize_model_parallel(tensor_model_parallel_size=world_size)
265
266 # configure vllm config for SequenceParallelismPass
269 - vllm_config = VllmConfig()
270 - vllm_config.compilation_config = CompilationConfig(
267 + compilation_config = CompilationConfig(
268 pass_config=PassConfig(
269 enable_sequence_parallelism=True,
270 enable_fusion=enable_fusion,
271 enable_noop=True,
272 )
273 ) # NoOp needed for fusion
277 - vllm_config.device_config = DeviceConfig(device=torch.device("cuda"))
274 + device_config = DeviceConfig(device=torch.device("cuda"))
275
276 # this is a fake model name to construct the model config
277 # in the vllm_config, it's not really used.
278 model_name = "RedHatAI/Llama-3.2-1B-Instruct-FP8"
282 - vllm_config.model_config = ModelConfig(
279 + model_config = ModelConfig(
280 model=model_name, trust_remote_code=True, dtype=dtype, seed=42
281 )
282
286 - noop_pass = NoOpEliminationPass(vllm_config)
287 - sequence_parallelism_pass = SequenceParallelismPass(vllm_config)
288 - assert (
289 - sequence_parallelism_pass.compilation_config.splitting_ops
290 - == vllm_config.compilation_config.splitting_ops
283 + vllm_config = VllmConfig(
284 + model_config=model_config,
285 + device_config=device_config,
286 + compilation_config=compilation_config,
287 )
292 - assert (
293 - sequence_parallelism_pass.compilation_config.use_inductor_graph_partition
294 - == vllm_config.compilation_config.use_inductor_graph_partition
295 - )
296 - func_pass = FixFunctionalizationPass(vllm_config)
297 - cleanup_pass = PostCleanupPass(vllm_config)
298 -
299 - passes_for_backend: list[VllmInductorPass] = [noop_pass, sequence_parallelism_pass]
288
301 - if enable_fusion:
302 - fusion_pass = RMSNormQuantFusionPass(vllm_config)
303 - passes_for_backend.append(fusion_pass)
289 + with set_current_vllm_config(vllm_config):
290 + noop_pass = NoOpEliminationPass(vllm_config)
291 + sequence_parallelism_pass = SequenceParallelismPass(vllm_config)
292 + func_pass = FixFunctionalizationPass(vllm_config)
293 + cleanup_pass = PostCleanupPass(vllm_config)
294 + assert (
295 + sequence_parallelism_pass.compilation_config.splitting_ops
296 + == vllm_config.compilation_config.splitting_ops
297 + )
298 + assert (
299 + sequence_parallelism_pass.compilation_config.use_inductor_graph_partition
300 + == vllm_config.compilation_config.use_inductor_graph_partition
301 + )
302 + passes_for_backend: list[VllmInductorPass] = [
303 + noop_pass,
304 + sequence_parallelism_pass,
305 + ]
306
305 - passes_for_backend.append(cleanup_pass)
307 + if enable_fusion:
308 + fusion_pass = RMSNormQuantFusionPass(vllm_config)
309 + passes_for_backend.append(fusion_pass)
310
307 - backend_no_func = TestBackend(*passes_for_backend)
308 - backend_func = TestBackend(*passes_for_backend, func_pass)
311 + passes_for_backend.append(cleanup_pass)
312
310 - model = test_model_cls(hidden_size, hidden_size * 2, vllm_config=vllm_config)
313 + backend_no_func = TestBackend(*passes_for_backend)
314 + backend_func = TestBackend(*passes_for_backend, func_pass)
315
312 - hidden_states = torch.randn((batch_size * seq_len, hidden_size), dtype=dtype)
313 - residual = torch.randn((batch_size * seq_len, hidden_size), dtype=dtype)
316 + model = test_model_cls(hidden_size, hidden_size * 2)
317
315 - compiled_model_no_func = torch.compile(model, backend=backend_no_func)
316 - compiled_model_no_func(hidden_states, residual)
317 - compiled_model_func = torch.compile(model, backend=backend_func)
318 - compiled_model_func(hidden_states, residual)
318 + hidden_states = torch.randn((batch_size * seq_len, hidden_size), dtype=dtype)
319 + residual = torch.randn((batch_size * seq_len, hidden_size), dtype=dtype)
320
320 - assert sequence_parallelism_pass.matched_count == 1
321 + compiled_model_no_func = torch.compile(model, backend=backend_no_func)
322 + compiled_model_no_func(hidden_states, residual)
323 + compiled_model_func = torch.compile(model, backend=backend_func)
324 + compiled_model_func(hidden_states, residual)
325
322 - # In pre-nodes, all reduce should be there,
323 - # reduce scatter and all gather should not
324 - backend_no_func.check_before_ops(model.ops_in_model_before())
326 + assert sequence_parallelism_pass.matched_count == 1
327
326 - # In post-nodes, reduce scatter and all gather should be there,
327 - # all reduce should not
328 - backend_no_func.check_after_ops(model.ops_in_model_after())
328 + # In pre-nodes, all reduce should be there,
329 + # reduce scatter and all gather should not
330 + backend_no_func.check_before_ops(model.ops_in_model_before())
331
330 - # check if the functionalization pass is applied
331 - for op in model.ops_in_model():
332 - find_auto_fn(backend_no_func.graph_post_pass.nodes, op)
333 - assert find_auto_fn_maybe(backend_func.graph_post_pass.nodes, op) is None
332 + # In post-nodes, reduce scatter and all gather should be there,
333 + # all reduce should not
334 + backend_no_func.check_after_ops(model.ops_in_model_after())
335
335 - # make sure the ops were all de-functionalized
336 - found = dict()
337 - for node in backend_func.graph_post_pass.nodes:
336 + # check if the functionalization pass is applied
337 for op in model.ops_in_model():
339 - if is_func(node, op):
340 - found[op] = True
341 - assert all(found[op] for op in model.ops_in_model())
338 + find_auto_fn(backend_no_func.graph_post_pass.nodes, op)
339 + assert find_auto_fn_maybe(backend_func.graph_post_pass.nodes, op) is None
340 +
341 + # make sure the ops were all de-functionalized
342 + found = dict()
343 + for node in backend_func.graph_post_pass.nodes:
344 + for op in model.ops_in_model():
345 + if is_func(node, op):
346 + found[op] = True
347 + assert all(found[op] for op in model.ops_in_model())
tests/conftest.py
+101 -3
@@ -1,10 +1,13 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -
4 -# ruff: noqa
3 +import contextlib
4 +import pathlib
5 +from copy import deepcopy
6
7 from tblib import pickling_support
8
9 +# ruff: noqa
10 +
11 # Install support for pickling exceptions so that we can nicely propagate
12 # failures from tests running in a subprocess.
13 # This should be run before any custom exception subclasses are defined.
@@ -40,7 +43,7 @@ from transformers import (
43 from transformers.models.auto.auto_factory import _BaseAutoModelClass
44
45 from tests.models.utils import TokensTextLogprobs, TokensTextLogprobsPromptLogprobs
43 -from vllm import LLM, SamplingParams
46 +from vllm import LLM, SamplingParams, envs
47 from vllm.assets.audio import AudioAsset
48 from vllm.assets.image import ImageAsset
49 from vllm.assets.video import VideoAsset
@@ -1070,6 +1073,101 @@ def caplog_vllm(temporary_enable_log_propagate, caplog):
1073 yield caplog
1074
1075
1076 +@pytest.fixture()
1077 +def caplog_mp_fork():
1078 + """
1079 + This fixture enables capturing logs from a forked MP subprocess.
1080 + It should be used in conjunction with caplog_vllm.
1081 +
1082 + By default, subprocess logs do not go through the parent process.
1083 + We instead create a queue listener in the parent process which
1084 + forwards logs to the logger's other handlers, and add a QueueHandler
1085 + to the root logger. Forked subprocesses will inherit the root logger
1086 + and pass their messages to the queue, which the listener will forward
1087 + to the root logger, which can be captured by caplog.
1088 +
1089 + Note that this workaround only works for fork; with spawn, the subprocess
1090 + reinitializes logging and does not automatically inherit the queue.
1091 + We'd have to manually pass the queue to the subprocess at the spawn point.
1092 + See caplog_mp_spawn below.
1093 + """
1094 +
1095 + @contextlib.contextmanager
1096 + def ctx():
1097 + import logging.handlers
1098 + import multiprocessing as mp
1099 +
1100 + logger_queue: mp.Queue[logging.LogRecord] = mp.Queue()
1101 + logger = logging.getLogger()
1102 + handlers = logger.handlers
1103 +
1104 + # The listener works on a background thread, not inherited by the child.
1105 + queue_listener = logging.handlers.QueueListener(logger_queue, *handlers)
1106 + queue_listener.start()
1107 +
1108 + # Add queue handler after creating the listener to avoid cycle
1109 + logger.addHandler(logging.handlers.QueueHandler(logger_queue))
1110 + yield
1111 + queue_listener.stop()
1112 +
1113 + return ctx
1114 +
1115 +
1116 +class LogHolder:
1117 + def __init__(self):
1118 + self.text = None
1119 +
1120 +
1121 +@pytest.fixture()
1122 +def caplog_mp_spawn(tmp_path, monkeypatch):
1123 + """
1124 + This fixture enables capturing logs from a forked MP subprocess.
1125 + It does not require caplog_vllm (but it only contains logs from the child).
1126 +
1127 + By default, subprocess logs do not go through the parent process.
1128 + We instead add a FileHandler to the config so the spawned child process
1129 + writes its logs to a temp file.
1130 + In the parent, we read the file and return the contents.
1131 +
1132 + Note: this method could be extended to fork by either reconfiguring logging
1133 + in the parent or using a SocketHandler:
1134 + https://docs.python.org/3/howto/logging-cookbook.html#sending-and-receiving-logging-events-across-a-network # noqa: E501
1135 + """
1136 +
1137 + @contextlib.contextmanager
1138 + def ctx(level: int | str):
1139 + from vllm.logger import DEFAULT_LOGGING_CONFIG
1140 +
1141 + config_path = tmp_path / "vllm_logging_config.json"
1142 + log_path = tmp_path / "vllm.log"
1143 + log_holder = LogHolder()
1144 +
1145 + config = deepcopy(DEFAULT_LOGGING_CONFIG)
1146 + if envs.VLLM_LOGGING_CONFIG_PATH:
1147 + path = pathlib.Path(envs.VLLM_LOGGING_CONFIG_PATH)
1148 + assert path.exists()
1149 + config = json.loads(path.read_text())
1150 +
1151 + config["loggers"]["vllm"]["handlers"] += ["vllm_file"]
1152 + config["handlers"]["vllm_file"] = {
1153 + "class": "logging.FileHandler",
1154 + "formatter": "vllm",
1155 + "level": level,
1156 + "filename": log_path.as_posix(),
1157 + }
1158 +
1159 + config_path.write_text(json.dumps(config))
1160 +
1161 + with monkeypatch.context() as monkeypatch_ctx:
1162 + monkeypatch_ctx.setenv("VLLM_LOGGING_CONFIG_PATH", config_path.as_posix())
1163 + monkeypatch_ctx.setenv("VLLM_CONFIGURE_LOGGING", "1")
1164 + yield log_holder
1165 +
1166 + log_holder.text = log_path.read_text()
1167 +
1168 + return ctx
1169 +
1170 +
1171 @pytest.fixture(scope="session")
1172 def num_gpus_available():
1173 """Get number of GPUs without initializing the CUDA context
tests/kernels/quant_utils.py
+1 -1
@@ -103,7 +103,7 @@ def ref_dynamic_per_tensor_fp8_quant(
103 .clamp(fp8_traits_min, fp8_traits_max)
104 .to(FP8_DTYPE)
105 )
106 - return ref_out, ref_scale.view((1,))
106 + return ref_out, ref_scale.view((1, 1))
107
108
109 def native_w8a8_block_matmul(
tests/test_logger.py
+46
@@ -501,3 +501,49 @@ def test_streaming_complete_logs_full_text_content():
501 assert call_args[1] == "test-streaming-full-text"
502 assert call_args[2] == " (streaming complete)"
503 assert call_args[5] == "streaming_complete"
504 +
505 +
506 +# Add vllm prefix to make sure logs go through the vllm logger
507 +test_logger = init_logger("vllm.test_logger")
508 +
509 +
510 +def mp_function(**kwargs):
511 + # This function runs in a subprocess
512 +
513 + test_logger.warning("This is a subprocess: %s", kwargs.get("a"))
514 + test_logger.error("This is a subprocess error.")
515 + test_logger.debug("This is a subprocess debug message: %s.", kwargs.get("b"))
516 +
517 +
518 +def test_caplog_mp_fork(caplog_vllm, caplog_mp_fork):
519 + with caplog_vllm.at_level(logging.DEBUG), caplog_mp_fork():
520 + import multiprocessing
521 +
522 + ctx = multiprocessing.get_context("fork")
523 + p = ctx.Process(
524 + target=mp_function,
525 + name=f"SubProcess{1}",
526 + kwargs={"a": "AAAA", "b": "BBBBB"},
527 + )
528 + p.start()
529 + p.join()
530 +
531 + assert "AAAA" in caplog_vllm.text
532 + assert "BBBBB" in caplog_vllm.text
533 +
534 +
535 +def test_caplog_mp_spawn(caplog_mp_spawn):
536 + with caplog_mp_spawn(logging.DEBUG) as log_holder:
537 + import multiprocessing
538 +
539 + ctx = multiprocessing.get_context("spawn")
540 + p = ctx.Process(
541 + target=mp_function,
542 + name=f"SubProcess{1}",
543 + kwargs={"a": "AAAA", "b": "BBBBB"},
544 + )
545 + p.start()
546 + p.join()
547 +
548 + assert "AAAA" in log_holder.text
549 + assert "BBBBB" in log_holder.text
tests/utils.py
+22 -1
@@ -6,6 +6,7 @@ import contextlib
6 import copy
7 import functools
8 import importlib
9 +import itertools
10 import json
11 import os
12 import random
@@ -15,7 +16,7 @@ import sys
16 import tempfile
17 import time
18 import warnings
18 -from collections.abc import Callable
19 +from collections.abc import Callable, Iterable
20 from contextlib import ExitStack, contextmanager, suppress
21 from multiprocessing import Process
22 from pathlib import Path
@@ -1261,3 +1262,23 @@ def check_answers(
1262 frac_ok = numok / len(answer)
1263 print(f"Num OK: {numok}/{len(answer)} {frac_ok}")
1264 assert frac_ok >= accept_rate
1265 +
1266 +
1267 +def flat_product(*iterables: Iterable[Any]):
1268 + """
1269 + Flatten lists of tuples of the cartesian product.
1270 + Useful when we want to avoid nested tuples to allow
1271 + test params to be unpacked directly from the decorator.
1272 +
1273 + Example:
1274 + flat_product([(1, 2), (3, 4)], ["a", "b"]) ->
1275 + [
1276 + (1, 2, "a"),
1277 + (1, 2, "b"),
1278 + (3, 4, "a"),
1279 + (3, 4, "b"),
1280 + ]
1281 + """
1282 + for element in itertools.product(*iterables):
1283 + normalized = (e if isinstance(e, tuple) else (e,) for e in element)
1284 + yield tuple(itertools.chain(*normalized))
tests/utils_/test_utils.py
+23 -1
@@ -40,7 +40,7 @@ from vllm.utils import (
40 unique_filepath,
41 )
42
43 -from ..utils import create_new_process_for_each_test
43 +from ..utils import create_new_process_for_each_test, flat_product
44
45
46 def test_get_open_port(monkeypatch: pytest.MonkeyPatch):
@@ -771,3 +771,25 @@ def test_unique_filepath():
771 paths.add(path)
772 assert len(paths) == 10
773 assert len(list(Path(temp_dir).glob("*.txt"))) == 10
774 +
775 +
776 +def test_flat_product():
777 + # Check regular itertools.product behavior
778 + result1 = list(flat_product([1, 2, 3], ["a", "b"]))
779 + assert result1 == [
780 + (1, "a"),
781 + (1, "b"),
782 + (2, "a"),
783 + (2, "b"),
784 + (3, "a"),
785 + (3, "b"),
786 + ]
787 +
788 + # check that the tuples get flattened
789 + result2 = list(flat_product([(1, 2), (3, 4)], ["a", "b"], [(5, 6)]))
790 + assert result2 == [
791 + (1, 2, "a", 5, 6),
792 + (1, 2, "b", 5, 6),
793 + (3, 4, "a", 5, 6),
794 + (3, 4, "b", 5, 6),
795 + ]
vllm/_custom_ops.py
+1 -1
@@ -1507,7 +1507,7 @@ def scaled_fp8_quant(
1507 output, input, scale, scale_ub
1508 )
1509 else:
1510 - scale = torch.empty(1, device=input.device, dtype=torch.float32)
1510 + scale = torch.empty((1, 1), device=input.device, dtype=torch.float32)
1511 torch.ops._C.dynamic_scaled_fp8_quant(output, input, scale)
1512 else:
1513 assert scale.numel() == 1, f"{scale.shape}"
vllm/compilation/collective_fusion.py
+70 -149
@@ -17,10 +17,14 @@ from vllm.distributed.parallel_state import (
17 get_tensor_model_parallel_world_size,
18 )
19 from vllm.logger import init_logger
20 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
21 + kFp8StaticTensorSym,
22 +)
23 from vllm.platforms import current_platform
24 from vllm.utils import direct_register_custom_op
25
26 from .inductor_pass import enable_fake_mode
27 +from .matcher_utils import MatcherFusedAddRMSNorm, MatcherQuantFP8, MatcherRMSNorm
28 from .vllm_inductor_pass import VllmInductorPass, VllmPatternMatcherPass
29
30 FP8_DTYPE = current_platform.fp8_dtype()
@@ -41,11 +45,8 @@ else:
45
46 logger = init_logger(__name__)
47
44 -ALLREDUCE_OP = torch.ops.vllm.all_reduce.default
45 -RMS_OP = torch.ops._C.rms_norm.default
46 -RMS_ADD_OP = torch.ops._C.fused_add_rms_norm.default
47 -STATIC_FP8_QUANT_OP = torch.ops._C.static_scaled_fp8_quant.default
48 -STATIC_FP4_QUANT_OP = torch.ops._C.scaled_fp4_quant.default
48 +if hasattr(torch.ops._C, "scaled_fp4_quant"):
49 + STATIC_FP4_QUANT_OP = torch.ops._C.scaled_fp4_quant.default
50
51
52 class BasePattern:
@@ -669,33 +670,24 @@ class AllReduceRMSNormPattern(BasePattern):
670 super().__init__(dtype, device)
671 self.epsilon = epsilon
672 self.allreduce_params = allreduce_params
673 + self.rmsnorm_matcher = MatcherRMSNorm(epsilon)
674
675 def get_inputs(self):
674 - input = torch.empty([1, 8, 4], device=self.device, dtype=self.dtype)
675 - rms_result = torch.empty([1, 8, 4], device=self.device, dtype=self.dtype)
676 - weight = torch.empty([4], device=self.device, dtype=self.dtype)
676 + input, weight = self.rmsnorm_matcher.inputs()
677
678 - return [input, rms_result, weight]
678 + # input goes through allreduce first, always 16-bit
679 + return [input.to(self.dtype), weight]
680
681 def register(self, pm_pass: PatternMatcherPass):
681 - def pattern(
682 - input: torch.Tensor, rms_result: torch.Tensor, weight: torch.Tensor
683 - ):
682 + def pattern(input: torch.Tensor, weight: torch.Tensor):
683 allreduce_output = tensor_model_parallel_all_reduce(input)
685 - rms = auto_functionalized(
686 - RMS_OP,
687 - result=rms_result,
688 - input=allreduce_output,
689 - weight=weight,
690 - epsilon=self.epsilon,
691 - )
692 - # rms_result, allreduce_output
693 - return rms[1], allreduce_output
684 + rms = self.rmsnorm_matcher(allreduce_output, weight)
685
695 - def replacement(
696 - input: torch.Tensor, rms_result: torch.Tensor, weight: torch.Tensor
697 - ):
686 + return rms, allreduce_output
687 +
688 + def replacement(input: torch.Tensor, weight: torch.Tensor):
689 residual = torch.zeros_like(input)
690 + rms_result = torch.empty_like(input)
691 allreduce = auto_functionalized(
692 flashinfer_trtllm_fused_allreduce_norm,
693 allreduce_in=input,
@@ -733,29 +725,19 @@ class AllReduceFusedAddRMSNormPattern(BasePattern):
725 super().__init__(dtype, device)
726 self.epsilon = epsilon
727 self.allreduce_params = allreduce_params
728 + self.rmsnorm_matcher = MatcherFusedAddRMSNorm(epsilon)
729
730 def get_inputs(self):
738 - input = torch.empty([4, 4], device=self.device, dtype=self.dtype)
739 - residual = torch.empty([4, 4], device=self.device, dtype=self.dtype)
740 - weight = torch.empty([4, 4], device=self.device, dtype=self.dtype)
741 - return [
742 - residual,
743 - input,
744 - weight,
745 - ]
731 + input, residual, weight = self.rmsnorm_matcher.inputs()
732 +
733 + # input goes through allreduce first, always 16-bit
734 + return [residual, input.to(self.dtype), weight]
735
736 def register(self, pm_pass: PatternMatcherPass):
737 def pattern(residual: torch.Tensor, input: torch.Tensor, weight: torch.Tensor):
738 allreduce_output = tensor_model_parallel_all_reduce(input)
750 - rms = auto_functionalized(
751 - RMS_ADD_OP,
752 - input=allreduce_output,
753 - residual=residual,
754 - weight=weight,
755 - epsilon=self.epsilon,
756 - )
757 - # input, residual
758 - return rms[1], rms[2]
739 + rms, residual = self.rmsnorm_matcher(allreduce_output, weight, residual)
740 + return rms, residual
741
742 def replacement(
743 residual: torch.Tensor, input: torch.Tensor, weight: torch.Tensor
@@ -779,6 +761,18 @@ class AllReduceFusedAddRMSNormPattern(BasePattern):
761 pattern, replacement, self.get_inputs(), pm.fwd_only, pm_pass
762 )
763
764 + # Same pattern, but only return the output and not residual
765 + # (helpful for end of graph where residual is not used again)
766 + first_return_only = lambda fn: lambda a, b, c: fn(a, b, c)[0]
767 +
768 + pm.register_replacement(
769 + first_return_only(pattern),
770 + first_return_only(replacement),
771 + self.get_inputs(),
772 + pm.fwd_only,
773 + pm_pass,
774 + )
775 +
776
777 class AllReduceFusedRMSNormStaticQuantFP8Pattern(BasePattern):
778 """
@@ -799,60 +793,37 @@ class AllReduceFusedRMSNormStaticQuantFP8Pattern(BasePattern):
793 self.epsilon = epsilon
794 self.allreduce_params = allreduce_params
795 self.quant_dtype = torch.float8_e4m3fn
796 + self.rmsnorm_matcher = MatcherRMSNorm(epsilon)
797 + self.quant_matcher = MatcherQuantFP8(kFp8StaticTensorSym)
798
799 def register(self, pm_pass: PatternMatcherPass):
800 def get_inputs():
805 - input = torch.zeros([1, 8, 4], device=self.device, dtype=self.dtype)
806 - rmsnorm_result = torch.empty(
807 - [1, 8, 4], device=self.device, dtype=self.dtype
808 - )
809 - quant_result = torch.empty(
810 - [1, 8, 4], device=self.device, dtype=self.quant_dtype
811 - )
812 - weight = torch.empty([4], device=self.device, dtype=self.dtype)
813 - scale = torch.tensor(1.0, device=self.device, dtype=torch.float32)
814 - return [input, rmsnorm_result, quant_result, weight, scale]
801 + input, weight = self.rmsnorm_matcher.inputs()
802 + _, scale = self.quant_matcher.inputs()
803 +
804 + # input goes through allreduce first, always 16-bit
805 + return [input.to(self.dtype), weight, scale]
806
807 def pattern(
808 input: torch.Tensor,
818 - rmsnorm_result: torch.Tensor,
819 - quant_result: torch.Tensor,
809 weight: torch.Tensor,
810 scale: torch.Tensor,
811 ):
812 all_reduce = tensor_model_parallel_all_reduce(input)
824 - rmsnorm_out_tuple = auto_functionalized(
825 - RMS_OP,
826 - result=rmsnorm_result,
827 - input=all_reduce,
828 - weight=weight,
829 - epsilon=self.epsilon,
830 - )
831 -
832 - quant_out_tuple = auto_functionalized(
833 - STATIC_FP8_QUANT_OP,
834 - result=quant_result,
835 - input=rmsnorm_out_tuple[1],
836 - scale=scale,
837 - )
838 -
839 - # quant_out, allreduce_output
840 - return quant_out_tuple[1], all_reduce
813 + rms = self.rmsnorm_matcher(all_reduce, weight)
814 + quant, _ = self.quant_matcher(rms, scale)
815 + return quant, all_reduce
816
842 - def replacement(
843 - input: torch.Tensor,
844 - result_rms: torch.Tensor,
845 - quant_result: torch.Tensor,
846 - weight: torch.Tensor,
847 - scale: torch.Tensor,
848 - ):
817 + def replacement(input: torch.Tensor, weight: torch.Tensor, scale: torch.Tensor):
818 residual = torch.zeros_like(input)
819 + result_rms = torch.empty_like(input)
820 + result_quant = torch.empty_like(input, dtype=self.quant_dtype)
821 allreduce = auto_functionalized(
822 flashinfer_trtllm_fused_allreduce_norm,
823 allreduce_in=input,
824 residual=residual,
825 norm_out=result_rms,
855 - quant_out=quant_result,
826 + quant_out=result_quant,
827 scale_out=None,
828 rms_gamma=weight,
829 rms_eps=self.epsilon,
@@ -892,64 +863,42 @@ class AllReduceFusedAddRMSNormStaticQuantFP8Pattern(BasePattern):
863 self.allreduce_params = allreduce_params
864 self.quant_dtype = torch.float8_e4m3fn
865
866 + self.rmsnorm_matcher = MatcherFusedAddRMSNorm(epsilon)
867 + self.quant_matcher = MatcherQuantFP8(kFp8StaticTensorSym)
868 +
869 def register(self, pm_pass: PatternMatcherPass):
870 def get_inputs():
897 - input = torch.empty([4, 4], device=self.device, dtype=self.dtype)
871 + input, residual, weight = self.rmsnorm_matcher.inputs()
872 + _, scale = self.quant_matcher.inputs()
873
899 - residual = torch.empty([4, 4], device=self.device, dtype=self.dtype)
900 - weight = torch.empty([4, 4], device=self.device, dtype=self.dtype)
901 - quant_result = torch.empty(
902 - [4, 4], device=self.device, dtype=self.quant_dtype
903 - )
904 - scale = torch.empty([1, 1], device=self.device, dtype=torch.float32)
905 -
906 - return [
907 - quant_result,
908 - residual,
909 - input,
910 - weight,
911 - scale,
912 - ]
874 + # input goes through allreduce first, always 16-bit
875 + return [residual, input.to(self.dtype), weight, scale]
876
877 def pattern(
915 - quant_result: torch.Tensor,
878 residual: torch.Tensor,
879 input: torch.Tensor,
880 weight: torch.Tensor,
881 scale: torch.Tensor,
882 ):
883 allreduce_output = tensor_model_parallel_all_reduce(input)
884 + rms, res = self.rmsnorm_matcher(allreduce_output, weight, residual)
885 + quant, _ = self.quant_matcher(rms, scale)
886
923 - fused_add_rmsnorm_out_tuple = auto_functionalized(
924 - RMS_ADD_OP,
925 - input=allreduce_output,
926 - residual=residual,
927 - weight=weight,
928 - epsilon=self.epsilon,
929 - )
930 - quant_out_tuple = auto_functionalized(
931 - STATIC_FP8_QUANT_OP,
932 - result=quant_result,
933 - input=fused_add_rmsnorm_out_tuple[1],
934 - scale=scale,
935 - )
936 -
937 - # quant_out, allreduce_output
938 - return quant_out_tuple[1], fused_add_rmsnorm_out_tuple[2]
887 + return quant, res
888
889 def replacement(
941 - quant_result: torch.Tensor,
890 residual: torch.Tensor,
891 input: torch.Tensor,
892 weight: torch.Tensor,
893 scale: torch.Tensor,
894 ):
895 + result_quant = torch.empty_like(input, dtype=self.quant_dtype)
896 allreduce = auto_functionalized(
897 flashinfer_trtllm_fused_allreduce_norm,
898 allreduce_in=input,
899 residual=residual,
900 norm_out=None,
952 - quant_out=quant_result,
901 + quant_out=result_quant,
902 scale_out=None,
903 rms_gamma=weight,
904 rms_eps=self.epsilon,
@@ -986,14 +935,11 @@ class AllReduceFusedRMSNormStaticQuantNVFP4Pattern(BasePattern):
935 super().__init__(dtype, device)
936 self.epsilon = epsilon
937 self.allreduce_params = allreduce_params
938 + self.rmsnorm_matcher = MatcherRMSNorm(epsilon)
939
940 def register(self, pm_pass: PatternMatcherPass):
941 def get_inputs():
942 input = torch.empty([1, 16, 16], device=self.device, dtype=self.dtype)
993 -
994 - rmsnorm_result = torch.empty(
995 - [1, 16, 16], device=self.device, dtype=self.dtype
996 - )
943 quant_result = torch.empty((16, 8), device=self.device, dtype=torch.uint8)
944 input_global_scale = torch.empty(
945 [1, 1], device=self.device, dtype=torch.float32
@@ -1001,36 +947,21 @@ class AllReduceFusedRMSNormStaticQuantNVFP4Pattern(BasePattern):
947 weight = torch.empty([16], device=self.device, dtype=self.dtype)
948 output_scale = torch.empty([128, 4], device=self.device, dtype=torch.int32)
949
1004 - return [
1005 - input,
1006 - rmsnorm_result,
1007 - quant_result,
1008 - weight,
1009 - input_global_scale,
1010 - output_scale,
1011 - ]
950 + return [input, quant_result, weight, input_global_scale, output_scale]
951
952 def pattern(
953 input: torch.Tensor,
1015 - rmsnorm_result: torch.Tensor,
954 quant_result: torch.Tensor,
955 weight: torch.Tensor,
956 input_global_scale: torch.Tensor,
957 output_scale: torch.Tensor,
958 ):
959 all_reduce = tensor_model_parallel_all_reduce(input)
1022 - rmsnorm_out_tuple = auto_functionalized(
1023 - RMS_OP,
1024 - result=rmsnorm_result,
1025 - input=all_reduce,
1026 - weight=weight,
1027 - epsilon=self.epsilon,
1028 - )
1029 -
960 + rms = self.rmsnorm_matcher(all_reduce, weight)
961 quant_out_tuple = auto_functionalized(
962 STATIC_FP4_QUANT_OP,
963 output=quant_result,
1033 - input=rmsnorm_out_tuple[1],
964 + input=rms,
965 output_scale=output_scale,
966 input_scale=input_global_scale,
967 )
@@ -1040,13 +971,13 @@ class AllReduceFusedRMSNormStaticQuantNVFP4Pattern(BasePattern):
971
972 def replacement(
973 input: torch.Tensor,
1043 - result_rms: torch.Tensor,
974 quant_result: torch.Tensor,
975 weight: torch.Tensor,
976 input_global_scale: torch.Tensor,
977 output_scale: torch.Tensor,
978 ):
979 residual = torch.zeros_like(input)
980 + result_rms = torch.empty_like(input)
981 allreduce = auto_functionalized(
982 flashinfer_trtllm_fused_allreduce_norm,
983 allreduce_in=input,
@@ -1090,6 +1021,7 @@ class AllReduceFusedAddRMSNormStaticQuantNVFP4Pattern(BasePattern):
1021 super().__init__(dtype, device)
1022 self.epsilon = epsilon
1023 self.allreduce_params = allreduce_params
1024 + self.rmsnorm_matcher = MatcherFusedAddRMSNorm(epsilon)
1025
1026 def register(self, pm_pass: PatternMatcherPass):
1027 def get_inputs():
@@ -1121,28 +1053,17 @@ class AllReduceFusedAddRMSNormStaticQuantNVFP4Pattern(BasePattern):
1053 input_global_scale: torch.Tensor,
1054 ):
1055 allreduce_output = tensor_model_parallel_all_reduce(input)
1124 -
1125 - fused_add_rmsnorm_out_tuple = auto_functionalized(
1126 - RMS_ADD_OP,
1127 - input=allreduce_output,
1128 - residual=residual,
1129 - weight=weight,
1130 - epsilon=self.epsilon,
1131 - )
1056 + rms, residual = self.rmsnorm_matcher(allreduce_output, weight, residual)
1057 quant_out_tuple = auto_functionalized(
1058 STATIC_FP4_QUANT_OP,
1059 output=quant_result,
1135 - input=fused_add_rmsnorm_out_tuple[1],
1060 + input=rms,
1061 output_scale=output_scale,
1062 input_scale=input_global_scale,
1063 )
1064
1065 # quant_out, allreduce_output, output_scale
1141 - return (
1142 - quant_out_tuple[1],
1143 - fused_add_rmsnorm_out_tuple[2],
1144 - quant_out_tuple[2],
1145 - )
1066 + return quant_out_tuple[1], residual, quant_out_tuple[2]
1067
1068 def replacement(
1069 quant_result: torch.Tensor,
vllm/compilation/fusion.py
+68 -129
@@ -9,7 +9,7 @@ from torch._higher_order_ops.auto_functionalize import auto_functionalized
9 from torch._inductor.pattern_matcher import PatternMatcherPass
10 from torch._ops import OpOverload
11
12 -from vllm.config import VllmConfig
12 +from vllm.config import VllmConfig, get_current_vllm_config
13 from vllm.logger import init_logger
14 from vllm.model_executor.layers.quantization.utils.quant_utils import (
15 GroupShape,
@@ -24,6 +24,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
24 from vllm.platforms import current_platform
25
26 from .inductor_pass import enable_fake_mode
27 +from .matcher_utils import MatcherFusedAddRMSNorm, MatcherQuantFP8, MatcherRMSNorm
28 from .vllm_inductor_pass import VllmInductorPass, VllmPatternMatcherPass
29
30 logger = init_logger(__name__)
@@ -92,13 +93,19 @@ class RMSNormQuantPattern:
93 def __init__(self, epsilon: float, key: FusedRMSQuantKey):
94 self.epsilon = epsilon
95 self.quant_dtype = key.quant.dtype
95 -
96 - assert key.quant in QUANT_OPS, f"unsupported quantization scheme {key.quant}"
97 - self.QUANT_OP = QUANT_OPS[key.quant]
96 + config = get_current_vllm_config()
97 + self.model_dtype = config.model_config.dtype if config.model_config else None
98
99 assert key in FUSED_OPS, f"unsupported fused rmsnorm+quant op for {key}"
100 self.FUSED_OP = FUSED_OPS[key]
101
102 + self.rmsnorm_matcher = (
103 + MatcherRMSNorm(epsilon)
104 + if not key.fused_add
105 + else MatcherFusedAddRMSNorm(epsilon)
106 + )
107 + self.quant_matcher = MatcherQuantFP8(key.quant)
108 +
109
110 class RMSNormStaticQuantPattern(RMSNormQuantPattern):
111 def __init__(self, epsilon: float, quant_dtype: torch.dtype, symmetric=True):
@@ -112,34 +119,18 @@ class RMSNormStaticQuantPattern(RMSNormQuantPattern):
119
120 def register(self, pm_pass: PatternMatcherPass):
121 # Cannot use methods, as the self argument affects tracing
115 - def pattern(
116 - result: torch.Tensor,
117 - result_rms: torch.Tensor,
118 - input: torch.Tensor,
119 - weight: torch.Tensor,
120 - scale: torch.Tensor,
121 - ):
122 - at1 = auto_functionalized(
123 - RMS_OP,
124 - result=result_rms,
125 - input=input,
126 - weight=weight,
127 - epsilon=self.epsilon,
128 - )
129 - at2 = auto_functionalized(
130 - self.QUANT_OP, result=result, input=at1[1], scale=scale
131 - )
122 + def pattern(input: torch.Tensor, weight: torch.Tensor, scale: torch.Tensor):
123 + result_rms = self.rmsnorm_matcher(input, weight)
124 + return self.quant_matcher(result_rms, scale)[0]
125
133 - # result
134 - return at2[1]
126 + def replacement(input: torch.Tensor, weight: torch.Tensor, scale: torch.Tensor):
127 + # In case we're matching native rms-norm, conversions might be
128 + # optimized out. We convert here just to be safe.
129 + input = input.to(dtype=self.model_dtype)
130
136 - def replacement(
137 - result: torch.Tensor,
138 - result_rms: torch.Tensor,
139 - input: torch.Tensor,
140 - weight: torch.Tensor,
141 - scale: torch.Tensor,
142 - ):
131 + result = torch.empty(
132 + input.shape, device=input.device, dtype=self.quant_dtype
133 + )
134 at = auto_functionalized(
135 self.FUSED_OP,
136 result=result,
@@ -153,12 +144,11 @@ class RMSNormStaticQuantPattern(RMSNormQuantPattern):
144 return at[1]
145
146 inputs = [
156 - torch.empty(5, 4, device="cuda", dtype=self.quant_dtype), # result
157 - empty_bf16(5, 4), # result_rms
158 - empty_bf16(5, 4), # input
159 - empty_bf16(1, 5), # weight
160 - empty_fp32(1, 1), # scale
147 + # input, weight
148 + *self.rmsnorm_matcher.inputs(),
149 + self.quant_matcher.inputs()[1], # scale
150 ]
151 + pattern(*inputs)
152
153 pm.register_replacement(pattern, replacement, inputs, pm.fwd_only, pm_pass)
154
@@ -175,33 +165,27 @@ class FusedAddRMSNormStaticQuantPattern(RMSNormQuantPattern):
165
166 def register(self, pm_pass: PatternMatcherPass):
167 def pattern(
178 - result: torch.Tensor,
168 input: torch.Tensor,
180 - residual: torch.Tensor,
169 weight: torch.Tensor,
170 + residual: torch.Tensor,
171 scale: torch.Tensor,
172 ):
184 - at = auto_functionalized(
185 - RMS_ADD_OP,
186 - input=input,
187 - residual=residual,
188 - weight=weight,
189 - epsilon=self.epsilon,
190 - )
191 - at1 = auto_functionalized(
192 - self.QUANT_OP, result=result, input=at[1], scale=scale
193 - )
173 + result_rms, residual = self.rmsnorm_matcher(input, weight, residual)
174 + result, _ = self.quant_matcher(result_rms, scale)
175
195 - # result, residual
196 - return at1[1], at[2]
176 + return result, residual
177
178 def replacement(
199 - result: torch.Tensor,
179 input: torch.Tensor,
201 - residual: torch.Tensor,
180 weight: torch.Tensor,
181 + residual: torch.Tensor,
182 scale: torch.Tensor,
183 ):
184 + # In case we're matching native rms-norm, conversions might be
185 + # optimized out. We convert here just to be safe.
186 + input = input.to(dtype=self.model_dtype)
187 +
188 + result = torch.empty_like(input, dtype=self.quant_dtype)
189 at = auto_functionalized(
190 self.FUSED_OP,
191 result=result,
@@ -216,11 +200,9 @@ class FusedAddRMSNormStaticQuantPattern(RMSNormQuantPattern):
200 return at[1], at[2]
201
202 inputs = [
219 - torch.empty(5, 4, device="cuda", dtype=self.quant_dtype), # result
220 - empty_bf16(5, 4), # input
221 - empty_bf16(5, 4), # residual
222 - empty_bf16(1, 5), # weight
223 - empty_fp32(1, 1), # scale
203 + # input, weight, residual
204 + *self.rmsnorm_matcher.inputs(),
205 + self.quant_matcher.inputs()[1], # scale
206 ]
207
208 pm.register_replacement(
@@ -248,34 +230,18 @@ class RMSNormDynamicQuantPattern(RMSNormQuantPattern):
230 super().__init__(epsilon, key)
231
232 def register(self, pm_pass: PatternMatcherPass):
251 - def pattern(
252 - result: torch.Tensor,
253 - result_rms: torch.Tensor,
254 - input: torch.Tensor,
255 - weight: torch.Tensor,
256 - scale: torch.Tensor,
257 - ):
258 - at1 = auto_functionalized(
259 - RMS_OP,
260 - result=result_rms,
261 - input=input,
262 - weight=weight,
263 - epsilon=self.epsilon,
264 - )
265 - at2 = auto_functionalized(
266 - self.QUANT_OP, result=result, input=at1[1], scale=scale, scale_ub=None
267 - )
268 -
233 + def pattern(input: torch.Tensor, weight: torch.Tensor):
234 + result_rms = self.rmsnorm_matcher(input, weight)
235 # result, scale
270 - return at2[1], at2[2]
236 + return self.quant_matcher(result_rms)
237
272 - def replacement(
273 - result: torch.Tensor,
274 - result_rms: torch.Tensor,
275 - input: torch.Tensor,
276 - weight: torch.Tensor,
277 - scale: torch.Tensor,
278 - ):
238 + def replacement(input: torch.Tensor, weight: torch.Tensor):
239 + # In case we're matching native rms-norm, conversions might be
240 + # optimized out. We convert here just to be safe.
241 + input = input.to(dtype=self.model_dtype)
242 +
243 + result = torch.empty_like(input, dtype=self.quant_dtype)
244 + scale = self.quant_matcher.make_scale(input)
245 at = auto_functionalized(
246 self.FUSED_OP,
247 result=result,
@@ -290,18 +256,10 @@ class RMSNormDynamicQuantPattern(RMSNormQuantPattern):
256 # result, scale
257 return at[1], at[2]
258
293 - inputs = [
294 - torch.empty(5, 4, device="cuda", dtype=self.quant_dtype), # result
295 - empty_bf16(5, 4), # result_rms
296 - empty_bf16(5, 4), # input
297 - empty_bf16(1, 5), # weight
298 - empty_fp32(1, 1), # scale
299 - ]
300 -
259 pm.register_replacement(
260 pattern,
261 replacement,
304 - inputs,
262 + self.rmsnorm_matcher.inputs(),
263 pm.fwd_only,
264 pm_pass,
265 )
@@ -323,34 +281,21 @@ class FusedAddRMSNormDynamicQuantPattern(RMSNormQuantPattern):
281 super().__init__(epsilon, key)
282
283 def register(self, pm_pass: PatternMatcherPass):
326 - def pattern(
327 - result: torch.Tensor,
328 - input: torch.Tensor,
329 - residual: torch.Tensor,
330 - weight: torch.Tensor,
331 - scale: torch.Tensor,
332 - ):
333 - at = auto_functionalized(
334 - RMS_ADD_OP,
335 - input=input,
336 - residual=residual,
337 - weight=weight,
338 - epsilon=self.epsilon,
339 - )
340 - at1 = auto_functionalized(
341 - self.QUANT_OP, result=result, input=at[1], scale=scale, scale_ub=None
342 - )
284 + def pattern(input: torch.Tensor, weight: torch.Tensor, residual: torch.Tensor):
285 + result_rms, residual = self.rmsnorm_matcher(input, weight, residual)
286 + result, scale = self.quant_matcher(result_rms)
287
344 - # result, residual, scale
345 - return at1[1], at[2], at1[2]
288 + return result, residual, scale
289
290 def replacement(
348 - result: torch.Tensor,
349 - input: torch.Tensor,
350 - residual: torch.Tensor,
351 - weight: torch.Tensor,
352 - scale: torch.Tensor,
291 + input: torch.Tensor, weight: torch.Tensor, residual: torch.Tensor
292 ):
293 + # In case we're matching native rms-norm, conversions might be
294 + # optimized out. We convert here just to be safe.
295 + input = input.to(dtype=self.model_dtype)
296 +
297 + result = torch.empty_like(input, dtype=self.quant_dtype)
298 + scale = self.quant_matcher.make_scale(input)
299 at = auto_functionalized(
300 self.FUSED_OP,
301 result=result,
@@ -365,18 +310,10 @@ class FusedAddRMSNormDynamicQuantPattern(RMSNormQuantPattern):
310 # result, residual, scale
311 return at[1], at[3], at[2]
312
368 - inputs = [
369 - torch.empty(5, 4, device="cuda", dtype=self.quant_dtype), # result
370 - empty_bf16(5, 4), # input
371 - empty_bf16(5, 4), # residual
372 - empty_bf16(1, 5), # weight
373 - empty_fp32(1, 1), # scale
374 - ]
375 -
313 pm.register_replacement(
314 pattern,
315 replacement,
379 - inputs,
316 + self.rmsnorm_matcher.inputs(),
317 pm.fwd_only,
318 pm_pass,
319 )
@@ -396,23 +333,25 @@ class RMSNormQuantFusionPass(VllmPatternMatcherPass):
333 pass_name="rmsnorm_quant_fusion_pass"
334 )
335
336 + # Make sure fused add patterns are before simple rms norm,
337 + # as the latter is a subset of the former in torch ops
338 for epsilon in [1e-5, 1e-6]:
400 - # Fuse rms_norm + static fp8 quant
401 - RMSNormStaticQuantPattern(epsilon, FP8_DTYPE).register(self.patterns)
402 -
339 # Fuse fused_add_rms_norm + static fp8 quant
340 FusedAddRMSNormStaticQuantPattern(epsilon, FP8_DTYPE).register(
341 self.patterns
342 )
343
408 - # Fuse rms_norm + dynamic per-token fp8 quant
409 - RMSNormDynamicQuantPattern(epsilon, FP8_DTYPE).register(self.patterns)
344 + # Fuse rms_norm + static fp8 quant
345 + RMSNormStaticQuantPattern(epsilon, FP8_DTYPE).register(self.patterns)
346
347 # Fuse fused_add_rms_norm + dynamic per-token fp8 quant
348 FusedAddRMSNormDynamicQuantPattern(epsilon, FP8_DTYPE).register(
349 self.patterns
350 )
351
352 + # Fuse rms_norm + dynamic per-token fp8 quant
353 + RMSNormDynamicQuantPattern(epsilon, FP8_DTYPE).register(self.patterns)
354 +
355 self.dump_patterns(config, self.patterns)
356
357 @VllmInductorPass.time_and_log
vllm/compilation/fusion_attn.py
+37 -18
@@ -2,9 +2,11 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from abc import ABC, abstractmethod
5 +from collections.abc import Callable
6
7 import torch
8 import torch._inductor.pattern_matcher as pm
9 +from torch import fx
10 from torch._higher_order_ops.auto_functionalize import auto_functionalized
11 from torch._inductor.pattern_matcher import PatternMatcherPass
12
@@ -20,7 +22,9 @@ from vllm.platforms import current_platform
22 from vllm.utils import round_up
23
24 from .fusion import QUANT_OPS, empty_bf16, empty_fp32, empty_i32
25 +from .fx_utils import is_func
26 from .inductor_pass import enable_fake_mode
27 +from .matcher_utils import MatcherQuantFP8
28 from .vllm_inductor_pass import VllmInductorPass, VllmPatternMatcherPass
29
30 logger = init_logger(__name__)
@@ -66,9 +70,13 @@ class AttentionQuantPattern(ABC):
70 return torch.empty(*args, **kwargs)
71
72 @staticmethod
69 - def wrap_trace_fn(process_fx, trace_fn):
73 + def wrap_trace_fn(trace_fn, *process_fx_fns: Callable[[fx.GraphModule], None]):
74 def wrapped(*args, **kwargs):
71 - return process_fx(trace_fn(*args, **kwargs))
75 + gm = trace_fn(*args, **kwargs)
76 + for process_fx in process_fx_fns:
77 + process_fx(gm)
78 +
79 + return gm
80
81 return wrapped
82
@@ -77,7 +85,20 @@ class AttentionQuantPattern(ABC):
85 from torch._inductor.fx_passes.post_grad import view_to_reshape
86
87 view_to_reshape(gm)
80 - return gm
88 +
89 + @staticmethod
90 + def remove_noop_permutes(gm: torch.fx.GraphModule):
91 + for node in gm.graph.nodes:
92 + if not is_func(node, torch.ops.aten.permute.default):
93 + continue
94 +
95 + dims = node.args[1]
96 + if any(dim != i for i, dim in enumerate(dims)):
97 + continue
98 +
99 + # this is now an identity op, remove
100 + node.replace_all_uses_with(node.args[0])
101 + gm.graph.erase_node(node)
102
103 def register_if_supported(self, pm_pass: PatternMatcherPass):
104 if self.layer.impl.fused_output_quant_supported(self.quant_key):
@@ -108,6 +129,7 @@ class AttentionFp8StaticQuantPattern(AttentionQuantPattern):
129 dtype=FP8_DTYPE, scale=kStaticTensorScale, symmetric=symmetric
130 )
131 super().__init__(layer, quant_key, dtype)
132 + self.quant_matcher = MatcherQuantFP8(quant_key)
133
134 def _register(self, pm_pass: PatternMatcherPass):
135 def pattern(
@@ -115,7 +137,6 @@ class AttentionFp8StaticQuantPattern(AttentionQuantPattern):
137 k: torch.Tensor,
138 v: torch.Tensor,
139 output_attn: torch.Tensor,
118 - output_quant: torch.Tensor,
140 scale: torch.Tensor,
141 ):
142 at1 = auto_functionalized(
@@ -131,17 +152,14 @@ class AttentionFp8StaticQuantPattern(AttentionQuantPattern):
152 attn_out_view = RESHAPE_OP(
153 at1[1], [q.shape[0], self.num_heads * self.head_size]
154 )
134 - at2 = auto_functionalized(
135 - self.QUANT_OP, result=output_quant, input=attn_out_view, scale=scale
136 - )
137 - return at2[1]
155 +
156 + return self.quant_matcher(attn_out_view, scale)[0]
157
158 def replacement(
159 q: torch.Tensor,
160 k: torch.Tensor,
161 v: torch.Tensor,
162 output_attn: torch.Tensor,
144 - output_quant: torch.Tensor,
163 scale: torch.Tensor,
164 ):
165 # attn output in quant_dtype
@@ -164,13 +182,10 @@ class AttentionFp8StaticQuantPattern(AttentionQuantPattern):
182 return RESHAPE_OP(at1[1], [-1, self.num_heads * self.head_size])
183
184 inputs = [
167 - self.empty(5, self.num_heads, self.head_size, dtype=self.dtype), # q
168 - self.empty(5, self.num_heads, self.head_size, dtype=self.dtype), # k
169 - self.empty(5, self.num_heads, self.head_size, dtype=self.dtype), # v
170 - self.empty(
171 - 5, self.num_heads, self.head_size, dtype=self.dtype
172 - ), # attn_output
173 - self.empty_quant(5, self.num_heads * self.head_size), # quant_output
185 + self.empty(5, self.num_heads, self.head_size), # q
186 + self.empty(5, self.num_heads, self.head_size), # k
187 + self.empty(5, self.num_heads, self.head_size), # v
188 + self.empty(5, self.num_heads, self.head_size), # attn_output
189 empty_fp32(1, 1), # scale
190 ]
191
@@ -179,7 +194,9 @@ class AttentionFp8StaticQuantPattern(AttentionQuantPattern):
194 replacement,
195 inputs,
196 AttentionQuantPattern.wrap_trace_fn(
182 - AttentionQuantPattern.fx_view_to_reshape, pm.fwd_only
197 + pm.fwd_only,
198 + AttentionQuantPattern.fx_view_to_reshape,
199 + AttentionQuantPattern.remove_noop_permutes,
200 ),
201 pm_pass,
202 )
@@ -279,7 +296,9 @@ class AttentionNvfp4QuantPattern(AttentionQuantPattern):
296 replacement,
297 inputs,
298 AttentionQuantPattern.wrap_trace_fn(
282 - AttentionQuantPattern.fx_view_to_reshape, pm.fwd_only
299 + pm.fwd_only,
300 + AttentionQuantPattern.fx_view_to_reshape,
301 + AttentionQuantPattern.remove_noop_permutes,
302 ),
303 pm_pass,
304 )
vllm/compilation/fx_utils.py
+12 -2
@@ -6,7 +6,7 @@ from collections.abc import Iterable, Iterator
6
7 from torch import fx
8 from torch._higher_order_ops.auto_functionalize import auto_functionalized
9 -from torch._ops import OpOverload
9 +from torch._ops import OpOverload, OpOverloadPacket
10
11
12 def is_func(node: fx.Node, target) -> bool:
@@ -64,7 +64,17 @@ def find_getitem(node: fx.Node, idx: int) -> fx.Node:
64
65
66 # An auto-functionalization-aware utility for finding nodes with a specific op
67 -def find_op_nodes(op: OpOverload, graph: fx.Graph) -> Iterator[fx.Node]:
67 +# Also handles op overload packets and finds all overloads
68 +def find_op_nodes(
69 + op: OpOverload | OpOverloadPacket, graph: fx.Graph
70 +) -> Iterator[fx.Node]:
71 + if isinstance(op, OpOverloadPacket):
72 + for overload in op.overloads():
73 + overload_op = getattr(op, overload)
74 + yield from find_op_nodes(overload_op, graph)
75 + return
76 +
77 + assert isinstance(op, OpOverload)
78 if not op._schema.is_mutable:
79 yield from graph.find_nodes(op="call_function", target=op)
80
vllm/compilation/matcher_utils.py new
+208
@@ -0,0 +1,208 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from abc import ABC, abstractmethod
4 +
5 +import torch
6 +from torch._higher_order_ops import auto_functionalized
7 +from torch._ops import OpOverload
8 +
9 +from vllm.config import get_current_vllm_config
10 +from vllm.model_executor.layers.layernorm import RMSNorm
11 +from vllm.model_executor.layers.quantization.input_quant_fp8 import QuantFP8
12 +from vllm.model_executor.layers.quantization.utils.quant_utils import (
13 + QuantKey,
14 + _normalize_quant_group_shape,
15 + kFp8DynamicTensorSym,
16 + kFp8DynamicTokenSym,
17 + kFp8StaticTensorSym,
18 + kNvfp4Quant,
19 +)
20 +from vllm.platforms import current_platform
21 +
22 +RMS_OP = torch.ops._C.rms_norm.default
23 +RMS_ADD_OP = torch.ops._C.fused_add_rms_norm.default
24 +
25 +QUANT_OPS: dict[QuantKey, OpOverload] = {
26 + kFp8StaticTensorSym: torch.ops._C.static_scaled_fp8_quant.default, # noqa: E501
27 + kFp8DynamicTensorSym: torch.ops._C.dynamic_scaled_fp8_quant.default, # noqa: E501
28 + kFp8DynamicTokenSym: torch.ops._C.dynamic_per_token_scaled_fp8_quant.default, # noqa: E501
29 +}
30 +
31 +if current_platform.is_cuda() and hasattr(torch.ops._C, "scaled_fp4_quant"):
32 + QUANT_OPS[kNvfp4Quant] = torch.ops._C.scaled_fp4_quant.default # noqa: E501
33 +
34 +
35 +class MatcherCustomOp(ABC):
36 + def __init__(self, enabled: bool):
37 + config = get_current_vllm_config()
38 + self.model_dtype = config.model_config.dtype if config.model_config else None
39 + self.device = config.device_config.device if config.device_config else None
40 +
41 + self.enabled = enabled
42 + self.forward = self.forward_custom if enabled else self.forward_native
43 +
44 + @abstractmethod
45 + def forward_custom(self, *args, **kws):
46 + pass
47 +
48 + @abstractmethod
49 + def forward_native(self, *args, **kws):
50 + pass
51 +
52 + def __call__(self, *args, **kws):
53 + return self.forward(*args, **kws)
54 +
55 + def empty(self, *args, **kws):
56 + return torch.empty(*args, dtype=self.model_dtype, device=self.device, **kws)
57 +
58 + def empty_f32(self, *args, **kws):
59 + return torch.empty(*args, dtype=torch.float32, device=self.device, **kws)
60 +
61 + def inputs(self) -> list[torch.Tensor]:
62 + """Utility for inputs to the pattern"""
63 + raise NotImplementedError
64 +
65 +
66 +class MatcherRMSNorm(MatcherCustomOp):
67 + def __init__(self, epsilon: float, enabled: bool | None = None):
68 + if enabled is None:
69 + enabled = RMSNorm.enabled()
70 +
71 + super().__init__(enabled)
72 + self.epsilon = epsilon
73 +
74 + def inputs(self):
75 + input = self.empty(5, 16) if self.enabled else self.empty_f32(5, 16)
76 + weight = self.empty(16)
77 + return [input, weight]
78 +
79 + def forward_custom(
80 + self,
81 + input: torch.Tensor,
82 + weight: torch.Tensor,
83 + ) -> torch.Tensor:
84 + result = torch.empty_like(input)
85 + _, result = auto_functionalized(
86 + RMS_OP,
87 + result=result,
88 + input=input,
89 + weight=weight,
90 + epsilon=self.epsilon,
91 + )
92 +
93 + return result
94 +
95 + def forward_native(
96 + self,
97 + input: torch.Tensor,
98 + weight: torch.Tensor,
99 + ) -> torch.Tensor:
100 + return RMSNorm.forward_static(
101 + input, self.epsilon, input.size(-1), self.model_dtype, weight
102 + )
103 +
104 +
105 +class MatcherFusedAddRMSNorm(MatcherCustomOp):
106 + def __init__(self, epsilon: float, enabled: bool | None = None):
107 + if enabled is None:
108 + enabled = RMSNorm.enabled()
109 +
110 + super().__init__(enabled)
111 + self.epsilon = epsilon
112 +
113 + def inputs(self):
114 + input = self.empty(5, 16) if self.enabled else self.empty_f32(5, 16)
115 + weight = self.empty(16)
116 + residual = self.empty(5, 16)
117 + return [input, weight, residual]
118 +
119 + def forward_custom(
120 + self,
121 + input: torch.Tensor,
122 + weight: torch.Tensor,
123 + residual: torch.Tensor,
124 + ) -> tuple[torch.Tensor, torch.Tensor]:
125 + _, result, residual = auto_functionalized(
126 + RMS_ADD_OP,
127 + input=input,
128 + residual=residual,
129 + weight=weight,
130 + epsilon=self.epsilon,
131 + )
132 +
133 + return result, residual
134 +
135 + def forward_native(
136 + self,
137 + input: torch.Tensor,
138 + weight: torch.Tensor,
139 + residual: torch.Tensor,
140 + ) -> tuple[torch.Tensor, torch.Tensor]:
141 + return RMSNorm.forward_static(
142 + input, self.epsilon, input.size(-1), self.model_dtype, weight, residual
143 + )
144 +
145 +
146 +class MatcherQuantFP8(MatcherCustomOp):
147 + def __init__(self, quant_key: QuantKey, enabled: bool | None = None):
148 + if enabled is None:
149 + enabled = QuantFP8.enabled()
150 +
151 + super().__init__(enabled)
152 + self.quant_key = quant_key
153 + assert quant_key in QUANT_OPS, f"unsupported quantization scheme {quant_key}"
154 + self.QUANT_OP = QUANT_OPS[quant_key]
155 +
156 + assert quant_key.dtype == current_platform.fp8_dtype(), (
157 + "Only QuantFP8 supported by"
158 + )
159 + assert quant_key.scale2 is None
160 + self.quant_fp8 = QuantFP8(quant_key.scale.static, quant_key.scale.group_shape)
161 +
162 + def forward_custom(
163 + self,
164 + input: torch.Tensor,
165 + scale: torch.Tensor | None = None,
166 + ) -> tuple[torch.Tensor, torch.Tensor]:
167 + result = torch.empty(
168 + input.shape, device=input.device, dtype=self.quant_key.dtype
169 + )
170 +
171 + if self.quant_key.scale.static:
172 + assert scale is not None
173 + _, result = auto_functionalized(
174 + self.QUANT_OP, result=result, input=input, scale=scale
175 + )
176 + return result, scale
177 + else:
178 + assert scale is None
179 + scale = self.make_scale(input)
180 + _, result, scale = auto_functionalized(
181 + self.QUANT_OP, result=result, input=input, scale=scale, scale_ub=None
182 + )
183 + return result, scale
184 +
185 + def forward_native(
186 + self,
187 + input: torch.Tensor,
188 + scale: torch.Tensor | None = None,
189 + ) -> tuple[torch.Tensor, torch.Tensor]:
190 + return self.quant_fp8(input, scale)
191 +
192 + def make_scale(self, input: torch.Tensor):
193 + normalized_group_shape = _normalize_quant_group_shape(
194 + input, self.quant_key.scale.group_shape
195 + )
196 + scale_shape = (
197 + input.shape[0] // normalized_group_shape[0],
198 + input.shape[1] // normalized_group_shape[1],
199 + )
200 +
201 + return torch.empty(scale_shape, device=input.device, dtype=torch.float32)
202 +
203 + def inputs(self) -> list[torch.Tensor]:
204 + input = self.empty(5, 16)
205 + if self.quant_key.scale.static:
206 + return [input, self.empty_f32(1, 1)]
207 +
208 + return [input]
vllm/compilation/monitor.py
+1
@@ -22,6 +22,7 @@ def start_monitoring_torch_compile(vllm_config: VllmConfig):
22 import depyf
23
24 path.mkdir(parents=True, exist_ok=True)
25 + logger.debug("Dumping depyf output to %s", path)
26 global context_manager
27 context_manager = depyf.prepare_debug(path.as_posix())
28 context_manager.__enter__()
vllm/compilation/pass_manager.py
+20 -17
@@ -5,7 +5,7 @@ import functools
5 from torch import fx as fx
6
7 from vllm import envs
8 -from vllm.config import VllmConfig
8 +from vllm.config import VllmConfig, set_current_vllm_config
9 from vllm.logger import init_logger
10 from vllm.platforms import current_platform
11 from vllm.utils import set_env_var
@@ -88,27 +88,30 @@ class PostGradPassManager(CustomGraphPass):
88
89 def configure(self, config: VllmConfig):
90 self.pass_config = config.compilation_config.pass_config
91 - if self.pass_config.enable_noop:
92 - self.passes += [NoOpEliminationPass(config)]
91
94 - if self.pass_config.enable_sequence_parallelism:
95 - self.passes += [SequenceParallelismPass(config)]
96 - if self.pass_config.enable_async_tp:
97 - self.passes += [AsyncTPPass(config)]
92 + # Set the current vllm config to allow tracing CustomOp instances
93 + with set_current_vllm_config(config, check_compile=False):
94 + if self.pass_config.enable_noop:
95 + self.passes += [NoOpEliminationPass(config)]
96
99 - if self.pass_config.enable_fi_allreduce_fusion:
100 - self.passes += [AllReduceFusionPass(config)]
97 + if self.pass_config.enable_sequence_parallelism:
98 + self.passes += [SequenceParallelismPass(config)]
99 + if self.pass_config.enable_async_tp:
100 + self.passes += [AsyncTPPass(config)]
101
102 - if self.pass_config.enable_fusion:
103 - self.passes += [RMSNormQuantFusionPass(config)]
104 - self.passes += [ActivationQuantFusionPass(config)]
102 + if self.pass_config.enable_fi_allreduce_fusion:
103 + self.passes += [AllReduceFusionPass(config)]
104
106 - if self.pass_config.enable_attn_fusion:
107 - self.passes += [AttnFusionPass(config)]
105 + if self.pass_config.enable_fusion:
106 + self.passes += [RMSNormQuantFusionPass(config)]
107 + self.passes += [ActivationQuantFusionPass(config)]
108
109 - # needs a functional graph
110 - self.post_cleanup = PostCleanupPass(config)
111 - self.fix_functionalization = FixFunctionalizationPass(config)
109 + if self.pass_config.enable_attn_fusion:
110 + self.passes += [AttnFusionPass(config)]
111 +
112 + # needs a functional graph
113 + self.post_cleanup = PostCleanupPass(config)
114 + self.fix_functionalization = FixFunctionalizationPass(config)
115
116 # [HACK: Bug with Inductor graph partition and torch.compile cache]
117 # In PyTorch 2.9, torch.compile has a bug where the graph
vllm/compilation/vllm_inductor_pass.py
+2 -1
@@ -128,7 +128,8 @@ class VllmPatternMatcherPass(VllmInductorPass):
128 f" please add to dump_patterns if there are any errors.\n\n"
129 f"from torch._higher_order_ops.auto_functionalize import "
130 f"auto_functionalized as auto_functionalized\n"
131 - f"from torch._inductor.pattern_matcher import *",
131 + f"from torch._inductor.pattern_matcher import *\n"
132 + f"vllm = torch.ops.vllm",
133 file=f,
134 )
135
vllm/model_executor/layers/layernorm.py
+39 -20
@@ -178,14 +178,11 @@ class RMSNorm(CustomOp):
178 self.variance_size_override = (
179 None if var_hidden_size == hidden_size else var_hidden_size
180 )
181 + weight_dtype = dtype or torch.get_default_dtype()
182 self.has_weight = has_weight
182 - if dtype is not None:
183 - self.weight = torch.ones(hidden_size, dtype=dtype)
184 - else:
185 - self.weight = torch.ones(hidden_size)
183 + self.weight = torch.ones(hidden_size, dtype=weight_dtype)
184 if self.has_weight:
185 self.weight = nn.Parameter(self.weight)
188 - weight_dtype = self.weight.data.dtype
186
187 if current_platform.is_rocm():
188 self.rocm_norm_func = dispatch_rocm_rmsnorm_func(
@@ -195,47 +192,69 @@ class RMSNorm(CustomOp):
192 with_fused_add=True, dtype=weight_dtype
193 )
194
198 - def forward_native(
199 - self,
195 + @staticmethod
196 + def forward_static(
197 x: torch.Tensor,
198 + variance_epsilon: float,
199 + hidden_size: int,
200 + orig_dtype: torch.dtype,
201 + weight: torch.Tensor | None = None,
202 residual: torch.Tensor | None = None,
203 + variance_size_override: int | None = None,
204 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
205 """PyTorch-native implementation equivalent to forward()."""
204 - orig_dtype = x.dtype
206 x = x.to(torch.float32)
207 if residual is not None:
207 - x = x + residual.to(torch.float32)
208 + # residual promoted f16->f32 automatically,
209 + # otherwise Inductor eliminates the casts to and from f16,
210 + # increasing memory usage (and complicating pattern matching)
211 + x = x + residual
212 residual = x.to(orig_dtype)
213
210 - hidden_size = x.shape[-1]
211 - if hidden_size != self.hidden_size:
214 + if x.shape[-1] != hidden_size:
215 raise ValueError(
213 - "Expected hidden_size to be "
214 - f"{self.hidden_size}, but found: {hidden_size}"
216 + f"Expected hidden_size to be {hidden_size}, but found: {x.shape[-1]}"
217 )
218
217 - if self.variance_size_override is None:
219 + if variance_size_override is None:
220 x_var = x
221 else:
220 - if hidden_size < self.variance_size_override:
222 + if hidden_size < variance_size_override:
223 raise ValueError(
224 "Expected hidden_size to be at least "
223 - f"{self.variance_size_override}, but found: {hidden_size}"
225 + f"{variance_size_override}, but found: {hidden_size}"
226 )
227
226 - x_var = x[:, :, : self.variance_size_override]
228 + x_var = x[:, :, :variance_size_override]
229
230 variance = x_var.pow(2).mean(dim=-1, keepdim=True)
231
230 - x = x * torch.rsqrt(variance + self.variance_epsilon)
232 + x = x * torch.rsqrt(variance + variance_epsilon)
233 x = x.to(orig_dtype)
232 - if self.has_weight:
233 - x = x * self.weight
234 + if weight is not None:
235 + x = x * weight
236 if residual is None:
237 return x
238 else:
239 return x, residual
240
241 + def forward_native(
242 + self,
243 + x: torch.Tensor,
244 + residual: torch.Tensor | None = None,
245 + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
246 + """PyTorch-native implementation equivalent to forward()."""
247 +
248 + return self.forward_static(
249 + x,
250 + self.variance_epsilon,
251 + self.hidden_size,
252 + x.dtype,
253 + self.weight.data if self.has_weight else None,
254 + residual,
255 + self.variance_size_override,
256 + )
257 +
258 def forward_cuda(
259 self,
260 x: torch.Tensor,