main
py 444 lines 17.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import pytest
4 import torch
5
6 # This registers op implementations
7 import vllm.kernels # noqa: F401
8 from tests.ir.ir_test_utils import (
9 COMMON_HIDDEN_SIZES,
10 NUM_TOKENS,
11 assert_close,
12 clone_args,
13 supported_providers,
14 )
15 from tests.utils import set_random_seed
16 from vllm import ir
17 from vllm.platforms import current_platform
18
19 pytestmark = pytest.mark.skip_global_cleanup
20
21 rms_norm_native = ir.ops.rms_norm.impls["native"].impl_fn
22
23 IS_GPGPU_DEVICE = current_platform.is_cuda_alike() or current_platform.is_xpu()
24
25
26 @pytest.mark.skipif(
27 not IS_GPGPU_DEVICE,
28 reason="Currently only kernels on CUDA, ROCm and XPU",
29 )
30 def test_rms_norm_registration():
31 expected = {
32 "native": True,
33 "vllm_c": IS_GPGPU_DEVICE,
34 "aiter": current_platform.is_rocm(),
35 "oink": current_platform.has_device_capability(100)
36 and hasattr(torch.ops, "oink")
37 and hasattr(torch.ops.oink, "rmsnorm"),
38 }
39
40 actual = {
41 provider: impl.supported for provider, impl in ir.ops.rms_norm.impls.items()
42 }
43
44 assert actual == expected
45
46
47 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
48 @pytest.mark.parametrize("n_tokens", NUM_TOKENS)
49 @pytest.mark.parametrize("hidden_size", COMMON_HIDDEN_SIZES)
50 @pytest.mark.parametrize("epsilon", [1e-6, 1e-5])
51 @pytest.mark.skipif(
52 not IS_GPGPU_DEVICE,
53 reason="Currently only kernels on CUDA, ROCm and XPU",
54 )
55 class TestRMSNorm:
56 def test_native_semantics(self, dtype, n_tokens, hidden_size, epsilon):
57 set_random_seed(0)
58 x, weight, epsilon = ir.ops.rms_norm.generate_inputs(
59 num_tokens=4,
60 hidden_size=8,
61 dtype=dtype,
62 epsilon=epsilon,
63 device=current_platform.device_type,
64 )
65 out = rms_norm_native(x, weight, epsilon=epsilon)
66
67 # Check shape, dtype, device
68 assert out.shape == x.shape
69 assert out.dtype == x.dtype
70 assert out.device == x.device
71
72 # Check the scaling property of rms norm. This holds only
73 # approximately: epsilon does not scale with x, so
74 # rms_norm(2x) = x / sqrt(mean(x^2) + epsilon/4), which differs from
75 # rms_norm(x) by the epsilon term. Use the op's declared tolerance
76 # rather than a tighter hard-coded one.
77 out2 = rms_norm_native(x * 2.0, weight, epsilon=epsilon)
78 assert_close(ir.ops.rms_norm, out2, out)
79
80 # Mean square should be approximately 1 (ignoring epsilon and weight scaling)
81 combined_norm = out.float() / weight.float()
82 variance = combined_norm.pow(2).mean(dim=-1)
83 # After RMS normalization, variance should be close to 1
84 torch.testing.assert_close(
85 variance, torch.ones_like(variance), rtol=1e-2, atol=1e-2
86 )
87
88 # Check behavior with and without weight
89 weight1 = torch.ones_like(weight)
90 out3 = rms_norm_native(x, weight1, epsilon=epsilon)
91 out4 = rms_norm_native(x, None, epsilon=epsilon)
92 torch.testing.assert_close(out3, out4)
93
94 @pytest.mark.parametrize("provider", supported_providers(ir.ops.rms_norm))
95 def test_impls(self, dtype, n_tokens, hidden_size, epsilon, provider):
96 impl = ir.ops.rms_norm.impls[provider]
97 x, weight, eps = ir.ops.rms_norm.generate_inputs(
98 num_tokens=n_tokens,
99 hidden_size=hidden_size,
100 dtype=dtype,
101 epsilon=epsilon,
102 device=current_platform.device_type,
103 )
104 args = (x, weight, eps)
105
106 if not impl.supports_args(*args):
107 pytest.skip(f"{provider} does not support args")
108
109 ref_output = rms_norm_native(*clone_args(args))
110 output = impl.impl_fn(*clone_args(args))
111 assert_close(ir.ops.rms_norm, output, ref_output)
112
113 # check that dispatched call matches direct call
114 with ir.ops.rms_norm.set_priority([provider, "native"]):
115 out_dispatched = ir.ops.rms_norm(*args)
116 out_direct = impl.impl_fn(*args)
117 torch.testing.assert_close(out_dispatched, out_direct, rtol=0.0, atol=0.0)
118
119 # none of these support variance_size override
120 assert not impl.supports_args(x, weight, eps, 4)
121 assert not impl.supports_args(x, weight, eps, variance_size=4)
122
123 # test weight=None behavior
124 out_no_weight = impl.impl_fn(x, None, eps)
125 out_unit_weight = impl.impl_fn(x, torch.ones_like(weight), eps)
126 assert_close(ir.ops.rms_norm, out_no_weight, out_unit_weight)
127
128 @pytest.mark.parametrize("provider", ["vllm_c", "aiter", "native"])
129 def test_torch_opcheck(self, dtype, n_tokens, hidden_size, epsilon, provider):
130 if not ir.ops.rms_norm.impls[provider].supported:
131 pytest.skip(f"{provider} impl not supported on this platform")
132
133 args = ir.ops.rms_norm.generate_inputs(
134 num_tokens=n_tokens,
135 hidden_size=hidden_size,
136 dtype=dtype,
137 epsilon=epsilon,
138 device=current_platform.device_type,
139 )
140
141 # When checking the torch op, we have to set priority and use dispatch
142 with ir.ops.rms_norm.set_priority([provider, "native"]):
143 torch.library.opcheck(torch.ops.vllm_ir.rms_norm, args)
144
145
146 @pytest.mark.skipif(
147 not current_platform.is_rocm(),
148 reason="aiter is only supported on ROCm",
149 )
150 def test_aiter_rejects_unsupported_dtypes():
151 impl = ir.ops.rms_norm.impls["aiter"]
152 for dtype in [torch.float32, torch.float64]:
153 args = ir.ops.rms_norm.generate_inputs(
154 num_tokens=8,
155 hidden_size=4096,
156 dtype=dtype,
157 epsilon=1e-5,
158 device=current_platform.device_type,
159 )
160 assert not impl.supports_args(*args), f"aiter should reject dtype={dtype}"
161
162
163 @pytest.mark.skipif(
164 not current_platform.is_rocm(),
165 reason="ROCm vllm_c RMSNorm needs explicit ND input handling",
166 )
167 def test_vllm_c_rms_norm_accepts_nd_input():
168 impl = ir.ops.rms_norm.impls["vllm_c"]
169 if not impl.supported:
170 pytest.skip("vllm_c impl not supported on this platform")
171
172 base = torch.randn(
173 3, 8, 192, dtype=torch.float16, device=current_platform.device_type
174 )
175 x = base.split(64, dim=-1)[0].view(3, 8, 4, 16)
176 assert not x.is_contiguous()
177 weight = torch.randn(16, dtype=torch.float16, device=current_platform.device_type)
178 epsilon = 1e-5
179
180 output = impl.impl_fn(x, weight, epsilon)
181 ref_output = rms_norm_native(x, weight, epsilon)
182
183 assert output.shape == x.shape
184 assert_close(ir.ops.rms_norm, output, ref_output)
185
186
187 @pytest.mark.skipif(
188 not current_platform.is_rocm(),
189 reason="ROCm vllm_c RMSNorm needs a contiguous output for strided inputs",
190 )
191 def test_vllm_c_rms_norm_accepts_transposed_input():
192 impl = ir.ops.rms_norm.impls["vllm_c"]
193 if not impl.supported:
194 pytest.skip("vllm_c impl not supported on this platform")
195
196 x = torch.randn(
197 1, 320, 120, dtype=torch.float16, device=current_platform.device_type
198 ).transpose(1, 2)
199 assert x.reshape(-1, x.shape[-1]).stride(-1) != 1
200 weight = torch.randn(320, dtype=torch.float16, device=current_platform.device_type)
201 epsilon = 1e-5
202
203 output = impl.impl_fn(x, weight, epsilon)
204 ref_output = rms_norm_native(x, weight, epsilon)
205
206 assert output.shape == x.shape
207 assert_close(ir.ops.rms_norm, output, ref_output)
208
209
210 fused_add_rms_norm_native = ir.ops.fused_add_rms_norm.impls["native"].impl_fn
211
212
213 @pytest.mark.skipif(
214 not IS_GPGPU_DEVICE,
215 reason="Currently only kernels on CUDA, ROCm and XPU",
216 )
217 def test_fused_add_rms_norm_registration():
218 expected = {
219 "native": True,
220 "vllm_c": IS_GPGPU_DEVICE,
221 "aiter": current_platform.is_rocm(),
222 "oink": current_platform.has_device_capability(100)
223 and hasattr(torch.ops, "oink")
224 and hasattr(torch.ops.oink, "fused_add_rms_norm"),
225 }
226
227 actual = {
228 provider: impl.supported
229 for provider, impl in ir.ops.fused_add_rms_norm.impls.items()
230 }
231
232 assert actual == expected
233
234
235 @pytest.mark.skipif(
236 not current_platform.is_rocm(),
237 reason="ROCm vllm_c fused_add_rms_norm needs explicit ND input handling",
238 )
239 def test_vllm_c_fused_add_rms_norm_accepts_nd_input():
240 impl = ir.ops.fused_add_rms_norm.impls["vllm_c"]
241 if not impl.supported:
242 pytest.skip("vllm_c impl not supported on this platform")
243
244 base = torch.randn(
245 3, 8, 192, dtype=torch.float16, device=current_platform.device_type
246 )
247 residual_base = torch.randn(
248 3, 8, 192, dtype=torch.float16, device=current_platform.device_type
249 )
250 x = base.split(64, dim=-1)[0].view(3, 8, 4, 16)
251 x_residual = residual_base.split(64, dim=-1)[0].view(3, 8, 4, 16)
252 assert not x.is_contiguous()
253 assert not x_residual.is_contiguous()
254 weight = torch.randn(16, dtype=torch.float16, device=current_platform.device_type)
255 epsilon = 1e-5
256
257 output, residual = impl.impl_fn(x.clone(), x_residual.clone(), weight, epsilon)
258 ref_output, ref_residual = fused_add_rms_norm_native(x, x_residual, weight, epsilon)
259
260 assert output.shape == x.shape
261 assert residual.shape == x_residual.shape
262 assert_close(ir.ops.fused_add_rms_norm, output, ref_output)
263 assert_close(ir.ops.fused_add_rms_norm, residual, ref_residual)
264
265
266 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
267 @pytest.mark.parametrize("n_tokens", NUM_TOKENS)
268 @pytest.mark.parametrize("hidden_size", COMMON_HIDDEN_SIZES)
269 @pytest.mark.parametrize("epsilon", [1e-6, 1e-5])
270 @pytest.mark.skipif(
271 not IS_GPGPU_DEVICE,
272 reason="Currently only kernels on CUDA, ROCm and XPU",
273 )
274 class TestFusedAddRMSNorm:
275 def test_native_semantics(self, dtype, n_tokens, hidden_size, epsilon):
276 set_random_seed(0)
277 x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
278 num_tokens=4,
279 hidden_size=8,
280 dtype=dtype,
281 epsilon=epsilon,
282 device=current_platform.device_type,
283 )
284 out, residual_out = fused_add_rms_norm_native(x, x_residual, weight, eps)
285
286 # Check shape, dtype, device
287 assert out.shape == x.shape
288 assert out.dtype == x.dtype
289 assert out.device == x.device
290 assert residual_out.shape == x_residual.shape
291 assert residual_out.dtype == x_residual.dtype
292 assert residual_out.device == x_residual.device
293
294 # Check that residual_out = x + x_residual
295 expected_residual = (x.float() + x_residual.float()).to(dtype)
296 torch.testing.assert_close(
297 residual_out, expected_residual, rtol=1e-3, atol=1e-3
298 )
299
300 # Verify that the output is RMS normalized version of (x + x_residual)
301 expected_out = rms_norm_native(expected_residual, weight, epsilon)
302 assert_close(
303 ir.ops.fused_add_rms_norm,
304 (out, residual_out),
305 (expected_out, expected_residual),
306 )
307
308 # Check the scaling property of rms norm
309 out1, _ = fused_add_rms_norm_native(
310 x, torch.zeros_like(x), weight, epsilon=epsilon
311 )
312 out2, _ = fused_add_rms_norm_native(
313 x * 2.0, torch.zeros_like(x), weight, epsilon=epsilon
314 )
315 assert_close(ir.ops.fused_add_rms_norm, out2, out1)
316
317 # Check behavior with and without weight
318 weight1 = torch.ones_like(weight)
319 out3, _ = fused_add_rms_norm_native(x, x_residual, weight1, eps)
320 out4, _ = fused_add_rms_norm_native(x, x_residual, None, eps)
321 torch.testing.assert_close(out3, out4)
322
323 @pytest.mark.parametrize("provider", supported_providers(ir.ops.fused_add_rms_norm))
324 def test_impls(self, dtype, n_tokens, hidden_size, epsilon, provider):
325 impl = ir.ops.fused_add_rms_norm.impls[provider]
326 x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
327 num_tokens=n_tokens,
328 hidden_size=hidden_size,
329 dtype=dtype,
330 epsilon=epsilon,
331 device=current_platform.device_type,
332 )
333 args = (x, x_residual, weight, eps, None)
334
335 if not impl.supports_args(*args):
336 pytest.skip(f"{provider} does not support args")
337
338 ref_output, ref_residual = fused_add_rms_norm_native(*clone_args(args))
339 output, residual = impl.impl_fn(*clone_args(args))
340 assert_close(ir.ops.fused_add_rms_norm, output, ref_output)
341 assert_close(ir.ops.fused_add_rms_norm, residual, ref_residual)
342
343 # check that dispatched call matches direct call
344 with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
345 out_dispatched, residual_dispatched = ir.ops.fused_add_rms_norm(*args[:4])
346 out_direct, residual_direct = impl.impl_fn(*clone_args(args))
347 torch.testing.assert_close(out_dispatched, out_direct, rtol=0.0, atol=0.0)
348 torch.testing.assert_close(
349 residual_dispatched, residual_direct, rtol=0.0, atol=0.0
350 )
351
352 # none of these support variance_size override
353 assert not impl.supports_args(x, x_residual, weight, epsilon, 4)
354 assert not impl.supports_args(x, x_residual, weight, epsilon, variance_size=4)
355
356 # test weight=None behavior
357 out_no_weight, residual_no_weight = impl.impl_fn(
358 x.clone(), x_residual.clone(), None, epsilon
359 )
360 out_unit_weight, residual_unit_weight = impl.impl_fn(
361 x.clone(), x_residual.clone(), torch.ones_like(weight), epsilon
362 )
363 assert_close(ir.ops.fused_add_rms_norm, out_no_weight, out_unit_weight)
364 assert_close(
365 ir.ops.fused_add_rms_norm, residual_no_weight, residual_unit_weight
366 )
367
368 @pytest.mark.parametrize("provider", ["vllm_c"])
369 def test_inplace_semantics(self, dtype, n_tokens, hidden_size, epsilon, provider):
370 """Test that inplace implementations reuse inputs,
371 for maybe_inplace overload but not for default overload."""
372 impl = ir.ops.fused_add_rms_norm.impls[provider]
373 if not impl.supported:
374 pytest.skip(f"{provider} impl not supported on this platform")
375
376 x, x_residual, weight, eps = ir.ops.fused_add_rms_norm.generate_inputs(
377 num_tokens=n_tokens,
378 hidden_size=hidden_size,
379 dtype=dtype,
380 epsilon=epsilon,
381 device=current_platform.device_type,
382 )
383
384 # Test default overload - should NOT modify inputs even with inplace impl
385 x_default = x.clone()
386 x_residual_default = x_residual.clone()
387 x_default_ptr = x_default.data_ptr()
388 x_residual_default_ptr = x_residual_default.data_ptr()
389
390 with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
391 out_default, residual_default = ir.ops.fused_add_rms_norm(
392 x_default, x_residual_default, weight, eps
393 )
394
395 # Default should NOT be inplace (even with inplace implementation)
396 assert out_default.data_ptr() != x_default_ptr
397 assert residual_default.data_ptr() != x_residual_default_ptr
398 torch.testing.assert_close(x, x_default, rtol=0.0, atol=0.0)
399 torch.testing.assert_close(x_residual, x_residual_default, rtol=0.0, atol=0.0)
400
401 # Test maybe_inplace overload - should modify inputs with inplace impl
402 x_inplace = x.clone()
403 x_residual_inplace = x_residual.clone()
404 x_inplace_ptr = x_inplace.data_ptr()
405 x_residual_inplace_ptr = x_residual_inplace.data_ptr()
406
407 with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
408 out_inplace, residual_inplace = ir.ops.fused_add_rms_norm.maybe_inplace(
409 x_inplace, x_residual_inplace, weight, eps
410 )
411
412 # maybe_inplace should be inplace
413 assert out_inplace.data_ptr() == x_inplace_ptr
414 assert residual_inplace.data_ptr() == x_residual_inplace_ptr
415
416 # Both should produce same results
417 torch.testing.assert_close(out_default, out_inplace, atol=0.0, rtol=0.0)
418 torch.testing.assert_close(
419 residual_default, residual_inplace, atol=0.0, rtol=0.0
420 )
421
422 @pytest.mark.parametrize("provider", supported_providers(ir.ops.fused_add_rms_norm))
423 def test_torch_opcheck(self, dtype, n_tokens, hidden_size, epsilon, provider):
424 args = ir.ops.fused_add_rms_norm.generate_inputs(
425 num_tokens=n_tokens,
426 hidden_size=hidden_size,
427 dtype=dtype,
428 epsilon=epsilon,
429 device=current_platform.device_type,
430 )
431 args = args + (None,) # Add variance_size parameter
432
433 # When checking the torch op, we have to set priority and use dispatch
434 with ir.ops.fused_add_rms_norm.set_priority([provider, "native"]):
435 torch.library.opcheck(torch.ops.vllm_ir.fused_add_rms_norm.default, args)
436
437 # Only test maybe_inplace with non-inplace implementations
438 # Inplace implementations return aliases of inputs which is not allowed.
439 # We break this invariant, but we also convert maybe_inplace to the default
440 # overload during compilation, so maybe_inplace never reaches Inductor.
441 if not ir.ops.fused_add_rms_norm.impls[provider].inplace:
442 torch.library.opcheck(
443 torch.ops.vllm_ir.fused_add_rms_norm.maybe_inplace, args
444 )