[Perf][Qwen] Replace MOE all-reduce with reduce-scatter (#47006)

Signed-off-by: gcanlin <canlinguosdu@gmail.com> Signed-off-by: yewentao256 <zhyanwentao@126.com> Co-authored-by: yewentao256 <zhyanwentao@126.com>

Canlin Guo committed Jul 12, 2026 at 14:14 UTC a02984ed471488c0f0e8f73cab21be4325992d4c
3 files changed +113 -11
vllm/model_executor/layers/mamba/gdn/qwen_gdn_linear_attn.py
+2
@@ -437,6 +437,7 @@ class QwenGatedDeltaNetAttention(GatedDeltaNetAttention):
437 vllm_config: VllmConfig,
438 prefix: str = "",
439 gqa_interleaved_layout=False,
440 + reduce_results: bool = True,
441 ) -> None:
442 super().__init__(config, vllm_config, prefix)
443
@@ -547,6 +548,7 @@ class QwenGatedDeltaNetAttention(GatedDeltaNetAttention):
548 self.hidden_size,
549 bias=False,
550 input_is_parallel=True,
551 + reduce_results=reduce_results,
552 quant_config=self.quant_config,
553 prefix=f"{prefix}.out_proj",
554 )
vllm/model_executor/models/qwen3_5.py
+9
@@ -121,10 +121,17 @@ class Qwen3_5DecoderLayer(Qwen3NextDecoderLayer):
121 config = vllm_config.model_config.hf_text_config
122 model_config = vllm_config.model_config
123 cache_config = vllm_config.cache_config
124 + parallel_config = vllm_config.parallel_config
125 quant_config = vllm_config.quant_config
126
127 self.layer_type = layer_type
128 self.layer_idx = extract_layer_index(prefix)
129 + is_moe_layer = config.model_type == "qwen3_5_moe_text"
130 + self.use_attn_reduce_scatter_for_moe = (
131 + parallel_config.use_sequence_parallel_moe
132 + and parallel_config.pipeline_parallel_size == 1
133 + and is_moe_layer
134 + )
135
136 if self.layer_type == "linear_attention":
137 self.linear_attn = QwenGatedDeltaNetAttention(
@@ -132,6 +139,7 @@ class Qwen3_5DecoderLayer(Qwen3NextDecoderLayer):
139 vllm_config=vllm_config,
140 prefix=f"{prefix}.linear_attn",
141 gqa_interleaved_layout=False,
142 + reduce_results=not self.use_attn_reduce_scatter_for_moe,
143 )
144 elif self.layer_type == "full_attention":
145 self.self_attn = Qwen3NextAttention(
@@ -140,6 +148,7 @@ class Qwen3_5DecoderLayer(Qwen3NextDecoderLayer):
148 cache_config=cache_config,
149 quant_config=quant_config,
150 prefix=f"{prefix}.self_attn",
151 + reduce_results=not self.use_attn_reduce_scatter_for_moe,
152 )
153 else:
154 raise ValueError(f"Invalid layer_type {self.layer_type}")
vllm/model_executor/models/qwen3_next.py
+102 -11
@@ -16,6 +16,7 @@ from vllm.distributed import (
16 get_pp_group,
17 get_tensor_model_parallel_world_size,
18 tensor_model_parallel_all_gather,
19 + tensor_model_parallel_reduce_scatter,
20 )
21 from vllm.logger import init_logger
22 from vllm.model_executor.layers.attention import Attention
@@ -192,13 +193,17 @@ class Qwen3NextSparseMoeBlock(nn.Module):
193 else None,
194 )
195
195 - def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
196 + def forward(
197 + self,
198 + hidden_states: torch.Tensor,
199 + already_sequence_parallel: bool = False,
200 + ) -> torch.Tensor:
201 # NOTE: hidden_states can have either 1D or 2D shape.
202 orig_shape = hidden_states.shape
203 num_tokens, hidden_dim = hidden_states.shape
204 hidden_states = hidden_states.view(-1, hidden_dim)
205
201 - if self.is_sequence_parallel:
206 + if self.is_sequence_parallel and not already_sequence_parallel:
207 hidden_states = sequence_parallel_chunk(hidden_states)
208
209 if self.experts.is_internal_router:
@@ -213,7 +218,7 @@ class Qwen3NextSparseMoeBlock(nn.Module):
218 hidden_states=hidden_states, router_logits=router_logits
219 )
220
216 - if self.is_sequence_parallel:
221 + if self.is_sequence_parallel and not already_sequence_parallel:
222 final_hidden_states = tensor_model_parallel_all_gather(
223 final_hidden_states, 0
224 )
@@ -229,6 +234,7 @@ class Qwen3NextAttention(nn.Module):
234 model_config: ModelConfig | None = None,
235 cache_config: CacheConfig | None = None,
236 quant_config: QuantizationConfig | None = None,
237 + reduce_results: bool = True,
238 prefix: str = "",
239 ) -> None:
240 super().__init__()
@@ -271,6 +277,7 @@ class Qwen3NextAttention(nn.Module):
277 self.total_num_heads * self.head_dim,
278 config.hidden_size,
279 bias=False,
280 + reduce_results=reduce_results,
281 quant_config=quant_config,
282 prefix=f"{prefix}.o_proj",
283 )
@@ -405,16 +412,31 @@ class Qwen3NextDecoderLayer(nn.Module):
412 model_config = vllm_config.model_config
413 cache_config = vllm_config.cache_config
414 quant_config = vllm_config.quant_config
415 + parallel_config = vllm_config.parallel_config
416
417 self.layer_type = layer_type
418 self.layer_idx = extract_layer_index(prefix)
419
420 + mlp_only_layers = (
421 + [] if not hasattr(config, "mlp_only_layers") else config.mlp_only_layers
422 + )
423 + is_moe_layer = (self.layer_idx not in mlp_only_layers) and (
424 + config.num_experts > 0
425 + and (self.layer_idx + 1) % config.decoder_sparse_step == 0
426 + )
427 + self.use_attn_reduce_scatter_for_moe = (
428 + parallel_config.use_sequence_parallel_moe
429 + and parallel_config.pipeline_parallel_size == 1
430 + and is_moe_layer
431 + )
432 +
433 if self.layer_type == "linear_attention":
434 self.linear_attn = QwenGatedDeltaNetAttention(
435 config,
436 vllm_config=vllm_config,
437 prefix=f"{prefix}.linear_attn",
438 gqa_interleaved_layout=True,
439 + reduce_results=not self.use_attn_reduce_scatter_for_moe,
440 )
441 elif self.layer_type == "full_attention":
442 self.self_attn = Qwen3NextAttention(
@@ -422,18 +444,13 @@ class Qwen3NextDecoderLayer(nn.Module):
444 model_config=model_config,
445 cache_config=cache_config,
446 quant_config=quant_config,
447 + reduce_results=not self.use_attn_reduce_scatter_for_moe,
448 prefix=f"{prefix}.self_attn",
449 )
450 else:
451 raise ValueError(f"Invalid layer_type {self.layer_type}")
452
430 - mlp_only_layers = (
431 - [] if not hasattr(config, "mlp_only_layers") else config.mlp_only_layers
432 - )
433 - if (self.layer_idx not in mlp_only_layers) and (
434 - config.num_experts > 0
435 - and (self.layer_idx + 1) % config.decoder_sparse_step == 0
436 - ):
453 + if is_moe_layer:
454 self.mlp = Qwen3NextSparseMoeBlock(
455 vllm_config=vllm_config,
456 prefix=f"{prefix}.mlp",
@@ -478,12 +495,23 @@ class Qwen3NextDecoderLayer(nn.Module):
495 positions: torch.Tensor = None,
496 **kwargs: object,
497 ):
498 + full_num_tokens = positions.shape[-1]
499 + input_is_sequence_parallel = (
500 + self.use_attn_reduce_scatter_for_moe
501 + and residual is not None
502 + and hidden_states.shape[0] != full_num_tokens
503 + )
504 +
505 if residual is None:
506 residual = hidden_states
507 hidden_states = self.input_layernorm(hidden_states)
508 else:
509 hidden_states, residual = self.input_layernorm(hidden_states, residual)
510
511 + if input_is_sequence_parallel:
512 + hidden_states = tensor_model_parallel_all_gather(hidden_states, 0)
513 + hidden_states = hidden_states[:full_num_tokens]
514 +
515 if self.layer_type == "linear_attention":
516 hidden_states = self.linear_attn(hidden_states=hidden_states)
517 elif self.layer_type == "full_attention":
@@ -504,9 +532,25 @@ class Qwen3NextDecoderLayer(nn.Module):
532 self.attn_layer_scale.to(hidden_states.dtype) + 1
533 )
534
535 + if self.use_attn_reduce_scatter_for_moe:
536 + tp_world_size = get_tensor_model_parallel_world_size()
537 + # small trick using minus, eg. -17 % 8 = 7
538 + sp_pad = (-hidden_states.shape[0]) % tp_world_size
539 + # pad if not divisible by world size
540 + hidden_states = torch.nn.functional.pad(hidden_states, (0, 0, 0, sp_pad))
541 + hidden_states = tensor_model_parallel_reduce_scatter(hidden_states, 0)
542 + if not input_is_sequence_parallel:
543 + residual = sequence_parallel_chunk(residual)
544 +
545 # Fully Connected
546 hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
509 - hidden_states = self.mlp(hidden_states)
547 + if self.use_attn_reduce_scatter_for_moe:
548 + hidden_states = self.mlp(
549 + hidden_states,
550 + already_sequence_parallel=True,
551 + )
552 + else:
553 + hidden_states = self.mlp(hidden_states)
554
555 if self.layer_scale:
556 if len(hidden_states.shape) == 2:
@@ -525,6 +569,24 @@ class Qwen3NextDecoderLayer(nn.Module):
569 return hidden_states, residual
570
571
572 +def _all_gather_hidden_and_residual(
573 + hidden_states: torch.Tensor,
574 + residual: torch.Tensor | None,
575 + full_num_tokens: int,
576 + hidden_size: int,
577 +) -> tuple[torch.Tensor, torch.Tensor | None]:
578 + if residual is None:
579 + hidden_states = tensor_model_parallel_all_gather(hidden_states, 0)
580 + hidden_states = hidden_states[:full_num_tokens]
581 + return hidden_states, None
582 +
583 + combined_states = torch.cat([hidden_states, residual], dim=-1)
584 + combined_states = tensor_model_parallel_all_gather(combined_states, 0)
585 + combined_states = combined_states[:full_num_tokens]
586 + hidden_states, residual = combined_states.split([hidden_size, hidden_size], dim=-1)
587 + return hidden_states, residual
588 +
589 +
590 @support_torch_compile
591 class Qwen3NextModel(nn.Module, EagleModelMixin):
592 hf_to_vllm_mapper = WeightsMapper(
@@ -577,6 +639,8 @@ class Qwen3NextModel(nn.Module, EagleModelMixin):
639 else:
640 self.norm = PPMissingLayer()
641
642 + self.aux_hidden_state_layers: tuple[int, ...] = ()
643 +
644 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
645 return self.embed_tokens(input_ids)
646
@@ -598,16 +662,36 @@ class Qwen3NextModel(nn.Module, EagleModelMixin):
662 hidden_states = intermediate_tensors["hidden_states"]
663 residual = intermediate_tensors["residual"]
664
665 + full_num_tokens = positions.shape[-1]
666 aux_hidden_states = self._maybe_add_hidden_state([], 0, hidden_states, residual)
667 for layer_idx, layer in enumerate(
668 islice(self.layers, self.start_layer, self.end_layer),
669 start=self.start_layer,
670 ):
671 + if (
672 + hidden_states.shape[0] != full_num_tokens
673 + and not layer.use_attn_reduce_scatter_for_moe
674 + ):
675 + hidden_states, residual = _all_gather_hidden_and_residual(
676 + hidden_states,
677 + residual,
678 + full_num_tokens,
679 + self.config.hidden_size,
680 + )
681 hidden_states, residual = layer(
682 positions=positions,
683 hidden_states=hidden_states,
684 residual=residual,
685 )
686 + if (layer_idx + 1) in self.aux_hidden_state_layers and hidden_states.shape[
687 + 0
688 + ] != full_num_tokens:
689 + hidden_states, residual = _all_gather_hidden_and_residual(
690 + hidden_states,
691 + residual,
692 + full_num_tokens,
693 + self.config.hidden_size,
694 + )
695 self._maybe_add_hidden_state(
696 aux_hidden_states, layer_idx + 1, hidden_states, residual
697 )
@@ -616,6 +700,13 @@ class Qwen3NextModel(nn.Module, EagleModelMixin):
700 return IntermediateTensors(
701 {"hidden_states": hidden_states, "residual": residual}
702 )
703 + if hidden_states.shape[0] != full_num_tokens:
704 + hidden_states, residual = _all_gather_hidden_and_residual(
705 + hidden_states,
706 + residual,
707 + full_num_tokens,
708 + self.config.hidden_size,
709 + )
710 hidden_states, _ = self.norm(hidden_states, residual)
711 if aux_hidden_states:
712 return hidden_states, aux_hidden_states