main
py 340 lines 11.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import copy
5
6 import pytest
7 import torch
8
9 from tests.compile.backend import TestBackend
10 from tests.utils import TestFP8Layer
11 from vllm.compilation.passes.fusion.act_quant_fusion import (
12 ActivationQuantFusionPass,
13 )
14 from vllm.compilation.passes.fusion.rms_quant_fusion import RMSNormQuantFusionPass
15 from vllm.compilation.passes.fx_utils import find_auto_fn, find_auto_fn_maybe, is_func
16 from vllm.compilation.passes.utility.fix_functionalization import (
17 FixFunctionalizationPass,
18 )
19 from vllm.compilation.passes.utility.noop_elimination import NoOpEliminationPass
20 from vllm.compilation.passes.utility.post_cleanup import PostCleanupPass
21 from vllm.config import (
22 CompilationConfig,
23 ModelConfig,
24 PassConfig,
25 VllmConfig,
26 get_current_vllm_config,
27 set_current_vllm_config,
28 )
29 from vllm.model_executor.layers.activation import SiluAndMul
30 from vllm.model_executor.layers.layernorm import RMSNorm
31 from vllm.model_executor.layers.quantization.utils.quant_utils import (
32 kFp8StaticTensorSym,
33 )
34 from vllm.model_executor.layers.rotary_embedding import get_rope
35 from vllm.platforms import current_platform
36 from vllm.utils.torch_utils import direct_register_custom_op
37
38 TEST_FP8 = current_platform.supports_fp8()
39 FP8_DTYPE = current_platform.fp8_dtype()
40
41
42 class TestSiluMul(torch.nn.Module):
43 quant_key = kFp8StaticTensorSym
44
45 def __init__(self, hidden_size: int = 128):
46 super().__init__()
47 self.silu_and_mul = SiluAndMul()
48 if TEST_FP8:
49 self.fp8_linear = TestFP8Layer(
50 weight_shape=(hidden_size, hidden_size),
51 activation_quant_key=self.quant_key,
52 weight_quant_key=self.quant_key,
53 input_dtype=get_current_vllm_config().model_config.dtype,
54 )
55
56 def forward(self, x):
57 y = self.silu_and_mul(x)
58 if TEST_FP8:
59 return self.fp8_linear(y)
60 else:
61 return y
62
63 def example_inputs(self, num_tokens=32, hidden_size=128):
64 return (torch.rand(num_tokens, hidden_size * 2),)
65
66 def ops_in_model(self, do_fusion):
67 if TEST_FP8 and do_fusion:
68 return [torch.ops._C.silu_and_mul_quant.default]
69 else:
70 return [torch.ops._C.silu_and_mul.default]
71
72 def ops_not_in_model(self):
73 return []
74
75
76 class TestFusedAddRMSNorm(torch.nn.Module):
77 quant_key = kFp8StaticTensorSym
78
79 def __init__(self, hidden_size=16, intermediate_size=32):
80 super().__init__()
81 self.hidden_size = hidden_size
82 self.intermediate_size = intermediate_size
83
84 self.gate_proj = torch.nn.Parameter(
85 torch.empty((intermediate_size, hidden_size))
86 )
87 self.norm = RMSNorm(intermediate_size, 1e-05)
88 self.norm.weight = torch.nn.Parameter(torch.ones(intermediate_size))
89
90 torch.nn.init.normal_(self.gate_proj, std=0.02)
91
92 if TEST_FP8:
93 self.fp8_linear = TestFP8Layer(
94 weight_shape=(hidden_size, intermediate_size),
95 activation_quant_key=self.quant_key,
96 weight_quant_key=self.quant_key,
97 input_dtype=get_current_vllm_config().model_config.dtype,
98 )
99
100 def forward(self, hidden_states, residual):
101 # Reshape input
102 view = hidden_states.reshape(-1, self.hidden_size)
103
104 # matrix multiplication
105 permute = self.gate_proj.permute(1, 0)
106 mm = torch.mm(view, permute)
107
108 # layer normalization
109 norm_output, residual_output = self.norm(mm, residual)
110
111 if TEST_FP8:
112 # scaled_mm with static input quantization
113 fp8_linear_result = self.fp8_linear(norm_output)
114
115 return fp8_linear_result, residual_output
116
117 else:
118 return norm_output, residual_output
119
120 def example_inputs(self, batch_size=8, seq_len=16):
121 hidden_states = torch.randn((batch_size * seq_len, self.hidden_size))
122 residual = torch.randn((batch_size * seq_len, self.intermediate_size))
123 return (hidden_states, residual)
124
125 def ops_in_model(self, do_fusion):
126 if TEST_FP8 and do_fusion:
127 return [torch.ops._C.fused_add_rms_norm_static_fp8_quant.default]
128 else:
129 return []
130
131 def ops_not_in_model(self):
132 return []
133
134
135 class TestRotaryEmbedding(torch.nn.Module):
136 def __init__(self, head_dim=64, max_position=2048, base=10000):
137 super().__init__()
138 self.head_dim = head_dim
139
140 self.rotary_emb = get_rope(
141 self.head_dim,
142 max_position=max_position,
143 rope_parameters={"rope_type": "default", "rope_theta": base},
144 )
145
146 def forward(self, positions, q, k):
147 q_rotated, k_rotated = self.rotary_emb(positions, q, k)
148 return q_rotated, k_rotated
149
150 def example_inputs(self, num_tokens=32, head_dim=64):
151 positions = torch.arange(num_tokens, dtype=torch.long)
152 q = torch.randn(num_tokens, head_dim)
153 k = torch.randn(num_tokens, head_dim)
154 return (positions, q, k)
155
156 def ops_in_model(self, do_fusion):
157 return [torch.ops._C.rotary_embedding.default]
158
159 def ops_not_in_model(self):
160 return []
161
162
163 class TestRotaryEmbeddingSliceScatter(torch.nn.Module):
164 def __init__(self, head_dim=64, num_heads=4, max_position=2048, base=10000):
165 super().__init__()
166 self.head_dim = head_dim
167 self.num_heads = num_heads
168 self.hidden_size = head_dim * num_heads
169
170 self.qkv_proj = torch.nn.Linear(
171 self.hidden_size, self.hidden_size * 3, bias=False
172 )
173
174 self.rotary_emb = get_rope(
175 self.head_dim,
176 max_position=max_position,
177 rope_parameters={"rope_type": "default", "rope_theta": base},
178 )
179
180 def forward(self, positions, hidden_states):
181 # Simulate the pattern: mm -> split_with_sizes -> rotary_embedding
182 # -> slice_scatter -> split_with_sizes
183
184 qkv = self.qkv_proj(hidden_states)
185 split_sizes = [self.hidden_size, self.hidden_size, self.hidden_size]
186 q, k, v = torch.split(qkv, split_sizes, dim=-1)
187
188 q_rotated, k_rotated = self.rotary_emb(positions, q, k)
189
190 qkv_updated = torch.cat([q_rotated, k_rotated, v], dim=-1)
191 return qkv_updated
192
193 def example_inputs(self, num_tokens=32, head_dim=64, num_heads=4):
194 hidden_size = head_dim * num_heads
195 positions = torch.arange(num_tokens, dtype=torch.long)
196 hidden_states = torch.randn(num_tokens, hidden_size)
197 return (positions, hidden_states)
198
199 def ops_in_model(self, do_fusion):
200 return [torch.ops._C.rotary_embedding.default]
201
202 def ops_not_in_model(self):
203 return [torch.ops.aten.slice_scatter.default]
204
205
206 class TestFunctionWithMutatedArgsAndReturn(torch.nn.Module):
207 OP_REGISTERED = False
208
209 def __init__(self):
210 super().__init__()
211 self.register_test_custom_op()
212
213 @classmethod
214 def register_test_custom_op(cls):
215 if not cls.OP_REGISTERED:
216
217 def function_with_mutated_args_and_return_impl(
218 x: torch.Tensor,
219 ) -> torch.Tensor:
220 ret = x + 1
221 x.add_(2)
222 return ret
223
224 def function_with_mutated_args_and_return_fake(
225 x: torch.Tensor,
226 ) -> torch.Tensor:
227 return torch.empty_like(x)
228
229 direct_register_custom_op(
230 op_name="function_with_mutated_args_and_return",
231 op_func=function_with_mutated_args_and_return_impl,
232 mutates_args=["x"],
233 fake_impl=function_with_mutated_args_and_return_fake,
234 )
235
236 cls.OP_REGISTERED = True
237
238 def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
239 # Clone x to avoid mutating the original tensor
240 ret = torch.ops.vllm.function_with_mutated_args_and_return(x)
241 return x, ret
242
243 def example_inputs(self, num_tokens=32):
244 hidden_states = torch.randn(num_tokens)
245 return (hidden_states,)
246
247 def ops_in_model(self, do_fusion):
248 return [torch.ops.vllm.function_with_mutated_args_and_return.default]
249
250 def ops_not_in_model(self):
251 return []
252
253
254 MODELS_AND_DO_FUSION = {
255 TestSiluMul: [True, False],
256 TestFusedAddRMSNorm: [True, False],
257 TestRotaryEmbedding: [False],
258 TestRotaryEmbeddingSliceScatter: [False],
259 TestFunctionWithMutatedArgsAndReturn: [False],
260 }
261
262
263 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
264 @pytest.mark.parametrize(
265 "model_class, do_fusion",
266 [
267 (model_class, do_fusion)
268 for model_class, fusions in MODELS_AND_DO_FUSION.items()
269 for do_fusion in fusions
270 ],
271 )
272 @pytest.mark.skipif(
273 not current_platform.is_cuda_alike(),
274 reason="Only test on cuda and rocm platform",
275 )
276 def test_fix_functionalization(
277 model_class: torch.nn.Module, do_fusion: bool, dtype: torch.dtype
278 ):
279 torch.set_default_device("cuda")
280 torch.set_default_dtype(dtype)
281 torch.manual_seed(0)
282
283 vllm_config = VllmConfig(
284 model_config=ModelConfig(dtype=dtype),
285 compilation_config=CompilationConfig(
286 custom_ops=["all"],
287 pass_config=PassConfig(
288 fuse_norm_quant=do_fusion,
289 fuse_act_quant=do_fusion,
290 eliminate_noops=True,
291 ),
292 ),
293 )
294
295 with set_current_vllm_config(vllm_config):
296 assert RMSNorm.enabled()
297 noop_pass = NoOpEliminationPass(vllm_config)
298 fusion_pass = RMSNormQuantFusionPass(vllm_config)
299 cleanup_pass = PostCleanupPass(vllm_config)
300 act_quant_fusion_pass = ActivationQuantFusionPass(vllm_config)
301
302 passes = (
303 [noop_pass, fusion_pass, act_quant_fusion_pass, cleanup_pass]
304 if do_fusion
305 else [noop_pass, cleanup_pass]
306 )
307 func_pass = FixFunctionalizationPass(vllm_config)
308
309 backend_func = TestBackend(*passes, func_pass)
310 backend_no_func = TestBackend(*passes)
311
312 model = model_class()
313 inputs_func = model.example_inputs()
314 inputs_no_func = copy.deepcopy(inputs_func)
315 model_func = copy.deepcopy(model)
316 model_no_func = copy.deepcopy(model)
317 model_func = torch.compile(model_func, backend=backend_func)
318 model_no_func = torch.compile(model_no_func, backend=backend_no_func)
319
320 # deepcopy inputs to prevent potential in place mutation
321 outputs_func = model_func(*copy.deepcopy(inputs_func))
322 outputs_no_func = model_no_func(*copy.deepcopy(inputs_no_func))
323 torch.testing.assert_close(outputs_func, outputs_no_func)
324
325 # check if the functionalization pass is applied
326 for op in model.ops_in_model(do_fusion):
327 find_auto_fn(backend_no_func.graph_post_pass.nodes, op)
328 assert find_auto_fn_maybe(backend_func.graph_post_pass.nodes, op) is None
329
330 # make sure the ops were all de-functionalized
331 found = dict()
332 for node in backend_func.graph_post_pass.nodes:
333 for op in model.ops_in_model(do_fusion):
334 if is_func(node, op):
335 found[op] = True
336 for op in model.ops_not_in_model():
337 if is_func(node, op):
338 found[op] = True
339 assert all(found[op] for op in model.ops_in_model(do_fusion))
340 assert all(not found.get(op) for op in model.ops_not_in_model())