main
py 851 lines 30.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for Marlin thread-tile padding of TP-sharded weight shapes.
4
5 Run `pytest tests/kernels/quantization/test_marlin_tile_padding.py`.
6 """
7
8 from types import SimpleNamespace
9
10 import pytest
11 import torch
12
13 from vllm import _custom_ops as ops
14 from vllm.model_executor.layers.linear import LinearBase
15 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
16 GPTQ_MARLIN_TILE,
17 apply_gptq_marlin_linear,
18 get_marlin_workspace,
19 marlin_make_empty,
20 marlin_make_workspace_new,
21 marlin_moe_padded_intermediate,
22 marlin_pad_qweight,
23 marlin_pad_scales,
24 marlin_padded_nk,
25 marlin_permute_scales,
26 marlin_repacked_nk,
27 marlin_zero_points,
28 )
29 from vllm.model_executor.layers.quantization.utils.marlin_utils_fp4 import (
30 apply_fp4_marlin_linear,
31 is_fp4_marlin_supported,
32 prepare_fp4_layer_for_marlin,
33 )
34 from vllm.model_executor.layers.quantization.utils.marlin_utils_fp8 import (
35 apply_fp8_marlin_linear,
36 apply_mxfp8_marlin_linear,
37 is_fp8_marlin_supported,
38 prepare_fp8_layer_for_marlin,
39 prepare_mxfp8_layer_for_marlin,
40 )
41 from vllm.model_executor.layers.quantization.utils.quant_utils import (
42 gptq_pack,
43 gptq_quantize_weights,
44 quantize_weights,
45 )
46 from vllm.platforms import current_platform
47 from vllm.scalar_type import scalar_types
48
49 # (size_n, size_k) rank-local shapes that violate Marlin tile alignment,
50 # e.g. produced by TP-sharding dims that are valid at TP=1.
51 ODD_SHAPES = [
52 (200, 288), # N padded
53 (256, 208), # K padded
54 (200, 208), # both padded
55 (4640, 512), # Nemotron-Super-120B q_proj shard at TP=4
56 ]
57 ALIGNED_SHAPES = [(64, 128), (128, 64), (256, 256), (4608, 4096)]
58
59
60 def _is_tile_aligned(size_n: int, size_k: int) -> bool:
61 return (size_n % 64 == 0 and size_k % 128 == 0) or (
62 size_n % 128 == 0 and size_k % 64 == 0
63 )
64
65
66 @pytest.mark.parametrize("shape", ODD_SHAPES + ALIGNED_SHAPES)
67 @pytest.mark.parametrize("group_size", [-1, 16, 32, 64, 128])
68 def test_marlin_padded_nk(shape, group_size):
69 size_n, size_k = shape
70 padded_n, padded_k = marlin_padded_nk(size_n, size_k, group_size)
71
72 assert padded_n >= size_n and padded_k >= size_k
73 assert _is_tile_aligned(padded_n, padded_k)
74 if group_size > 0:
75 assert padded_k % group_size == 0
76
77 # Aligned shapes must pass through unchanged (zero hot-path cost).
78 if _is_tile_aligned(size_n, size_k) and (
79 group_size <= 0 or size_k % group_size == 0
80 ):
81 assert (padded_n, padded_k) == (size_n, size_k)
82
83 # Minimal: no valid shape with a smaller padded area exists.
84 area = padded_n * padded_k
85 for cand_n in range(size_n, padded_n + 1):
86 for cand_k in range(size_k, padded_k + 1):
87 if (
88 _is_tile_aligned(cand_n, cand_k)
89 and (group_size <= 0 or cand_k % group_size == 0)
90 and cand_n * cand_k < area
91 ):
92 pytest.fail(f"({cand_n}, {cand_k}) beats ({padded_n}, {padded_k})")
93
94 # Apply-time derivation from the repacked-tensor shape must round-trip.
95 for num_bits in (4, 8):
96 pack_factor = 32 // num_bits
97 repacked_shape = (
98 padded_k // GPTQ_MARLIN_TILE,
99 padded_n * GPTQ_MARLIN_TILE // pack_factor,
100 )
101 repacked = torch.empty(repacked_shape, device="meta")
102 assert marlin_repacked_nk(repacked, num_bits) == (padded_n, padded_k)
103
104
105 def test_marlin_pad_helpers_shapes():
106 size_n, size_k, group_size = 200, 208, 16
107 padded_n, padded_k = marlin_padded_nk(size_n, size_k, group_size)
108
109 qweight = torch.zeros(size_k // 8, size_n, dtype=torch.int32)
110 padded = marlin_pad_qweight(qweight, size_n, size_k, padded_n, padded_k)
111 assert padded.shape == (padded_k // 8, padded_n)
112
113 scales = torch.ones(size_k // group_size, size_n)
114 padded = marlin_pad_scales(scales, size_n, size_k, padded_n, padded_k, group_size)
115 assert padded.shape == (padded_k // group_size, padded_n)
116 assert padded[:, size_n:].abs().sum() == 0
117
118 channelwise = torch.ones(1, size_n)
119 padded = marlin_pad_scales(channelwise, size_n, size_k, padded_n, padded_k, -1)
120 assert padded.shape == (1, padded_n)
121
122
123 # Rank-local MoE intermediate sizes. group<=0 / 32 with a non-multiple-of-64
124 # size is where tile padding triggers; 64/128 are already tile-aligned.
125 MOE_INTERMEDIATE_SIZES = [64, 96, 100, 176, 192, 256, 2816]
126
127
128 @pytest.mark.parametrize("intermediate", MOE_INTERMEDIATE_SIZES)
129 @pytest.mark.parametrize("group_size", [-1, 32, 64, 128])
130 def test_marlin_moe_padded_intermediate(intermediate, group_size):
131 # The MoE gate only admits shapes where the group does not straddle the
132 # boundary, i.e. group divides the intermediate size.
133 if group_size > 0 and intermediate % group_size != 0:
134 pytest.skip("group straddles the boundary; rejected by the MoE gate")
135
136 padded = marlin_moe_padded_intermediate(intermediate, group_size)
137 assert padded >= intermediate
138 # Valid MoE thread tile: gate-up n = 2*intermediate % 128, down k % 64.
139 assert (2 * padded) % 128 == 0
140 assert padded % 64 == 0
141 if group_size > 0:
142 assert padded % group_size == 0
143
144 # Minimal: no smaller valid intermediate exists.
145 for cand in range(intermediate, padded):
146 if (
147 (2 * cand) % 128 == 0
148 and cand % 64 == 0
149 and (group_size <= 0 or cand % group_size == 0)
150 ):
151 pytest.fail(f"{cand} beats {padded}")
152
153 # Already-tile-aligned sizes pass through unchanged (zero hot-path cost).
154 if intermediate % 64 == 0:
155 assert padded == intermediate
156
157
158 def test_marlin_moe_pad_helpers_shapes():
159 from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
160 _pad_rows,
161 _pad_w13_bias,
162 _pad_w13_shard_cols,
163 )
164
165 E, rows, N, padded_N = 2, 8, 96, 128
166
167 # w13 stores the two gate/up shards along the last dim; padding each shard
168 # must preserve the loaded values and zero the padded columns.
169 w13 = torch.arange(E * rows * 2 * N).reshape(E, rows, 2 * N).float()
170 padded = _pad_w13_shard_cols(w13, N, padded_N)
171 assert padded.shape == (E, rows, 2 * padded_N)
172 shards = padded.view(E, rows, 2, padded_N)
173 orig = w13.view(E, rows, 2, N)
174 assert torch.equal(shards[..., :N], orig)
175 assert shards[..., N:].abs().sum() == 0
176
177 # w2 stores the intermediate dim in the rows.
178 w2 = torch.ones(E, N // 32, 16)
179 padded = _pad_rows(w2, padded_N // 32)
180 assert padded.shape == (E, padded_N // 32, 16)
181 assert padded[:, N // 32 :, :].abs().sum() == 0
182
183 bias = torch.arange(E * 2 * N).reshape(E, 2 * N).float()
184 padded = _pad_w13_bias(bias, N, padded_N)
185 assert padded.shape == (E, 2 * padded_N)
186 bias_shards = padded.view(E, 2, padded_N)
187 assert torch.equal(bias_shards[..., :N], bias.view(E, 2, N))
188 assert bias_shards[..., N:].abs().sum() == 0
189
190
191 def _gpu_marlin_unsupported() -> bool:
192 return not (
193 current_platform.is_cuda() and current_platform.has_device_capability(80)
194 )
195
196
197 @pytest.mark.skipif(
198 _gpu_marlin_unsupported() or not is_fp8_marlin_supported(),
199 reason="FP8 Marlin is not supported on this GPU type.",
200 )
201 @pytest.mark.parametrize("shape", ODD_SHAPES)
202 @pytest.mark.parametrize("use_bias", [False, True])
203 def test_fp8_marlin_padded_round_trip(shape, use_bias):
204 size_n, size_k = shape
205 dtype = torch.float16
206 layer = torch.nn.Module()
207 layer.output_size_per_partition = size_n
208 layer.input_size_per_partition = size_k
209 layer.orig_dtype = dtype
210
211 weight = torch.randn(size_k, size_n, dtype=dtype, device="cuda") / size_k**0.5
212 scale = weight.abs().max() / 448
213 weight_fp8 = (weight / scale).to(torch.float8_e4m3fn)
214 layer.weight = torch.nn.Parameter(weight_fp8, requires_grad=False)
215 layer.weight_scale = torch.nn.Parameter(
216 scale.to(torch.float32), requires_grad=False
217 )
218 bias = None
219 if use_bias:
220 bias = torch.randn(size_n, dtype=dtype, device="cuda")
221 layer.bias = torch.nn.Parameter(bias.clone(), requires_grad=False)
222
223 prepare_fp8_layer_for_marlin(layer, size_k_first=True)
224
225 x = torch.randn(8, size_k, dtype=dtype, device="cuda")
226 output = apply_fp8_marlin_linear(
227 input=x,
228 weight=layer.weight,
229 weight_scale=layer.weight_scale,
230 workspace=get_marlin_workspace(layer.weight.device),
231 size_n=size_n,
232 size_k=size_k,
233 bias=layer.bias if use_bias else None,
234 )
235 ref = x @ (weight_fp8.to(dtype) * scale.to(dtype))
236 if use_bias:
237 ref = ref + bias
238
239 assert output.shape == (8, size_n)
240 torch.testing.assert_close(output, ref, rtol=2e-2, atol=2e-2)
241
242
243 def _dequant_fp4(packed: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
244 """Dequantize packed e2m1 nibbles (N, K // 2) -> (N, K) in dtype."""
245 lo = (packed & 0b10000000) | ((packed & 0b01110000) >> 2)
246 lo = lo.view(torch.float8_e4m3fn).to(dtype) * (2**6)
247 hi_bits = packed << 4
248 hi = (hi_bits & 0b10000000) | ((hi_bits & 0b01110000) >> 2)
249 hi = hi.view(torch.float8_e4m3fn).to(dtype) * (2**6)
250 return torch.cat([hi.unsqueeze(2), lo.unsqueeze(2)], 2).view(packed.size(0), -1)
251
252
253 @pytest.mark.skipif(
254 _gpu_marlin_unsupported() or not is_fp4_marlin_supported(),
255 reason="FP4 Marlin is not supported on this GPU type.",
256 )
257 @pytest.mark.parametrize("shape", ODD_SHAPES)
258 def test_nvfp4_marlin_padded_round_trip(shape):
259 size_n, size_k = shape
260 group_size = 16
261 dtype = torch.float16
262 layer = torch.nn.Module()
263 layer.output_size_per_partition = size_n
264 layer.input_size_per_partition = size_k
265 layer.params_dtype = dtype
266
267 packed = torch.randint(
268 0, 256, (size_n, size_k // 2), dtype=torch.uint8, device="cuda"
269 )
270 scales = (torch.rand(size_n, size_k // group_size, device="cuda") + 0.25).to(
271 torch.float8_e4m3fn
272 )
273 global_scale = torch.tensor([0.002], dtype=torch.float32, device="cuda")
274
275 ref_weight = (
276 _dequant_fp4(packed, dtype)
277 * scales.to(dtype).repeat_interleave(group_size, 1)
278 * global_scale.to(dtype)
279 )
280
281 layer.weight = torch.nn.Parameter(packed, requires_grad=False)
282 layer.weight_scale = torch.nn.Parameter(scales, requires_grad=False)
283 layer.weight_global_scale = torch.nn.Parameter(global_scale, requires_grad=False)
284
285 prepare_fp4_layer_for_marlin(layer)
286
287 x = torch.randn(8, size_k, dtype=dtype, device="cuda") / size_k**0.5
288 output = apply_fp4_marlin_linear(
289 input=x,
290 weight=layer.weight,
291 weight_scale=layer.weight_scale,
292 weight_global_scale=layer.weight_global_scale,
293 workspace=get_marlin_workspace(layer.weight.device),
294 size_n=size_n,
295 size_k=size_k,
296 )
297 ref = x @ ref_weight.T
298
299 assert output.shape == (8, size_n)
300 torch.testing.assert_close(output, ref, rtol=2e-2, atol=2e-2)
301
302
303 @pytest.mark.skipif(
304 _gpu_marlin_unsupported(),
305 reason="Marlin is not supported on this GPU type.",
306 )
307 @pytest.mark.parametrize("shape", ODD_SHAPES)
308 @pytest.mark.parametrize("group_size", [-1, 128])
309 def test_gptq_marlin_padded_round_trip(shape, group_size):
310 """Pad-then-repack a GPTQ int4 weight the way MarlinLinearKernel does and
311 check the GEMM against the dequantized reference.
312
313 Symmetric int4's quantized zero decodes to -8, so this exercises the
314 zero-padded-scales cancellation, not just zero weights.
315 """
316 size_n, size_k = shape
317 if group_size > 0 and size_k % group_size != 0:
318 pytest.skip("group must divide the rank-local K (not fixable by padding)")
319 dtype = torch.float16
320 quant_type = scalar_types.uint4b8
321 device = torch.device("cuda")
322
323 weight = torch.randn(size_k, size_n, dtype=dtype, device=device) / size_k**0.5
324 w_ref, q_w, s = gptq_quantize_weights(weight, quant_type, group_size)
325 qweight = gptq_pack(q_w, quant_type.size_bits, size_k, size_n)
326
327 padded_n, padded_k = marlin_padded_nk(size_n, size_k, group_size)
328 qweight = marlin_pad_qweight(qweight, size_n, size_k, padded_n, padded_k)
329 marlin_qweight = ops.gptq_marlin_repack(
330 b_q_weight=qweight,
331 size_k=padded_k,
332 size_n=padded_n,
333 num_bits=quant_type.size_bits,
334 )
335 s = marlin_pad_scales(s, size_n, size_k, padded_n, padded_k, group_size)
336 marlin_s = marlin_permute_scales(
337 s, size_k=padded_k, size_n=padded_n, group_size=group_size
338 )
339
340 x = torch.randn(8, size_k, dtype=dtype, device=device)
341 output = apply_gptq_marlin_linear(
342 input=x,
343 weight=marlin_qweight,
344 weight_scale=marlin_s,
345 weight_zp=marlin_make_empty(device),
346 workspace=marlin_make_workspace_new(device),
347 wtype=quant_type,
348 output_size_per_partition=size_n,
349 input_size_per_partition=size_k,
350 )
351 ref = x @ w_ref
352
353 assert output.shape == (8, size_n)
354 torch.testing.assert_close(output, ref, rtol=2e-2, atol=2e-2)
355
356
357 @pytest.mark.skipif(
358 _gpu_marlin_unsupported() or not is_fp8_marlin_supported(),
359 reason="FP8 Marlin is not supported on this GPU type.",
360 )
361 @pytest.mark.parametrize("shape", [(200, 512), (4640, 512)])
362 def test_fp8_block_marlin_padded_round_trip(shape):
363 """Block-quantized FP8 (e.g. Nemotron NVFP4 checkpoints' FP8 layers):
364 group_size=128 exercises the lcm K-alignment in marlin_padded_nk and the
365 weight_scale_inv group-wise scale padding."""
366 size_n, size_k = shape
367 block = 128
368 dtype = torch.float16
369 layer = torch.nn.Module()
370 layer.output_size_per_partition = size_n
371 layer.input_size_per_partition = size_k
372 layer.orig_dtype = dtype
373 layer.weight_block_size = [block, block]
374
375 weight = torch.randn(size_n, size_k, dtype=dtype, device="cuda") / size_k**0.5
376 n_blocks, k_blocks = (size_n + block - 1) // block, size_k // block
377 padded = torch.zeros(n_blocks * block, size_k, dtype=dtype, device="cuda")
378 padded[:size_n] = weight
379 scales = padded.view(n_blocks, block, k_blocks, block).abs().amax(dim=(1, 3)) / 448
380 scales_expanded = scales.repeat_interleave(block, 0)[:size_n].repeat_interleave(
381 block, 1
382 )
383 weight_fp8 = (weight / scales_expanded).to(torch.float8_e4m3fn)
384
385 layer.weight = torch.nn.Parameter(weight_fp8, requires_grad=False)
386 layer.weight_scale_inv = torch.nn.Parameter(
387 scales.to(torch.float32), requires_grad=False
388 )
389
390 prepare_fp8_layer_for_marlin(layer, size_k_first=False)
391
392 x = torch.randn(8, size_k, dtype=dtype, device="cuda")
393 output = apply_fp8_marlin_linear(
394 input=x,
395 weight=layer.weight,
396 weight_scale=layer.weight_scale_inv,
397 workspace=get_marlin_workspace(layer.weight.device),
398 size_n=size_n,
399 size_k=size_k,
400 bias=None,
401 )
402 ref = x @ (weight_fp8.to(dtype) * scales_expanded.to(dtype)).T
403
404 assert output.shape == (8, size_n)
405 torch.testing.assert_close(output, ref, rtol=2e-2, atol=2e-2)
406
407
408 @pytest.mark.skipif(
409 _gpu_marlin_unsupported() or not is_fp8_marlin_supported(),
410 reason="FP8 Marlin is not supported on this GPU type.",
411 )
412 @pytest.mark.parametrize(
413 "shape", [(32, 256), (96, 256), (130, 256), (200, 288), (4640, 512)]
414 )
415 def test_mxfp8_marlin_padded_round_trip(shape):
416 """MXFP8 exercises the e8m0 scale path, where padded 0.0 scales clamp to
417 2^-127 instead of zero and must still contribute nothing."""
418 size_n, size_k = shape
419 group_size = 32
420 # The e8m0-scale Marlin kernels are only instantiated for bf16 activations.
421 dtype = torch.bfloat16
422 layer = torch.nn.Module()
423 layer.output_size_per_partition = size_n
424 layer.input_size_per_partition = size_k
425
426 weight_fp8 = (torch.randn(size_n, size_k, dtype=dtype, device="cuda") / 4).to(
427 torch.float8_e4m3fn
428 )
429 # e8m0 exponents around 1.0 (127): scales in [2^-6, 2^0]
430 scales = torch.randint(
431 121, 128, (size_n, size_k // group_size), dtype=torch.uint8, device="cuda"
432 )
433 ref_weight = weight_fp8.to(dtype) * (
434 2.0 ** (scales.to(dtype) - 127)
435 ).repeat_interleave(group_size, 1)
436
437 layer.weight = torch.nn.Parameter(weight_fp8, requires_grad=False)
438 layer.weight_scale = torch.nn.Parameter(scales, requires_grad=False)
439
440 prepare_mxfp8_layer_for_marlin(layer)
441
442 x = torch.randn(8, size_k, dtype=dtype, device="cuda") / size_k**0.5
443 output = apply_mxfp8_marlin_linear(
444 input=x,
445 weight=layer.weight,
446 weight_scale=layer.weight_scale,
447 workspace=get_marlin_workspace(layer.weight.device),
448 size_n=size_n,
449 size_k=size_k,
450 )
451 ref = x @ ref_weight.T
452
453 assert output.shape == (8, size_n)
454 torch.testing.assert_close(output, ref, rtol=2e-2, atol=2e-2)
455
456
457 @pytest.mark.skipif(
458 _gpu_marlin_unsupported(),
459 reason="Marlin is not supported on this GPU type.",
460 )
461 @pytest.mark.parametrize("shape", [(200, 512), (4640, 512)])
462 def test_awq_zp_marlin_padded_round_trip(shape):
463 """AWQ-style uint4 with runtime zero-points, padded the way
464 MarlinLinearKernel does: padded columns rely on (q=0 - zp=0) * scale=0."""
465 size_n, size_k = shape
466 group_size = 128
467 dtype = torch.float16
468 quant_type = scalar_types.uint4
469 device = torch.device("cuda")
470
471 weight = torch.randn(size_k, size_n, dtype=dtype, device=device) / size_k**0.5
472 w_ref, q_w, s, zp = quantize_weights(
473 weight, quant_type, group_size, zero_points=True
474 )
475 qweight = gptq_pack(q_w, quant_type.size_bits, size_k, size_n)
476
477 padded_n, padded_k = marlin_padded_nk(size_n, size_k, group_size)
478 qweight = marlin_pad_qweight(qweight, size_n, size_k, padded_n, padded_k)
479 marlin_qweight = ops.gptq_marlin_repack(
480 b_q_weight=qweight,
481 size_k=padded_k,
482 size_n=padded_n,
483 num_bits=quant_type.size_bits,
484 )
485 s = marlin_pad_scales(s, size_n, size_k, padded_n, padded_k, group_size)
486 marlin_s = marlin_permute_scales(
487 s, size_k=padded_k, size_n=padded_n, group_size=group_size
488 )
489 zp = marlin_pad_scales(zp, size_n, size_k, padded_n, padded_k, group_size)
490 marlin_zp = marlin_zero_points(
491 zp,
492 size_k=padded_k // group_size,
493 size_n=padded_n,
494 num_bits=quant_type.size_bits,
495 )
496
497 x = torch.randn(8, size_k, dtype=dtype, device=device)
498 output = apply_gptq_marlin_linear(
499 input=x,
500 weight=marlin_qweight,
501 weight_scale=marlin_s,
502 weight_zp=marlin_zp,
503 workspace=marlin_make_workspace_new(device),
504 wtype=quant_type,
505 output_size_per_partition=size_n,
506 input_size_per_partition=size_k,
507 )
508 ref = x @ w_ref
509
510 assert output.shape == (8, size_n)
511 torch.testing.assert_close(output, ref, rtol=2e-2, atol=2e-2)
512
513
514 class _FakeLinear:
515 def __init__(self, size_n, size_k, input_size=None):
516 self.output_size_per_partition = size_n
517 self.input_size_per_partition = size_k
518 self.output_size = size_n
519 self.input_size = input_size if input_size is not None else size_k
520
521
522 def test_check_marlin_supports_layer_allow_tile_padding():
523 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
524 check_marlin_supports_layer,
525 )
526
527 # Tile-misaligned but group-aligned: rejected strictly, allowed w/ padding
528 layer: LinearBase = _FakeLinear(4640, 512, input_size=2048)
529 assert not check_marlin_supports_layer(layer, 128)
530 assert check_marlin_supports_layer(layer, 128, allow_tile_padding=True)
531 assert check_marlin_supports_layer(layer, -1, allow_tile_padding=True)
532
533 # A group straddling the TP shard cannot be fixed by padding
534 layer = _FakeLinear(4608, 4672, input_size=18688)
535 assert not check_marlin_supports_layer(layer, 128, allow_tile_padding=True)
536
537
538 @pytest.mark.skipif(
539 _gpu_marlin_unsupported(),
540 reason="Marlin is not supported on this GPU type.",
541 )
542 @pytest.mark.parametrize("group_size", [-1, 32])
543 @pytest.mark.parametrize("shape", [(96, 256, 8), (160, 512, 4)])
544 def test_gptq_marlin_moe_padded_round_trip(shape, group_size):
545 """Pad a tile-misaligned MoE intermediate the way the WNA16 Marlin MoE prep
546 does, run the real repack + fused_marlin_moe, and check against the
547 dequantized reference. Symmetric int4's quantized zero decodes to -8, so the
548 padded region only stays out of the output via the zero-padded scales.
549 """
550 from tests.kernels.utils import torch_experts
551 from vllm.config import VllmConfig, set_current_vllm_config
552 from vllm.model_executor.layers.fused_moe import fused_topk
553 from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
554 fused_marlin_moe,
555 )
556 from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
557 _pad_rows,
558 _pad_w13_shard_cols,
559 )
560 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
561 marlin_moe_padded_intermediate,
562 marlin_moe_permute_scales,
563 )
564
565 n, k, e = shape
566 topk, m = 2, 33
567 padded_n = marlin_moe_padded_intermediate(n, group_size)
568 assert padded_n != n, "test should exercise padding"
569
570 dtype = torch.float16
571 device = torch.device("cuda")
572 quant_type = scalar_types.uint4b8
573 bits = quant_type.size_bits
574 pack = 32 // bits
575
576 a = torch.randn((m, k), device=device, dtype=dtype) / 10
577 w1 = torch.randn((e, 2 * n, k), device=device, dtype=dtype) / k**0.5
578 w2 = torch.randn((e, k, n), device=device, dtype=dtype) / n**0.5
579
580 def quant(w, size_k, size_n):
581 # w is (size_n, size_k); gptq expects (size_k, size_n).
582 ref, q_w, s = gptq_quantize_weights(w.T, quant_type, group_size)
583 return ref, gptq_pack(q_w, bits, size_k, size_n), s
584
585 w13_qw, w13_s, w13_ref = [], [], []
586 w2_qw, w2_s, w2_ref = [], [], []
587 for i in range(e):
588 ref, qw, s = quant(w1[i], k, 2 * n)
589 w13_ref.append(ref.T) # (2n, k)
590 w13_qw.append(qw)
591 w13_s.append(s)
592 ref, qw, s = quant(w2[i], n, k)
593 w2_ref.append(ref.T) # (k, n)
594 w2_qw.append(qw)
595 w2_s.append(s)
596
597 w13_qweight = torch.stack(w13_qw)
598 w2_qweight = torch.stack(w2_qw)
599 w13_scales = torch.stack(w13_s)
600 w2_scales = torch.stack(w2_s)
601 w1_ref = torch.stack(w13_ref) # (e, 2n, k)
602 w2_ref = torch.stack(w2_ref) # (e, k, n)
603
604 # Pad the intermediate via the production helpers.
605 w13_qweight = _pad_w13_shard_cols(w13_qweight, n, padded_n)
606 w2_qweight = _pad_rows(w2_qweight, padded_n // pack)
607 w13_scales = _pad_w13_shard_cols(w13_scales, n, padded_n)
608 if group_size > 0:
609 w2_scales = _pad_rows(w2_scales, padded_n // group_size)
610
611 marlin_w13 = ops.gptq_marlin_moe_repack(
612 w13_qweight, w13_qweight.shape[1] * pack, w13_qweight.shape[2], bits
613 )
614 marlin_w2 = ops.gptq_marlin_moe_repack(
615 w2_qweight, w2_qweight.shape[1] * pack, w2_qweight.shape[2], bits
616 )
617 group_or_pack = group_size if group_size != -1 else pack
618 marlin_w13_s = marlin_moe_permute_scales(
619 s=w13_scales, size_k=n, size_n=w13_scales.shape[2], group_size=group_size
620 )
621 marlin_w2_s = marlin_moe_permute_scales(
622 s=w2_scales,
623 size_k=w2_scales.shape[1] * group_or_pack,
624 size_n=w2_scales.shape[2],
625 group_size=group_size,
626 )
627
628 score = torch.randn((m, e), device=device, dtype=dtype)
629 topk_weights, topk_ids, _ = fused_topk(a, score, topk, False)
630
631 marlin_out = fused_marlin_moe(
632 a,
633 marlin_w13,
634 marlin_w2,
635 None,
636 None,
637 marlin_w13_s,
638 marlin_w2_s,
639 topk_weights,
640 topk_ids,
641 quant_type_id=quant_type.id,
642 global_num_experts=e,
643 )
644 with set_current_vllm_config(VllmConfig()):
645 ref = torch_experts(
646 a,
647 w1_ref,
648 w2_ref,
649 topk_weight=topk_weights,
650 topk_ids=topk_ids,
651 global_num_experts=e,
652 )
653
654 torch.testing.assert_close(marlin_out, ref, atol=5e-2, rtol=0)
655
656
657 @pytest.mark.skipif(
658 current_platform.is_rocm(),
659 reason="MoE Marlin is not selected on ROCm.",
660 )
661 def test_check_moe_marlin_supports_layer_padding():
662 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
663 check_moe_marlin_supports_layer,
664 )
665
666 def make_layer(hidden, intermediate):
667 layer = SimpleNamespace()
668 layer.hidden_size = hidden
669 layer.apply_router_weight_on_input = False
670 layer.moe_config = SimpleNamespace(
671 hidden_dim=hidden, intermediate_size_per_partition_unpadded=intermediate
672 )
673 return layer
674
675 # group=32 with intermediate % 64 != 0: rejected strictly, accepted w/ padding
676 layer = make_layer(4096, 96)
677 assert not check_moe_marlin_supports_layer(layer, 32)
678 assert check_moe_marlin_supports_layer(layer, 32, allow_tile_padding=True)
679 # channelwise misaligned intermediate is paddable
680 assert check_moe_marlin_supports_layer(layer, -1, allow_tile_padding=True)
681
682 # A group straddling the boundary cannot be fixed by padding
683 layer = make_layer(4096, 176)
684 assert not check_moe_marlin_supports_layer(layer, 128, allow_tile_padding=True)
685
686 # hidden_size is the MoE I/O extent and is never padded
687 layer = make_layer(4090, 128)
688 assert not check_moe_marlin_supports_layer(layer, 64, allow_tile_padding=True)
689
690
691 @pytest.mark.skipif(
692 _gpu_marlin_unsupported() or not is_fp8_marlin_supported(),
693 reason="FP8 Marlin is not supported on this GPU type.",
694 )
695 @pytest.mark.parametrize("quant", ["channel", "tensor"])
696 @pytest.mark.parametrize("shape", [(96, 256, 8), (160, 512, 4)])
697 def test_fp8_marlin_moe_padded_round_trip(shape, quant):
698 """FP8 weight-only MoE: pad a tile-misaligned intermediate and check the
699 real prepare + fused_marlin_moe against the dequantized reference."""
700 from tests.kernels.utils import torch_experts
701 from vllm.config import VllmConfig, set_current_vllm_config
702 from vllm.model_executor.layers.fused_moe import fused_topk
703 from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
704 fused_marlin_moe,
705 )
706 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
707 marlin_moe_intermediate_size,
708 marlin_moe_padded_intermediate,
709 )
710 from vllm.model_executor.layers.quantization.utils.marlin_utils_fp8 import (
711 prepare_fp8_moe_layer_for_marlin,
712 )
713
714 n, k, e = shape
715 topk, m = 2, 33
716 fp8 = torch.float8_e4m3fn
717 dtype = torch.bfloat16
718 device = torch.device("cuda")
719 padded_n = marlin_moe_padded_intermediate(n, -1)
720 assert padded_n != n
721
722 def q(w): # (out, in) -> fp8 weight, scale, dequant reference
723 dim = None if quant == "tensor" else 1
724 s = (w.abs().amax(dim, keepdim=dim is not None) / 448.0).clamp(min=1e-8)
725 wq = (w / s).clamp(-448, 448).to(fp8)
726 ref = wq.to(dtype) * s.to(dtype)
727 s = s.reshape(1) if quant == "tensor" else s.squeeze(1)
728 return wq, s, ref
729
730 a = torch.randn((m, k), device=device, dtype=dtype) / 10
731 w1 = torch.randn((e, 2 * n, k), device=device, dtype=dtype) / k**0.5
732 w2 = torch.randn((e, k, n), device=device, dtype=dtype) / n**0.5
733 w13_q, w13_s, w1_ref = zip(*(q(w1[i]) for i in range(e)))
734 w2_q, w2_s, w2_ref = zip(*(q(w2[i]) for i in range(e)))
735
736 w13_weight, w2_weight = torch.stack(w13_q), torch.stack(w2_q)
737 layer = SimpleNamespace(
738 num_experts=e,
739 hidden_size=k,
740 intermediate_size_per_partition=n,
741 orig_dtype=dtype,
742 w13_weight=w13_weight,
743 )
744 pw13, pw2, ps13, ps2 = prepare_fp8_moe_layer_for_marlin(
745 layer, w13_weight, w2_weight, torch.stack(w13_s), torch.stack(w2_s)
746 )
747 assert marlin_moe_intermediate_size(pw13, pw2) == padded_n
748
749 score = torch.randn((m, e), device=device, dtype=dtype)
750 topk_weights, topk_ids, _ = fused_topk(a, score, topk, False)
751 out = fused_marlin_moe(
752 a,
753 pw13,
754 pw2,
755 None,
756 None,
757 ps13,
758 ps2,
759 topk_weights,
760 topk_ids,
761 quant_type_id=scalar_types.float8_e4m3fn.id,
762 global_num_experts=e,
763 workspace=get_marlin_workspace(device),
764 )
765 with set_current_vllm_config(VllmConfig()):
766 ref = torch_experts(
767 a,
768 torch.stack(w1_ref),
769 torch.stack(w2_ref),
770 topk_weight=topk_weights,
771 topk_ids=topk_ids,
772 global_num_experts=e,
773 )
774 torch.testing.assert_close(out, ref, atol=8e-2, rtol=0)
775
776
777 @pytest.mark.skipif(
778 _gpu_marlin_unsupported() or not is_fp8_marlin_supported(),
779 reason="FP8 Marlin is not supported on this GPU type.",
780 )
781 @pytest.mark.parametrize("shape", [(96, 256, 8), (160, 512, 4)])
782 def test_mxfp8_marlin_moe_padded_round_trip(shape):
783 """MXFP8 weight-only MoE round-trip at a tile-misaligned intermediate, with
784 unit e8m0 scales so the reference is the exact fp8 dequant."""
785 from tests.kernels.utils import torch_experts
786 from vllm.config import VllmConfig, set_current_vllm_config
787 from vllm.model_executor.layers.fused_moe import fused_topk
788 from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
789 fused_marlin_moe,
790 )
791 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
792 marlin_moe_intermediate_size,
793 marlin_moe_padded_intermediate,
794 )
795 from vllm.model_executor.layers.quantization.utils.marlin_utils_fp8 import (
796 prepare_mxfp8_moe_layer_for_marlin,
797 )
798
799 n, k, e = shape
800 topk, m, gs, e8m0_one = 2, 33, 32, 127
801 fp8 = torch.float8_e4m3fn
802 dtype = torch.bfloat16
803 device = torch.device("cuda")
804 padded_n = marlin_moe_padded_intermediate(n, gs)
805 assert padded_n != n
806
807 a = torch.randn((m, k), device=device, dtype=dtype) / 10
808 w13_weight = torch.randn((e, 2 * n, k), device=device, dtype=dtype) / k**0.5
809 w2_weight = torch.randn((e, k, n), device=device, dtype=dtype) / n**0.5
810 w13_weight = w13_weight.clamp(-448, 448).to(fp8)
811 w2_weight = w2_weight.clamp(-448, 448).to(fp8)
812 w13_scale = torch.full(
813 (e, 2 * n, k // gs), e8m0_one, dtype=torch.uint8, device=device
814 )
815 w2_scale = torch.full((e, k, n // gs), e8m0_one, dtype=torch.uint8, device=device)
816
817 layer = SimpleNamespace(
818 num_experts=e, hidden_size=k, intermediate_size_per_partition=n
819 )
820 with set_current_vllm_config(VllmConfig()):
821 pw13, pw2, ps13, ps2 = prepare_mxfp8_moe_layer_for_marlin(
822 layer, w13_weight, w2_weight, w13_scale, w2_scale
823 )
824 assert marlin_moe_intermediate_size(pw13, pw2) == padded_n
825
826 score = torch.randn((m, e), device=device, dtype=dtype)
827 topk_weights, topk_ids, _ = fused_topk(a, score, topk, False)
828 out = fused_marlin_moe(
829 a,
830 pw13,
831 pw2,
832 None,
833 None,
834 ps13,
835 ps2,
836 topk_weights,
837 topk_ids,
838 quant_type_id=scalar_types.float8_e4m3fn.id,
839 global_num_experts=e,
840 workspace=get_marlin_workspace(device),
841 )
842 with set_current_vllm_config(VllmConfig()):
843 ref = torch_experts(
844 a,
845 w13_weight.to(dtype),
846 w2_weight.to(dtype),
847 topk_weight=topk_weights,
848 topk_ids=topk_ids,
849 global_num_experts=e,
850 )
851 torch.testing.assert_close(out, ref, atol=8e-2, rtol=0)