main
py 354 lines 11.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from types import SimpleNamespace
5
6 import pytest
7 import torch
8
9 from vllm.platforms import current_platform
10
11 if not current_platform.is_device_capability_family(120):
12 pytest.skip(
13 reason="FlashInfer B12x MoE requires SM120 (RTX Pro 6000 / DGX Spark).",
14 allow_module_level=True,
15 )
16
17 from vllm.utils.flashinfer import has_flashinfer_b12x_moe
18
19 if not has_flashinfer_b12x_moe():
20 pytest.skip(
21 reason=(
22 "FlashInfer B12xMoEWrapper not available in installed "
23 "FlashInfer (needs PR #3080)."
24 ),
25 allow_module_level=True,
26 )
27
28 # Import fp4_quantize after the skip guard — FlashInfer must be installed.
29 from flashinfer.fp4_quantization import fp4_quantize
30
31 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
32 from tests.kernels.moe.utils import make_dummy_moe_config
33 from tests.kernels.utils import torch_moe
34 from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config
35 from vllm.model_executor.layers.fused_moe import fused_topk
36 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
37 from vllm.model_executor.layers.fused_moe.all2all_utils import (
38 maybe_make_prepare_finalize,
39 )
40 from vllm.model_executor.layers.fused_moe.config import nvfp4_moe_quant_config
41 from vllm.model_executor.layers.fused_moe.experts.flashinfer_b12x_moe import (
42 FlashInferB12xExperts,
43 )
44 from vllm.model_executor.layers.quantization.utils.flashinfer_fp4_moe import (
45 reorder_w1w3_to_w3w1,
46 )
47 from vllm.utils.torch_utils import set_random_seed
48
49 # Dimensions chosen to satisfy FP4 alignment requirements (k multiple of 256,
50 # n multiple of 128) while keeping tests fast.
51 MNK_FACTORS = [
52 (2, 128, 256),
53 (2, 256, 512),
54 (16, 128, 256),
55 (64, 256, 512),
56 ]
57
58
59 def _process_b12x_weights(
60 experts: FlashInferB12xExperts,
61 w1_scale: torch.Tensor,
62 w2_scale: torch.Tensor,
63 w1_scale_2: torch.Tensor,
64 w2_scale_2: torch.Tensor,
65 ) -> None:
66 layer = SimpleNamespace(
67 w13_weight_scale=w1_scale,
68 w13_weight_scale_2=w1_scale_2,
69 w2_weight_scale=w2_scale,
70 w2_weight_scale_2=w2_scale_2,
71 )
72 experts.process_weights_after_loading(layer)
73
74
75 @pytest.mark.parametrize("m,n,k", MNK_FACTORS)
76 @pytest.mark.parametrize("e", [8, 16])
77 @pytest.mark.parametrize("topk", [1, 2, 4])
78 @pytest.mark.parametrize("dtype", [torch.bfloat16])
79 @torch.inference_mode()
80 def test_flashinfer_b12x_moe(
81 m: int,
82 n: int,
83 k: int,
84 e: int,
85 topk: int,
86 dtype: torch.dtype,
87 workspace_init,
88 ):
89 """Test FlashInferB12xExperts against a BF16 torch reference.
90
91 The SM12x kernel takes BF16 hidden states directly and fuses token
92 dispatch, W1 GEMM, SwiGLU, and W2 GEMM into one call. We verify
93 correctness against ``torch_moe`` using generous tolerances to account
94 for the internal FP4 quantization of activations and weights.
95
96 Scale convention
97 ----------------
98 The SM12x kernel uses ``w1_alpha`` as *both* the activation-quantisation
99 global scale and the weight dequantisation factor. These two roles are
100 conflated into a single parameter in ``launch_sm120_moe``, so they must
101 equal the same value. We use ``global_scale = 1.0`` for
102 ``fp4_quantize`` so that ``w1_alpha = ones`` satisfies both roles
103 simultaneously. The alternative — vLLM's convention of baking a large
104 ``w_gs`` into block-scale values and compensating with
105 ``g1_alphas = 1/w_gs`` — is incompatible with this kernel.
106 """
107 set_random_seed(7)
108 with set_current_vllm_config(
109 VllmConfig(parallel_config=ParallelConfig(pipeline_parallel_size=1))
110 ):
111 a = torch.randn((m, k), device="cuda", dtype=dtype) / 10
112
113 # Generate BF16 reference weights in [gate, up] order.
114 # Shape: w1=(e, 2n, k), w2=(e, k, n).
115 w1_bf16 = torch.randn((e, 2 * n, k), device="cuda", dtype=dtype) / 15
116 w2_bf16 = torch.randn((e, k, n), device="cuda", dtype=dtype) / 15
117
118 # ------------------------------------------------------------------ #
119 # Quantise weights for the SM12x kernel using FlashInfer's convention:
120 # global_scale = 1.0 → block_scale = max_abs_block / fp4_max
121 # w1_alpha = 1.0 (no extra global factor to compensate)
122 #
123 # The scale factors returned by fp4_quantize(..., is_sf_swizzled_layout=True)
124 # are already in the swizzled 2D layout expected by convert_sf_to_mma_layout.
125 # No additional swizzle_blockscale() call is needed.
126 # ------------------------------------------------------------------ #
127 gs = torch.ones(1, device="cuda", dtype=torch.float32)
128 sf_vec_size = 16
129
130 # W1: reorder BF16 from [gate, up] → [up, gate], then quantise.
131 # Note: in reorder_w1w3_to_w3w1, "w1" refers to the gate projection
132 # and "w3" refers to the up projection.
133 # A dummy scale is passed and discarded; real scales come from
134 # fp4_quantize after reordering.
135 w1_reordered, _ = reorder_w1w3_to_w3w1(
136 w1_bf16.clone(),
137 torch.ones((e, 2 * n, 1), device="cuda", dtype=torch.float32),
138 )
139 w1_flat = w1_reordered.reshape(e * 2 * n, k)
140 w1_q_flat, w1_sf_flat = fp4_quantize(
141 w1_flat,
142 global_scale=gs,
143 sf_vec_size=sf_vec_size,
144 is_sf_swizzled_layout=True,
145 )
146 w1_q = w1_q_flat.view(e, 2 * n, k // 2) # uint8, packed FP4
147 w1_blockscale = w1_sf_flat.view(e, 2 * n, w1_sf_flat.shape[1]) # float8
148
149 # W2: no row reordering needed for the down-projection.
150 w2_flat = w2_bf16.reshape(e * k, n)
151 w2_q_flat, w2_sf_flat = fp4_quantize(
152 w2_flat,
153 global_scale=gs,
154 sf_vec_size=sf_vec_size,
155 is_sf_swizzled_layout=True,
156 )
157 w2_q = w2_q_flat.view(e, k, n // 2) # uint8, packed FP4
158 w2_blockscale = w2_sf_flat.view(e, k, w2_sf_flat.shape[1]) # float8
159
160 # All per-expert alphas are 1.0 (global_scale = 1.0, no compensation).
161 ones_e = torch.ones(e, device="cuda", dtype=torch.float32)
162
163 quant_config = nvfp4_moe_quant_config(
164 g1_alphas=ones_e,
165 g2_alphas=ones_e,
166 a1_gscale=ones_e,
167 a2_gscale=ones_e,
168 w1_scale=w1_blockscale,
169 w2_scale=w2_blockscale,
170 )
171
172 moe_config = make_dummy_moe_config(
173 num_experts=e,
174 experts_per_token=topk,
175 hidden_dim=k,
176 intermediate_size=n,
177 in_dtype=dtype,
178 )
179
180 experts = FlashInferB12xExperts(
181 moe_config=moe_config,
182 quant_config=quant_config,
183 )
184
185 _process_b12x_weights(
186 experts,
187 w1_blockscale,
188 w2_blockscale,
189 ones_e,
190 ones_e,
191 )
192
193 kernel = mk.FusedMoEKernel(
194 maybe_make_prepare_finalize(
195 moe=moe_config,
196 quant_config=quant_config,
197 allow_new_interface=True,
198 use_monolithic=False,
199 ),
200 experts,
201 )
202
203 score = torch.randn((m, e), device="cuda", dtype=dtype)
204 topk_weights, topk_ids, _ = fused_topk(a, score, topk, renormalize=False)
205
206 sm12x_output = kernel.apply(
207 hidden_states=a,
208 w1=w1_q,
209 w2=w2_q,
210 topk_weights=topk_weights,
211 topk_ids=topk_ids,
212 global_num_experts=e,
213 activation=MoEActivation.SILU,
214 apply_router_weight_on_input=False,
215 expert_map=None,
216 )
217
218 # Reference: BF16 torch MoE using original [gate, up] BF16 weights.
219 # torch_moe's SiluAndMul expects [gate, up] order, matching w1_bf16.
220 torch_output = torch_moe(a, w1_bf16, w2_bf16, score, topk)
221
222 torch.testing.assert_close(sm12x_output, torch_output, atol=2e-1, rtol=2e-1)
223
224
225 @pytest.mark.parametrize("m,n,k", MNK_FACTORS)
226 @pytest.mark.parametrize("e", [8, 16])
227 @pytest.mark.parametrize("topk", [1, 2, 4])
228 @pytest.mark.parametrize("dtype", [torch.bfloat16])
229 @torch.inference_mode()
230 def test_flashinfer_b12x_moe_relu2(
231 m: int,
232 n: int,
233 k: int,
234 e: int,
235 topk: int,
236 dtype: torch.dtype,
237 workspace_init,
238 ):
239 """Test FlashInferB12xExperts with ReLU2 (non-gated) activation.
240
241 ReLU2 is used by Nemotron-H style models. Unlike the gated SiLU
242 path, w1 has shape [E, N, K] (not [E, 2N, K]) and the activation
243 is relu(x)^2 without a gate/up split.
244 """
245 set_random_seed(7)
246 with set_current_vllm_config(
247 VllmConfig(parallel_config=ParallelConfig(pipeline_parallel_size=1))
248 ):
249 a = torch.randn((m, k), device="cuda", dtype=dtype) / 10
250
251 # Non-gated: w1 shape is (e, n, k), not (e, 2n, k).
252 w1_bf16 = torch.randn((e, n, k), device="cuda", dtype=dtype) / 15
253 w2_bf16 = torch.randn((e, k, n), device="cuda", dtype=dtype) / 15
254
255 gs = torch.ones(1, device="cuda", dtype=torch.float32)
256 sf_vec_size = 16
257
258 # W1: no gate/up reordering for non-gated.
259 w1_flat = w1_bf16.reshape(e * n, k)
260 w1_q_flat, w1_sf_flat = fp4_quantize(
261 w1_flat,
262 global_scale=gs,
263 sf_vec_size=sf_vec_size,
264 is_sf_swizzled_layout=True,
265 )
266 w1_q = w1_q_flat.view(e, n, k // 2)
267 w1_blockscale = w1_sf_flat.view(e, n, w1_sf_flat.shape[1])
268
269 w2_flat = w2_bf16.reshape(e * k, n)
270 w2_q_flat, w2_sf_flat = fp4_quantize(
271 w2_flat,
272 global_scale=gs,
273 sf_vec_size=sf_vec_size,
274 is_sf_swizzled_layout=True,
275 )
276 w2_q = w2_q_flat.view(e, k, n // 2)
277 w2_blockscale = w2_sf_flat.view(e, k, w2_sf_flat.shape[1])
278
279 ones_e = torch.ones(e, device="cuda", dtype=torch.float32)
280
281 quant_config = nvfp4_moe_quant_config(
282 g1_alphas=ones_e,
283 g2_alphas=ones_e,
284 a1_gscale=ones_e,
285 a2_gscale=ones_e,
286 w1_scale=w1_blockscale,
287 w2_scale=w2_blockscale,
288 )
289
290 moe_config = make_dummy_moe_config(
291 num_experts=e,
292 experts_per_token=topk,
293 hidden_dim=k,
294 intermediate_size=n,
295 in_dtype=dtype,
296 activation=MoEActivation.RELU2_NO_MUL,
297 )
298
299 experts = FlashInferB12xExperts(
300 moe_config=moe_config,
301 quant_config=quant_config,
302 )
303 _process_b12x_weights(
304 experts,
305 w1_blockscale,
306 w2_blockscale,
307 ones_e,
308 ones_e,
309 )
310
311 kernel = mk.FusedMoEKernel(
312 maybe_make_prepare_finalize(
313 moe=moe_config,
314 quant_config=quant_config,
315 allow_new_interface=True,
316 use_monolithic=False,
317 ),
318 experts,
319 )
320
321 score = torch.randn((m, e), device="cuda", dtype=dtype)
322 topk_weights, topk_ids, _ = fused_topk(a, score, topk, renormalize=False)
323
324 b12x_output = kernel.apply(
325 hidden_states=a,
326 w1=w1_q,
327 w2=w2_q,
328 topk_weights=topk_weights,
329 topk_ids=topk_ids,
330 global_num_experts=e,
331 activation=MoEActivation.RELU2_NO_MUL,
332 apply_router_weight_on_input=False,
333 expert_map=None,
334 )
335
336 torch_output = torch_moe(
337 a,
338 w1_bf16,
339 w2_bf16,
340 score,
341 topk,
342 activation=MoEActivation.RELU2_NO_MUL,
343 )
344
345 torch.testing.assert_close(
346 b12x_output,
347 torch_output,
348 atol=2e-1,
349 rtol=2e-1,
350 )
351
352
353 if __name__ == "__main__":
354 test_flashinfer_b12x_moe(16, 128, 256, 8, 2, torch.bfloat16)