main
py 373 lines 13.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Test comparing Marlin INT4 MoE vs FlashInfer TRT-LLM MXINT4 MoE."""
4
5 import pytest
6 import torch
7
8 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
9 from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
10 fused_marlin_moe,
11 )
12 from vllm.model_executor.layers.fused_moe.experts.trtllm_mxint4_moe import (
13 TrtLlmMxint4ExpertsMonolithic,
14 )
15 from vllm.model_executor.layers.fused_moe.router.grouped_topk_router import (
16 grouped_topk,
17 )
18 from vllm.model_executor.layers.quantization.utils.flashinfer_mxint4_moe import (
19 prepare_static_weights_for_trtllm_mxint4_moe,
20 )
21 from vllm.platforms import current_platform
22 from vllm.scalar_type import scalar_types
23 from vllm.utils.torch_utils import set_random_seed
24
25
26 def mxint4_quantize(
27 x: torch.Tensor, sf_vec_size: int = 32
28 ) -> tuple[torch.Tensor, torch.Tensor]:
29 """Quantize BF16 tensor to MXINT4 with block scaling (group_size=sf_vec_size).
30
31 Returns:
32 - uint8 packed (2 INT4/byte): [..., k//2] - stores SIGNED INT4 [-8, 7]
33 - scales in BF16: [..., k//sf_vec_size]
34
35 """
36 x_reshaped = x.reshape(-1, sf_vec_size)
37 x_max = x_reshaped.max(dim=-1, keepdim=True)[0].to(torch.float32)
38 x_min = x_reshaped.min(dim=-1, keepdim=True)[0].to(torch.float32)
39 x_max = x_max * 8.0 / 7.0
40 amax = torch.where(x_max > -x_min, x_max, -x_min)
41 scales = amax / 8.0
42 x_scaled = x_reshaped * scales.reciprocal()
43 x_int8 = (
44 x_scaled.round().clamp(-8, 7).to(torch.int8).reshape(-1, sf_vec_size // 2, 2)
45 )
46 x_int4 = (x_int8[..., 0] & 0x0F) | ((x_int8[..., 1] & 0x0F) << 4)
47 return (
48 x_int4.to(torch.uint8).reshape(*x.shape[:-1], x.shape[-1] // 2),
49 scales.to(x.dtype).reshape(*x.shape[:-1], x.shape[-1] // sf_vec_size),
50 )
51
52
53 def mxint4_quantize_moe_weights(
54 weights_bf16: torch.Tensor, group_size: int = 32
55 ) -> tuple[torch.Tensor, torch.Tensor]:
56 """Quantize MoE weights [e, n, k] to MxInt4 format.
57
58 Args:
59 weights_bf16: BF16 weights of shape [num_experts, out_features, in_features]
60 group_size: Quantization group size (default: 32)
61
62 Returns:
63 - weights_mxint4: Quantized weights [e, n, k//2] uint8
64 - scales_mxint4: Quantization scales [e, n, k//group_size] bf16
65
66 """
67 e = weights_bf16.shape[0]
68 weight_list = []
69 scale_list = []
70
71 for i in range(e):
72 w_q, w_s = mxint4_quantize(weights_bf16[i], sf_vec_size=group_size)
73 weight_list.append(w_q)
74 scale_list.append(w_s)
75
76 return torch.stack(weight_list), torch.stack(scale_list)
77
78
79 __all__ = [
80 "mxint4_quantize",
81 "mxint4_quantize_moe_weights",
82 "marlin_quantize_moe_weights",
83 ]
84
85
86 def test_trtllm_mxint4_activation_supports_vllm_gated_silu():
87 assert TrtLlmMxint4ExpertsMonolithic._supports_activation(MoEActivation.SILU)
88 assert TrtLlmMxint4ExpertsMonolithic._supports_activation(MoEActivation.SWIGLUOAI)
89 assert not TrtLlmMxint4ExpertsMonolithic._supports_activation(
90 MoEActivation.RELU2_NO_MUL
91 )
92
93
94 def marlin_quantize_moe_weights(
95 weights_bf16: torch.Tensor, group_size: int = 32
96 ) -> tuple[torch.Tensor, torch.Tensor]:
97 """Quantize MoE weights [e, n, k] to Marlin INT4 format.
98
99 Args:
100 weights_bf16: BF16 weights of shape [num_experts, out_features, in_features]
101 group_size: Quantization group size (default: 32)
102
103 Returns:
104 - weights_marlin: Marlin quantized weights [e, k//8, n] int32
105 - scales_marlin: Marlin quantization scales [e, k//group_size, n] bf16
106
107 """
108 from vllm.model_executor.layers.quantization.utils.marlin_utils_test import (
109 marlin_quantize,
110 )
111
112 e, n, k = weights_bf16.shape
113 weight_list = []
114 scale_list = []
115
116 for i in range(e):
117 # Transpose for Marlin: [n, k] → [k, n]
118 w_t = weights_bf16[i].T.contiguous()
119 _, w_q, w_s = marlin_quantize(w_t, scalar_types.uint4b8, group_size)
120 weight_list.append(w_q)
121 scale_list.append(w_s)
122
123 # Stack to get [e, ...] shape
124 weights_marlin = torch.stack(weight_list) # [e, k // 8, n]
125 scales_marlin = torch.stack(scale_list) # [e, k // group_size, n]
126
127 return weights_marlin, scales_marlin
128
129
130 TRTLLM_GEN_AVAILABLE = (
131 current_platform.is_cuda() and current_platform.is_device_capability_family(100)
132 )
133
134
135 @pytest.mark.skipif(not TRTLLM_GEN_AVAILABLE, reason="Skip for non SM100")
136 @pytest.mark.parametrize("m", [1, 33])
137 @pytest.mark.parametrize("n", [7168])
138 @pytest.mark.parametrize("k", [512])
139 @pytest.mark.parametrize("e", [384])
140 @pytest.mark.parametrize("topk", [8])
141 @pytest.mark.parametrize("group_size", [32])
142 def test_marlin_vs_trtllm_mxint4_moe_kimik2(monkeypatch, m, n, k, e, topk, group_size):
143 """Compare Marlin INT4 MoE vs FlashInfer TRT-LLM MXINT4 MoE.
144
145 Uses mxint4_quantize() to generate common INT4 weights + BF16 scales,
146 then runs both Marlin and TRT-LLM kernels and compares outputs.
147 """
148 pytest.importorskip("flashinfer")
149 monkeypatch.setenv("VLLM_USE_FLASHINFER_MOE_INT4", "1")
150
151 set_random_seed(0)
152
153 dtype = torch.bfloat16
154
155 # DeepSeekV3 routing config (from Kimi-K2-Thinking config.json)
156 n_group = 1 # n_group from model config
157 topk_group = 1 # topk_group from model config
158 routed_scaling = 2.827 # routed_scaling_factor from model config
159
160 # Input - realistic activation range for LLM (after LayerNorm: mean~0, std~1)
161 a = torch.randn((m, k), device="cuda", dtype=dtype) * 0.5
162
163 # Generate routing logits and bias (DeepSeekV3 expects float logits)
164 # Realistic ranges: logits typically [-3, 3], bias [-2, 2]
165 routing_logits = torch.randn((m, e), device="cuda", dtype=torch.float32) * 1.5
166 routing_bias = torch.randn(e, device="cuda", dtype=torch.float32) * 0.8
167
168 # 1. Generate BF16 weights (SHARED between both paths)
169 # Realistic weight initialization: Xavier/Glorot uniform scaling
170 # std = sqrt(2 / (fan_in + fan_out))
171 std_w1 = (2.0 / (k + 2 * n)) ** 0.5
172 std_w2 = (2.0 / (n + k)) ** 0.5
173 w1_bf16 = torch.randn((e, 2 * n, k), device="cuda", dtype=dtype) * std_w1
174 w2_bf16 = torch.randn((e, k, n), device="cuda", dtype=dtype) * std_w2
175
176 # === Path 1: TRT-LLM FlashInfer MXINT4 MoE ===
177 # Similar to: if self.use_flashinfer_mxint4_moe
178 # Quantize using MXINT4 method (signed INT4)
179 w1_int4, w1_scales = mxint4_quantize_moe_weights(w1_bf16, group_size)
180 w2_int4, w2_scales = mxint4_quantize_moe_weights(w2_bf16, group_size)
181
182 trtllm_weights = prepare_static_weights_for_trtllm_mxint4_moe(
183 gemm1_weights=w1_int4,
184 gemm1_scales=w1_scales,
185 gemm2_weights=w2_int4,
186 gemm2_scales=w2_scales,
187 )
188
189 from flashinfer import RoutingMethodType
190 from flashinfer.fused_moe import trtllm_mxint4_block_scale_moe
191
192 # Routing handled internally by trtllm_mxint4_block_scale_moe
193 trtllm_output = trtllm_mxint4_block_scale_moe(
194 routing_logits=routing_logits,
195 routing_bias=routing_bias.to(torch.bfloat16),
196 hidden_states=a,
197 gemm1_weights=trtllm_weights["gemm1_weights"],
198 gemm1_weights_scale=trtllm_weights["gemm1_scales"],
199 gemm1_alpha=None,
200 gemm1_beta=None,
201 gemm1_clamp_limit=None,
202 gemm2_weights=trtllm_weights["gemm2_weights"],
203 gemm2_weights_scale=trtllm_weights["gemm2_scales"],
204 num_experts=e,
205 top_k=topk,
206 n_group=n_group,
207 topk_group=topk_group,
208 intermediate_size=n,
209 local_expert_offset=0,
210 local_num_experts=e,
211 routed_scaling_factor=routed_scaling,
212 routing_method_type=RoutingMethodType.DeepSeekV3,
213 enable_pdl=None,
214 output=None,
215 tune_max_num_tokens=8192,
216 ).to(dtype)
217
218 # === Path 2: Marlin INT4 MoE ===
219 # Similar to: else (non-flashinfer path)
220 # Quantize using Marlin's method (UINT4b8)
221 w1_marlin, w1_scales_marlin = marlin_quantize_moe_weights(w1_bf16, group_size)
222 w2_marlin, w2_scales_marlin = marlin_quantize_moe_weights(w2_bf16, group_size)
223
224 # Use production routing kernel (same as router.select_experts internally uses)
225 topk_weights, topk_ids = grouped_topk(
226 hidden_states=a,
227 gating_output=routing_logits,
228 topk=topk,
229 renormalize=False, # DeepSeekV3 doesn't renormalize
230 num_expert_group=n_group,
231 topk_group=topk_group,
232 scoring_func="sigmoid", # DeepSeekV3 uses sigmoid
233 routed_scaling_factor=routed_scaling,
234 e_score_correction_bias=routing_bias,
235 )
236
237 marlin_output = fused_marlin_moe(
238 hidden_states=a,
239 w1=w1_marlin,
240 w2=w2_marlin,
241 bias1=None,
242 bias2=None,
243 w1_scale=w1_scales_marlin,
244 w2_scale=w2_scales_marlin,
245 topk_weights=topk_weights,
246 topk_ids=topk_ids,
247 quant_type_id=scalar_types.uint4b8.id,
248 global_num_experts=e,
249 expert_map=None,
250 global_scale1=None,
251 global_scale2=None,
252 input_global_scale1=None,
253 input_global_scale2=None,
254 w1_zeros=None,
255 w2_zeros=None,
256 input_dtype=dtype,
257 )
258
259 # Sanity check: manually compute BF16 reference for comparison
260 # Use same routing as Marlin path for consistency
261 bf16_output = torch.zeros((m, k), device="cuda", dtype=dtype)
262 for token_idx in range(m):
263 for expert_rank in range(topk):
264 expert_id = topk_ids[token_idx, expert_rank].item()
265 weight = topk_weights[token_idx, expert_rank].item()
266 # w1: [2*n, k] @ [k] -> [2*n]
267 up_gate = a[token_idx] @ w1_bf16[expert_id].T # [2*n]
268 gate, up = up_gate.chunk(2, dim=0)
269 intermediate = torch.nn.functional.silu(gate) * up # [n]
270 # w2: [k, n] @ [n] -> [k]
271 expert_out = intermediate @ w2_bf16[expert_id].T # [k]
272 bf16_output[token_idx] += weight * expert_out
273 # Compare against BF16 reference.
274 torch.testing.assert_close(marlin_output, bf16_output, atol=0.3, rtol=1.0)
275 torch.testing.assert_close(trtllm_output, bf16_output, atol=0.3, rtol=1.0)
276
277 # Compare against each other for sanity.
278 # Note: Different quantization schemes (UINT4b8 vs signed MXINT4) cause
279 # some differences
280 torch.testing.assert_close(marlin_output, trtllm_output, atol=0.3, rtol=6.0)
281
282
283 @pytest.mark.skipif(not TRTLLM_GEN_AVAILABLE, reason="Skip for non SM100")
284 @pytest.mark.parametrize("m", [1, 33])
285 @pytest.mark.parametrize("n", [7168])
286 @pytest.mark.parametrize("k", [512])
287 @pytest.mark.parametrize("e", [384])
288 @pytest.mark.parametrize("topk", [8])
289 @torch.inference_mode()
290 def test_flashinfer_trtllm_mxint4_moe_wrapper(m, n, k, e, topk):
291 """Test that the flashinfer_trtllm_mxint4_moe wrapper matches the raw
292 trtllm_mxint4_block_scale_moe kernel call."""
293 pytest.importorskip("flashinfer")
294 from flashinfer import RoutingMethodType
295 from flashinfer.fused_moe import trtllm_mxint4_block_scale_moe
296
297 from vllm.model_executor.layers.quantization.utils.flashinfer_mxint4_moe import (
298 flashinfer_trtllm_mxint4_moe,
299 )
300
301 set_random_seed(0)
302 dtype = torch.bfloat16
303
304 a = torch.randn((m, k), device="cuda", dtype=dtype) * 0.5
305 router_logits = torch.randn((m, e), device="cuda", dtype=torch.float32) * 1.5
306 routing_bias = torch.randn(e, device="cuda", dtype=torch.float32) * 0.8
307
308 std_w1 = (2.0 / (k + 2 * n)) ** 0.5
309 std_w2 = (2.0 / (n + k)) ** 0.5
310 w1_bf16 = torch.randn((e, 2 * n, k), device="cuda", dtype=dtype) * std_w1
311 w2_bf16 = torch.randn((e, k, n), device="cuda", dtype=dtype) * std_w2
312
313 w1_int4, w1_scales = mxint4_quantize_moe_weights(w1_bf16)
314 w2_int4, w2_scales = mxint4_quantize_moe_weights(w2_bf16)
315
316 prepared = prepare_static_weights_for_trtllm_mxint4_moe(
317 gemm1_weights=w1_int4,
318 gemm1_scales=w1_scales,
319 gemm2_weights=w2_int4,
320 gemm2_scales=w2_scales,
321 )
322
323 # Raw kernel call (reference)
324 raw_out = trtllm_mxint4_block_scale_moe(
325 routing_logits=router_logits.to(torch.float32),
326 routing_bias=routing_bias.to(torch.bfloat16),
327 hidden_states=a,
328 gemm1_weights=prepared["gemm1_weights"].data,
329 gemm1_weights_scale=prepared["gemm1_scales"].data,
330 gemm1_alpha=None,
331 gemm1_beta=None,
332 gemm1_clamp_limit=None,
333 gemm2_weights=prepared["gemm2_weights"].data,
334 gemm2_weights_scale=prepared["gemm2_scales"].data,
335 num_experts=e,
336 top_k=topk,
337 n_group=1,
338 topk_group=1,
339 intermediate_size=n,
340 local_expert_offset=0,
341 local_num_experts=e,
342 routed_scaling_factor=None,
343 routing_method_type=RoutingMethodType.DeepSeekV3,
344 enable_pdl=None,
345 output=None,
346 tune_max_num_tokens=8192,
347 )
348 if not isinstance(raw_out, torch.Tensor):
349 raw_out = raw_out[0]
350 raw_out = raw_out.to(dtype)
351
352 # Wrapper call
353 wrapper_out = flashinfer_trtllm_mxint4_moe(
354 x=a,
355 router_logits=router_logits,
356 w13_weight_packed=prepared["gemm1_weights"],
357 w13_weight_scale=prepared["gemm1_scales"],
358 w2_weight_packed=prepared["gemm2_weights"],
359 w2_weight_scale=prepared["gemm2_scales"],
360 global_num_experts=e,
361 top_k=topk,
362 intermediate_size_per_partition=n,
363 local_num_experts=e,
364 ep_rank=0,
365 num_expert_group=1,
366 topk_group=1,
367 e_score_correction_bias=routing_bias,
368 routing_method_type=RoutingMethodType.DeepSeekV3,
369 )
370
371 assert wrapper_out.shape == (m, k)
372 assert wrapper_out.dtype == dtype
373 torch.testing.assert_close(wrapper_out, raw_out, atol=0.0, rtol=0.0)