Skip to content

Commit e8484dc

Browse files
Andrii Skliarbryanfarrell
authored andcommitted
[SM120/121] Refactor b12x experts for dynamic quantization + MMA layout
Carries upstream PR vllm-project#41244 (vllm-project/vllm) onto the fiosco release line as a single net-change commit, built on top of pr-40082-cherry-pick. Builds on the b12x kernels added in PR vllm-project#40082. Refactors FlashInferB12xExperts to handle dynamic quantization and MMA layout conversion in-kernel, fixes the local_expert_offset argument wiring, and removes leftover dead-code paths. Enables Nemotron-3-Nano MoE (ReLU^2 activation) on the b12x backend. One-line addition beyond the original PR diff: an 'assert self._wrapper is not None' after _ensure_wrapper() to satisfy mypy's union-attr check (the original PR's CI didn't run our local mypy hook). Signed-off-by: Andrii Skliar <askliar@nvidia.com> Signed-off-by: Bryan Farrell <12701870+bryanfarrell@users.noreply.github.com>
1 parent 4819a2c commit e8484dc

4 files changed

Lines changed: 239 additions & 64 deletions

File tree

tests/kernels/moe/test_flashinfer_b12x_moe.py

Lines changed: 159 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# SPDX-License-Identifier: Apache-2.0
22
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
33

4+
from types import SimpleNamespace
5+
46
import pytest
57
import torch
68

79
from vllm.platforms import current_platform
810

911
if 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

@@ -18,8 +19,8 @@
1819
if 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
)
@@ -40,7 +41,6 @@
4041
from 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
4444
from 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+
228353
if __name__ == "__main__":
229354
test_flashinfer_b12x_moe(16, 128, 256, 8, 2, torch.bfloat16)

tests/kernels/moe/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def make_dummy_moe_config(
5353
hidden_dim: int = 1,
5454
intermediate_size_per_partition: int = 1,
5555
in_dtype: torch.dtype = torch.bfloat16,
56+
activation: MoEActivation = MoEActivation.SILU,
57+
is_act_and_mul: bool = True,
5658
) -> FusedMoEConfig:
5759
"""
5860
This is a dummy config for the mk constructor interface
@@ -69,7 +71,8 @@ def make_dummy_moe_config(
6971
num_local_experts=num_experts,
7072
num_logical_experts=num_experts,
7173
moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
72-
activation=MoEActivation.SILU,
74+
activation=activation,
75+
is_act_and_mul=is_act_and_mul,
7376
in_dtype=in_dtype,
7477
device="cuda",
7578
routing_method=RoutingMethodType.TopK,

0 commit comments

Comments
 (0)