11# SPDX-License-Identifier: Apache-2.0
22# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
33
4+ from types import SimpleNamespace
5+
46import pytest
57import torch
68
79from vllm .platforms import current_platform
810
911if not current_platform .is_device_capability_family (120 ):
1012 pytest .skip (
11- reason = "FlashInfer CuteDSL SM12x MoE requires SM120 "
12- "(RTX Pro 6000 / DGX Spark)." ,
13+ reason = "FlashInfer B12x MoE requires SM120 (RTX Pro 6000 / DGX Spark)." ,
1314 allow_module_level = True ,
1415 )
1516
1819if not has_flashinfer_b12x_moe ():
1920 pytest .skip (
2021 reason = (
21- "FlashInfer cute_dsl_fused_moe_nvfp4 / convert_sf_to_mma_layout "
22- "not available in installed FlashInfer (needs PRs #3051 and #3066 )."
22+ "FlashInfer B12xMoEWrapper not available in installed "
23+ "FlashInfer (needs PR #3080 )."
2324 ),
2425 allow_module_level = True ,
2526 )
4041from vllm .model_executor .layers .fused_moe .experts .flashinfer_b12x_moe import (
4142 FlashInferB12xExperts ,
4243)
43- from vllm .utils .flashinfer import flashinfer_convert_sf_to_mma_layout
4444from vllm .utils .torch_utils import set_random_seed
4545
4646# Dimensions chosen to satisfy FP4 alignment requirements (k multiple of 256,
@@ -59,7 +59,7 @@ def _reorder_gate_up_to_up_gate(
5959) -> tuple [torch .Tensor , torch .Tensor ]:
6060 """Swap gate and up-projection halves along dim=1 to [up, gate] order.
6161
62- The SM12x kernel expects weights in [up (w3), gate (w1)] order while the
62+ The B12x kernel expects weights in [up (w3), gate (w1)] order while the
6363 BF16 reference uses [gate (w1), up (w3)]. This replicates the reordering
6464 done at model-load time by ``prepare_nvfp4_moe_layer_for_fi_or_cutlass``.
6565 """
@@ -70,6 +70,22 @@ def _reorder_gate_up_to_up_gate(
7070 )
7171
7272
73+ def _process_b12x_weights (
74+ experts : FlashInferB12xExperts ,
75+ w1_scale : torch .Tensor ,
76+ w2_scale : torch .Tensor ,
77+ w1_scale_2 : torch .Tensor ,
78+ w2_scale_2 : torch .Tensor ,
79+ ) -> None :
80+ layer = SimpleNamespace (
81+ w13_weight_scale = w1_scale ,
82+ w13_weight_scale_2 = w1_scale_2 ,
83+ w2_weight_scale = w2_scale ,
84+ w2_weight_scale_2 = w2_scale_2 ,
85+ )
86+ experts .process_weights_after_loading (layer )
87+
88+
7389@pytest .mark .parametrize ("m,n,k" , MNK_FACTORS )
7490@pytest .mark .parametrize ("e" , [8 , 16 ])
7591@pytest .mark .parametrize ("topk" , [1 , 2 , 4 ])
@@ -86,21 +102,10 @@ def test_flashinfer_b12x_moe(
86102):
87103 """Test FlashInferB12xExperts against a BF16 torch reference.
88104
89- The SM12x kernel takes BF16 hidden states directly and fuses token
105+ The B12x kernel takes BF16 hidden states directly and fuses token
90106 dispatch, W1 GEMM, SwiGLU, and W2 GEMM into one call. We verify
91107 correctness against ``torch_moe`` using generous tolerances to account
92108 for the internal FP4 quantization of activations and weights.
93-
94- Scale convention
95- ----------------
96- The SM12x kernel uses ``w1_alpha`` as *both* the activation-quantisation
97- global scale and the weight dequantisation factor. These two roles are
98- conflated into a single parameter in ``launch_sm120_moe``, so they must
99- equal the same value. We use ``global_scale = 1.0`` for
100- ``fp4_quantize`` so that ``w1_alpha = ones`` satisfies both roles
101- simultaneously. The alternative — vLLM's convention of baking a large
102- ``w_gs`` into block-scale values and compensating with
103- ``g1_alphas = 1/w_gs`` — is incompatible with this kernel.
104109 """
105110 set_random_seed (7 )
106111 with set_current_vllm_config (
@@ -174,22 +179,12 @@ def test_flashinfer_b12x_moe(
174179 moe_config = moe_config ,
175180 quant_config = quant_config ,
176181 )
177- # In production, process_weights_after_loading computes these after
178- # normalizing block scales. In the test the scales are already in final
179- # form (global_scale=1.0), so we compute the MMA layouts directly.
180- num_experts_w1 , m1 , k1_sf = w1_blockscale .shape
181- experts .w1_sf_mma = flashinfer_convert_sf_to_mma_layout (
182- w1_blockscale .reshape (num_experts_w1 * m1 , k1_sf ),
183- m = m1 ,
184- k = k1_sf * 16 ,
185- num_groups = num_experts_w1 ,
186- )
187- num_experts_w2 , m2 , k2_sf = w2_blockscale .shape
188- experts .w2_sf_mma = flashinfer_convert_sf_to_mma_layout (
189- w2_blockscale .reshape (num_experts_w2 * m2 , k2_sf ),
190- m = m2 ,
191- k = k2_sf * 16 ,
192- num_groups = num_experts_w2 ,
182+ _process_b12x_weights (
183+ experts ,
184+ w1_blockscale ,
185+ w2_blockscale ,
186+ ones_e ,
187+ ones_e ,
193188 )
194189
195190 kernel = mk .FusedMoEKernel (
@@ -225,5 +220,135 @@ def test_flashinfer_b12x_moe(
225220 torch .testing .assert_close (sm12x_output , torch_output , atol = 2e-1 , rtol = 2e-1 )
226221
227222
223+ @pytest .mark .parametrize ("m,n,k" , MNK_FACTORS )
224+ @pytest .mark .parametrize ("e" , [8 , 16 ])
225+ @pytest .mark .parametrize ("topk" , [1 , 2 , 4 ])
226+ @pytest .mark .parametrize ("dtype" , [torch .bfloat16 ])
227+ @torch .inference_mode ()
228+ def test_flashinfer_b12x_moe_relu2 (
229+ m : int ,
230+ n : int ,
231+ k : int ,
232+ e : int ,
233+ topk : int ,
234+ dtype : torch .dtype ,
235+ workspace_init ,
236+ ):
237+ """Test FlashInferB12xExperts with ReLU2 (non-gated) activation.
238+
239+ ReLU2 is used by Nemotron-H style models. Unlike the gated SiLU
240+ path, w1 has shape [E, N, K] (not [E, 2N, K]) and the activation
241+ is relu(x)^2 without a gate/up split.
242+ """
243+ set_random_seed (7 )
244+ with set_current_vllm_config (
245+ VllmConfig (parallel_config = ParallelConfig (pipeline_parallel_size = 1 ))
246+ ):
247+ a = torch .randn ((m , k ), device = "cuda" , dtype = dtype ) / 10
248+
249+ # Non-gated: w1 shape is (e, n, k), not (e, 2n, k).
250+ w1_bf16 = torch .randn ((e , n , k ), device = "cuda" , dtype = dtype ) / 15
251+ w2_bf16 = torch .randn ((e , k , n ), device = "cuda" , dtype = dtype ) / 15
252+
253+ gs = torch .ones (1 , device = "cuda" , dtype = torch .float32 )
254+ sf_vec_size = 16
255+
256+ # W1: no gate/up reordering for non-gated.
257+ w1_flat = w1_bf16 .reshape (e * n , k )
258+ w1_q_flat , w1_sf_flat = fp4_quantize (
259+ w1_flat ,
260+ global_scale = gs ,
261+ sf_vec_size = sf_vec_size ,
262+ is_sf_swizzled_layout = True ,
263+ )
264+ w1_q = w1_q_flat .view (e , n , k // 2 )
265+ w1_blockscale = w1_sf_flat .view (e , n , w1_sf_flat .shape [1 ])
266+
267+ w2_flat = w2_bf16 .reshape (e * k , n )
268+ w2_q_flat , w2_sf_flat = fp4_quantize (
269+ w2_flat ,
270+ global_scale = gs ,
271+ sf_vec_size = sf_vec_size ,
272+ is_sf_swizzled_layout = True ,
273+ )
274+ w2_q = w2_q_flat .view (e , k , n // 2 )
275+ w2_blockscale = w2_sf_flat .view (e , k , w2_sf_flat .shape [1 ])
276+
277+ ones_e = torch .ones (e , device = "cuda" , dtype = torch .float32 )
278+
279+ quant_config = nvfp4_moe_quant_config (
280+ g1_alphas = ones_e ,
281+ g2_alphas = ones_e ,
282+ a1_gscale = ones_e ,
283+ a2_gscale = ones_e ,
284+ w1_scale = w1_blockscale ,
285+ w2_scale = w2_blockscale ,
286+ )
287+
288+ moe_config = make_dummy_moe_config (
289+ num_experts = e ,
290+ experts_per_token = topk ,
291+ hidden_dim = k ,
292+ intermediate_size_per_partition = n ,
293+ in_dtype = dtype ,
294+ activation = MoEActivation .RELU2_NO_MUL ,
295+ is_act_and_mul = False ,
296+ )
297+
298+ experts = FlashInferB12xExperts (
299+ moe_config = moe_config ,
300+ quant_config = quant_config ,
301+ )
302+ _process_b12x_weights (
303+ experts ,
304+ w1_blockscale ,
305+ w2_blockscale ,
306+ ones_e ,
307+ ones_e ,
308+ )
309+
310+ kernel = mk .FusedMoEKernel (
311+ maybe_make_prepare_finalize (
312+ moe = moe_config ,
313+ quant_config = quant_config ,
314+ allow_new_interface = True ,
315+ use_monolithic = False ,
316+ ),
317+ experts ,
318+ inplace = False ,
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+
228353if __name__ == "__main__" :
229354 test_flashinfer_b12x_moe (16 , 128 , 256 , 8 , 2 , torch .bfloat16 )
0 commit comments