main
py 260 lines 9.08 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
7 from tests.kernels.quant_utils import FP8_DTYPE
8 from tests.kernels.utils import fp8_allclose, fp8_ulp_distance, opcheck
9 from vllm import ir
10 from vllm.model_executor.layers.layernorm import GemmaRMSNorm, RMSNorm
11 from vllm.platforms import current_platform
12 from vllm.utils.torch_utils import set_random_seed
13
14 if current_platform.is_rocm():
15 from vllm.platforms.rocm import on_gfx90a
16
17 on_mi250 = on_gfx90a()
18 else:
19 on_mi250 = False
20
21 DTYPES = [torch.half, torch.bfloat16, torch.float]
22 NUM_TOKENS = [7, 83, 4096] # Arbitrary values for testing
23 HIDDEN_SIZES = [8, 768, 769, 5120, 5125, 8192] # Arbitrary values for testing
24 ADD_RESIDUAL = [False, True] if not on_mi250 else [True]
25 SEEDS = [0]
26 CUDA_DEVICES = [
27 f"cuda:{i}" for i in range(1 if torch.accelerator.device_count() == 1 else 2)
28 ]
29
30
31 def _rms_norm_tolerance(dtype: torch.dtype) -> dict[str, float]:
32 return ir.ops.rms_norm.get_tolerance(dtype)
33
34
35 @pytest.mark.parametrize("num_tokens", NUM_TOKENS)
36 @pytest.mark.parametrize("hidden_size", HIDDEN_SIZES)
37 @pytest.mark.parametrize("add_residual", ADD_RESIDUAL)
38 @pytest.mark.parametrize("dtype", DTYPES)
39 @pytest.mark.parametrize("seed", SEEDS)
40 @pytest.mark.parametrize("device", CUDA_DEVICES)
41 @pytest.mark.parametrize("strided_input", [False, True])
42 @torch.inference_mode()
43 def test_rms_norm(
44 default_vllm_config,
45 num_tokens: int,
46 hidden_size: int,
47 add_residual: bool,
48 dtype: torch.dtype,
49 seed: int,
50 device: str,
51 strided_input: bool,
52 ) -> None:
53 set_random_seed(seed)
54 torch.set_default_device(device)
55 layer = RMSNorm(hidden_size).to(dtype=dtype)
56 layer.weight.data.normal_(mean=1.0, std=0.1)
57 scale = 1 / (2 * hidden_size)
58 last_dim = 2 * hidden_size if strided_input else hidden_size
59 x = torch.randn(num_tokens, last_dim, dtype=dtype)
60 x = x[..., :hidden_size]
61 assert x.is_contiguous() != strided_input
62 x *= scale
63 residual = x.new_empty_strided(x.size(), x.stride()) if add_residual else None
64 if residual is not None:
65 residual.normal_(std=scale)
66
67 # NOTE(woosuk): The reference implementation should be executed first
68 # because the custom kernel is in-place.
69 ref_out = layer.forward_native(x, residual)
70 out = layer(x, residual)
71 # NOTE(woosuk): LayerNorm operators (including RMS) typically have larger
72 # numerical errors than other operators because they involve reductions.
73 # Therefore, we use a larger tolerance.
74 if add_residual:
75 torch.testing.assert_close(out[0], ref_out[0], atol=1e-2, rtol=1e-2)
76 torch.testing.assert_close(out[1], ref_out[1], atol=1e-2, rtol=1e-2)
77 else:
78 torch.testing.assert_close(out, ref_out, atol=1e-2, rtol=1e-2)
79
80 if residual is not None:
81 opcheck(
82 torch.ops._C.fused_add_rms_norm,
83 (x, residual, layer.weight.data, layer.variance_epsilon),
84 )
85 else:
86 opcheck(
87 torch.ops._C.rms_norm, (out, x, layer.weight.data, layer.variance_epsilon)
88 )
89
90
91 @pytest.mark.parametrize("num_tokens", NUM_TOKENS)
92 @pytest.mark.parametrize("hidden_size", HIDDEN_SIZES)
93 @pytest.mark.parametrize("add_residual", ADD_RESIDUAL)
94 @pytest.mark.parametrize("dtype", DTYPES)
95 @pytest.mark.parametrize("seed", SEEDS)
96 @pytest.mark.parametrize("device", CUDA_DEVICES)
97 @torch.inference_mode()
98 def test_rms_norm_weightless(
99 default_vllm_config,
100 num_tokens: int,
101 hidden_size: int,
102 add_residual: bool,
103 dtype: torch.dtype,
104 seed: int,
105 device: str,
106 ) -> None:
107 set_random_seed(seed)
108 torch.set_default_device(device)
109 layer = RMSNorm(hidden_size, has_weight=False).to(dtype=dtype)
110 x = torch.randn(num_tokens, hidden_size, dtype=dtype)
111 residual = torch.randn_like(x) if add_residual else None
112
113 ref_out = layer.forward_native(x, residual)
114 out = layer(x, residual)
115 tol = _rms_norm_tolerance(dtype)
116 if add_residual:
117 torch.testing.assert_close(out[0], ref_out[0], **tol)
118 torch.testing.assert_close(out[1], ref_out[1], **tol)
119 else:
120 torch.testing.assert_close(out, ref_out, **tol)
121
122 if residual is not None:
123 opcheck(
124 torch.ops._C.fused_add_rms_norm,
125 (x, residual, None, layer.variance_epsilon),
126 )
127 else:
128 opcheck(
129 torch.ops._C.rms_norm,
130 (out, x, None, layer.variance_epsilon),
131 )
132
133
134 @pytest.mark.parametrize("num_tokens", NUM_TOKENS)
135 @pytest.mark.parametrize("hidden_size", HIDDEN_SIZES)
136 @pytest.mark.parametrize("add_residual", ADD_RESIDUAL)
137 @pytest.mark.parametrize("dtype", DTYPES)
138 @pytest.mark.parametrize("quant_scale", [0.01, 1.0, 10.0])
139 @pytest.mark.parametrize("seed", SEEDS)
140 @pytest.mark.parametrize("device", CUDA_DEVICES)
141 @pytest.mark.parametrize("strided_input", [False, True])
142 def test_fused_rms_norm_quant(
143 num_tokens: int,
144 hidden_size: int,
145 add_residual: bool,
146 dtype: torch.dtype,
147 quant_scale: float,
148 seed: int,
149 device: str,
150 strided_input: bool,
151 ) -> None:
152 set_random_seed(seed)
153 torch.set_default_device(device)
154
155 weight = torch.empty(hidden_size, dtype=dtype).normal_(mean=1.0, std=0.1)
156 scale = 1 / (2 * hidden_size)
157 last_dim = 2 * hidden_size if strided_input else hidden_size
158 x_base = torch.randn(num_tokens, last_dim, dtype=dtype)
159 x = x_base[..., :hidden_size]
160 assert x.is_contiguous() != strided_input
161
162 x *= scale
163 if add_residual:
164 residual = torch.randn_like(x) * scale
165 residual_fused = residual.clone()
166 else:
167 residual = residual_fused = None
168
169 out_norm = torch.empty_like(x)
170 out_quant = torch.empty_like(x, dtype=FP8_DTYPE)
171 out_quant_fused = torch.empty_like(out_quant)
172
173 quant_scale_t = torch.tensor(quant_scale, dtype=torch.float32)
174
175 if add_residual:
176 torch.ops._C.fused_add_rms_norm_static_fp8_quant(
177 out_quant_fused, x, residual_fused, weight, quant_scale_t, 1e-6
178 )
179
180 # Unfused kernel is in-place so it goes second
181 # Also use a separate clone of x to avoid modifying the input
182 x_unfused_base = x_base.clone()
183 x_unfused = x_unfused_base[..., :hidden_size]
184 assert x_unfused.is_contiguous() != strided_input
185 torch.ops._C.fused_add_rms_norm(x_unfused, residual, weight, 1e-6)
186 torch.ops._C.static_scaled_fp8_quant(
187 out_quant, x_unfused.contiguous(), quant_scale_t
188 )
189
190 torch.accelerator.synchronize()
191 torch.testing.assert_close(residual_fused, residual, atol=1e-2, rtol=1e-2)
192 opcheck(
193 torch.ops._C.fused_add_rms_norm_static_fp8_quant,
194 (out_quant_fused, x, residual_fused, weight, quant_scale_t, 1e-6),
195 )
196 else:
197 torch.ops._C.rms_norm_static_fp8_quant(
198 out_quant_fused, x, weight, quant_scale_t, 1e-6
199 )
200
201 torch.ops._C.rms_norm(out_norm, x, weight, 1e-6)
202 torch.ops._C.static_scaled_fp8_quant(out_quant, out_norm, quant_scale_t)
203
204 opcheck(
205 torch.ops._C.rms_norm_static_fp8_quant,
206 (out_quant_fused, x, weight, quant_scale_t, 1e-6),
207 )
208
209 if current_platform.is_rocm():
210 from vllm.platforms.rocm import on_gfx950
211
212 if on_gfx950() and dtype == torch.float16 and not add_residual:
213 # Fusion may round normalized FP16 across an E4M3 boundary on gfx950.
214 assert fp8_allclose(out_quant_fused, out_quant, rtol=0.125, atol=2e-3)
215 assert int(fp8_ulp_distance(out_quant_fused, out_quant).max()) <= 1
216 else:
217 # Fused and unfused FP8 paths can land on opposite sides of an E4M3
218 # tie; tolerate a tiny number of isolated fp8 outliers on ROCm.
219 ulp = fp8_ulp_distance(out_quant, out_quant_fused)
220 max_outliers = ulp.numel() // 100_000 + 8
221 num_outliers = int((ulp > 0).sum().item())
222 assert num_outliers <= max_outliers, (
223 f"FP8 quant mismatch: {num_outliers} fp8 outliers "
224 f"(allowed {max_outliers})"
225 )
226 else:
227 torch.testing.assert_close(
228 out_quant.to(dtype=torch.float32),
229 out_quant_fused.to(dtype=torch.float32),
230 atol=1e-3,
231 rtol=1e-3,
232 )
233
234
235 @torch.inference_mode()
236 def test_gemma_rms_norm_mixed_input_weight_dtype(default_vllm_config) -> None:
237 if not torch.cuda.is_available():
238 pytest.skip("CUDA required")
239
240 device = CUDA_DEVICES[0]
241 torch.set_default_device(device)
242
243 num_tokens, hidden_size = 32, 1024
244 x = torch.randn(num_tokens, hidden_size, dtype=torch.bfloat16, device=device)
245 layer = GemmaRMSNorm(hidden_size, eps=1e-6).to(device=device)
246 layer.weight.data.normal_(mean=0.0, std=0.1)
247
248 # Gemma uses fp32 weight parameter while activations can be bf16.
249 assert layer.weight.dtype == torch.float32
250 out = layer(x)
251
252 x_fp32 = x.float()
253 weight_fp32 = layer.weight.data.float() + 1.0
254 variance = x_fp32.pow(2).mean(dim=-1, keepdim=True)
255 ref = (x_fp32 * torch.rsqrt(variance + layer.variance_epsilon) * weight_fp32).to(
256 x.dtype
257 )
258
259 assert out.dtype == x.dtype
260 torch.testing.assert_close(out, ref, atol=1e-2, rtol=1e-2)