main
py 450 lines 13.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for the machete kernel.
4
5 Run `pytest tests/kernels/quantization/test_machete_mm.py`.
6 """
7
8 import math
9 from dataclasses import dataclass, fields
10
11 import pytest
12 import torch
13
14 from tests.kernels.utils import opcheck
15 from vllm import _custom_ops as ops
16 from vllm.model_executor.layers.quantization.utils.machete_utils import (
17 query_machete_supported_group_sizes,
18 )
19 from vllm.model_executor.layers.quantization.utils.quant_utils import (
20 pack_rows,
21 quantize_weights,
22 )
23 from vllm.platforms import current_platform
24 from vllm.scalar_type import ScalarType, scalar_types
25
26 if current_platform.is_rocm():
27 pytest.skip(
28 "These tests require machete_prepack_B, not supported on ROCm.",
29 allow_module_level=True,
30 )
31
32 CUDA_DEVICES = [
33 f"cuda:{i}" for i in range(1 if torch.accelerator.device_count() == 1 else 2)
34 ]
35
36 # TODO: in future PR refactor this and `is_quant_method_supported` in the kernel
37 # unit tests to a common utility function. Currently the use of
38 # `is_quant_method_supported` conflates kernels with quantization methods
39 # an assumption which is breaking down as quantizations methods can have
40 # have kernels and some kernels support multiple quantization methods.
41 capability = current_platform.get_device_capability()
42 IS_SUPPORTED_BY_GPU = capability is not None and capability[0] >= 9
43
44 MNK_SHAPES = [
45 (1, 128, 128),
46 (1, 8192, 28672),
47 (13, 8192, 4096),
48 (26, 4096, 8192),
49 (64, 4096, 4096),
50 (64, 8192, 28672),
51 (257, 128, 4096),
52 (257, 4224, 4160),
53 (1024, 8192, 4096),
54 ]
55
56
57 @dataclass
58 class TypeConfig:
59 act_type: torch.dtype
60 weight_type: ScalarType
61 output_type: torch.dtype | None
62 group_scale_type: torch.dtype | None
63 group_zero_type: torch.dtype | None
64 channel_scale_type: torch.dtype | None
65 token_scale_type: torch.dtype | None
66
67
68 @dataclass
69 class Tensors:
70 w_ref: torch.Tensor
71 a_ref: torch.Tensor
72 a: torch.Tensor
73 w_q: torch.Tensor
74 w_g_s: torch.Tensor | None
75 w_g_zp: torch.Tensor | None
76 w_ch_s: torch.Tensor | None
77 w_tok_s: torch.Tensor | None
78
79
80 # (Act Type, Weight Type, Output Type, Scale Type, ZeroPoints,
81 # Ch Scales Type, Tok Scales Type)
82 # NOTE: None "Scale Type" means the act type is floating point
83 # None "Output Type" means the output type is the same as the act type
84 TestTypeTuple = tuple[
85 list[torch.dtype], ScalarType, torch.dtype | None, torch.dtype | None, bool
86 ]
87 TEST_TYPES = [
88 # GPTQ style
89 *(
90 TypeConfig(
91 act_type=a_type,
92 weight_type=w_type,
93 output_type=None,
94 group_scale_type=a_type,
95 group_zero_type=None,
96 channel_scale_type=None,
97 token_scale_type=None,
98 )
99 for w_type in [scalar_types.uint4b8, scalar_types.uint8b128]
100 for a_type in [torch.float16, torch.bfloat16]
101 ),
102 # AWQ style
103 *(
104 TypeConfig(
105 act_type=a_type,
106 weight_type=w_type,
107 output_type=None,
108 group_scale_type=a_type,
109 group_zero_type=a_type,
110 channel_scale_type=None,
111 token_scale_type=None,
112 )
113 for w_type in [scalar_types.uint4, scalar_types.uint8]
114 for a_type in [torch.float16, torch.bfloat16]
115 ),
116 # # QQQ style
117 # *(TypeConfig(act_type=torch.int8,
118 # weight_type=scalar_types.uint4b8,
119 # output_type=torch.float16,
120 # group_scale_type=group_scale_type,
121 # group_zero_type=None,
122 # channel_scale_type=torch.float,
123 # token_scale_type=torch.float)
124 # for group_scale_type in [None, torch.float16]),
125 # *(TypeConfig(act_type=torch.float8_e4m3fn,
126 # weight_type=scalar_types.uint4b8,
127 # output_type=torch.float16,
128 # group_scale_type=group_scale_type,
129 # group_zero_type=None,
130 # channel_scale_type=torch.float,
131 # token_scale_type=torch.float)
132 # for group_scale_type in [None, torch.float16]),
133 ]
134
135 # TODO: in future PR refactor this and `is_quant_method_supported` in the kernel
136 # unit tests to a common utility function. Currently the use of
137 # `is_quant_method_supported` conflates kernels with quantization methods
138 # an assumption which is breaking down as quantizations methods can have
139 # have kernels and some kernels support multiple quantization methods.
140 IS_SUPPORTED_BY_GPU = current_platform.has_device_capability(90)
141
142
143 def rand_data(shape, dtype=torch.float16, scale=1, offset=0):
144 if dtype.is_floating_point:
145 return (scale * torch.rand(shape, device="cuda") - offset).to(dtype)
146 else:
147 return torch.randint(-8, 7, shape, dtype=dtype, device="cuda")
148
149
150 def maybe_convert_zeropoints(zps: torch.Tensor | None, s: torch.Tensor):
151 return zps if zps is None else -1 * s * (zps.to(s.dtype))
152
153
154 def group_size_valid(shape: tuple[int, int, int], group_size: int | None) -> bool:
155 return group_size is None or group_size == -1 or shape[2] % group_size == 0
156
157
158 def machete_quantize_and_pack(
159 atype: torch.dtype,
160 w: torch.Tensor,
161 wtype: ScalarType,
162 stype: torch.dtype | None,
163 group_size: int | None,
164 zero_points: bool = False,
165 ):
166 assert wtype.is_integer(), "TODO: support floating point weights"
167
168 w_ref, w_q, w_s, w_zp = quantize_weights(
169 w,
170 wtype,
171 group_size=group_size,
172 zero_points=zero_points,
173 # to match how the kernel applies zps
174 ref_zero_points_after_scales=True,
175 )
176
177 w_q = pack_rows(w_q, wtype.size_bits, *w_q.shape)
178 w_q = w_q.t().contiguous().t() # convert to col major
179
180 w_q_machete = ops.machete_prepack_B(w_q, atype, wtype, stype)
181 opcheck(torch.ops._C.machete_prepack_B, (w_q, atype, wtype.id, stype))
182
183 return w_ref, w_q_machete, w_s, w_zp
184
185
186 def create_test_tensors(
187 shape: tuple[int, int, int],
188 types: TypeConfig,
189 group_size: int | None,
190 subset_stride_factor: int | None = None,
191 ) -> Tensors:
192 m, n, k = shape
193 factor = subset_stride_factor or 1
194
195 print(
196 "create_test_tensors, shape:", shape, "types:", types, "group_size:", group_size
197 )
198
199 a = rand_data((m * factor, k * factor), types.act_type, scale=3, offset=2)
200 w = rand_data((k * factor, n * factor), types.act_type, scale=3, offset=1)
201
202 if factor > 1:
203 a = a[0:m, 0:k]
204 w = w[0:k, 0:n]
205
206 if types.group_scale_type is not None:
207 w = w.to(types.group_scale_type)
208 if w.dtype.itemsize == 1:
209 w = w.to(torch.float16)
210
211 w_ref, w_q_packed, w_s, w_zp = machete_quantize_and_pack(
212 a.dtype,
213 w,
214 types.weight_type,
215 types.group_scale_type,
216 group_size,
217 types.group_zero_type is not None,
218 )
219
220 if not a.dtype.is_floating_point:
221 aiinfo = torch.iinfo(a.dtype)
222 w_ref = w_ref.round().clamp(aiinfo.min, aiinfo.max)
223
224 a_ref = a.to(torch.float32)
225 w_ref = w_ref.to(torch.float32)
226
227 w_ch_s = (
228 None
229 if types.channel_scale_type is None
230 else rand_data((n,), types.channel_scale_type)
231 )
232 w_tok_s = (
233 None
234 if types.token_scale_type is None
235 else rand_data((m,), types.token_scale_type)
236 )
237
238 return Tensors(
239 w_ref=w_ref,
240 a_ref=a_ref,
241 a=a,
242 w_q=w_q_packed,
243 w_g_s=w_s,
244 w_g_zp=maybe_convert_zeropoints(w_zp, w_s),
245 w_ch_s=w_ch_s,
246 w_tok_s=w_tok_s,
247 )
248
249
250 # None stype means scales use the same dtype as a
251 def machete_mm_test_helper(
252 types: TypeConfig,
253 tensors: Tensors,
254 group_size: int | None = None,
255 schedule: str | None = None,
256 ):
257 output_ref = torch.matmul(tensors.a_ref, tensors.w_ref)
258 output_ref_type = output_ref.dtype
259
260 if tensors.w_ch_s is not None:
261 output_ref = (
262 output_ref.to(tensors.w_ch_s.dtype) * tensors.w_ch_s.unsqueeze(0)
263 ).to(output_ref_type)
264 if tensors.w_tok_s is not None:
265 output_ref = (
266 output_ref.to(tensors.w_tok_s.dtype) * tensors.w_tok_s.unsqueeze(1)
267 ).to(output_ref_type)
268
269 output = ops.machete_mm(
270 a=tensors.a,
271 b_q=tensors.w_q,
272 b_type=types.weight_type,
273 b_group_scales=tensors.w_g_s,
274 b_group_zeros=tensors.w_g_zp,
275 b_group_size=group_size,
276 b_channel_scales=tensors.w_ch_s,
277 a_token_scales=tensors.w_tok_s,
278 out_type=types.output_type,
279 schedule=schedule,
280 )
281
282 print(output)
283 print(output_ref)
284
285 # Relax atol as our reduction dim becomes larger (more rounding error)
286 # Relax atol when we have zeropoints since the way machete applies
287 # zeropoints (after scales) causes noise around 0
288 atol = (
289 1
290 if tensors.w_g_zp is not None
291 else min(5e-2 * math.sqrt(tensors.a.shape[1]), 1)
292 )
293 rtol = 1e-1 if tensors.a.element_size() >= 2 else 2e-1
294 torch.testing.assert_close(
295 output, output_ref.to(output.dtype), rtol=rtol, atol=atol
296 )
297
298
299 @pytest.mark.skipif(
300 not IS_SUPPORTED_BY_GPU, reason="Machete is not supported on this GPU type."
301 )
302 @pytest.mark.parametrize("shape", MNK_SHAPES, ids=lambda x: "x".join(str(v) for v in x))
303 @pytest.mark.parametrize("types", TEST_TYPES)
304 def test_machete_all_schedules(shape, types: TypeConfig):
305 group_sizes: list[int | None] = []
306 if types.group_scale_type is None:
307 group_sizes = [None]
308 else:
309 group_sizes = list(query_machete_supported_group_sizes(types.act_type))
310
311 for group_size in group_sizes:
312 if not group_size_valid(shape, group_size):
313 continue
314
315 tensors = create_test_tensors(shape, types, group_size)
316 print(f"MNK = {shape}")
317 for schedule in ops.machete_supported_schedules(
318 types.act_type,
319 types.weight_type,
320 group_scales_type=types.group_scale_type,
321 group_zeros_type=types.group_scale_type,
322 out_type=types.output_type,
323 ):
324 print(f"Testing schedule {schedule}")
325 machete_mm_test_helper(types, tensors, group_size, schedule)
326
327
328 @pytest.mark.skipif(
329 not IS_SUPPORTED_BY_GPU, reason="Machete is not supported on this GPU type."
330 )
331 @pytest.mark.parametrize("shape", MNK_SHAPES, ids=lambda x: "x".join(str(v) for v in x))
332 @pytest.mark.parametrize("types", TEST_TYPES)
333 def test_machete_heuristic(shape, types: TypeConfig):
334 group_sizes: list[int | None] = []
335 if types.group_scale_type is None:
336 group_sizes = [None]
337 else:
338 group_sizes = list(query_machete_supported_group_sizes(types.act_type))
339
340 for group_size in group_sizes:
341 if not group_size_valid(shape, group_size):
342 continue
343
344 tensors = create_test_tensors(shape, types, group_size)
345 machete_mm_test_helper(types, tensors, group_size)
346
347
348 # Test working on other devices
349 @pytest.mark.skipif(
350 not IS_SUPPORTED_BY_GPU, reason="Machete is not supported on this GPU type."
351 )
352 @pytest.mark.parametrize("device", CUDA_DEVICES)
353 def test_machete_devices(device: str):
354 group_size = 128
355
356 type_config = TypeConfig(
357 act_type=torch.float16,
358 weight_type=scalar_types.uint4b8,
359 output_type=None,
360 group_scale_type=torch.float16,
361 group_zero_type=None,
362 channel_scale_type=None,
363 token_scale_type=None,
364 )
365
366 tensors = create_test_tensors((512, 4096, 4096), type_config, group_size)
367
368 for field in fields(Tensors):
369 tensor = getattr(tensors, field.name)
370 if isinstance(tensor, torch.Tensor):
371 setattr(tensors, field.name, tensor.to(device))
372
373 machete_mm_test_helper(type_config, tensors, group_size)
374
375
376 # Test working with a subset of A and B
377 @pytest.mark.skipif(
378 not IS_SUPPORTED_BY_GPU, reason="Machete is not supported on this GPU type."
379 )
380 def test_machete_subset():
381 group_size = 128
382
383 type_config = TypeConfig(
384 act_type=torch.float16,
385 weight_type=scalar_types.uint4b8,
386 output_type=None,
387 group_scale_type=torch.float16,
388 group_zero_type=None,
389 channel_scale_type=None,
390 token_scale_type=None,
391 )
392
393 tensors = create_test_tensors(
394 (512, 4096, 4096), type_config, group_size, subset_stride_factor=2
395 )
396 machete_mm_test_helper(type_config, tensors, group_size)
397
398
399 # Test to make sure cuda graphs work
400 class MacheteLayer(torch.nn.Module):
401 def __init__(self, **kwargs):
402 super().__init__()
403 self.kwargs = kwargs
404
405 def forward(self, a):
406 return ops.machete_mm(a=a, **self.kwargs)
407
408
409 @pytest.mark.skipif(
410 not IS_SUPPORTED_BY_GPU, reason="Machete is not supported on this GPU type."
411 )
412 def test_machete_cuda_graph():
413 m, n, k = 512, 4096, 4096
414
415 a = rand_data((m, k), torch.float16)
416 b = rand_data((k, n), torch.float16)
417 wtype = scalar_types.uint4b8
418 stype = torch.float16
419 group_size = 128
420 zero_points = False
421
422 w_ref, w_q_packed, w_s, w_zp = machete_quantize_and_pack(
423 a.dtype, b, wtype, stype, group_size, zero_points
424 )
425
426 # Construct a trivial model with a single layer that calls a machete kernel
427 model = MacheteLayer(
428 b_q=w_q_packed,
429 b_type=wtype,
430 b_group_scales=w_s,
431 b_group_zeros=maybe_convert_zeropoints(w_zp, w_s),
432 b_group_size=group_size,
433 )
434
435 output_ref = torch.matmul(a, w_ref)
436
437 # Run the model with a cuda graph
438 stream = torch.cuda.Stream()
439 with torch.cuda.stream(stream):
440 g = torch.cuda.CUDAGraph()
441 with torch.cuda.graph(g):
442 output = model(a)
443 output.zero_()
444 g.replay()
445
446 # Relax atol as our reduction dim becomes larger (more rounding error)
447 # Relax atol when we have zeropoints since the way machete applies
448 # zeropoints (after scales) causes noise around 0
449 atol = 1 if zero_points else min(5e-2 * math.sqrt(k), 1)
450 torch.testing.assert_close(output, output_ref, rtol=1e-1, atol=atol)