main
py 338 lines 9.61 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for the CUTLASS-based W4A8 grouped GEMM kernel and the full MoE layer."""
4
5 import random
6 from dataclasses import dataclass
7
8 import pytest
9 import torch
10
11 from vllm import _custom_ops as ops
12 from vllm.model_executor.layers.quantization.utils.quant_utils import (
13 pack_rows,
14 quantize_weights,
15 )
16 from vllm.platforms import current_platform
17 from vllm.scalar_type import ScalarType, scalar_types
18 from vllm.utils.torch_utils import set_random_seed
19
20 capability = current_platform.get_device_capability()
21 IS_SUPPORTED_BY_GPU = (
22 current_platform.is_cuda() and capability is not None and capability[0] >= 9
23 )
24
25
26 def to_fp8(tensor: torch.Tensor) -> torch.Tensor:
27 finfo = torch.finfo(torch.float8_e4m3fn)
28 return tensor.clamp(min=finfo.min, max=finfo.max).to(dtype=torch.float8_e4m3fn)
29
30
31 def cutlass_quantize(
32 atype: torch.dtype,
33 w: torch.Tensor,
34 wtype: ScalarType,
35 stype: torch.dtype | None,
36 group_size: int | None,
37 zero_points: bool = False,
38 ):
39 """Quantize weights into W4 and compute reference dequantized weights.
40
41 Encoding/reordering of weights and packing of scales is deferred
42 until after all experts are combined.
43 """
44 assert wtype.is_integer(), "TODO: support floating point weights"
45
46 w_ref, w_q, w_s, w_zp = quantize_weights(
47 w, wtype, group_size=group_size, zero_points=zero_points
48 )
49
50 # Since scales are later cast to fp8, recompute w_ref in atype here.
51 w_ref = (
52 w_q.to(torch.float32)
53 * w_s.to(atype).to(torch.float32).repeat_interleave(group_size, dim=0)
54 ).to(atype)
55
56 # Bit mask prevents sign extension of int4 when packing.
57 w_q = pack_rows(w_q & 0x0F, wtype.size_bits, *w_q.shape)
58 # Make weights row-major (N, K).
59 w_q = w_q.t().contiguous()
60
61 return w_ref, w_q, w_s.to(atype), w_zp
62
63
64 def cutlass_preprocess(
65 w_q_experts: list[torch.Tensor], w_s_experts: list[torch.Tensor]
66 ):
67 """Reorder/encode expert weights and pack scales.
68
69 Returns:
70 w_q_packed: Packed/encoded int4 weights for all experts.
71 w_s_packed: Packed fp8 scales for all experts.
72 packed_layout: Layout/stride metadata for grouped GEMM.
73
74 """
75 w_s_packed = ops.cutlass_pack_scale_fp8(torch.stack(w_s_experts))
76 w_q_packed, packed_layout = ops.cutlass_encode_and_reorder_int4b_grouped(
77 torch.stack(w_q_experts)
78 ) # expects dim 3
79 return w_q_packed, w_s_packed, packed_layout
80
81
82 GROUP_SIZE = 128
83 # (num_experts, N, K)
84 TEST_SHAPES = [
85 (8, 512, 2048),
86 (8, 2048, 2048),
87 (64, 512, 1024),
88 (64, 2048, 2048),
89 (4, 2048, 768),
90 (8, 768, 2048),
91 (64, 1536, 2048),
92 (128, 8192, 4096), # test overflow int32
93 ]
94 ALIGNMENT = 16 # torch._scaled_mm alignment for M, needed for reference check
95
96
97 @dataclass
98 class MoETestSetup:
99 num_experts: int
100 K: int
101 N: int
102 Ms: list[int]
103 M_full: int
104 a: torch.Tensor
105 a_ref: torch.Tensor
106 a_strides: torch.Tensor
107 out: torch.Tensor
108 c_strides: torch.Tensor
109 per_tok_scales: torch.Tensor
110 per_chan_scales: torch.Tensor
111 w_refs: list[torch.Tensor]
112 w_q_packed: torch.Tensor
113 w_s_packed: torch.Tensor
114 problem_sizes: torch.Tensor
115 expert_offsets: torch.Tensor
116 b_strides: torch.Tensor
117 group_scale_strides: torch.Tensor
118
119
120 def make_moe_test_setup(
121 num_experts: int,
122 K: int,
123 N: int,
124 *,
125 alignment: int = ALIGNMENT,
126 max_blocks: int = 64,
127 device: str = "cuda",
128 random_zero: bool = False,
129 ) -> MoETestSetup:
130 """Create a full set of tensors for testing cutlass_w4a8_moe_mm."""
131 assert K % GROUP_SIZE == 0
132 # Token counts per expert (multiples of `alignment`).
133 Ms = [alignment * random.randint(1, max_blocks) for _ in range(num_experts)]
134
135 # set random experts to 0 tokens
136 if random_zero and num_experts > 1:
137 num_zero = max(1, num_experts // 8)
138 zero_indices = random.sample(range(num_experts), k=num_zero)
139 for idx in zero_indices:
140 Ms[idx] = 0
141
142 M_full = sum(Ms)
143 assert M_full > 0
144
145 # Activations.
146 a = to_fp8(torch.randn((M_full, K), device=device))
147 a_ref = a.to(torch.float32)
148 a_strides = torch.full((num_experts,), K, dtype=torch.int64, device=device)
149
150 # Output buffer.
151 out = torch.empty((M_full, N), dtype=torch.bfloat16, device=device)
152 c_strides = torch.full((num_experts,), N, dtype=torch.int64, device=device)
153
154 # Channel/token scales.
155 per_tok_scales = torch.randn((M_full, 1), dtype=torch.float32, device=device)
156 per_chan_scales = torch.randn(
157 (num_experts, N, 1), dtype=torch.float32, device=device
158 )
159
160 # Expert weights and scales.
161 wtype = scalar_types.int4
162 atype = stype = torch.float8_e4m3fn
163 w_refs, w_qs, w_ss = [], [], []
164 for _ in range(num_experts):
165 b = to_fp8(torch.randn((K, N), device=device))
166 w_ref, w_q, w_s, _ = cutlass_quantize(
167 atype, b.to(torch.float16), wtype, stype, GROUP_SIZE, zero_points=False
168 )
169 w_refs.append(w_ref)
170 w_qs.append(w_q)
171 w_ss.append(w_s)
172
173 w_q_packed, w_s_packed, packed_layout = cutlass_preprocess(w_qs, w_ss)
174
175 problem_sizes = torch.tensor(
176 [[N, M, K] for M in Ms], dtype=torch.int32, device=device
177 )
178
179 expert_offsets = torch.cat(
180 [
181 torch.tensor([0], dtype=torch.int64),
182 torch.cumsum(torch.tensor(Ms, dtype=torch.int64), dim=0)[:-1],
183 ]
184 ).to(device=device)
185
186 # B strides and group scale strides.
187 b_strides = packed_layout
188 group_scale_strides = torch.zeros(
189 (num_experts, 2), dtype=torch.int64, device=device
190 )
191 group_scale_strides[:, 0] = N
192
193 return MoETestSetup(
194 num_experts=num_experts,
195 K=K,
196 N=N,
197 Ms=Ms,
198 M_full=M_full,
199 a=a,
200 a_ref=a_ref,
201 a_strides=a_strides,
202 out=out,
203 c_strides=c_strides,
204 per_tok_scales=per_tok_scales,
205 per_chan_scales=per_chan_scales,
206 w_refs=w_refs,
207 w_q_packed=w_q_packed,
208 w_s_packed=w_s_packed,
209 problem_sizes=problem_sizes,
210 expert_offsets=expert_offsets,
211 b_strides=b_strides,
212 group_scale_strides=group_scale_strides,
213 )
214
215
216 def compute_moe_reference_output(setup: MoETestSetup) -> torch.Tensor:
217 """Compute reference output using torch._scaled_mm per expert."""
218 out_ref = torch.empty_like(setup.out)
219
220 ends = torch.cumsum(torch.tensor(setup.Ms), 0).tolist()
221 starts = setup.expert_offsets.cpu().tolist()
222
223 for i in range(setup.num_experts):
224 start, end = starts[i], ends[i]
225 if start == end:
226 continue
227
228 out_ref_i = torch._scaled_mm(
229 setup.a_ref[start:end].to(torch.float8_e4m3fn),
230 setup.w_refs[i].to(torch.float8_e4m3fn).t().contiguous().t(),
231 setup.per_tok_scales[start:end], # (M, 1)
232 setup.per_chan_scales[i].reshape(1, -1), # (1, N)
233 out_dtype=torch.bfloat16,
234 use_fast_accum=True,
235 )
236 out_ref[start:end] = out_ref_i
237
238 return out_ref
239
240
241 @pytest.mark.skipif(
242 not IS_SUPPORTED_BY_GPU,
243 reason="W4A8 Grouped GEMM is not supported on this GPU type.",
244 )
245 @pytest.mark.parametrize("shape", TEST_SHAPES)
246 @pytest.mark.parametrize("random_zero", [True, False])
247 def test_cutlass_w4a8_moe_mm_end_to_end(shape, random_zero):
248 num_experts, N, K = shape
249 set_random_seed(42)
250 setup = make_moe_test_setup(
251 num_experts=num_experts, K=K, N=N, max_blocks=64, random_zero=random_zero
252 )
253
254 ops.cutlass_w4a8_moe_mm(
255 setup.out,
256 setup.a,
257 setup.w_q_packed,
258 setup.per_tok_scales,
259 setup.per_chan_scales,
260 setup.w_s_packed,
261 GROUP_SIZE,
262 setup.expert_offsets,
263 setup.problem_sizes,
264 setup.a_strides,
265 setup.b_strides,
266 setup.c_strides,
267 setup.group_scale_strides,
268 )
269 torch.accelerator.synchronize()
270
271 out_ref = compute_moe_reference_output(setup)
272 torch.testing.assert_close(setup.out, out_ref, rtol=1e-2, atol=1e-2)
273
274
275 class W4A8MoELayer(torch.nn.Module):
276 """Minimal wrapper module to test cuda graphs."""
277
278 def __init__(self, setup: MoETestSetup):
279 super().__init__()
280 self.setup = setup
281
282 def forward(self, a: torch.Tensor) -> torch.Tensor:
283 s = self.setup
284 ops.cutlass_w4a8_moe_mm(
285 s.out,
286 a,
287 s.w_q_packed,
288 s.per_tok_scales,
289 s.per_chan_scales,
290 s.w_s_packed,
291 GROUP_SIZE,
292 s.expert_offsets,
293 s.problem_sizes,
294 s.a_strides,
295 s.b_strides,
296 s.c_strides,
297 s.group_scale_strides,
298 )
299 return s.out
300
301
302 @pytest.mark.skipif(
303 not IS_SUPPORTED_BY_GPU,
304 reason="W4A8 Grouped GEMM is not supported on this GPU type.",
305 )
306 def test_cutlass_w4a8_moe_mm_cuda_graph():
307 set_random_seed(42)
308 # Fixed config for CUDA graph test (single parameter point).
309 num_experts = 8
310 K = 512
311 N = 2048
312
313 setup = make_moe_test_setup(
314 num_experts=num_experts,
315 K=K,
316 N=N,
317 max_blocks=32,
318 )
319
320 # Construct model that calls the grouped GEMM kernel.
321 model = W4A8MoELayer(setup)
322
323 # Build reference output once.
324 out_ref = compute_moe_reference_output(setup)
325
326 # Capture and run the model in a CUDA graph.
327 a_static = setup.a.clone() # static input tensor for graph replay
328
329 stream = torch.cuda.Stream()
330 with torch.cuda.stream(stream):
331 g = torch.cuda.CUDAGraph()
332 with torch.cuda.graph(g):
333 out_static = model(a_static)
334
335 out_static.zero_()
336 g.replay()
337
338 torch.testing.assert_close(out_static, out_ref, rtol=1e-2, atol=1e-2)