[Perf] Minimax M3 - Support cross-layer allreduce-norm fusion (#47631)
Signed-off-by: wzhao18 <wzhao18.sz@gmail.com> Co-authored-by: Yongye Zhu <zyy1102000@gmail.com>
Wei Zhao committed
Jul 8, 2026 at 00:16 UTC
2afa3f7e950264bb179d030c23a1ed1f46558fd9
7 files changed
+65
-17
vllm/model_executor/layers/fused_moe/layer.py
+13
@@ -127,6 +127,7 @@ def FusedMoE(
127
num_redundant_experts: int = 0,
128
has_bias: bool = False,
129
is_sequence_parallel: bool = False,
130
+ reduce_results: bool = True,
131
ckpt_names: tuple[str, str, str] = ("gate_proj", "down_proj", "up_proj"),
132
n_shared_experts: int | None = None,
133
router_logits_dtype: torch.dtype | None = None,
@@ -184,6 +185,9 @@ def FusedMoE(
185
num_redundant_experts: Number of redundant experts for EPLB
186
has_bias: Whether expert layers have bias terms
187
is_sequence_parallel: Whether sequence parallelism is enabled
188
+ reduce_results: Whether to all-reduce the final output. Setting this
189
+ to False (to fuse the all-reduce downstream) is only honored on the
190
+ late-AR path.
191
expert_mapping: Expert parameter mapping for weight loading
192
n_shared_experts: Number of shared experts to fuse into the routed
193
grouped GEMM (ROCm; requires aiter FSE or the router-append path)
@@ -220,6 +224,14 @@ def FusedMoE(
224
parallel_config=vllm_config.parallel_config,
225
)
226
227
+ # Resolve the deferred all-reduce request against the parallel config.
228
+ skip_final_all_reduce = (
229
+ not reduce_results
230
+ and not moe_parallel_config.use_all2all_kernels
231
+ and not moe_parallel_config.is_sequence_parallel
232
+ and zero_expert_type is None
233
+ )
234
+
235
global_num_experts, logical_num_experts, num_fused_shared_experts = (
236
determine_expert_counts(
237
num_experts,
@@ -340,6 +352,7 @@ def FusedMoE(
352
swiglu_alpha=swiglu_alpha,
353
swiglu_beta=swiglu_beta,
354
max_capture_size=vllm_config.compilation_config.max_cudagraph_capture_size,
355
+ skip_final_all_reduce=skip_final_all_reduce,
356
)
357
358
logger.debug("FusedMoEConfig = %s", moe_config)
vllm/model_executor/layers/fused_moe/runner/moe_runner.py
+7
-1
@@ -428,7 +428,6 @@ class MoERunner(MoERunnerInterface):
428
if (
429
shared_output is not None
430
and not self.moe_config.is_sequence_parallel
431
- and not self.moe_config.skip_final_all_reduce
431
and self._fused_output_is_reduced
432
):
433
shared_output = tensor_model_parallel_all_reduce(shared_output)
@@ -446,6 +445,13 @@ class MoERunner(MoERunnerInterface):
445
here. Skipped when sequence-parallel is active (SP handles its
446
own reduction) or when the early path already reduced both outputs.
447
"""
448
+ # skip_final_all_reduce must not coexist with a pre-reduced fused
449
+ # output. This should be enforced by MoE config initialization.
450
+ if self.moe_config.skip_final_all_reduce:
451
+ assert not self._fused_output_is_reduced, (
452
+ "skip_final_all_reduce requires an un-reduced fused output"
453
+ )
454
+
455
# We don't need to reduce the final output if:
456
# - We are not running with TP or DP
457
# - The MK already reduced the fused output itself.
vllm/model_executor/models/deepseek_v2.py
+2
@@ -279,6 +279,7 @@ class DeepseekV2MoE(nn.Module):
279
config: DeepseekV2Config | DeepseekV3Config,
280
parallel_config: ParallelConfig,
281
quant_config: QuantizationConfig | None = None,
282
+ reduce_results: bool = True,
283
prefix: str = "",
284
apply_routed_scale_to_output: bool = False,
285
):
@@ -379,6 +380,7 @@ class DeepseekV2MoE(nn.Module):
380
enable_eplb=self.enable_eplb,
381
num_redundant_experts=self.n_redundant_experts,
382
is_sequence_parallel=self.is_sequence_parallel,
383
+ reduce_results=reduce_results,
384
n_shared_experts=config.n_shared_experts
385
if self.is_fusion_moe_shared_experts_enabled
386
else None,
vllm/models/deepseek_v32/nvidia/model.py
+4
-4
@@ -72,17 +72,17 @@ class DeepseekV32DecoderLayer(torch.nn.Module):
72
and layer_idx >= config.first_k_dense_replace
73
and layer_idx % moe_layer_freq == 0
74
):
75
+ # Defer the MoE cross-rank all-reduce; it is fused into the next
76
+ # layer's input_layernorm (or the final norm) via
77
+ # fused_allreduce_rms_norm.
78
self.mlp = DeepseekV2MoE(
79
config=config,
80
parallel_config=parallel_config,
81
quant_config=quant_config,
82
+ reduce_results=False,
83
prefix=f"{prefix}.mlp",
84
apply_routed_scale_to_output=False,
85
)
82
- # Defer the MoE cross-rank all-reduce; it is fused into the next
83
- # layer's input_layernorm (or the final norm) via
84
- # fused_allreduce_rms_norm. self.mlp.experts is the MoERunner.
85
- self.mlp.experts.moe_config.skip_final_all_reduce = True
86
else:
87
self.mlp = DeepseekV2MLP(
88
hidden_size=config.hidden_size,
vllm/models/deepseek_v32/nvidia/mtp.py
+1
-1
@@ -89,7 +89,7 @@ class DeepseekV32MultiTokenPredictorLayer(nn.Module):
89
hidden_states, residual = self.mtp_block(
90
positions=positions, hidden_states=hidden_states, residual=None
91
)
92
- # mtp_block's MoE output is left un-reduced (skip_final_all_reduce); the
92
+ # mtp_block's MoE output is left un-reduced (reduce_results=False); the
93
# main model fuses that all-reduce into the next norm, but here the
94
# recycle hidden is consumed directly, so reduce it now.
95
hidden_states = tensor_model_parallel_all_reduce(hidden_states)
vllm/models/minimax_m3/nvidia/model.py
+37
-10
@@ -196,6 +196,7 @@ class MiniMaxM3MoE(nn.Module):
196
config: PretrainedConfig,
197
layer_id: int,
198
quant_config: QuantizationConfig | None = None,
199
+ reduce_results: bool = True,
200
prefix: str = "",
201
) -> None:
202
super().__init__()
@@ -261,6 +262,7 @@ class MiniMaxM3MoE(nn.Module):
262
router_logits_dtype=self.gate.out_dtype,
263
shared_experts=self.shared_experts,
264
quant_config=quant_config,
265
+ reduce_results=reduce_results,
266
prefix=f"{prefix}.experts",
267
)
268
@@ -658,14 +660,10 @@ class MiniMaxM3DecoderLayer(nn.Module):
660
layer_id = int(prefix.split(sep=".")[-1])
661
self.layer_id = layer_id
662
661
- # Complete the preceding dense MLP's deferred all-reduce
662
- # (reduce_results=False), fused into this layer's input_layernorm.
663
- # Disable this fusion when PP is set
664
- self.fuse_input_allreduce = (
665
- layer_id > 0
666
- and not _is_moe_layer(config, layer_id - 1)
667
- and vllm_config.parallel_config.pipeline_parallel_size == 1
668
- )
663
+ # When set, complete the preceding layer's deferred FFN all-reduce
664
+ # fused into this layer's input_layernorm.
665
+ # Configured by MiniMaxM3Model.__init__
666
+ self.fuse_input_allreduce = False
667
668
is_sparse_attention_layer = (
669
force_sparse_attn or layer_id in _sparse_attention_layer_ids(config)
@@ -691,12 +689,19 @@ class MiniMaxM3DecoderLayer(nn.Module):
689
690
# Dense layers store the FFN under `mlp`; MoE layers under
691
# `block_sparse_moe` -- matching the checkpoint's naming.
692
+ # Leave the FFN output un-reduced so its all-reduce fuses into the
693
+ # next RMSNorm. MTP blocks add the residual directly and PP sends
694
+ # hidden states across stages, so both must reduce.
695
+ reduce_results = (
696
+ is_mtp_block or vllm_config.parallel_config.pipeline_parallel_size > 1
697
+ )
698
self.is_moe_layer = force_moe or _is_moe_layer(config, layer_id)
699
if self.is_moe_layer:
700
self.block_sparse_moe = MiniMaxM3MoE(
701
config=config,
702
layer_id=layer_id,
703
quant_config=quant_config,
704
+ reduce_results=reduce_results,
705
prefix=f"{prefix}.block_sparse_moe",
706
)
707
else:
@@ -705,7 +710,7 @@ class MiniMaxM3DecoderLayer(nn.Module):
710
intermediate_size=config.dense_intermediate_size,
711
quant_config=quant_config,
712
prefix=f"{prefix}.mlp",
708
- reduce_results=vllm_config.parallel_config.pipeline_parallel_size > 1,
713
+ reduce_results=reduce_results,
714
)
715
716
# config.use_gemma_norm is True for M3 -> Gemma-style RMSNorm.
@@ -744,6 +749,14 @@ class MiniMaxM3DecoderLayer(nn.Module):
749
hidden_states = ffn(hidden_states)
750
return hidden_states, residual
751
752
+ @property
753
+ def ffn_all_reduce_deferred(self) -> bool:
754
+ """This layer's FFN output is left un-reduced; the caller fuses the
755
+ all-reduce into the next RMSNorm."""
756
+ if self.is_moe_layer:
757
+ return self.block_sparse_moe.experts.moe_config.skip_final_all_reduce
758
+ return not self.mlp.down_proj.reduce_results
759
+
760
761
class MiniMaxM3Model(nn.Module, EagleModelMixin):
762
fall_back_to_pt_during_load = False
@@ -807,6 +820,15 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
820
["hidden_states", "residual"], config.hidden_size
821
)
822
823
+ # Configure cross-layer all-reduce/RMSNorm fusion: a layer whose FFN output
824
+ # is left un-reduced has that all-reduce fused into the next layer's
825
+ # input_layernorm (or the final norm).
826
+ prev_defers = False
827
+ for idx, layer in enumerate(self.layers[self.start_layer : self.end_layer]):
828
+ layer.fuse_input_allreduce = idx > 0 and prev_defers
829
+ prev_defers = layer.ffn_all_reduce_deferred
830
+ self.fuse_final_norm_allreduce = prev_defers
831
+
832
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
833
return self.embed_tokens(input_ids)
834
@@ -841,7 +863,12 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
863
{"hidden_states": hidden_states, "residual": residual}
864
)
865
844
- hidden_states, _ = self.norm(hidden_states, residual)
866
+ if self.fuse_final_norm_allreduce:
867
+ hidden_states, _ = fused_allreduce_gemma_rms_norm(
868
+ hidden_states, residual, self.norm
869
+ )
870
+ else:
871
+ hidden_states, _ = self.norm(hidden_states, residual)
872
873
if len(aux_hidden_states) > 0:
874
return hidden_states, aux_hidden_states
vllm/v1/attention/backends/mla/prefill/selector.py
+1
-1
@@ -134,7 +134,7 @@ def get_mla_prefill_backend(
134
f"Reason: {invalid_reasons}"
135
)
136
assert backend_cls is not None
137
- logger.info("Using %s MLA prefill backend.", selected_backend.name)
137
+ logger.info_once("Using %s MLA prefill backend.", selected_backend.name)
138
return backend_cls
139
140
return _auto_select_mla_prefill_backend(