main
py 1,274 lines 38.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for the b12x tensor-parallel MoE integration."""
4
5 from dataclasses import dataclass, replace
6 from types import SimpleNamespace
7
8 import pytest
9 import torch
10
11 import vllm.model_executor.layers.fused_moe.b12x as b12x
12 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
13 import vllm.model_executor.layers.fused_moe.oracle.mxfp4 as mxfp4_oracle
14 import vllm.model_executor.layers.fused_moe.oracle.nvfp4 as nvfp4_oracle
15 from tests.kernels.moe.utils import make_dummy_moe_config
16 from tests.kernels.quantization.nvfp4_utils import (
17 dequantize_nvfp4_to_dtype,
18 quant_nvfp4_tensor,
19 )
20 from tests.kernels.utils import torch_moe
21 from tests.quantization.reference_mxfp4 import dq_mxfp4_torch
22 from vllm import _custom_ops as ops
23 from vllm.config import ParallelConfig, VllmConfig, set_current_vllm_config
24 from vllm.model_executor.layers.fused_moe import fused_topk
25 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
26 from vllm.model_executor.layers.fused_moe.all2all_utils import (
27 maybe_make_prepare_finalize,
28 )
29 from vllm.model_executor.layers.fused_moe.b12x import B12xExperts
30 from vllm.model_executor.layers.fused_moe.config import (
31 FusedMoEParallelConfig,
32 FusedMoEQuantConfig,
33 mxfp4_w4a16_moe_quant_config,
34 nvfp4_w4a16_moe_quant_config,
35 )
36 from vllm.model_executor.layers.fused_moe.oracle.mxfp4 import (
37 Mxfp4MoeBackend,
38 select_deepseek_v4_mxfp4_moe_backend,
39 select_mxfp4_moe_backend,
40 )
41 from vllm.model_executor.layers.fused_moe.oracle.nvfp4 import (
42 NvFp4MoeBackend,
43 select_nvfp4_moe_backend,
44 )
45 from vllm.model_executor.layers.quantization.utils.b12x_moe import (
46 prepare_nvfp4_moe_layer_for_b12x,
47 )
48 from vllm.model_executor.layers.quantization.utils.mxfp4_utils import mxfp4_quantize
49 from vllm.model_executor.layers.quantization.utils.quant_utils import (
50 kMxfp4Static,
51 kMxfp8Dynamic,
52 kNvfp4Dynamic,
53 kNvfp4Static,
54 )
55 from vllm.platforms import current_platform
56 from vllm.utils.torch_utils import set_random_seed
57
58
59 def _quantize_nvfp4_linear(
60 weight: torch.Tensor,
61 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
62 weights_q = []
63 scales = []
64 global_scales = []
65 for expert_weight in weight:
66 weight_q, scale, global_scale = quant_nvfp4_tensor(
67 expert_weight,
68 is_sf_swizzled_layout=False,
69 )
70 weights_q.append(weight_q)
71 scales.append(scale)
72 global_scales.append(global_scale)
73 return torch.stack(weights_q), torch.stack(scales), torch.stack(global_scales)
74
75
76 def _dequantize_nvfp4_linear(
77 tensor_fp4: torch.Tensor,
78 tensor_sf: torch.Tensor,
79 global_scale: torch.Tensor,
80 dtype: torch.dtype,
81 ) -> torch.Tensor:
82 return dequantize_nvfp4_to_dtype(
83 tensor_fp4,
84 tensor_sf,
85 global_scale,
86 dtype=dtype,
87 device=tensor_fp4.device,
88 is_sf_linear_layout=True,
89 )
90
91
92 def _nvfp4_activation_reference(
93 hidden_states: torch.Tensor,
94 w1: torch.Tensor,
95 w2: torch.Tensor,
96 topk_weights: torch.Tensor,
97 topk_ids: torch.Tensor,
98 a1_scale: torch.Tensor,
99 a2_scale: torch.Tensor,
100 ) -> torch.Tensor:
101 tokens, hidden_size = hidden_states.shape
102 topk = topk_ids.shape[1]
103 routed_input = (
104 hidden_states[:, None, :]
105 .expand(-1, topk, -1)
106 .reshape(tokens * topk, hidden_size)
107 )
108 routed_output = torch.zeros(
109 tokens * topk,
110 hidden_size,
111 dtype=torch.float32,
112 device=hidden_states.device,
113 )
114 flat_ids = topk_ids.reshape(-1)
115
116 for expert in range(w1.shape[0]):
117 mask = flat_ids == expert
118 if not mask.any():
119 continue
120 a1_q, a1_block_scale = ops.scaled_fp4_quant(
121 routed_input[mask],
122 a1_scale[expert],
123 is_sf_swizzled_layout=False,
124 )
125 a1 = _dequantize_nvfp4_linear(
126 a1_q,
127 a1_block_scale,
128 a1_scale[expert],
129 torch.float32,
130 )
131 fc1 = a1 @ w1[expert].float().t()
132 gate, up = fc1.chunk(2, dim=-1)
133 intermediate = (torch.nn.functional.silu(gate) * up).to(torch.bfloat16)
134 a2_q, a2_block_scale = ops.scaled_fp4_quant(
135 intermediate,
136 a2_scale[expert],
137 is_sf_swizzled_layout=False,
138 )
139 a2 = _dequantize_nvfp4_linear(
140 a2_q,
141 a2_block_scale,
142 a2_scale[expert],
143 torch.float32,
144 )
145 routed_output[mask] = a2 @ w2[expert].float().t()
146
147 return (
148 routed_output.view(tokens, topk, hidden_size)
149 .mul(topk_weights[..., None])
150 .sum(dim=1)
151 .to(hidden_states.dtype)
152 )
153
154
155 def _has_b12x_moe() -> bool:
156 return (
157 torch.cuda.is_available()
158 and current_platform.is_device_capability_family(120)
159 and B12xExperts._supports_current_device()
160 )
161
162
163 def _count_fp4_negative_zeros(packed: torch.Tensor) -> int:
164 low = (packed & 0x0F) == 0x08
165 high = (packed & 0xF0) == 0x80
166 return int(low.sum().item() + high.sum().item())
167
168
169 def _make_b12x_moe_kernel(
170 hidden_states: torch.Tensor,
171 w1: torch.Tensor,
172 w2: torch.Tensor,
173 topk: int,
174 activation: MoEActivation,
175 quant_config: FusedMoEQuantConfig,
176 ) -> mk.FusedMoEKernel:
177 num_experts = w1.shape[0]
178 moe_config = make_dummy_moe_config(
179 num_experts=num_experts,
180 experts_per_token=topk,
181 hidden_dim=hidden_states.shape[1],
182 intermediate_size=w2.shape[2] * 2,
183 in_dtype=hidden_states.dtype,
184 activation=activation,
185 )
186 experts = B12xExperts(moe_config, quant_config)
187 layer = SimpleNamespace(
188 activation=activation,
189 apply_router_weight_on_input=False,
190 w13_weight=w1,
191 w2_weight=w2,
192 w13_weight_scale=quant_config.w1_scale,
193 w2_weight_scale=quant_config.w2_scale,
194 )
195 if quant_config.weight_quant_dtype == "nvfp4":
196 layer.w13_weight_scale_2 = quant_config.g1_alphas
197 layer.w2_weight_scale_2 = quant_config.g2_alphas
198 if quant_config.quant_dtype is not None:
199 assert quant_config.a1_gscale is not None
200 assert quant_config.a2_gscale is not None
201 layer.w13_input_scale = 1.0 / quant_config.a1_gscale
202 layer.w2_input_scale = 1.0 / quant_config.a2_gscale
203 experts.process_weights_after_loading(layer)
204 return mk.FusedMoEKernel(
205 maybe_make_prepare_finalize(
206 moe=moe_config,
207 quant_config=quant_config,
208 allow_new_interface=True,
209 use_monolithic=False,
210 ),
211 experts,
212 )
213
214
215 def _run_b12x_moe(
216 hidden_states: torch.Tensor,
217 w1: torch.Tensor,
218 w2: torch.Tensor,
219 score: torch.Tensor,
220 topk: int,
221 activation: MoEActivation,
222 quant_config: FusedMoEQuantConfig,
223 ) -> torch.Tensor:
224 num_experts = w1.shape[0]
225 kernel = _make_b12x_moe_kernel(
226 hidden_states,
227 w1,
228 w2,
229 topk,
230 activation,
231 quant_config,
232 )
233 topk_weights, topk_ids, _ = fused_topk(
234 hidden_states, score, topk, renormalize=False
235 )
236 return kernel.apply(
237 hidden_states=hidden_states,
238 w1=w1,
239 w2=w2,
240 topk_weights=topk_weights,
241 topk_ids=topk_ids,
242 activation=activation,
243 global_num_experts=num_experts,
244 expert_map=None,
245 apply_router_weight_on_input=False,
246 )
247
248
249 def _quant_config(weight_dtype: str, activation_dtype: str | None):
250 scale = torch.ones(1, dtype=torch.float32)
251 return FusedMoEQuantConfig.make(
252 quant_dtype=activation_dtype,
253 weight_dtype=weight_dtype,
254 w1_scale=scale,
255 w2_scale=scale,
256 g1_alphas=scale,
257 g2_alphas=scale,
258 a1_gscale=scale,
259 a2_gscale=scale,
260 )
261
262
263 def test_b12x_moe_supports_only_tensor_parallel() -> None:
264 parallel = FusedMoEParallelConfig.make_no_parallel()
265
266 assert B12xExperts._supports_parallel_config(parallel)
267 assert not B12xExperts._supports_parallel_config(
268 replace(parallel, use_ep=True, ep_size=2)
269 )
270 all2all = replace(parallel, use_ep=True, dp_size=2)
271 assert all2all.use_all2all_kernels
272 assert not B12xExperts._supports_parallel_config(all2all)
273 assert not B12xExperts._supports_parallel_config(
274 replace(parallel, enable_eplb=True)
275 )
276
277
278 _SITU_REASON = "kernel supports only SiTU beta=4 and linear_beta=25"
279 _UNINTERLEAVED_W4A8_REASON = "kernel does not support swigluoai_uninterleave with W4A8"
280
281
282 @pytest.mark.parametrize(
283 "config_kwargs,overrides,weight_key,activation_key,expected_reason",
284 [
285 pytest.param(
286 {"hidden_dim": 128, "activation": MoEActivation.SWIGLUOAI},
287 {},
288 kMxfp4Static,
289 None,
290 "kernel does not support MoEActivation.SWIGLUOAI activation",
291 id="interleaved-swigluoai",
292 ),
293 pytest.param(
294 {
295 "hidden_dim": 128,
296 "activation": MoEActivation.SWIGLUOAI_UNINTERLEAVE,
297 },
298 {},
299 kMxfp4Static,
300 kMxfp8Dynamic,
301 _UNINTERLEAVED_W4A8_REASON,
302 id="mxfp4-w4a8-uninterleaved-swigluoai",
303 ),
304 pytest.param(
305 {
306 "hidden_dim": 128,
307 "activation": MoEActivation.SWIGLUOAI_UNINTERLEAVE,
308 },
309 {},
310 kMxfp4Static,
311 None,
312 None,
313 id="w4a16-uninterleaved-swigluoai",
314 ),
315 pytest.param(
316 {"activation": MoEActivation.RELU2_NO_MUL},
317 {},
318 kMxfp4Static,
319 kMxfp8Dynamic,
320 "MXFP4 W4A8 supports only SiLU and SiTU",
321 id="mxfp4-w4a8-relu2",
322 ),
323 pytest.param(
324 {"hidden_dim": 128},
325 {},
326 kMxfp4Static,
327 kMxfp8Dynamic,
328 (
329 "MXFP4 W4A8 requires hidden size divisible by 256 and per-rank "
330 "intermediate size divisible by 32"
331 ),
332 id="mxfp4-w4a8-alignment",
333 ),
334 pytest.param(
335 {"hidden_dim": 128, "intermediate_size": 48},
336 {"intermediate_size_per_partition": 64},
337 kMxfp4Static,
338 None,
339 "MXFP4 requires the per-rank intermediate size to be divisible by 32",
340 id="mxfp4-tp-scale-groups",
341 ),
342 pytest.param(
343 {"activation": MoEActivation.SITU},
344 {"activation_situ_beta": 3.0, "activation_situ_linear_beta": 25.0},
345 kMxfp4Static,
346 None,
347 _SITU_REASON,
348 id="situ-beta",
349 ),
350 pytest.param(
351 {"activation": MoEActivation.SITU},
352 {"activation_situ_beta": 4.0, "activation_situ_linear_beta": 25.0},
353 kMxfp4Static,
354 None,
355 None,
356 id="situ-standard-parameters",
357 ),
358 ],
359 )
360 def test_b12x_moe_config_support(
361 monkeypatch: pytest.MonkeyPatch,
362 config_kwargs,
363 overrides,
364 weight_key,
365 activation_key,
366 expected_reason: str | None,
367 ) -> None:
368 monkeypatch.setattr(B12xExperts, "_supports_current_device", lambda: True)
369 config = make_dummy_moe_config(
370 **{"hidden_dim": 256, "intermediate_size": 64, **config_kwargs}
371 )
372 for name, value in overrides.items():
373 setattr(config, name, value)
374
375 supported, reason = B12xExperts.is_supported_config(
376 B12xExperts,
377 config,
378 weight_key,
379 activation_key,
380 mk.FusedMoEActivationFormat.Standard,
381 )
382
383 assert (supported, reason) == (expected_reason is None, expected_reason)
384
385
386 @pytest.mark.parametrize(
387 "activation_key,force_a16,expected_backend",
388 [
389 (kMxfp8Dynamic, False, Mxfp4MoeBackend.B12X_MXFP4_MXFP8),
390 (None, False, Mxfp4MoeBackend.B12X_MXFP4_MXFP8),
391 ],
392 )
393 def test_explicit_b12x_mxfp4_selection(
394 monkeypatch: pytest.MonkeyPatch,
395 activation_key,
396 force_a16: bool,
397 expected_backend: Mxfp4MoeBackend,
398 ) -> None:
399 monkeypatch.setattr(B12xExperts, "_supports_current_device", lambda: True)
400 monkeypatch.setattr(mxfp4_oracle, "_user_moe_activation_override", lambda: None)
401 monkeypatch.setattr(
402 mxfp4_oracle.envs,
403 "VLLM_B12X_MOE_FP4_FORCE_A16",
404 force_a16,
405 )
406 config = make_dummy_moe_config(hidden_dim=256, intermediate_size=64)
407 config.moe_backend = "b12x"
408
409 backend, experts_cls = select_mxfp4_moe_backend(
410 config,
411 activation_key=activation_key,
412 )
413
414 assert backend == expected_backend
415 assert experts_cls is B12xExperts
416
417
418 def test_explicit_b12x_mxfp4_force_a16_uses_a16_contract(
419 monkeypatch: pytest.MonkeyPatch,
420 ) -> None:
421 monkeypatch.setattr(B12xExperts, "_supports_current_device", lambda: True)
422 monkeypatch.setattr(mxfp4_oracle, "_user_moe_activation_override", lambda: None)
423 monkeypatch.setattr(
424 mxfp4_oracle.envs,
425 "VLLM_B12X_MOE_FP4_FORCE_A16",
426 True,
427 )
428 config = make_dummy_moe_config(hidden_dim=128, intermediate_size=64)
429 config.moe_backend = "b12x"
430
431 backend, experts_cls = select_mxfp4_moe_backend(
432 config,
433 activation_key=kMxfp8Dynamic,
434 )
435
436 assert backend == Mxfp4MoeBackend.B12X_MXFP4_BF16
437 assert experts_cls is B12xExperts
438
439
440 @pytest.mark.parametrize(
441 "force_a16,expected_backend",
442 [
443 (False, Mxfp4MoeBackend.B12X_MXFP4_MXFP8),
444 (True, Mxfp4MoeBackend.B12X_MXFP4_BF16),
445 ],
446 )
447 def test_deepseek_v4_b12x_activation_selection(
448 monkeypatch: pytest.MonkeyPatch,
449 force_a16: bool,
450 expected_backend: Mxfp4MoeBackend,
451 ) -> None:
452 monkeypatch.setattr(B12xExperts, "_supports_current_device", lambda: True)
453 monkeypatch.setattr(
454 mxfp4_oracle.envs,
455 "VLLM_B12X_MOE_FP4_FORCE_A16",
456 force_a16,
457 )
458 config = make_dummy_moe_config(hidden_dim=256, intermediate_size=64)
459 config.moe_backend = "b12x"
460
461 backend, experts_cls = select_deepseek_v4_mxfp4_moe_backend(config)
462
463 assert backend == expected_backend
464 assert experts_cls is B12xExperts
465
466
467 def test_deepseek_v4_flashinfer_cutlass_falls_through_to_w4a8(
468 monkeypatch: pytest.MonkeyPatch,
469 ) -> None:
470 """Explicit flashinfer_cutlass must try the W4A8 variant when the BF16
471 variant is unsupported (the BF16 variant is gated to SM90)."""
472 from vllm.model_executor.layers.fused_moe.experts.flashinfer_cutlass_moe import (
473 FlashInferExperts,
474 )
475
476 monkeypatch.setattr(FlashInferExperts, "_supports_current_device", lambda: True)
477
478 def sm120_quant_gate(weight_key, activation_key):
479 return (weight_key, activation_key) == (
480 mxfp4_oracle.kMxfp4Static,
481 mxfp4_oracle.kMxfp8Dynamic,
482 )
483
484 monkeypatch.setattr(
485 FlashInferExperts, "_supports_quant_scheme", staticmethod(sm120_quant_gate)
486 )
487 config = make_dummy_moe_config(hidden_dim=256, intermediate_size=64)
488 config.moe_backend = "flashinfer_cutlass"
489
490 backend, experts_cls = select_deepseek_v4_mxfp4_moe_backend(config)
491
492 assert backend == Mxfp4MoeBackend.FLASHINFER_CUTLASS_MXFP4_MXFP8
493 assert experts_cls is FlashInferExperts
494
495
496 def test_compressed_tensors_mxfp4_preserves_checkpoint_packing(
497 monkeypatch: pytest.MonkeyPatch,
498 ) -> None:
499 from vllm.model_executor.layers.quantization.compressed_tensors.compressed_tensors_moe import ( # noqa: E501
500 compressed_tensors_moe_w4a4_mxfp4 as ct_mxfp4,
501 )
502
503 monkeypatch.setattr(
504 ct_mxfp4.CutlassExpertsMxfp4,
505 "_supports_current_device",
506 lambda: False,
507 )
508 monkeypatch.setattr(
509 ct_mxfp4,
510 "select_mxfp4_moe_backend",
511 lambda moe: (Mxfp4MoeBackend.B12X_MXFP4_MXFP8, B12xExperts),
512 )
513 monkeypatch.setattr(
514 ct_mxfp4,
515 "prepare_moe_fp4_layer_for_marlin",
516 lambda layer: pytest.fail("b12x must not use Marlin packing"),
517 )
518 moe_config = SimpleNamespace(w13_num_shards=2, moe_backend="b12x")
519 method = ct_mxfp4.CompressedTensorsW4A4Mxfp4MoEMethod(moe_config)
520 processed_layers: list[torch.nn.Module] = []
521 fake_experts = SimpleNamespace(
522 process_weights_after_loading=processed_layers.append
523 )
524 kernel = SimpleNamespace(fused_experts=fake_experts)
525 monkeypatch.setattr(method, "get_fused_moe_quant_config", lambda _: object())
526 monkeypatch.setattr(ct_mxfp4, "make_mxfp4_moe_kernel", lambda **_: kernel)
527 layer = torch.nn.Module()
528 layer._expert_routing_tables = lambda: ()
529 method.create_weights(
530 layer,
531 num_experts=2,
532 hidden_size=64,
533 intermediate_size_per_partition=32,
534 params_dtype=torch.bfloat16,
535 )
536 w13_packed_data = layer.w13_weight_packed.data
537 w2_packed_data = layer.w2_weight_packed.data
538
539 method.process_weights_after_loading(layer)
540
541 assert layer.w13_weight.data.data_ptr() == w13_packed_data.data_ptr()
542 assert layer.w2_weight.data.data_ptr() == w2_packed_data.data_ptr()
543 assert processed_layers == [layer]
544
545
546 def test_b12x_mxfp4_falls_back_to_a16(
547 monkeypatch: pytest.MonkeyPatch,
548 ) -> None:
549 monkeypatch.setattr(B12xExperts, "_supports_current_device", lambda: True)
550 monkeypatch.setattr(mxfp4_oracle, "_user_moe_activation_override", lambda: None)
551 monkeypatch.setattr(
552 mxfp4_oracle.envs,
553 "VLLM_B12X_MOE_FP4_FORCE_A16",
554 False,
555 )
556 config = make_dummy_moe_config(hidden_dim=128, intermediate_size=64)
557 config.moe_backend = "b12x"
558
559 backend, experts_cls = select_mxfp4_moe_backend(config)
560
561 assert backend == Mxfp4MoeBackend.B12X_MXFP4_BF16
562 assert experts_cls is B12xExperts
563
564
565 @pytest.mark.parametrize(
566 "activation_key,force_a16,expected_activation_key",
567 [
568 (kNvfp4Dynamic, False, kNvfp4Dynamic),
569 (kMxfp8Dynamic, False, kMxfp8Dynamic),
570 (kNvfp4Dynamic, True, None),
571 ],
572 )
573 def test_explicit_b12x_nvfp4_selection(
574 monkeypatch: pytest.MonkeyPatch,
575 activation_key,
576 force_a16: bool,
577 expected_activation_key,
578 ) -> None:
579 selected_activation_keys = []
580
581 def is_supported_config(cls, config, weight_key, activation_key, activation_format):
582 selected_activation_keys.append(activation_key)
583 return True, None
584
585 monkeypatch.setattr(B12xExperts, "is_supported_config", is_supported_config)
586 monkeypatch.setattr(
587 nvfp4_oracle.envs,
588 "VLLM_B12X_MOE_FP4_FORCE_A16",
589 force_a16,
590 )
591 config = make_dummy_moe_config(hidden_dim=128, intermediate_size=64)
592 config.moe_backend = "b12x"
593
594 backend, experts_cls = select_nvfp4_moe_backend(
595 config,
596 weight_key=kNvfp4Static,
597 activation_key=activation_key,
598 )
599
600 assert backend == NvFp4MoeBackend.B12X
601 assert experts_cls is B12xExperts
602 assert selected_activation_keys == [expected_activation_key]
603
604
605 @pytest.mark.parametrize(
606 "force_a16,expected_quant_dtype", [(False, "nvfp4"), (True, None)]
607 )
608 def test_b12x_nvfp4_force_a16_updates_quant_config(
609 monkeypatch: pytest.MonkeyPatch,
610 force_a16: bool,
611 expected_quant_dtype,
612 ) -> None:
613 monkeypatch.setattr(
614 nvfp4_oracle.envs,
615 "VLLM_B12X_MOE_FP4_FORCE_A16",
616 force_a16,
617 )
618 scale = torch.ones(1)
619
620 quant_config = nvfp4_oracle.make_nvfp4_moe_quant_config(
621 backend=NvFp4MoeBackend.B12X,
622 w13_scale=scale,
623 w2_scale=scale,
624 w13_scale_2=scale,
625 w2_scale_2=scale,
626 a13_scale=scale,
627 a2_scale=scale,
628 )
629
630 assert quant_config.quant_dtype == expected_quant_dtype
631
632
633 def test_b12x_nvfp4_force_a16_updates_weight_preparation(
634 monkeypatch: pytest.MonkeyPatch,
635 ) -> None:
636 monkeypatch.setattr(
637 nvfp4_oracle.envs,
638 "VLLM_B12X_MOE_FP4_FORCE_A16",
639 True,
640 )
641 reorder_w13 = None
642
643 def prepare_for_b12x(**kwargs):
644 nonlocal reorder_w13
645 reorder_w13 = kwargs["reorder_w13"]
646 return (
647 kwargs["w13"],
648 kwargs["w13_scale"],
649 kwargs["w13_scale_2"],
650 kwargs["a13_scale"],
651 kwargs["w2"],
652 kwargs["w2_scale"],
653 kwargs["w2_scale_2"],
654 kwargs["a2_scale"],
655 )
656
657 monkeypatch.setattr(
658 nvfp4_oracle,
659 "prepare_nvfp4_moe_layer_for_b12x",
660 prepare_for_b12x,
661 )
662 tensor = torch.ones(1)
663
664 nvfp4_oracle.convert_to_nvfp4_moe_kernel_format(
665 nvfp4_backend=NvFp4MoeBackend.B12X,
666 layer=SimpleNamespace(),
667 w13=tensor,
668 w13_scale=tensor,
669 w13_scale_2=tensor,
670 a13_scale=tensor,
671 w2=tensor,
672 w2_scale=tensor,
673 w2_scale_2=tensor,
674 a2_scale=tensor,
675 is_act_and_mul=True,
676 )
677
678 assert reorder_w13 is True
679
680
681 @pytest.mark.skipif(not torch.cuda.is_available(), reason="requires CUDA")
682 def test_b12x_nvfp4_preparation_pads_each_gated_half() -> None:
683 device = torch.device("cuda")
684 num_experts, hidden_size, intermediate_size = 2, 64, 48
685 w13 = torch.ones(
686 num_experts,
687 2 * intermediate_size,
688 hidden_size // 2,
689 dtype=torch.uint8,
690 device=device,
691 )
692 w13_scale = torch.ones(
693 num_experts,
694 2 * intermediate_size,
695 hidden_size // 16,
696 dtype=torch.float8_e4m3fn,
697 device=device,
698 )
699 w2 = torch.ones(
700 num_experts,
701 hidden_size,
702 intermediate_size // 2,
703 dtype=torch.uint8,
704 device=device,
705 )
706 w2_scale = torch.ones(
707 num_experts,
708 hidden_size,
709 intermediate_size // 16,
710 dtype=torch.float8_e4m3fn,
711 device=device,
712 )
713 global_scale = torch.ones(num_experts, device=device)
714 input_scale = torch.tensor([[1.0, 2.0], [3.0, 1.0]], device=device)
715
716 prepared = prepare_nvfp4_moe_layer_for_b12x(
717 w13,
718 w13_scale,
719 global_scale,
720 input_scale,
721 w2,
722 w2_scale,
723 global_scale,
724 input_scale,
725 is_act_and_mul=True,
726 )
727
728 prepared_w13, prepared_w13_scale, _, prepared_a13 = prepared[:4]
729 prepared_w2, prepared_w2_scale, _, prepared_a2 = prepared[4:]
730 assert prepared_w13.shape == (num_experts, 128, hidden_size // 2)
731 assert prepared_w13_scale.shape == (num_experts, 128, hidden_size // 16)
732 assert prepared_w2.shape == (num_experts, hidden_size, 32)
733 assert prepared_w2_scale.shape == (num_experts, 128, 4)
734 torch.testing.assert_close(prepared_a13, torch.tensor([2.0, 3.0], device=device))
735 torch.testing.assert_close(prepared_a2, torch.tensor([2.0, 3.0], device=device))
736
737
738 def test_b12x_moe_uses_minimax_swiglu_parameters() -> None:
739 config = make_dummy_moe_config(
740 hidden_dim=128,
741 intermediate_size=64,
742 activation=MoEActivation.SWIGLUOAI_UNINTERLEAVE,
743 )
744 config.swiglu_limit = 7.0
745 config.swiglu_alpha = 1.702
746 config.swiglu_beta = 1.0
747 experts = B12xExperts(config, _quant_config("mxfp4", None))
748
749 assert experts._swiglu_params(config.activation) == (7.0, 1.702, 1.0)
750
751
752 def test_b12x_moe_warmup_runs_each_planner_regime_once(
753 monkeypatch: pytest.MonkeyPatch,
754 ) -> None:
755 experts = B12xExperts(
756 make_dummy_moe_config(
757 num_experts=4,
758 experts_per_token=2,
759 hidden_dim=128,
760 intermediate_size=64,
761 ),
762 _quant_config("mxfp4", None),
763 )
764 prepared = SimpleNamespace(
765 num_experts=4,
766 hidden_size=128,
767 intermediate_size=64,
768 w1_fp4=torch.empty(0),
769 )
770 layer = SimpleNamespace(
771 activation=MoEActivation.SILU,
772 apply_router_weight_on_input=False,
773 )
774 planned_tokens = []
775 launched_tokens = []
776
777 with pytest.raises(RuntimeError, match="process_weights_after_loading"):
778 experts.warmup_launches(layer, token_counts=(1,))
779 experts._prepared_experts = prepared
780
781 def fake_execution_plan(**kwargs):
782 tokens = kwargs["tokens"]
783 if tokens <= 2:
784 signature = ("micro", "decode")
785 elif tokens <= 4:
786 signature = ("dynamic", "small")
787 else:
788 signature = ("dynamic", "large")
789 return SimpleNamespace(
790 implementation=signature[0],
791 execution=signature[1],
792 )
793
794 def fake_plan(**kwargs):
795 planned_tokens.append(kwargs["tokens"])
796 return SimpleNamespace(
797 scratch_specs=lambda: [SimpleNamespace(dtype=torch.uint8, shape=(64,))]
798 )
799
800 def fake_run(**kwargs):
801 launched_tokens.append(kwargs["hidden_states"].shape[0])
802
803 monkeypatch.setattr(b12x, "_b12x_moe_execution_plan", fake_execution_plan)
804 monkeypatch.setattr(b12x, "_run_b12x_moe_plan", fake_run)
805 monkeypatch.setattr(experts, "_plan", fake_plan)
806
807 warmed = experts.warmup_launches(layer, token_counts=(1, 2, 3, 4, 8))
808
809 assert warmed == 3
810 assert planned_tokens == [1, 3, 8]
811 assert launched_tokens == planned_tokens
812
813
814 def test_b12x_moe_reload_reprepares_current_parameters(
815 monkeypatch: pytest.MonkeyPatch,
816 ) -> None:
817 plan = SimpleNamespace(
818 discards_source_parameters=False,
819 quant_modes=("w4a16",),
820 io_dtype="bfloat16",
821 activation="silu",
822 )
823 prepared_inputs: list[
824 tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]
825 ] = []
826
827 def prepare_weights(**kwargs):
828 prepared_inputs.append(
829 (
830 kwargs["w1_fp4"],
831 kwargs["w2_fp4"],
832 kwargs["w1_blockscale"],
833 kwargs["w2_blockscale"],
834 )
835 )
836 return SimpleNamespace(plan=plan)
837
838 extension = SimpleNamespace(
839 plan_weights=lambda **_: plan, prepare_weights=prepare_weights
840 )
841 monkeypatch.setattr(b12x, "_require_b12x_fused_moe", lambda: extension)
842 experts = B12xExperts(
843 make_dummy_moe_config(num_experts=2, hidden_dim=4, intermediate_size=8),
844 _quant_config("mxfp4", None),
845 )
846 layer = SimpleNamespace(
847 activation=MoEActivation.SILU,
848 apply_router_weight_on_input=False,
849 w13_weight=torch.full((2, 8, 2), 1, dtype=torch.uint8),
850 w2_weight=torch.full((2, 4, 4), 1, dtype=torch.uint8),
851 w13_weight_scale=torch.full((2, 8, 1), 1, dtype=torch.uint8),
852 w2_weight_scale=torch.full((2, 4, 1), 1, dtype=torch.uint8),
853 )
854
855 experts.process_weights_after_loading(layer)
856 layer.w13_weight = torch.full_like(layer.w13_weight, 2)
857 layer.w2_weight = torch.full_like(layer.w2_weight, 2)
858 layer.w13_weight_scale = torch.full_like(layer.w13_weight_scale, 3)
859 layer.w2_weight_scale = torch.full_like(layer.w2_weight_scale, 3)
860 experts.process_weights_after_loading(layer)
861
862 assert len(prepared_inputs) == 2
863 assert prepared_inputs[-1][0] is layer.w13_weight
864 assert prepared_inputs[-1][1] is layer.w2_weight
865 assert prepared_inputs[-1][2] is layer.w13_weight_scale
866 assert prepared_inputs[-1][3] is layer.w2_weight_scale
867
868
869 def test_b12x_source_release_preserves_prepared_storage_owner() -> None:
870 layer = torch.nn.Module()
871 for name, shape in (
872 ("w13_weight", (4, 32, 16)),
873 ("w2_weight", (4, 64, 8)),
874 ("w13_weight_scale", (4, 32, 2)),
875 ("w2_weight_scale", (4, 64, 1)),
876 ):
877 layer.register_parameter(
878 name,
879 torch.nn.Parameter(
880 torch.empty(shape, dtype=torch.uint8),
881 requires_grad=False,
882 ),
883 )
884 experts = B12xExperts(
885 make_dummy_moe_config(hidden_dim=128, intermediate_size=64),
886 _quant_config("mxfp4", None),
887 )
888 owner = SimpleNamespace(
889 w1_fp4=layer.w13_weight,
890 w2_fp4=layer.w2_weight,
891 w1_blockscale=layer.w13_weight_scale,
892 w2_blockscale=layer.w2_weight_scale,
893 )
894 experts._prepared_experts = owner
895 owner_tensors = (
896 owner.w1_fp4,
897 owner.w2_fp4,
898 owner.w1_blockscale,
899 owner.w2_blockscale,
900 )
901 owner_ptrs = tuple(tensor.untyped_storage().data_ptr() for tensor in owner_tensors)
902
903 experts._release_source_parameters(layer)
904 experts._release_source_parameters(layer)
905
906 assert layer.w13_weight.numel() == 0
907 assert layer.w2_weight.numel() == 0
908 assert layer.w13_weight_scale.numel() == 0
909 assert layer.w2_weight_scale.numel() == 0
910 assert (
911 tuple(tensor.untyped_storage().data_ptr() for tensor in owner_tensors)
912 == owner_ptrs
913 )
914
915
916 def test_b12x_moe_rejects_router_weight_on_input_for_w4a8() -> None:
917 experts = B12xExperts(
918 make_dummy_moe_config(hidden_dim=256, intermediate_size=64),
919 _quant_config("mxfp4", "mxfp8"),
920 )
921 layer = SimpleNamespace(
922 activation=MoEActivation.SILU,
923 apply_router_weight_on_input=True,
924 )
925
926 with pytest.raises(
927 ValueError,
928 match="apply_router_weight_on_input only with W4A16",
929 ):
930 experts.process_weights_after_loading(layer)
931
932
933 def test_b12x_moe_workspace_uses_prepared_router_weight_contract(
934 monkeypatch: pytest.MonkeyPatch,
935 ) -> None:
936 experts = B12xExperts(
937 make_dummy_moe_config(hidden_dim=128, intermediate_size=64),
938 _quant_config("mxfp4", None),
939 )
940 prepared = SimpleNamespace(
941 plan=SimpleNamespace(discards_source_parameters=False),
942 )
943 layer = SimpleNamespace(
944 activation=MoEActivation.SILU,
945 apply_router_weight_on_input=True,
946 w13_weight=torch.empty(0),
947 w2_weight=torch.empty(0),
948 w13_weight_scale=torch.empty(0),
949 w2_weight_scale=torch.empty(0),
950 )
951 monkeypatch.setattr(experts, "_prepare_experts", lambda **kwargs: prepared)
952 planned = []
953
954 def fake_plan(**kwargs):
955 planned.append(kwargs)
956 return SimpleNamespace(
957 scratch_specs=lambda: [SimpleNamespace(dtype=torch.uint8, shape=(64,))]
958 )
959
960 monkeypatch.setattr(experts, "_plan", fake_plan)
961
962 experts.process_weights_after_loading(layer)
963 assert layer.b12x_warmup_provider is experts
964 experts.workspace_shapes(
965 8,
966 128,
967 128,
968 2,
969 4,
970 4,
971 None,
972 MoEActivation.SILU,
973 )
974
975 assert planned == [
976 {
977 "tokens": 8,
978 "topk": 2,
979 "activation": MoEActivation.SILU,
980 "apply_router_weight_on_input": True,
981 }
982 ]
983
984
985 @dataclass
986 class _B12xMoeCase:
987 hidden_states: torch.Tensor
988 score: torch.Tensor
989 w1: torch.Tensor
990 w2: torch.Tensor
991 w1_ref: torch.Tensor
992 w2_ref: torch.Tensor
993 quant_config: FusedMoEQuantConfig
994 activation: MoEActivation
995 activation_dtype: str | None
996 topk: int = 2
997
998
999 def _make_b12x_moe_case(
1000 weight_dtype: str,
1001 activation_dtype: str | None,
1002 *,
1003 activation: MoEActivation = MoEActivation.SILU,
1004 tokens: int = 16,
1005 seed: int = 19,
1006 ) -> _B12xMoeCase:
1007 set_random_seed(seed)
1008 num_experts, hidden_size, intermediate_size = 4, 512, 128
1009 dtype = torch.bfloat16
1010 hidden_states = torch.randn((tokens, hidden_size), device="cuda", dtype=dtype) / 10
1011 w1_rows = 2 * intermediate_size if activation.is_gated else intermediate_size
1012 w1 = (
1013 torch.randn(
1014 (num_experts, w1_rows, hidden_size),
1015 device="cuda",
1016 dtype=dtype,
1017 )
1018 / 15
1019 )
1020 w2 = (
1021 torch.randn(
1022 (num_experts, hidden_size, intermediate_size),
1023 device="cuda",
1024 dtype=dtype,
1025 )
1026 / 15
1027 )
1028
1029 if weight_dtype == "mxfp4":
1030 w1_q, w1_scale = mxfp4_quantize(w1)
1031 w2_q, w2_scale = mxfp4_quantize(w2)
1032 w1_ref = torch.stack(
1033 [dq_mxfp4_torch(w1_q[e], w1_scale[e], dtype) for e in range(num_experts)]
1034 )
1035 w2_ref = torch.stack(
1036 [dq_mxfp4_torch(w2_q[e], w2_scale[e], dtype) for e in range(num_experts)]
1037 )
1038 if activation_dtype is None:
1039 quant_config = mxfp4_w4a16_moe_quant_config(
1040 w1_scale=w1_scale,
1041 w2_scale=w2_scale,
1042 )
1043 else:
1044 quant_config = FusedMoEQuantConfig.make(
1045 quant_dtype=activation_dtype,
1046 weight_dtype=weight_dtype,
1047 w1_scale=w1_scale,
1048 w2_scale=w2_scale,
1049 )
1050 elif weight_dtype == "nvfp4":
1051 w1_q, w1_scale, w1_global_scale = _quantize_nvfp4_linear(w1)
1052 w2_q, w2_scale, w2_global_scale = _quantize_nvfp4_linear(w2)
1053 w1_ref = torch.stack(
1054 [
1055 _dequantize_nvfp4_linear(
1056 w1_q[e], w1_scale[e], w1_global_scale[e], dtype
1057 )
1058 for e in range(num_experts)
1059 ]
1060 )
1061 w2_ref = torch.stack(
1062 [
1063 _dequantize_nvfp4_linear(
1064 w2_q[e], w2_scale[e], w2_global_scale[e], dtype
1065 )
1066 for e in range(num_experts)
1067 ]
1068 )
1069 input_scale = torch.full(
1070 (num_experts,),
1071 1.0 if activation_dtype is None else 1.0 / 1024.0,
1072 device="cuda",
1073 dtype=torch.float32,
1074 )
1075 prepared = prepare_nvfp4_moe_layer_for_b12x(
1076 w1_q,
1077 w1_scale,
1078 1.0 / w1_global_scale,
1079 input_scale,
1080 w2_q,
1081 w2_scale,
1082 1.0 / w2_global_scale,
1083 input_scale,
1084 is_act_and_mul=activation.is_gated,
1085 reorder_w13=activation_dtype is None and activation.is_gated,
1086 )
1087 w1_q, w1_scale, w1_alpha, a1_scale = prepared[:4]
1088 w2_q, w2_scale, w2_alpha, a2_scale = prepared[4:]
1089 if activation_dtype is None:
1090 quant_config = nvfp4_w4a16_moe_quant_config(
1091 g1_alphas=w1_alpha,
1092 g2_alphas=w2_alpha,
1093 w1_scale=w1_scale,
1094 w2_scale=w2_scale,
1095 )
1096 else:
1097 quant_config = FusedMoEQuantConfig.make(
1098 quant_dtype=activation_dtype,
1099 weight_dtype=weight_dtype,
1100 w1_scale=w1_scale,
1101 w2_scale=w2_scale,
1102 g1_alphas=w1_alpha,
1103 g2_alphas=w2_alpha,
1104 a1_gscale=1.0 / a1_scale,
1105 a2_gscale=1.0 / a2_scale,
1106 )
1107 else:
1108 raise ValueError(f"unsupported test weight dtype {weight_dtype}")
1109
1110 return _B12xMoeCase(
1111 hidden_states=hidden_states,
1112 score=torch.randn((tokens, num_experts), device="cuda", dtype=dtype),
1113 w1=w1_q,
1114 w2=w2_q,
1115 w1_ref=w1_ref,
1116 w2_ref=w2_ref,
1117 quant_config=quant_config,
1118 activation=activation,
1119 activation_dtype=activation_dtype,
1120 )
1121
1122
1123 @pytest.mark.skipif(not _has_b12x_moe(), reason="requires b12x MoE on SM120")
1124 @pytest.mark.parametrize(
1125 "weight_dtype,activation_dtype,activation",
1126 [
1127 pytest.param("mxfp4", "mxfp8", MoEActivation.SILU, id="mxfp4-mxfp8"),
1128 pytest.param("mxfp4", None, MoEActivation.SILU, id="mxfp4-bf16"),
1129 pytest.param("nvfp4", "nvfp4", MoEActivation.SILU, id="nvfp4-nvfp4"),
1130 pytest.param("nvfp4", "mxfp8", MoEActivation.SILU, id="nvfp4-mxfp8"),
1131 pytest.param("nvfp4", None, MoEActivation.SILU, id="nvfp4-bf16-silu"),
1132 pytest.param(
1133 "nvfp4",
1134 None,
1135 MoEActivation.RELU2_NO_MUL,
1136 id="nvfp4-bf16-relu2",
1137 ),
1138 ],
1139 )
1140 @torch.inference_mode()
1141 def test_b12x_moe_matches_torch(
1142 weight_dtype: str,
1143 activation_dtype: str | None,
1144 activation: MoEActivation,
1145 workspace_init,
1146 ) -> None:
1147 with set_current_vllm_config(
1148 VllmConfig(parallel_config=ParallelConfig(pipeline_parallel_size=1))
1149 ):
1150 case = _make_b12x_moe_case(
1151 weight_dtype,
1152 activation_dtype,
1153 activation=activation,
1154 )
1155 reference = torch_moe(
1156 case.hidden_states,
1157 case.w1_ref,
1158 case.w2_ref,
1159 case.score,
1160 case.topk,
1161 activation=case.activation,
1162 )
1163 if activation_dtype == "nvfp4":
1164 topk_weights, topk_ids, _ = fused_topk(
1165 case.hidden_states,
1166 case.score,
1167 case.topk,
1168 renormalize=False,
1169 )
1170 reference = _nvfp4_activation_reference(
1171 case.hidden_states,
1172 case.w1_ref,
1173 case.w2_ref,
1174 topk_weights,
1175 topk_ids,
1176 case.quant_config.a1_gscale,
1177 case.quant_config.a2_gscale,
1178 )
1179
1180 checks_zero_canonicalization = (
1181 weight_dtype == "nvfp4" and activation_dtype is None
1182 )
1183 if checks_zero_canonicalization:
1184 assert _count_fp4_negative_zeros(case.w1) > 0
1185 assert _count_fp4_negative_zeros(case.w2) > 0
1186 output = _run_b12x_moe(
1187 case.hidden_states,
1188 case.w1,
1189 case.w2,
1190 case.score,
1191 case.topk,
1192 case.activation,
1193 case.quant_config,
1194 )
1195 if checks_zero_canonicalization:
1196 assert _count_fp4_negative_zeros(case.w1) == 0
1197 assert _count_fp4_negative_zeros(case.w2) == 0
1198
1199 torch.testing.assert_close(output, reference, atol=2e-1, rtol=2e-1)
1200 cosine = torch.nn.functional.cosine_similarity(
1201 output.flatten().float(),
1202 reference.flatten().float(),
1203 dim=0,
1204 )
1205 assert cosine > 0.99
1206
1207
1208 @pytest.mark.skipif(not _has_b12x_moe(), reason="requires b12x MoE on SM120")
1209 @pytest.mark.parametrize(
1210 "weight_dtype,activation_dtype",
1211 [
1212 pytest.param("mxfp4", "mxfp8", id="w4a8"),
1213 pytest.param("nvfp4", None, id="w4a16"),
1214 ],
1215 )
1216 @torch.inference_mode()
1217 def test_b12x_moe_cuda_graph_replay(
1218 weight_dtype: str,
1219 activation_dtype: str | None,
1220 workspace_init,
1221 ) -> None:
1222 from vllm.v1.worker.workspace import lock_workspace
1223
1224 with set_current_vllm_config(
1225 VllmConfig(parallel_config=ParallelConfig(pipeline_parallel_size=1))
1226 ):
1227 case = _make_b12x_moe_case(
1228 weight_dtype,
1229 activation_dtype,
1230 tokens=4,
1231 seed=23,
1232 )
1233 kernel = _make_b12x_moe_kernel(
1234 case.hidden_states,
1235 case.w1,
1236 case.w2,
1237 case.topk,
1238 case.activation,
1239 case.quant_config,
1240 )
1241 topk_weights, topk_ids, _ = fused_topk(
1242 case.hidden_states,
1243 case.score,
1244 case.topk,
1245 renormalize=False,
1246 )
1247 assert topk_weights.dtype == torch.float32 and topk_weights.is_contiguous()
1248 assert topk_ids.dtype == torch.int32 and topk_ids.is_contiguous()
1249
1250 def apply() -> torch.Tensor:
1251 return kernel.apply(
1252 hidden_states=case.hidden_states,
1253 w1=case.w1,
1254 w2=case.w2,
1255 topk_weights=topk_weights,
1256 topk_ids=topk_ids,
1257 activation=case.activation,
1258 global_num_experts=case.w1.shape[0],
1259 expert_map=None,
1260 apply_router_weight_on_input=False,
1261 )
1262
1263 expected = apply().clone()
1264 lock_workspace()
1265 graph = torch.cuda.CUDAGraph()
1266 stream = torch.cuda.Stream()
1267 with torch.cuda.graph(graph, stream=stream):
1268 actual = apply()
1269 graph.replay()
1270 torch.accelerator.synchronize()
1271
1272 assert torch.isfinite(expected).all()
1273 assert torch.isfinite(actual).all()
1274 torch.testing.assert_close(actual, expected, atol=2e-2, rtol=2e-2)