main
py 365 lines 13.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 import torch
6 import torch.nn.functional as F
7
8 from vllm.utils.import_utils import has_helion
9
10 if not has_helion():
11 pytest.skip(
12 "Helion is not installed. Install with: pip install vllm[helion]",
13 allow_module_level=True,
14 )
15
16 from vllm.kernels.helion.case_key import CaseKey
17 from vllm.kernels.helion.config_manager import ConfigManager
18 from vllm.kernels.helion.ops.silu_mul_fp8 import (
19 _pick_cache,
20 pick_silu_mul_fp8_config,
21 silu_mul_fp8,
22 silu_mul_fp8_baseline,
23 )
24
25
26 def skip_if_platform_unsupported():
27 try:
28 from vllm.kernels.helion.utils import get_canonical_gpu_name
29
30 if not torch.cuda.is_available():
31 pytest.skip("CUDA not available")
32
33 platform = get_canonical_gpu_name()
34
35 try:
36 config_manager = ConfigManager.get_instance()
37 except RuntimeError:
38 config_manager = ConfigManager()
39
40 configs = config_manager.get_platform_configs("silu_mul_fp8", platform)
41 if len(configs) == 0:
42 pytest.skip("Current GPU platform not supported for silu_mul_fp8 kernel")
43
44 except (ImportError, RuntimeError, KeyError):
45 pytest.skip("Error detecting platform support for silu_mul_fp8 kernel")
46
47
48 @pytest.fixture(autouse=True)
49 def reset_config_manager_singleton():
50 ConfigManager.reset_instance()
51 ConfigManager()
52 yield
53 ConfigManager.reset_instance()
54
55
56 class TestSiluMulFp8ConfigPicker:
57 def setup_method(self):
58 _pick_cache.clear()
59
60 def test_config_picker_exact_match(self):
61 config_keys = [
62 CaseKey({"intermediate": 2048, "numtokens": 256}),
63 CaseKey({"intermediate": 4096, "numtokens": 256}),
64 ]
65
66 input_tensor = torch.randn(32, 4096, dtype=torch.bfloat16, device="cuda")
67 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
68 args = (input_tensor, scale)
69
70 selected_key = pick_silu_mul_fp8_config(args, config_keys)
71 assert selected_key == CaseKey({"intermediate": 2048, "numtokens": 256})
72
73 def test_config_picker_closest_match(self):
74 config_keys = [
75 CaseKey({"intermediate": 2048, "numtokens": 256}),
76 CaseKey({"intermediate": 4096, "numtokens": 256}),
77 ]
78 input_tensor = torch.randn(32, 7000, dtype=torch.bfloat16, device="cuda")
79 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
80 args = (input_tensor, scale)
81
82 selected_key = pick_silu_mul_fp8_config(args, config_keys)
83 assert selected_key == CaseKey({"intermediate": 4096, "numtokens": 256})
84
85 def test_config_picker_no_configs(self):
86 config_keys: list[CaseKey] = []
87
88 input_tensor = torch.randn(32, 4096, dtype=torch.bfloat16, device="cuda")
89 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
90 args = (input_tensor, scale)
91
92 selected_key = pick_silu_mul_fp8_config(args, config_keys)
93 assert selected_key is None
94
95 @pytest.mark.parametrize("intermediate_size", [2048, 4096, 5120])
96 def test_config_picker_different_sizes(self, intermediate_size):
97 config_keys = [
98 CaseKey({"intermediate": 2048, "numtokens": 256}),
99 CaseKey({"intermediate": 4096, "numtokens": 256}),
100 CaseKey({"intermediate": 5120, "numtokens": 256}),
101 ]
102
103 input_tensor = torch.randn(
104 32, 2 * intermediate_size, dtype=torch.bfloat16, device="cuda"
105 )
106 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
107 args = (input_tensor, scale)
108
109 selected_key = pick_silu_mul_fp8_config(args, config_keys)
110 assert selected_key == {
111 "intermediate": intermediate_size,
112 "numtokens": 256,
113 }
114
115 def test_config_picker_numtokens_ceiling(self):
116 config_keys = [
117 CaseKey({"intermediate": 4096, "numtokens": 8}),
118 CaseKey({"intermediate": 4096, "numtokens": 32}),
119 CaseKey({"intermediate": 4096, "numtokens": 128}),
120 CaseKey({"intermediate": 4096, "numtokens": 256}),
121 ]
122 input_tensor = torch.randn(20, 8192, dtype=torch.bfloat16, device="cuda")
123 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
124
125 selected_key = pick_silu_mul_fp8_config((input_tensor, scale), config_keys)
126 assert selected_key == CaseKey({"intermediate": 4096, "numtokens": 32})
127
128 def test_config_picker_numtokens_exact(self):
129 config_keys = [
130 CaseKey({"intermediate": 4096, "numtokens": 8}),
131 CaseKey({"intermediate": 4096, "numtokens": 32}),
132 CaseKey({"intermediate": 4096, "numtokens": 128}),
133 ]
134 input_tensor = torch.randn(32, 8192, dtype=torch.bfloat16, device="cuda")
135 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
136
137 selected_key = pick_silu_mul_fp8_config((input_tensor, scale), config_keys)
138 assert selected_key == CaseKey({"intermediate": 4096, "numtokens": 32})
139
140 def test_config_picker_numtokens_fallback_to_largest(self):
141 config_keys = [
142 CaseKey({"intermediate": 4096, "numtokens": 8}),
143 CaseKey({"intermediate": 4096, "numtokens": 32}),
144 CaseKey({"intermediate": 4096, "numtokens": 128}),
145 ]
146 input_tensor = torch.randn(512, 8192, dtype=torch.bfloat16, device="cuda")
147 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
148
149 selected_key = pick_silu_mul_fp8_config((input_tensor, scale), config_keys)
150 assert selected_key == CaseKey({"intermediate": 4096, "numtokens": 128})
151
152
153 class TestSiluMulFp8Correctness:
154 @pytest.mark.parametrize("batch_size", [1, 8, 32, 128])
155 @pytest.mark.parametrize("intermediate_size", [2048, 3000, 3500, 4096, 5000])
156 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
157 def test_silu_mul_fp8_correctness(self, batch_size, intermediate_size, dtype):
158 skip_if_platform_unsupported()
159
160 input_size = 2 * intermediate_size
161 input_tensor = torch.randn(batch_size, input_size, dtype=dtype, device="cuda")
162 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
163
164 reference_output = silu_mul_fp8_baseline(input_tensor, scale)
165 helion_output = silu_mul_fp8(input_tensor, scale)
166
167 assert helion_output.shape == reference_output.shape
168 assert helion_output.dtype == torch.float8_e4m3fn
169 assert reference_output.dtype == torch.float8_e4m3fn
170
171 ref_f32 = reference_output.to(torch.float32)
172 helion_f32 = helion_output.to(torch.float32)
173 # FP8 E4M3 has limited precision. Values near quantization boundaries
174 # can round differently due to intermediate precision differences.
175 torch.testing.assert_close(
176 helion_f32,
177 ref_f32,
178 atol=0.05,
179 rtol=0.05,
180 msg=f"Mismatch at batch={batch_size}, size={intermediate_size}",
181 )
182
183 def test_silu_mul_fp8_shape_inference(self):
184 skip_if_platform_unsupported()
185 batch_size, input_size = 32, 8192
186 intermediate_size = input_size // 2
187
188 input_tensor = torch.randn(
189 batch_size, input_size, dtype=torch.bfloat16, device="cuda"
190 )
191 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
192
193 output = silu_mul_fp8(input_tensor, scale)
194
195 expected_shape = (batch_size, intermediate_size)
196 assert output.shape == expected_shape
197 assert output.dtype == torch.float8_e4m3fn
198
199 def test_silu_mul_fp8_scale_variations(self):
200 skip_if_platform_unsupported()
201 batch_size, input_size = 16, 4096
202
203 input_tensor = torch.randn(
204 batch_size, input_size, dtype=torch.bfloat16, device="cuda"
205 )
206
207 scales = [0.1, 0.5, 1.0, 2.0, 10.0]
208
209 for scale_val in scales:
210 scale = torch.tensor([scale_val], dtype=torch.float32, device="cuda")
211
212 reference_output = silu_mul_fp8_baseline(input_tensor, scale)
213 helion_output = silu_mul_fp8(input_tensor, scale)
214 ref_f32 = reference_output.to(torch.float32)
215 helion_f32 = helion_output.to(torch.float32)
216
217 torch.testing.assert_close(
218 helion_f32,
219 ref_f32,
220 atol=0.05,
221 rtol=0.05,
222 msg=f"Mismatch for scale={scale_val}",
223 )
224
225 @pytest.mark.parametrize(
226 "shape",
227 [
228 (1, 4096),
229 (16, 4096),
230 (128, 4096),
231 (1024, 4096),
232 (1, 8192),
233 (16, 8192),
234 (128, 8192),
235 ],
236 )
237 def test_silu_mul_fp8_various_shapes(self, shape):
238 skip_if_platform_unsupported()
239
240 input_tensor = torch.randn(*shape, dtype=torch.bfloat16, device="cuda")
241 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
242
243 reference_output = silu_mul_fp8_baseline(input_tensor, scale)
244 helion_output = silu_mul_fp8(input_tensor, scale)
245
246 assert helion_output.shape == reference_output.shape
247
248 ref_f32 = reference_output.to(torch.float32)
249 helion_f32 = helion_output.to(torch.float32)
250
251 torch.testing.assert_close(
252 helion_f32, ref_f32, atol=0.05, rtol=0.05, msg=f"Mismatch for shape={shape}"
253 )
254
255
256 def silu_mul_fp8_pytorch(input: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
257 """Pure PyTorch reference using F.silu.
258
259 This matches vLLM's SiluAndMul.forward_native exactly:
260 F.silu(x[..., :d]) * x[..., d:]
261 """
262 d = input.shape[-1] // 2
263 result = F.silu(input[..., :d]) * input[..., d:]
264 return (result.to(torch.float32) / scale).to(torch.float8_e4m3fn)
265
266
267 class TestSiluMulFp8PytorchReference:
268 """Tests comparing Helion kernel against pure PyTorch implementation.
269
270 Uses tighter tolerance since both use PyTorch's FP8 conversion
271 (same rounding mode), unlike the vLLM C++ baseline which uses
272 NVIDIA's hardware FP8 conversion with different rounding.
273 """
274
275 @pytest.mark.parametrize("batch_size", [1, 8, 32, 128, 256])
276 @pytest.mark.parametrize("intermediate_size", [1024, 2048, 4096])
277 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
278 def test_silu_mul_fp8_vs_pytorch(self, batch_size, intermediate_size, dtype):
279 skip_if_platform_unsupported()
280
281 input_tensor = torch.randn(
282 batch_size, 2 * intermediate_size, dtype=dtype, device="cuda"
283 )
284 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
285
286 pytorch_output = silu_mul_fp8_pytorch(input_tensor, scale)
287 helion_output = silu_mul_fp8(input_tensor, scale)
288
289 assert helion_output.shape == pytorch_output.shape
290 assert helion_output.dtype == torch.float8_e4m3fn
291
292 pytorch_f32 = pytorch_output.to(torch.float32)
293 helion_f32 = helion_output.to(torch.float32)
294
295 # Tolerance accounts for FP8 quantization boundary effects
296 torch.testing.assert_close(
297 helion_f32,
298 pytorch_f32,
299 atol=0.05,
300 rtol=0.05,
301 msg=(
302 f"Mismatch at batch={batch_size}, size={intermediate_size}, "
303 f"dtype={dtype}"
304 ),
305 )
306
307 @pytest.mark.parametrize(
308 "shape",
309 [
310 (1, 2, 4096), # 3D input
311 (2, 4, 2048), # 3D input
312 (1, 1, 1, 8192), # 4D input
313 ],
314 )
315 def test_silu_mul_fp8_multidim_vs_pytorch(self, shape):
316 skip_if_platform_unsupported()
317
318 input_tensor = torch.randn(*shape, dtype=torch.bfloat16, device="cuda")
319 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
320
321 pytorch_output = silu_mul_fp8_pytorch(input_tensor, scale)
322 helion_output = silu_mul_fp8(input_tensor, scale)
323
324 assert helion_output.shape == pytorch_output.shape
325
326 pytorch_f32 = pytorch_output.to(torch.float32)
327 helion_f32 = helion_output.to(torch.float32)
328
329 torch.testing.assert_close(
330 helion_f32,
331 pytorch_f32,
332 atol=0.05,
333 rtol=0.05,
334 msg=f"Mismatch for shape={shape}",
335 )
336
337
338 class TestSiluMulFp8Integration:
339 def test_kernel_registration_integration(self):
340 from vllm.kernels.helion.register import get_registered_kernels
341
342 registered_kernels = get_registered_kernels()
343 assert "silu_mul_fp8" in registered_kernels
344
345 kernel_wrapper = registered_kernels["silu_mul_fp8"]
346 assert kernel_wrapper.op_name == "silu_mul_fp8"
347 assert kernel_wrapper._config_picker is not None
348
349 def test_fake_impl_functionality(self):
350 skip_if_platform_unsupported()
351 from vllm.kernels.helion.register import get_registered_kernels
352
353 input_tensor = torch.randn(32, 4096, dtype=torch.bfloat16, device="cuda")
354 scale = torch.tensor([0.5], dtype=torch.float32, device="cuda")
355 registered_kernels = get_registered_kernels()
356 kernel_wrapper = registered_kernels["silu_mul_fp8"]
357 fake_impl = kernel_wrapper._fake_impl
358 assert fake_impl is not None
359
360 fake_output = fake_impl(input_tensor, scale)
361
362 expected_shape = (32, 2048)
363 assert fake_output.shape == expected_shape
364 assert fake_output.dtype == torch.float8_e4m3fn
365 assert fake_output.device == input_tensor.device