main
py 300 lines 10.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for the rms_norm_per_block_quant helion kernel.
4
5 Run `pytest tests/kernels/helion/test_rms_norm_per_block_quant.py`.
6 """
7
8 import itertools
9 from typing import Any
10
11 import pytest
12 import torch
13 from torch._subclasses.fake_tensor import FakeTensorMode
14
15 from tests.kernels.helion.utils import skip_if_platform_unsupported
16 from tests.kernels.quant_utils import FP8_DTYPE
17 from vllm.kernels.helion.case_key import CaseKey
18 from vllm.kernels.helion.config_manager import ConfigManager
19 from vllm.kernels.helion.ops.rms_norm_per_block_quant import (
20 _pick_cache,
21 baseline,
22 pick_config,
23 rms_norm_per_block_quant,
24 )
25 from vllm.utils.import_utils import has_helion
26 from vllm.utils.torch_utils import set_random_seed
27
28 if not has_helion():
29 pytest.skip(
30 "Helion is not installed. Install with: pip install vllm[helion]",
31 allow_module_level=True,
32 )
33
34
35 def _generate_fake_input(
36 num_tokens: int, hidden_size: int, group_size: int
37 ) -> tuple[Any, ...]:
38 with FakeTensorMode():
39 input = torch.randn(
40 (num_tokens, hidden_size), device="cuda", dtype=torch.bfloat16
41 )
42 result = torch.empty(input.shape, device=input.device, dtype=FP8_DTYPE)
43 scale = torch.empty(
44 (num_tokens, hidden_size // group_size),
45 device=input.device,
46 dtype=torch.float32,
47 )
48 scale_ub = torch.mean(input).to(scale.dtype)
49 residual = torch.randn_like(input)
50 weight = torch.normal(
51 mean=1.0,
52 std=1.0,
53 size=(hidden_size,),
54 dtype=input.dtype,
55 device=input.device,
56 )
57 epsilon = 1e-6
58 args = (
59 result,
60 input,
61 weight,
62 scale,
63 epsilon,
64 scale_ub,
65 residual,
66 group_size,
67 False,
68 )
69 return args
70
71
72 @pytest.fixture(autouse=True)
73 def reset_config_manager_singleton():
74 ConfigManager.reset_instance()
75 ConfigManager()
76 yield
77 ConfigManager.reset_instance()
78
79
80 class TestRmsNormPerBlockQuantConfigPicker:
81 def setup_method(self):
82 _pick_cache.clear()
83
84 def test_config_picker_exact_match(self):
85 config_keys = [
86 CaseKey({"hidden_size": 2048, "group_size": 64, "num_tokens": 16}),
87 CaseKey({"hidden_size": 4096, "group_size": 128, "num_tokens": 16}),
88 ]
89
90 args = _generate_fake_input(16, 4096, 128)
91 selected_key = pick_config(args, config_keys)
92 assert selected_key == CaseKey(
93 {"hidden_size": 4096, "group_size": 128, "num_tokens": 16}
94 )
95
96 def test_config_picker_closest_match(self):
97 config_keys = [
98 CaseKey({"hidden_size": 2048, "group_size": 64, "num_tokens": 16}),
99 CaseKey({"hidden_size": 2048, "group_size": 64, "num_tokens": 32}),
100 CaseKey({"hidden_size": 2048, "group_size": 128, "num_tokens": 16}),
101 CaseKey({"hidden_size": 2048, "group_size": 128, "num_tokens": 32}),
102 CaseKey({"hidden_size": 4096, "group_size": 64, "num_tokens": 16}),
103 CaseKey({"hidden_size": 4096, "group_size": 64, "num_tokens": 32}),
104 CaseKey({"hidden_size": 4096, "group_size": 128, "num_tokens": 16}),
105 CaseKey({"hidden_size": 4096, "group_size": 128, "num_tokens": 32}),
106 ]
107
108 args = _generate_fake_input(20, 3000, 70)
109 selected_key = pick_config(args, config_keys)
110 assert selected_key == CaseKey(
111 {"hidden_size": 2048, "group_size": 64, "num_tokens": 32}
112 )
113
114 def test_config_picker_no_configs(self):
115 config_keys: list[CaseKey] = []
116
117 args = _generate_fake_input(16, 4096, 128)
118 selected_key = pick_config(args, config_keys)
119 assert selected_key is None
120
121 def test_config_picker_fallback_to_largest(self):
122 config_keys = [
123 CaseKey({"hidden_size": 2048, "group_size": 64, "num_tokens": 16}),
124 CaseKey({"hidden_size": 2048, "group_size": 64, "num_tokens": 32}),
125 CaseKey({"hidden_size": 2048, "group_size": 128, "num_tokens": 16}),
126 CaseKey({"hidden_size": 2048, "group_size": 128, "num_tokens": 32}),
127 CaseKey({"hidden_size": 4096, "group_size": 64, "num_tokens": 16}),
128 CaseKey({"hidden_size": 4096, "group_size": 64, "num_tokens": 32}),
129 CaseKey({"hidden_size": 4096, "group_size": 128, "num_tokens": 16}),
130 CaseKey({"hidden_size": 4096, "group_size": 128, "num_tokens": 32}),
131 ]
132
133 args = _generate_fake_input(64, 8192, 256)
134 selected_key = pick_config(args, config_keys)
135 assert selected_key == CaseKey(
136 {"hidden_size": 4096, "group_size": 128, "num_tokens": 32}
137 )
138
139 def test_b200_configs_disable_reduction_warp_specialization(self):
140 config_set = ConfigManager.get_instance().load_config_set(
141 "rms_norm_per_block_quant"
142 )
143 configs = config_set.to_dict()["nvidia_b200"].values()
144
145 # TODO: Remove once the Triton pin includes
146 # https://github.com/triton-lang/triton/pull/9716. Tracked by
147 # https://github.com/triton-lang/triton/issues/10901.
148 assert all(
149 config["range_warp_specializes"][1] is not True for config in configs
150 )
151
152
153 DTYPES = [torch.bfloat16, torch.float]
154 QUANT_DTYPES = [torch.int8, FP8_DTYPE]
155 VEC_HIDDEN_SIZES = [64, 1024]
156 # Avoid combinatorial explosion with full Cartesian product
157 NUM_TOKENS_HIDDEN_SIZES = [
158 *[(1, i) for i in [64, 128, 1024, 5120]],
159 *[(2048, i) for i in [64, 1024]],
160 *[(4096, i) for i in [64]],
161 ]
162
163 ADD_RESIDUAL = [False, True]
164 SCALE_UBS = [True, False]
165 GROUP_SIZES = [64, 128]
166 TMA_ALIGNMENTS = [0, 4]
167 SEEDS = [0]
168 EPS = 1e-6
169
170
171 class TestRmsNormPerBlockQuantCorrectness:
172 @pytest.mark.parametrize("num_tokens, hidden_size", NUM_TOKENS_HIDDEN_SIZES)
173 @pytest.mark.parametrize("add_residual", ADD_RESIDUAL)
174 @pytest.mark.parametrize("has_scale_ub", SCALE_UBS)
175 @pytest.mark.parametrize("dtype", DTYPES)
176 @pytest.mark.parametrize("quant_dtype", QUANT_DTYPES)
177 @pytest.mark.parametrize("is_scale_transposed", [False, True])
178 @pytest.mark.parametrize(
179 "group_size, tma_alignment",
180 [*itertools.product(GROUP_SIZES, TMA_ALIGNMENTS)],
181 )
182 @pytest.mark.parametrize("seed", SEEDS)
183 def test_rms_norm_per_block_quant(
184 self,
185 num_tokens: int,
186 hidden_size: int,
187 add_residual: bool,
188 has_scale_ub: bool,
189 dtype: torch.dtype,
190 quant_dtype: torch.dtype,
191 is_scale_transposed: bool,
192 group_size: int,
193 tma_alignment: int,
194 seed: int,
195 ) -> None:
196 skip_if_platform_unsupported("rms_norm_per_block_quant")
197
198 set_random_seed(seed)
199
200 if hidden_size % group_size != 0:
201 # skip
202 return
203
204 if tma_alignment != 0 and hidden_size // group_size % tma_alignment == 0:
205 # Skip tests where TMA alignment doesn't create extra padding to save time
206 return
207
208 if has_scale_ub and quant_dtype != FP8_DTYPE:
209 # skip
210 return
211
212 scale = 1 / (hidden_size)
213 input = torch.randn(num_tokens, hidden_size, dtype=dtype, device="cuda") * scale
214 weight = torch.normal(
215 mean=1.0, std=1.0, size=(hidden_size,), dtype=dtype, device=input.device
216 )
217 residual = torch.randn_like(input) * scale if add_residual else None
218 scale_ub = (
219 torch.mean(input).to(dtype=torch.float32, device="cuda")
220 if has_scale_ub
221 else None
222 )
223 groups_per_row = hidden_size // group_size
224
225 ref_residual = residual.clone() if residual is not None else None
226 ops_residual = residual.clone() if residual is not None else None
227 ref_out = torch.empty(input.shape, device=input.device, dtype=quant_dtype)
228 ops_out = ref_out.clone()
229
230 if is_scale_transposed:
231 if tma_alignment == 0:
232 ref_scales = torch.empty(
233 (groups_per_row, num_tokens),
234 device=input.device,
235 dtype=torch.float32,
236 ).transpose(0, 1)
237 else:
238 tma_aligned_m = (
239 (num_tokens + tma_alignment - 1) // tma_alignment * tma_alignment
240 )
241 shape = (num_tokens, groups_per_row)
242 stride = (1, tma_aligned_m)
243 ref_scales = torch.empty_strided(
244 shape, stride, device=input.device, dtype=torch.float32
245 )
246 else:
247 ref_scales = torch.empty(
248 (num_tokens, groups_per_row), device=input.device, dtype=torch.float32
249 )
250
251 ops_scales = ref_scales.clone()
252
253 baseline(
254 ref_out,
255 input,
256 weight,
257 ref_scales,
258 EPS,
259 scale_ub,
260 ref_residual,
261 group_size,
262 is_scale_transposed,
263 )
264 ref_scales = ref_scales.contiguous()
265
266 rms_norm_per_block_quant(
267 ops_out,
268 input,
269 weight,
270 ops_scales,
271 EPS,
272 scale_ub,
273 ops_residual,
274 group_size,
275 is_scale_transposed,
276 )
277 ops_scales = ops_scales.contiguous()
278
279 torch.testing.assert_close(ref_scales, ops_scales)
280 # allow 1 ULP difference
281 assert (
282 ref_out.view(torch.uint8).to(torch.int16)
283 - ops_out.view(torch.uint8).to(torch.int16)
284 ).abs().max() <= 1
285
286 if add_residual:
287 torch.testing.assert_close(ref_residual, ops_residual)
288
289
290 class TestRmsNormPerBlockQuantIntegration:
291 def test_kernel_registration_integration(self):
292 from vllm.kernels.helion.register import get_registered_kernels
293
294 registered_kernels = get_registered_kernels()
295 assert "rms_norm_per_block_quant" in registered_kernels
296
297 kernel_wrapper = registered_kernels["rms_norm_per_block_quant"]
298 assert kernel_wrapper.op_name == "rms_norm_per_block_quant"
299 assert kernel_wrapper._config_picker is not None
300 assert kernel_wrapper._mutates_args == ["result", "scale", "residual"]