@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / c277fbdf31
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 10, 02:45 UTC

[Feat] Support non-gated MoE with Marlin, NVFP4 CUTLASS, FP8, INT8, compressed-tensors (#32257)

Signed-off-by: Tomer Natan <tbarnatan@computelab-frontend-8.nvidia.com> Signed-off-by: mgoin <mgoin64@gmail.com> Co-authored-by: Tomer Natan <tbarnatan@computelab-frontend-8.nvidia.com> Co-authored-by: mgoin <mgoin64@gmail.com> Co-authored-by: Tomer Natan <tbarnatan@ipp1-1429.ipp1a1.colossus.nvidia.com>

TomerBN-Nvidia committed Jan 16, 2026 at 02:15 UTC c277fbdf31f49c15049fe361ef92a7e34abc7b74
17 files changed +226 -127
tests/kernels/moe/test_cutlass_moe.py
+1 -1
@@ -526,7 +526,7 @@ def test_run_cutlass_moe_fp8(
526 c_strides1 = torch.full((e,), 2 * n, device="cuda", dtype=torch.int64)
527 c_strides2 = torch.full((e,), k, device="cuda", dtype=torch.int64)
528
529 - activation = lambda o, i: torch.ops._C.silu_and_mul(o, i)
529 + activation = "silu"
530 a1q, a1q_scale = moe_kernel_quantize_input(
531 mt.a, mt.a_scale, torch.float8_e4m3fn, per_act_token
532 )
tests/kernels/moe/test_moe.py
+80
@@ -1079,6 +1079,86 @@ def test_fused_marlin_moe_with_bias(m):
1079 torch.testing.assert_close(marlin_output, torch_output, atol=5e-2, rtol=0)
1080
1081
1082 +@pytest.mark.flaky(reruns=2)
1083 +@pytest.mark.skipif(current_platform.is_rocm(), reason="Skip for rocm")
1084 +@pytest.mark.parametrize("m", [1, 64, 256])
1085 +@pytest.mark.parametrize("n,k", [(1024, 1024), (2048, 2048)])
1086 +@pytest.mark.parametrize("e,topk", [(8, 2), (64, 4)])
1087 +def test_fused_marlin_moe_non_gated(m: int, n: int, k: int, e: int, topk: int):
1088 + """Test Marlin MoE with non-gated activation (relu2_no_mul).
1089 +
1090 + Non-gated activations like relu2 don't have the gate-up projection pattern,
1091 + so w1 has shape (e, n, k) instead of (e, 2*n, k).
1092 + """
1093 + torch.cuda.manual_seed(42)
1094 +
1095 + group_size = 16 # NVFP4 group size
1096 + is_k_full = True
1097 + quant_type = scalar_types.float4_e2m1f
1098 + dtype = torch.bfloat16
1099 +
1100 + a = torch.randn((m, k), device="cuda", dtype=dtype) / 10
1101 + # Non-gated: w1 shape is (e, n, k) not (e, 2*n, k)
1102 + w1 = torch.randn((e, n, k), device="cuda", dtype=dtype) / 10
1103 + w2 = torch.randn((e, k, n), device="cuda", dtype=dtype) / 10
1104 +
1105 + w1_data = MarlinMoEWeightData.make(
1106 + w=w1,
1107 + quant_type=quant_type,
1108 + group_size=group_size,
1109 + act_order=False,
1110 + )
1111 +
1112 + w2_data = MarlinMoEWeightData.make(
1113 + w=w2,
1114 + quant_type=quant_type,
1115 + group_size=group_size,
1116 + act_order=False,
1117 + )
1118 +
1119 + score = torch.randn((m, e), device="cuda", dtype=dtype)
1120 +
1121 + topk_weights, topk_ids, _ = fused_topk(a, score, topk, False)
1122 +
1123 + with set_current_vllm_config(vllm_config):
1124 + torch_output = torch_moe(
1125 + a,
1126 + w1_data.w_ref,
1127 + w2_data.w_ref,
1128 + score,
1129 + topk,
1130 + activation="relu2",
1131 + )
1132 +
1133 + marlin_output = fused_marlin_moe(
1134 + a,
1135 + w1_data.qweight,
1136 + w2_data.qweight,
1137 + None, # bias1
1138 + None, # bias2
1139 + w1_data.scales,
1140 + w2_data.scales,
1141 + score,
1142 + topk_weights,
1143 + topk_ids,
1144 + global_num_experts=e,
1145 + expert_map=None,
1146 + global_scale1=w1_data.global_scale,
1147 + global_scale2=w2_data.global_scale,
1148 + g_idx1=w1_data.g_idx,
1149 + g_idx2=w2_data.g_idx,
1150 + sort_indices1=w1_data.sort_indices,
1151 + sort_indices2=w2_data.sort_indices,
1152 + w1_zeros=w1_data.zeros,
1153 + w2_zeros=w2_data.zeros,
1154 + quant_type_id=quant_type.id,
1155 + is_k_full=is_k_full,
1156 + activation="relu2_no_mul",
1157 + )
1158 +
1159 + torch.testing.assert_close(marlin_output, torch_output, atol=1e-1, rtol=0)
1160 +
1161 +
1162 @pytest.mark.parametrize("ep_size", [1, 2])
1163 def test_moe_align_block_size_opcheck(ep_size):
1164 num_experts = 4
vllm/envs.py
+2
@@ -1451,6 +1451,7 @@ environment_variables: dict[str, Callable[[], Any]] = {
1451 # - "flashinfer-cudnn": use flashinfer cudnn GEMM backend
1452 # - "flashinfer-trtllm": use flashinfer trtllm GEMM backend
1453 # - "flashinfer-cutlass": use flashinfer cutlass GEMM backend
1454 + # - "marlin": use marlin GEMM backend (for GPUs without native FP4 support)
1455 # - <none>: automatically pick an available backend
1456 "VLLM_NVFP4_GEMM_BACKEND": env_with_choices(
1457 "VLLM_NVFP4_GEMM_BACKEND",
@@ -1460,6 +1461,7 @@ environment_variables: dict[str, Callable[[], Any]] = {
1461 "flashinfer-trtllm",
1462 "flashinfer-cutlass",
1463 "cutlass",
1464 + "marlin",
1465 ],
1466 ),
1467 # Controls garbage collection during CUDA graph capture.
vllm/model_executor/layers/fused_moe/cutlass_moe.py
+26 -18
@@ -2,8 +2,6 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """CUTLASS based Fused MoE kernels."""
4
5 -from collections.abc import Callable
6 -
5 import torch
6
7 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
@@ -21,7 +19,10 @@ from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
19 TopKWeightAndReduceDelegate,
20 TopKWeightAndReduceNoOP,
21 )
24 -from vllm.model_executor.layers.fused_moe.utils import _resize_cache
22 +from vllm.model_executor.layers.fused_moe.utils import (
23 + _resize_cache,
24 + apply_moe_activation,
25 +)
26 from vllm.scalar_type import scalar_types
27
28 logger = init_logger(__name__)
@@ -33,7 +34,7 @@ def run_cutlass_moe_fp8(
34 w1: torch.Tensor,
35 w2: torch.Tensor,
36 topk_ids: torch.Tensor,
36 - activation_callable: Callable,
37 + activation: str,
38 global_num_experts: int,
39 expert_map: torch.Tensor | None,
40 w1_scale: torch.Tensor | None,
@@ -55,6 +56,7 @@ def run_cutlass_moe_fp8(
56 ):
57 a1q = hidden_states
58
59 + assert not activation.endswith("_no_mul"), "Only gated activation is supported"
60 assert w1_scale is not None
61 assert w2_scale is not None
62 assert w1.dtype == torch.float8_e4m3fn
@@ -198,7 +200,7 @@ def run_cutlass_moe_fp8(
200 per_out_ch,
201 )
202
201 - activation_callable(act_out, mm1_out)
203 + apply_moe_activation(activation, act_out, mm1_out)
204
205 a2q, a2q_scale = ops.scaled_fp8_quant(
206 act_out, a2_scale, use_per_token_if_dynamic=per_act_token, output=quant_out
@@ -288,8 +290,6 @@ class CutlassExpertsFp8Base(mk.FusedMoEPermuteExpertsUnpermute):
290 if expert_tokens_meta is not None:
291 expert_num_tokens = expert_tokens_meta.expert_num_tokens
292
291 - activation_callable = lambda o, i: self.activation(activation, o, i)
292 -
293 use_batched_format = (
294 self.activation_formats[0] == mk.FusedMoEActivationFormat.BatchedExperts
295 )
@@ -301,7 +301,7 @@ class CutlassExpertsFp8Base(mk.FusedMoEPermuteExpertsUnpermute):
301 w1,
302 w2,
303 topk_ids,
304 - activation_callable,
304 + activation,
305 global_num_experts,
306 expert_map,
307 self.w1_scale,
@@ -436,6 +436,7 @@ def run_cutlass_moe_fp4(
436 w2_alphas: torch.Tensor,
437 topk_weights: torch.Tensor,
438 topk_ids: torch.Tensor,
439 + activation: str,
440 workspace13: torch.Tensor,
441 workspace2: torch.Tensor,
442 m: int,
@@ -544,8 +545,7 @@ def run_cutlass_moe_fp4(
545 num_topk,
546 )
547 c1 = _resize_cache(workspace13, (m * topk, n * 2))
547 - # Note: c2 workspace is no longer needed since SiLU is fused with quantization.
548 - # c3 reuses workspace13 after c1 is consumed.
548 + c2 = _resize_cache(workspace2, (m * topk, n))
549 c3 = _resize_cache(workspace13, (m * topk, k))
550 ops.cutlass_fp4_moe_mm(
551 c1,
@@ -559,10 +559,18 @@ def run_cutlass_moe_fp4(
559 blockscale_offsets[:-1],
560 )
561 del rep_a_fp4, rep_a_blockscale
562 - # Fused SiLU+Mul+NVFP4 quantization
563 - int_fp4, int_blockscale = ops.silu_and_mul_scaled_fp4_experts_quant(
564 - c1, a2_gscale, expert_offsets, blockscale_offsets, num_topk
565 - )
562 + if activation == "silu":
563 + # Fused SiLU+Mul+NVFP4 quantization
564 + # Note: c2 workspace is no longer needed since SiLU is fused with quantization.
565 + # c3 reuses workspace13 after c1 is consumed.
566 + int_fp4, int_blockscale = ops.silu_and_mul_scaled_fp4_experts_quant(
567 + c1, a2_gscale, expert_offsets, blockscale_offsets, num_topk
568 + )
569 + else:
570 + apply_moe_activation(activation, c2, c1)
571 + int_fp4, int_blockscale = ops.scaled_fp4_experts_quant(
572 + c2, a2_gscale, expert_offsets, blockscale_offsets, num_topk
573 + )
574
575 ops.cutlass_fp4_moe_mm(
576 c3,
@@ -693,6 +701,7 @@ class CutlassExpertsFp4(mk.FusedMoEPermuteExpertsUnpermute):
701 w2_alphas=self.g2_alphas,
702 topk_weights=topk_weights,
703 topk_ids=topk_ids,
704 + activation=activation,
705 workspace13=workspace13,
706 workspace2=workspace2,
707 m=m,
@@ -711,7 +720,7 @@ def run_cutlass_moe_w4a8_fp8(
720 w1: torch.Tensor,
721 w2: torch.Tensor,
722 topk_ids: torch.Tensor,
714 - activation_callable: Callable,
723 + activation: str,
724 global_num_experts: int,
725 expert_map: torch.Tensor | None,
726 w1_scale: torch.Tensor | None,
@@ -815,7 +824,7 @@ def run_cutlass_moe_w4a8_fp8(
824 s_strides1,
825 )
826
818 - activation_callable(act_out, mm1_out)
827 + apply_moe_activation(activation, act_out, mm1_out)
828
829 a2q, a2q_scale = ops.scaled_fp8_quant(
830 act_out, a2_scale, use_per_token_if_dynamic=per_act_token, output=quant_out
@@ -936,7 +945,6 @@ class CutlassExpertsW4A8Fp8(mk.FusedMoEPermuteExpertsUnpermute):
945 assert self.w2_zp is None, "w2_zp is not supported in CUTLASS MoE"
946
947 expert_num_tokens = None
939 - activation_callable = lambda o, i: self.activation(activation, o, i)
948
949 use_batched_format = (
950 self.activation_formats[0] == mk.FusedMoEActivationFormat.BatchedExperts
@@ -951,7 +959,7 @@ class CutlassExpertsW4A8Fp8(mk.FusedMoEPermuteExpertsUnpermute):
959 w1,
960 w2,
961 topk_ids,
954 - activation_callable,
962 + activation,
963 global_num_experts,
964 expert_map,
965 self.w1_scale,
vllm/model_executor/layers/fused_moe/fused_marlin_moe.py
+16 -24
@@ -17,7 +17,11 @@ from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
17 TopKWeightAndReduceDelegate,
18 TopKWeightAndReduceNoOP,
19 )
20 -from vllm.model_executor.layers.fused_moe.utils import _resize_cache, disable_inplace
20 +from vllm.model_executor.layers.fused_moe.utils import (
21 + _resize_cache,
22 + apply_moe_activation,
23 + disable_inplace,
24 +)
25 from vllm.model_executor.layers.quantization.utils.marlin_utils import (
26 marlin_make_workspace_new,
27 marlin_moe_intermediate_size,
@@ -27,21 +31,6 @@ from vllm.platforms import current_platform
31 from vllm.scalar_type import ScalarType, scalar_types
32
33
30 -def default_activation_func(
31 - activation: str, output: torch.Tensor, input: torch.Tensor
32 -) -> None:
33 - if activation == "silu":
34 - torch.ops._C.silu_and_mul(output, input)
35 - elif activation == "swigluoai":
36 - # alpha = 1.702, limit = 7.0
37 - torch.ops._C.swigluoai_and_mul(output, input)
38 - else:
39 - raise ValueError(
40 - f"Unsupported activation: {activation}. "
41 - "Only silu and swigluoai activations are supported."
42 - )
43 -
44 -
34 def _fused_marlin_moe(
35 hidden_states: torch.Tensor,
36 w1: torch.Tensor,
@@ -62,7 +51,7 @@ def _fused_marlin_moe(
51 activation: str = "silu",
52 activation_func: Callable[
53 [str, torch.Tensor, torch.Tensor], None
65 - ] = default_activation_func,
54 + ] = apply_moe_activation,
55 input_global_scale1: torch.Tensor | None = None,
56 input_global_scale2: torch.Tensor | None = None,
57 global_scale1: torch.Tensor | None = None,
@@ -83,13 +72,13 @@ def _fused_marlin_moe(
72 assert hidden_states.ndim == 2
73 M, K = hidden_states.size()
74 N = marlin_moe_intermediate_size(w1, w2)
86 -
75 + w13_num_shards = 1 if "no_mul" in activation else 2
76 if workspace is None:
77 workspace = marlin_make_workspace_new(hidden_states.device, 4)
78
79 if intermediate_cache13 is None:
80 intermediate_cache13 = torch.empty(
92 - (M * num_topk * max(2 * N, K),),
81 + (M * num_topk * max(w13_num_shards * N, K),),
82 device=hidden_states.device,
83 dtype=hidden_states.dtype,
84 )
@@ -101,7 +90,9 @@ def _fused_marlin_moe(
90 dtype=hidden_states.dtype,
91 )
92
104 - intermediate_cache1 = _resize_cache(intermediate_cache13, (M * num_topk, 2 * N))
93 + intermediate_cache1 = _resize_cache(
94 + intermediate_cache13, (M * num_topk, w13_num_shards * N)
95 + )
96
97 intermediate_cache3 = _resize_cache(intermediate_cache13, (M * num_topk, K))
98
@@ -137,16 +128,17 @@ def _fused_marlin_moe(
128 mul_topk_weights=apply_router_weight_on_input,
129 b_q_type=quant_type,
130 size_m=M,
140 - size_n=2 * N,
131 + size_n=w13_num_shards * N,
132 size_k=K,
133 is_k_full=is_k_full,
134 use_atomic_add=False,
135 use_fp32_reduce=True,
136 is_zp_float=False,
137 )
147 -
138 activation_func(
149 - activation, intermediate_cache2, intermediate_cache1.view(-1, 2 * N)
139 + activation,
140 + intermediate_cache2,
141 + intermediate_cache1.view(-1, w13_num_shards * N),
142 )
143
144 if output is None:
@@ -216,7 +208,7 @@ def fused_marlin_moe(
208 activation: str = "silu",
209 activation_func: Callable[
210 [str, torch.Tensor, torch.Tensor], None
219 - ] = default_activation_func,
211 + ] = apply_moe_activation,
212 moe_sum: Callable[[torch.Tensor, torch.Tensor], None] | None = None,
213 expert_map: torch.Tensor | None = None,
214 input_global_scale1: torch.Tensor | None = None,
vllm/model_executor/layers/fused_moe/layer.py
+3 -22
@@ -619,30 +619,11 @@ class FusedMoE(CustomOp):
619 # for heuristic purposes, so it must be initialized first.
620 self.quant_method: FusedMoEMethodBase = _get_quant_method()
621
622 - if not self.moe_config.is_act_and_mul:
623 - # Avoid circular import
624 - from vllm.model_executor.layers.quantization.modelopt import (
625 - ModelOptFp8MoEMethod,
626 - ModelOptNvFp4FusedMoE,
622 + if not self.moe_config.is_act_and_mul and not current_platform.is_cuda():
623 + raise NotImplementedError(
624 + "is_act_and_mul=False is supported only for CUDA for now"
625 )
626
629 - if not isinstance(
630 - self.quant_method,
631 - (
632 - UnquantizedFusedMoEMethod,
633 - ModelOptFp8MoEMethod,
634 - ModelOptNvFp4FusedMoE,
635 - ),
636 - ):
637 - raise NotImplementedError(
638 - "is_act_and_mul=False is supported only for unquantized "
639 - ", ModelOpt FP8, and ModelOpt NvFp4 checkpoints"
640 - )
641 - if not current_platform.is_cuda():
642 - raise NotImplementedError(
643 - "is_act_and_mul=False is supported only for CUDA for now"
644 - )
645 -
627 if self.enable_eplb and not self.quant_method.supports_eplb:
628 # TODO: Add support for additional quantization methods.
629 # The implementation for other quantization methods does not
vllm/model_executor/layers/fused_moe/oracle/fp8.py
+8 -3
@@ -52,7 +52,7 @@ def select_fp8_moe_backend(
52 block_quant: bool,
53 tp_size: int,
54 with_lora_support: bool,
55 - is_act_and_mul: bool = True,
55 + is_act_and_mul: bool,
56 allow_vllm_cutlass: bool = False,
57 ) -> Fp8MoeBackend:
58 """
@@ -128,7 +128,7 @@ def select_fp8_moe_backend(
128 scope="local",
129 )
130
131 - if use_deep_gemm and moe_use_deep_gemm and block_quant:
131 + if use_deep_gemm and moe_use_deep_gemm and block_quant and is_act_and_mul:
132 if not has_deep_gemm():
133 logger.warning_once(
134 "DeepGEMM backend requested but not available.", scope="local"
@@ -141,7 +141,12 @@ def select_fp8_moe_backend(
141 logger.info_once(_make_log_backend("ROCm AITER"), scope="local")
142 return Fp8MoeBackend.AITER
143
144 - if allow_vllm_cutlass and not block_quant and cutlass_group_gemm_supported():
144 + if (
145 + allow_vllm_cutlass
146 + and not block_quant
147 + and cutlass_group_gemm_supported()
148 + and is_act_and_mul
149 + ):
150 logger.info_once(_make_log_backend("vLLM CUTLASS"), scope="local")
151 return Fp8MoeBackend.VLLM_CUTLASS
152
vllm/model_executor/layers/fused_moe/oracle/nvfp4.py
+1
@@ -178,6 +178,7 @@ def convert_to_nvfp4_moe_kernel_format(
178 w2=w2,
179 w2_scale=w2_scale,
180 w2_scale_2=w2_scale_2,
181 + is_act_and_mul=is_act_and_mul,
182 )
183 else:
184 raise ValueError(f"Unknown NvFp4 backend for MoE: {nvfp4_backend}")
vllm/model_executor/layers/fused_moe/utils.py
+2 -1
@@ -367,7 +367,8 @@ def apply_moe_activation(
367 elif activation == GELU_NO_MUL:
368 output.copy_(F.gelu(input))
369 elif activation == RELU2_NO_MUL:
370 - torch.square(F.relu(input), out=output)
370 + F.relu(input, inplace=True)
371 + torch.square(input, out=output)
372 else:
373 raise ValueError(f"Unsupported FusedMoe activation: {activation}")
374
vllm/model_executor/layers/quantization/awq_marlin.py
-2
@@ -764,8 +764,6 @@ class AWQMarlinMoEMethod(FusedMoEMethodBase):
764 x: torch.Tensor,
765 router_logits: torch.Tensor,
766 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
767 - assert layer.activation == "silu", "Only SiLU activation is supported."
768 -
767 topk_weights, topk_ids = router.select_experts(
768 hidden_states=x,
769 router_logits=router_logits,
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe.py
+45 -27
@@ -370,12 +370,6 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
370 layer_name: str | None = None,
371 use_marlin: bool = False,
372 ):
373 - if not moe.is_act_and_mul:
374 - raise ValueError(
375 - "CompressedTensorsW4A4Nvfp4MoEMethod does not yet "
376 - "support non gated MoE models."
377 - )
378 -
373 super().__init__(moe)
374 self.group_size = 16
375 if use_marlin:
@@ -388,6 +382,16 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
382 )
383 else:
384 self.nvfp4_backend = select_nvfp4_moe_backend()
385 +
386 + # TODO: move this type of check into the oracle.
387 + if not self.moe.is_act_and_mul and self.nvfp4_backend not in [
388 + NvFp4MoeBackend.FLASHINFER_CUTLASS,
389 + NvFp4MoeBackend.MARLIN,
390 + ]:
391 + raise NotImplementedError(
392 + "Non-gated activations are only supported by FlashInfer "
393 + f"CUTLASS and Marlin NvFP4 MoE backends, not {self.nvfp4_backend}."
394 + )
395 self.use_global_sf = is_global_sf_supported_for_nvfp4_backend(
396 self.nvfp4_backend
397 )
@@ -404,11 +408,12 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
408 ):
409 layer.num_experts = num_experts
410 layer.params_dtype = params_dtype
411 + w13_num_shards = 2 if self.moe.is_act_and_mul else 1
412
413 w13_weight = torch.nn.Parameter(
414 torch.empty(
415 num_experts,
411 - 2 * intermediate_size_per_partition,
416 + w13_num_shards * intermediate_size_per_partition,
417 # 2 fp4 items are packed in the input dimension
418 hidden_size // 2,
419 requires_grad=False,
@@ -436,7 +441,7 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
441 w13_weight_scale = torch.nn.Parameter(
442 torch.empty(
443 num_experts,
439 - 2 * intermediate_size_per_partition,
444 + w13_num_shards * intermediate_size_per_partition,
445 # 2 fp4 items are packed in the input dimension
446 hidden_size // self.group_size,
447 dtype=torch.float8_e4m3fn,
@@ -467,7 +472,8 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
472
473 # Weight Global Scales
474 w13_weight_scale_2 = torch.nn.Parameter(
470 - torch.empty(num_experts, 2, dtype=torch.float32), requires_grad=False
475 + torch.empty(num_experts, w13_num_shards, dtype=torch.float32),
476 + requires_grad=False,
477 )
478 layer.register_parameter("w13_weight_global_scale", w13_weight_scale_2)
479 extra_weight_attrs.update(
@@ -486,7 +492,8 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
492
493 # Input Global Scales
494 w13_input_scale = torch.nn.Parameter(
489 - torch.empty(num_experts, 2, dtype=torch.float32), requires_grad=False
495 + torch.empty(num_experts, w13_num_shards, dtype=torch.float32),
496 + requires_grad=False,
497 )
498 layer.register_parameter("w13_input_global_scale", w13_input_scale)
499 extra_weight_attrs.update(
@@ -640,6 +647,7 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
647 x=x,
648 router_logits=router_logits,
649 top_k=layer.top_k,
650 + activation=layer.activation,
651 global_num_experts=layer.global_num_experts,
652 num_expert_group=layer.num_expert_group,
653 topk_group=layer.topk_group,
@@ -666,6 +674,7 @@ class CompressedTensorsW4A4Nvfp4MoEMethod(CompressedTensorsMoEMethod):
674 topk_ids=topk_ids,
675 topk_weights=topk_weights,
676 top_k=layer.top_k,
677 + activation=layer.activation,
678 global_num_experts=layer.global_num_experts,
679 )
680 else:
@@ -722,6 +731,7 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
731 block_quant=self.block_quant,
732 tp_size=moe.tp_size,
733 with_lora_support=moe.is_lora_enabled,
734 + is_act_and_mul=moe.is_act_and_mul,
735 # TODO(rob): enable selecting this externally.
736 allow_vllm_cutlass=True,
737 )
@@ -760,6 +770,7 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
770 layer.weight_block_size = None
771
772 params_dtype = torch.float8_e4m3fn
773 + w13_num_shards = 2 if self.moe.is_act_and_mul else 1
774
775 if self.block_quant:
776 assert self.weight_block_size is not None
@@ -791,7 +802,7 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
802 w13_weight = torch.nn.Parameter(
803 torch.empty(
804 num_experts,
794 - 2 * intermediate_size_per_partition,
805 + w13_num_shards * intermediate_size_per_partition,
806 hidden_size,
807 dtype=params_dtype,
808 ),
@@ -814,10 +825,12 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
825
826 # WEIGHT_SCALES
827 if self.weight_quant.strategy == QuantizationStrategy.TENSOR:
817 - # Allocate 2 scales for w1 and w3 respectively.
818 - # They are combined to a single scale after weight loading.
828 + # For gated MoE, allocate 2 scales for w1 and w3 respectively.
829 + # They will be combined to a single scale after weight loading.
830 + # For non-gated MoE, allocate 1 scale for w13.
831 w13_weight_scale = torch.nn.Parameter(
820 - torch.ones(num_experts, 2, dtype=torch.float32), requires_grad=False
832 + torch.ones(num_experts, w13_num_shards, dtype=torch.float32),
833 + requires_grad=False,
834 )
835 layer.register_parameter("w13_weight_scale", w13_weight_scale)
836 w2_weight_scale = torch.nn.Parameter(
@@ -835,7 +848,7 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
848 w13_weight_scale = torch.nn.Parameter(
849 torch.ones(
850 num_experts,
838 - 2 * intermediate_size_per_partition,
851 + w13_num_shards * intermediate_size_per_partition,
852 1,
853 dtype=torch.float32,
854 ),
@@ -858,7 +871,8 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
871 w13_weight_scale = torch.nn.Parameter(
872 torch.ones(
873 num_experts,
861 - 2 * ((intermediate_size_per_partition + block_n - 1) // block_n),
874 + w13_num_shards
875 + * ((intermediate_size_per_partition + block_n - 1) // block_n),
876 (hidden_size + block_k - 1) // block_k,
877 dtype=torch.float32,
878 ),
@@ -930,11 +944,12 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
944 # Per-tensor kernels use a single scale, for W13, but on disk there
945 # is a separate scale for W1 and W3. Requantize with the max scale.
946 if self.weight_quant.strategy == QuantizationStrategy.TENSOR:
933 - process_fp8_weight_tensor_strategy_moe(
947 + w13, w13_scale = process_fp8_weight_tensor_strategy_moe(
948 w13,
949 w13_scale,
950 shard_size=layer.intermediate_size_per_partition,
951 num_experts=layer.num_local_experts,
952 + is_act_and_mul=self.moe.is_act_and_mul,
953 )
954
955 w13, w2, w13_scale, w2_scale = convert_to_fp8_moe_kernel_format(
@@ -1166,12 +1181,13 @@ class CompressedTensorsW8A8Int8MoEMethod(CompressedTensorsMoEMethod):
1181 **extra_weight_attrs,
1182 ):
1183 params_dtype = torch.int8
1184 + w13_num_shards = 2 if self.moe.is_act_and_mul else 1
1185
1186 # WEIGHTS
1187 w13_weight = torch.nn.Parameter(
1188 torch.empty(
1189 num_experts,
1174 - 2 * intermediate_size_per_partition,
1190 + w13_num_shards * intermediate_size_per_partition,
1191 hidden_size,
1192 dtype=params_dtype,
1193 ),
@@ -1196,7 +1212,10 @@ class CompressedTensorsW8A8Int8MoEMethod(CompressedTensorsMoEMethod):
1212 assert self.weight_quant.strategy == QuantizationStrategy.CHANNEL
1213 w13_weight_scale = torch.nn.Parameter(
1214 torch.ones(
1199 - num_experts, 2 * intermediate_size_per_partition, 1, dtype=torch.float32
1215 + num_experts,
1216 + w13_num_shards * intermediate_size_per_partition,
1217 + 1,
1218 + dtype=torch.float32,
1219 ),
1220 requires_grad=False,
1221 )
@@ -1296,6 +1315,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
1315 **extra_weight_attrs,
1316 ):
1317 intermediate_size_full = extra_weight_attrs.pop("intermediate_size_full")
1318 + w13_num_shards = 2 if self.moe.is_act_and_mul else 1
1319
1320 # Will transpose the loaded weight along the
1321 # intermediate and hidden dim sizes. Will
@@ -1307,7 +1327,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
1327 torch.empty(
1328 num_experts,
1329 hidden_size // self.packed_factor,
1310 - 2 * intermediate_size_per_partition,
1330 + w13_num_shards * intermediate_size_per_partition,
1331 dtype=torch.int32,
1332 ),
1333 requires_grad=False,
@@ -1352,7 +1372,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
1372 torch.ones(
1373 num_experts,
1374 num_groups_w13,
1355 - 2 * intermediate_size_per_partition,
1375 + w13_num_shards * intermediate_size_per_partition,
1376 dtype=params_dtype,
1377 ),
1378 requires_grad=False,
@@ -1600,10 +1620,6 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
1620 x: torch.Tensor,
1621 router_logits: torch.Tensor,
1622 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
1603 - assert layer.activation == "silu", (
1604 - f"{layer.activation} not supported for Marlin MoE."
1605 - )
1606 -
1623 topk_weights, topk_ids = router.select_experts(
1624 hidden_states=x,
1625 router_logits=router_logits,
@@ -1625,6 +1641,7 @@ class CompressedTensorsWNA16MarlinMoEMethod(CompressedTensorsMoEMethod):
1641 quant_type_id=self.quant_type.id,
1642 apply_router_weight_on_input=layer.apply_router_weight_on_input,
1643 global_num_experts=layer.global_num_experts,
1644 + activation=layer.activation,
1645 expert_map=layer.expert_map,
1646 g_idx1=layer.w13_weight_g_idx,
1647 g_idx2=layer.w2_weight_g_idx,
@@ -1675,11 +1692,12 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
1692 extra_weight_attrs.update(
1693 {"is_transposed": True, "quant_method": self.strategy}
1694 )
1695 + w13_num_shards = 2 if self.moe.is_act_and_mul else 1
1696 w13_weight = torch.nn.Parameter(
1697 torch.empty(
1698 num_experts,
1699 hidden_size // self.packed_factor,
1682 - 2 * intermediate_size_per_partition,
1700 + w13_num_shards * intermediate_size_per_partition,
1701 dtype=torch.int32,
1702 ),
1703 requires_grad=False,
@@ -1712,7 +1730,7 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
1730 torch.ones(
1731 num_experts,
1732 num_groups_w13,
1715 - 2 * intermediate_size_per_partition,
1733 + w13_num_shards * intermediate_size_per_partition,
1734 dtype=params_dtype,
1735 ),
1736 requires_grad=False,
vllm/model_executor/layers/quantization/fp8.py
+1
@@ -637,6 +637,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
637 block_quant=self.block_quant,
638 tp_size=layer.moe_parallel_config.tp_size,
639 with_lora_support=self.moe.is_lora_enabled,
640 + is_act_and_mul=self.moe.is_act_and_mul,
641 )
642
643 if self.fp8_backend == Fp8MoeBackend.FLASHINFER_CUTLASS:
vllm/model_executor/layers/quantization/gptq_marlin.py
-2
@@ -900,8 +900,6 @@ class GPTQMarlinMoEMethod(FusedMoEMethodBase):
900 x: torch.Tensor,
901 router_logits: torch.Tensor,
902 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
903 - assert layer.activation == "silu", "Only SiLU activation is supported."
904 -
903 topk_weights, topk_ids = router.select_experts(
904 hidden_states=x,
905 router_logits=router_logits,
vllm/model_executor/layers/quantization/modelopt.py
+19 -17
@@ -733,6 +733,7 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
733 block_quant=False,
734 tp_size=moe_config.moe_parallel_config.tp_size,
735 with_lora_support=self.moe.is_lora_enabled,
736 + is_act_and_mul=self.moe.is_act_and_mul,
737 )
738 self.kernel: mk.FusedMoEModularKernel | None = None
739
@@ -789,15 +790,12 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
790 )
791 weight_loader = extra_weight_attrs.get("weight_loader")
792
792 - if self.moe.is_act_and_mul:
793 - w13_up_dim = 2 * intermediate_size_per_partition
794 - else:
795 - w13_up_dim = intermediate_size_per_partition
793 + w13_num_shards = 2 if self.moe.is_act_and_mul else 1
794
795 w13_weight = ModelWeightParameter(
796 data=torch.empty(
797 num_experts,
800 - w13_up_dim,
798 + w13_num_shards * intermediate_size_per_partition,
799 hidden_size,
800 dtype=weight_dtype,
801 ),
@@ -826,7 +824,7 @@ class ModelOptFp8MoEMethod(FusedMoEMethodBase):
824 # For non-gated MoE, allocate 1 scale for w13.
825 w13_weight_scale = PerTensorScaleParameter(
826 data=torch.full(
829 - (num_experts, 2 if self.moe.is_act_and_mul else 1),
827 + (num_experts, w13_num_shards),
828 1.0,
829 dtype=torch.float32,
830 ),
@@ -1132,6 +1130,9 @@ class ModelOptNvFp4LinearMethod(LinearMethodBase):
1130 elif envs.VLLM_NVFP4_GEMM_BACKEND == "cutlass":
1131 self.backend = "cutlass"
1132 assert cutlass_fp4_supported(), f"Cutlass is required for {self.backend}"
1133 + elif envs.VLLM_NVFP4_GEMM_BACKEND == "marlin":
1134 + self.backend = "marlin"
1135 + assert is_fp4_marlin_supported(), f"Marlin is required for {self.backend}"
1136
1137 if self.backend == "none":
1138 raise ValueError(
@@ -1337,13 +1338,13 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1338 self.quant_config = quant_config
1339 self.nvfp4_backend = select_nvfp4_moe_backend()
1340 # TODO: move this type of check into the oracle.
1340 - if (
1341 - not self.moe.is_act_and_mul
1342 - and not self.nvfp4_backend == NvFp4MoeBackend.FLASHINFER_CUTLASS
1343 - ):
1341 + if not self.moe.is_act_and_mul and self.nvfp4_backend not in [
1342 + NvFp4MoeBackend.FLASHINFER_CUTLASS,
1343 + NvFp4MoeBackend.MARLIN,
1344 + ]:
1345 raise NotImplementedError(
1346 "Non-gated activations are only supported by FlashInfer "
1346 - "CUTLASS NvFP4 MoE backend."
1347 + f"CUTLASS and Marlin NvFP4 MoE backends, not {self.nvfp4_backend}."
1348 )
1349
1350 self.use_global_sf = is_global_sf_supported_for_nvfp4_backend(
@@ -1409,11 +1410,12 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1410 weight_scale_dtype = torch.float8_e4m3fn
1411 weight_loader = extra_weight_attrs.get("weight_loader")
1412 global_num_experts = extra_weight_attrs.get("global_num_experts")
1413 + w13_num_shards = 2 if self.moe.is_act_and_mul else 1
1414 # GEMM 1
1415 w13_weight = ModelWeightParameter(
1416 data=torch.empty(
1417 num_experts,
1416 - (2 if self.moe.is_act_and_mul else 1) * intermediate_size_per_partition,
1418 + w13_num_shards * intermediate_size_per_partition,
1419 # 2 fp4 items are packed in the input dimension
1420 hidden_size // 2,
1421 dtype=weight_dtype,
@@ -1442,7 +1444,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1444 w13_weight_scale = ModelWeightParameter(
1445 data=torch.empty(
1446 num_experts,
1445 - (2 if self.moe.is_act_and_mul else 1) * intermediate_size_per_partition,
1447 + w13_num_shards * intermediate_size_per_partition,
1448 # 2 fp4 items are packed in the input dimension
1449 hidden_size // self.quant_config.group_size,
1450 dtype=weight_scale_dtype,
@@ -1472,9 +1474,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1474 )
1475
1476 w13_weight_scale_2 = PerTensorScaleParameter(
1475 - data=torch.empty(
1476 - num_experts, 2 if self.moe.is_act_and_mul else 1, dtype=torch.float32
1477 - ),
1477 + data=torch.empty(num_experts, w13_num_shards, dtype=torch.float32),
1478 weight_loader=weight_loader,
1479 )
1480 layer.register_parameter("w13_weight_scale_2", w13_weight_scale_2)
@@ -1495,7 +1495,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1495 w13_input_scale = PerTensorScaleParameter(
1496 data=torch.empty(
1497 global_sf_num_experts,
1498 - 2 if self.moe.is_act_and_mul else 1,
1498 + w13_num_shards,
1499 dtype=torch.float32,
1500 ),
1501 weight_loader=weight_loader,
@@ -1616,6 +1616,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1616 x=x,
1617 router_logits=router_logits,
1618 top_k=layer.top_k,
1619 + activation=layer.activation,
1620 global_num_experts=layer.global_num_experts,
1621 num_expert_group=layer.num_expert_group,
1622 topk_group=layer.topk_group,
@@ -1642,6 +1643,7 @@ class ModelOptNvFp4FusedMoE(FusedMoEMethodBase):
1643 topk_ids=topk_ids,
1644 topk_weights=topk_weights,
1645 top_k=layer.top_k,
1646 + activation=layer.activation,
1647 global_num_experts=layer.global_num_experts,
1648 )
1649 else:
vllm/model_executor/layers/quantization/utils/flashinfer_fp4_moe.py
+16
@@ -255,6 +255,7 @@ def flashinfer_trtllm_fp4_moe(
255 x: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
256 router_logits: torch.Tensor,
257 top_k: int,
258 + activation: str,
259 global_num_experts: int,
260 num_expert_group: int | None,
261 topk_group: int | None,
@@ -269,6 +270,7 @@ def flashinfer_trtllm_fp4_moe(
270 x: Input tensor
271 router_logits: Router logits for expert selection
272 top_k: Number of experts to select per token
273 + activation: Activation function to use
274 global_num_experts: Total number of experts across all ranks
275 num_expert_group: Number of expert groups (for grouped routing)
276 topk_group: Top-k within each group
@@ -282,6 +284,12 @@ def flashinfer_trtllm_fp4_moe(
284
285 from vllm.model_executor.models.llama4 import Llama4MoE
286
287 + # https://github.com/flashinfer-ai/flashinfer/blob/f0277fd1bff90e309e5c19cab36c5dae056d685d/flashinfer/fused_moe/core.py#L2404
288 + assert activation == "silu", (
289 + "Only SiLU activation is supported for FlashInfer TRTLLM FP4 MoE. "
290 + f"{activation} found instead."
291 + )
292 +
293 # Quantize input to FP4
294 if isinstance(x, tuple):
295 hidden_states_fp4, hidden_states_scale_linear_fp4 = x
@@ -352,6 +360,7 @@ def flashinfer_trtllm_fp4_routed_moe(
360 topk_ids: torch.Tensor,
361 topk_weights: torch.Tensor,
362 top_k: int,
363 + activation: str,
364 global_num_experts: int,
365 ) -> torch.Tensor:
366 """
@@ -364,6 +373,7 @@ def flashinfer_trtllm_fp4_routed_moe(
373 x: Input tensor
374 topk_ids: Ids of selected experts
375 top_k: Number of experts to select per token
376 + activation: Activation function to use
377 global_num_experts: Total number of experts across all ranks
378
379 Returns:
@@ -371,6 +381,12 @@ def flashinfer_trtllm_fp4_routed_moe(
381 """
382 import flashinfer
383
384 + # https://github.com/flashinfer-ai/flashinfer/blob/f0277fd1bff90e309e5c19cab36c5dae056d685d/flashinfer/fused_moe/core.py#L2535
385 + assert activation == "silu", (
386 + "Only SiLU activation is supported for FlashInfer TRTLLM FP4 Routed MoE. "
387 + f"{activation} found instead."
388 + )
389 +
390 # Pack top k ids and expert weights into a single int32 tensor, as
391 # required by TRT-LLM
392 packed_tensor = (topk_ids.to(torch.int32) << 16) | topk_weights.to(
vllm/model_executor/layers/quantization/utils/marlin_utils.py
+1 -8
@@ -233,8 +233,6 @@ def check_moe_marlin_supports_layer(layer: LinearBase, group_size: int) -> bool:
233 intermediate_size_per_partition = layer.intermediate_size_per_partition
234 # apply_router_weight_on_input is not supported for moe marlin
235 supports_router_weight = not layer.apply_router_weight_on_input
236 - # moe marlin requires the activation to be silu
237 - supports_activation = layer.activation == "silu"
236
237 # gate-up: (n, k) = (intermediate_size_per_partition * 2, hidden_size)
238 # down: (n, k) = (hidden_size, intermediate_size_per_partition)
@@ -244,12 +242,7 @@ def check_moe_marlin_supports_layer(layer: LinearBase, group_size: int) -> bool:
242 and intermediate_size_per_partition % max(64, group_size) == 0
243 )
244 supports_group_size = group_size in [-1, 32, 64, 128]
247 - return (
248 - supports_shape
249 - and supports_group_size
250 - and supports_router_weight
251 - and supports_activation
252 - )
245 + return supports_shape and supports_group_size and supports_router_weight
246
247
248 def marlin_moe_intermediate_size(w1_packed: torch.Tensor, w2_packed: torch.Tensor):
vllm/model_executor/layers/quantization/utils/marlin_utils_fp4.py
+5 -2
@@ -235,6 +235,7 @@ def prepare_nvfp4_moe_layer_for_marlin(
235 w2: torch.Tensor,
236 w2_scale: torch.Tensor,
237 w2_scale_2: torch.Tensor,
238 + is_act_and_mul: bool,
239 ) -> tuple[
240 torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor
241 ]:
@@ -266,8 +267,9 @@ def prepare_nvfp4_moe_layer_for_marlin(
267 # Repack weights to marlin format
268 def repack_weight(weight: torch.Tensor, name: str) -> torch.Tensor:
269 tensor_list = []
270 + num_shards = 2 if is_act_and_mul else 1
271 if "w13" in name:
270 - size_n, size_k = N * 2, K
272 + size_n, size_k = N * num_shards, K
273 else:
274 size_n, size_k = K, N
275
@@ -300,8 +302,9 @@ def prepare_nvfp4_moe_layer_for_marlin(
302 g_scales = g_scales.to(param_dtype)
303
304 tensor_list = []
305 + num_shards = 2 if is_act_and_mul else 1
306 if "w13" in name:
304 - size_n, size_k = N * 2, K
307 + size_n, size_k = N * num_shards, K
308 else:
309 size_n, size_k = K, N
310