[Bug] Fix batch invariance rms norm comparison (#49603)

Signed-off-by: yewentao256 <zhyanwentao@126.com>

Wentao Ye committed Jul 23, 2026 at 14:27 UTC b354734d17e04c66076f73511f14acde52dd81e3
1 file changed +35 -45
tests/v1/determinism/test_rms_norm_batch_invariant.py
+35 -45
@@ -1,11 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -"""
4 -Test batch-invariant RMS normalization against standard implementations.
5 -
6 -This test compares the Triton-based batch-invariant RMS norm implementation
7 -with the standard CUDA-based implementation to ensure numerical accuracy.
8 -"""
3 +"""Test batch-invariant RMS normalization against a PyTorch reference."""
4
5 import pytest
6 import torch
@@ -14,18 +9,30 @@ from utils import skip_if_not_cuda, skip_unsupported
9 from vllm.model_executor.layers.batch_invariant import (
10 rms_norm_batch_invariant,
11 )
17 -from vllm.model_executor.layers.layernorm import RMSNorm
12 from vllm.platforms import current_platform
13
14 DEVICE_TYPE = current_platform.device_type
15
16
17 +def _rms_norm_reference(
18 + input_tensor: torch.Tensor,
19 + weight: torch.Tensor,
20 + eps: float,
21 +) -> torch.Tensor:
22 + """Compute RMSNorm independently using PyTorch operations."""
23 + input_fp32 = input_tensor.float()
24 + output = input_fp32 * torch.rsqrt(
25 + input_fp32.square().mean(dim=-1, keepdim=True) + eps
26 + )
27 + return (output * weight.float()).to(input_tensor.dtype)
28 +
29 +
30 @skip_if_not_cuda
31 @pytest.mark.parametrize("batch_size", [1, 4, 16, 64])
32 @pytest.mark.parametrize("hidden_size", [512, 2048, 4096, 8192])
33 @pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
34 @pytest.mark.parametrize("eps", [1e-6, 1e-5])
28 -def test_rms_norm_batch_invariant_vs_standard(
35 +def test_rms_norm_batch_invariant_vs_reference(
36 default_vllm_config,
37 batch_size: int,
38 hidden_size: int,
@@ -33,11 +40,10 @@ def test_rms_norm_batch_invariant_vs_standard(
40 eps: float,
41 ):
42 """
36 - Compare batch-invariant Triton RMS norm against standard CUDA implementation.
43 + Compare batch-invariant Triton RMS norm against a PyTorch reference.
44
45 Tests that the Triton-based batch-invariant RMS norm produces numerically
39 - equivalent results to the standard CUDA implementation across various
40 - configurations.
46 + equivalent results to an independent implementation across configurations.
47 """
48 device = torch.device(DEVICE_TYPE)
49
@@ -46,11 +52,7 @@ def test_rms_norm_batch_invariant_vs_standard(
52 input_tensor = torch.randn(batch_size, hidden_size, dtype=dtype, device=device)
53 weight = torch.randn(hidden_size, dtype=dtype, device=device)
54
49 - # Standard implementation (CUDA ops)
50 - rms_norm_layer = RMSNorm(hidden_size, eps=eps, dtype=dtype).to(device)
51 - rms_norm_layer.weight.data = weight.clone()
52 -
53 - standard_output = rms_norm_layer.forward_cuda(input_tensor)
55 + reference_output = _rms_norm_reference(input_tensor, weight, eps)
56
57 # Batch-invariant implementation (Triton)
58 triton_output = rms_norm_batch_invariant(input_tensor, weight, eps=eps)
@@ -64,7 +66,7 @@ def test_rms_norm_batch_invariant_vs_standard(
66
67 torch.testing.assert_close(
68 triton_output,
67 - standard_output,
69 + reference_output,
70 rtol=rtol,
71 atol=atol,
72 msg=f"RMS norm mismatch for batch_size={batch_size}, "
@@ -127,7 +129,7 @@ def test_fused_add_rms_norm_batch_invariant_residual_path(
129 )
130
131 merged_single = x_single + residual_single
130 - ref_out = rms_norm_batch_invariant(merged_single, weight, eps=eps)
132 + ref_out = _rms_norm_reference(merged_single, weight, eps)
133
134 torch.testing.assert_close(
135 residual_out_single,
@@ -162,7 +164,7 @@ def test_fused_add_rms_norm_batch_invariant_residual_path(
164 rtol=rtol,
165 atol=atol,
166 msg="Fused add RMSNorm output should stay numerically close to the "
165 - "batch-invariant RMSNorm reference",
167 + "PyTorch RMSNorm reference",
168 )
169
170
@@ -189,10 +191,7 @@ def test_rms_norm_3d_input(
191 )
192 weight = torch.randn(hidden_size, dtype=dtype, device=device)
193
192 - # Standard implementation
193 - rms_norm_layer = RMSNorm(hidden_size, eps=eps, dtype=dtype).to(device)
194 - rms_norm_layer.weight.data = weight.clone()
195 - standard_output = rms_norm_layer.forward_cuda(input_tensor)
194 + reference_output = _rms_norm_reference(input_tensor, weight, eps)
195
196 # Batch-invariant implementation
197 triton_output = rms_norm_batch_invariant(input_tensor, weight, eps=eps)
@@ -202,7 +201,7 @@ def test_rms_norm_3d_input(
201
202 torch.testing.assert_close(
203 triton_output,
205 - standard_output,
204 + reference_output,
205 rtol=rtol,
206 atol=atol,
207 msg=f"RMS norm mismatch for 3D input with batch_size={batch_size}, "
@@ -238,20 +237,17 @@ def test_rms_norm_numerical_stability(default_vllm_config):
237 weight = torch.ones(hidden_size, dtype=dtype, device=device)
238
239 for idx, input_tensor in enumerate(test_cases):
241 - # Standard implementation
242 - rms_norm_layer = RMSNorm(hidden_size, eps=eps, dtype=dtype).to(device)
243 - rms_norm_layer.weight.data = weight.clone()
244 - standard_output = rms_norm_layer.forward_cuda(input_tensor)
240 + reference_output = _rms_norm_reference(input_tensor, weight, eps)
241
242 # Batch-invariant implementation
243 triton_output = rms_norm_batch_invariant(input_tensor, weight, eps=eps)
244
245 # Check for NaN or Inf
250 - assert not torch.isnan(standard_output).any(), (
251 - f"Standard RMS norm produced NaN for test case {idx}"
246 + assert not torch.isnan(reference_output).any(), (
247 + f"Reference RMS norm produced NaN for test case {idx}"
248 )
253 - assert not torch.isinf(standard_output).any(), (
254 - f"Standard RMS norm produced Inf for test case {idx}"
249 + assert not torch.isinf(reference_output).any(), (
250 + f"Reference RMS norm produced Inf for test case {idx}"
251 )
252 assert not torch.isnan(triton_output).any(), (
253 f"Triton RMS norm produced NaN for test case {idx}"
@@ -263,7 +259,7 @@ def test_rms_norm_numerical_stability(default_vllm_config):
259 # Compare outputs - very lenient for extreme values with float16
260 torch.testing.assert_close(
261 triton_output,
266 - standard_output,
262 + reference_output,
263 rtol=2e-1, # 20% tolerance for extreme values
264 atol=2e-1,
265 msg=f"RMS norm mismatch for extreme value test case {idx}",
@@ -321,10 +317,7 @@ def test_rms_norm_different_hidden_sizes(default_vllm_config, hidden_size: int):
317 input_tensor = torch.randn(batch_size, hidden_size, dtype=dtype, device=device)
318 weight = torch.randn(hidden_size, dtype=dtype, device=device)
319
324 - # Standard implementation
325 - rms_norm_layer = RMSNorm(hidden_size, eps=eps, dtype=dtype).to(device)
326 - rms_norm_layer.weight.data = weight.clone()
327 - standard_output = rms_norm_layer.forward_cuda(input_tensor)
320 + reference_output = _rms_norm_reference(input_tensor, weight, eps)
321
322 # Batch-invariant implementation
323 triton_output = rms_norm_batch_invariant(input_tensor, weight, eps=eps)
@@ -334,7 +327,7 @@ def test_rms_norm_different_hidden_sizes(default_vllm_config, hidden_size: int):
327
328 torch.testing.assert_close(
329 triton_output,
337 - standard_output,
330 + reference_output,
331 rtol=rtol,
332 atol=atol,
333 msg=f"RMS norm mismatch for hidden_size={hidden_size}",
@@ -420,21 +413,18 @@ if __name__ == "__main__":
413 input_tensor = torch.randn(batch_size, hidden_size, dtype=dtype, device=device)
414 weight = torch.randn(hidden_size, dtype=dtype, device=device)
415
423 - # Standard implementation
424 - rms_norm_layer = RMSNorm(hidden_size, eps=eps, dtype=dtype).to(device)
425 - rms_norm_layer.weight.data = weight.clone()
426 - standard_output = rms_norm_layer.forward_cuda(input_tensor)
416 + reference_output = _rms_norm_reference(input_tensor, weight, eps)
417
418 # Batch-invariant implementation
419 triton_output = rms_norm_batch_invariant(input_tensor, weight, eps=eps)
420
421 # Compare
432 - max_diff = (triton_output - standard_output).abs().max().item()
433 - mean_diff = (triton_output - standard_output).abs().mean().item()
422 + max_diff = (triton_output - reference_output).abs().max().item()
423 + mean_diff = (triton_output - reference_output).abs().mean().item()
424
425 print(f"Max difference: {max_diff:.6e}")
426 print(f"Mean difference: {mean_diff:.6e}")
437 - print(f"Standard output sample: {standard_output[0, :5].tolist()}")
427 + print(f"Reference output sample: {reference_output[0, :5].tolist()}")
428 print(f"Triton output sample: {triton_output[0, :5].tolist()}")
429
430 if max_diff < 1e-3: