[NVIDIA][comm] Merge EP+MoE-TP post-experts all-reduces into one _TP reduction (#32963)
This commit is contained in:
@@ -24,7 +24,6 @@ from sglang.srt.distributed import (
|
|||||||
attention_tensor_model_parallel_all_reduce,
|
attention_tensor_model_parallel_all_reduce,
|
||||||
attention_tensor_model_parallel_quant_all_reduce,
|
attention_tensor_model_parallel_quant_all_reduce,
|
||||||
get_tp_group,
|
get_tp_group,
|
||||||
moe_tensor_model_parallel_all_reduce,
|
|
||||||
tensor_model_parallel_all_reduce,
|
tensor_model_parallel_all_reduce,
|
||||||
)
|
)
|
||||||
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
||||||
@@ -60,6 +59,8 @@ from sglang.srt.layers.dp_attention import (
|
|||||||
)
|
)
|
||||||
from sglang.srt.layers.flashinfer_comm_fusion import is_flashinfer_allreduce_unavailable
|
from sglang.srt.layers.flashinfer_comm_fusion import is_flashinfer_allreduce_unavailable
|
||||||
from sglang.srt.layers.moe import (
|
from sglang.srt.layers.moe import (
|
||||||
|
can_merge_post_experts_all_reduce,
|
||||||
|
deferred_post_experts_all_reduce,
|
||||||
get_moe_a2a_backend,
|
get_moe_a2a_backend,
|
||||||
should_use_dp_reduce_scatterv,
|
should_use_dp_reduce_scatterv,
|
||||||
should_use_flashinfer_cutlass_moe_fp4_allgather,
|
should_use_flashinfer_cutlass_moe_fp4_allgather,
|
||||||
@@ -724,7 +725,9 @@ class LayerCommunicator:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
hidden_states = moe_tensor_model_parallel_all_reduce(hidden_states)
|
# Fusion was published but this shape can't use the kernel,
|
||||||
|
# so run the deferred reduction inline.
|
||||||
|
hidden_states = deferred_post_experts_all_reduce(hidden_states)
|
||||||
hidden_states, residual = self.input_layernorm(
|
hidden_states, residual = self.input_layernorm(
|
||||||
hidden_states, residual
|
hidden_states, residual
|
||||||
)
|
)
|
||||||
@@ -938,16 +941,16 @@ class LayerCommunicator:
|
|||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Fusing makes the next layer's residual+LN absorb the post-experts
|
# The fused residual+LN reduces over a single group. Hybrid EP+TP spans
|
||||||
# all-reduce, and that fused kernel reduces over a single group. Under
|
# two disjoint groups; post_experts_all_reduce() merges them into one
|
||||||
# hybrid EP+TP the post-experts reduction spans two disjoint groups
|
# _TP reduction when moe_dp_size == 1, which the fused kernel can absorb.
|
||||||
# (moe_expert_parallel_all_reduce over _MOE_EP, then
|
# When merging is blocked, no single group covers both, so fusion stays off.
|
||||||
# moe_tensor_model_parallel_all_reduce over _MOE_TP), and
|
|
||||||
# should_skip_post_experts_all_reduce() skips *both* once fusion is
|
|
||||||
# published -- so the fused reduce would cover only half the peers and
|
|
||||||
# silently return under-reduced activations.
|
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
if parallel.moe_ep_size > 1 and parallel.moe_tp_size > 1:
|
if (
|
||||||
|
parallel.moe_ep_size > 1
|
||||||
|
and parallel.moe_tp_size > 1
|
||||||
|
and not can_merge_post_experts_all_reduce()
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -647,6 +647,37 @@ def _get_workspace_manager(use_attn_tp_group: bool) -> FlashInferWorkspaceManage
|
|||||||
return manager
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_fusion_world_size(*, use_attn_tp_group: bool) -> int:
|
||||||
|
"""Peer count of the fusion group. Reads sizes only -- deliberately does not
|
||||||
|
reach for a group coordinator, which some callers hit before one exists."""
|
||||||
|
from sglang.srt.layers.moe.utils import can_merge_post_experts_all_reduce
|
||||||
|
|
||||||
|
parallel = get_parallel()
|
||||||
|
if use_attn_tp_group:
|
||||||
|
return parallel.attn_tp_size
|
||||||
|
if can_merge_post_experts_all_reduce():
|
||||||
|
return parallel.tp_size
|
||||||
|
return parallel.moe_ep_size if parallel.moe_ep_size > 1 else parallel.moe_tp_size
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_fusion_group(*, use_attn_tp_group: bool):
|
||||||
|
"""Return (world_size, rank, coordinator) for the fusion workspace.
|
||||||
|
|
||||||
|
Must match the group the fused residual+LN kernel reduces over; a mismatch
|
||||||
|
silently reduces across the wrong peers.
|
||||||
|
"""
|
||||||
|
from sglang.srt.layers.moe.utils import can_merge_post_experts_all_reduce
|
||||||
|
|
||||||
|
parallel = get_parallel()
|
||||||
|
if use_attn_tp_group:
|
||||||
|
return parallel.attn_tp_size, parallel.attn_tp_rank, get_attn_tp_group()
|
||||||
|
if can_merge_post_experts_all_reduce():
|
||||||
|
return parallel.tp_size, parallel.tp_rank, get_tp_group()
|
||||||
|
if parallel.moe_ep_size > 1:
|
||||||
|
return parallel.moe_ep_size, parallel.moe_ep_rank, get_moe_ep_group()
|
||||||
|
return parallel.moe_tp_size, parallel.moe_tp_rank, get_moe_tp_group()
|
||||||
|
|
||||||
|
|
||||||
def _sync_allreduce_unavailable_across_tp():
|
def _sync_allreduce_unavailable_across_tp():
|
||||||
"""Synchronize _flashinfer_allreduce_unavailable across all TP ranks.
|
"""Synchronize _flashinfer_allreduce_unavailable across all TP ranks.
|
||||||
|
|
||||||
@@ -694,19 +725,9 @@ def ensure_workspace_initialized(
|
|||||||
if not is_flashinfer_available() or _flashinfer_comm is None:
|
if not is_flashinfer_available() or _flashinfer_comm is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if use_attn_tp_group:
|
world_size, rank, coordinator = resolve_fusion_group(
|
||||||
world_size = get_parallel().attn_tp_size
|
use_attn_tp_group=use_attn_tp_group
|
||||||
rank = get_parallel().attn_tp_rank
|
)
|
||||||
coordinator = get_attn_tp_group()
|
|
||||||
else:
|
|
||||||
if get_parallel().moe_ep_size > 1:
|
|
||||||
world_size = get_parallel().moe_ep_size
|
|
||||||
rank = get_parallel().moe_ep_rank
|
|
||||||
coordinator = get_moe_ep_group()
|
|
||||||
else:
|
|
||||||
world_size = get_parallel().moe_tp_size
|
|
||||||
rank = get_parallel().moe_tp_rank
|
|
||||||
coordinator = get_moe_tp_group()
|
|
||||||
|
|
||||||
# Always pass the coordinator's groups: flashinfer >=0.6.10 reads the
|
# Always pass the coordinator's groups: flashinfer >=0.6.10 reads the
|
||||||
# rendezvous group from `group=...` (falling back to WORLD when None),
|
# rendezvous group from `group=...` (falling back to WORLD when None),
|
||||||
@@ -814,13 +835,7 @@ def flashinfer_allreduce_residual_rmsnorm(
|
|||||||
)
|
)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
if use_attn_tp_group:
|
world_size = resolve_fusion_world_size(use_attn_tp_group=use_attn_tp_group)
|
||||||
world_size = get_parallel().attn_tp_size
|
|
||||||
else:
|
|
||||||
if get_parallel().moe_ep_size > 1:
|
|
||||||
world_size = get_parallel().moe_ep_size
|
|
||||||
else:
|
|
||||||
world_size = get_parallel().moe_tp_size
|
|
||||||
|
|
||||||
if world_size <= 1:
|
if world_size <= 1:
|
||||||
logger.debug("Single GPU, no need for allreduce fusion")
|
logger.debug("Single GPU, no need for allreduce fusion")
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from sglang.srt.layers.moe.utils import (
|
|||||||
DeepEPMode,
|
DeepEPMode,
|
||||||
MoeA2ABackend,
|
MoeA2ABackend,
|
||||||
MoeRunnerBackend,
|
MoeRunnerBackend,
|
||||||
|
can_merge_post_experts_all_reduce,
|
||||||
|
deferred_post_experts_all_reduce,
|
||||||
get_deepep_config,
|
get_deepep_config,
|
||||||
get_deepep_mode,
|
get_deepep_mode,
|
||||||
get_moe_a2a_backend,
|
get_moe_a2a_backend,
|
||||||
@@ -11,6 +13,7 @@ from sglang.srt.layers.moe.utils import (
|
|||||||
initialize_moe_config,
|
initialize_moe_config,
|
||||||
is_moe_input_scattered_across_dp_ranks,
|
is_moe_input_scattered_across_dp_ranks,
|
||||||
is_tbo_enabled,
|
is_tbo_enabled,
|
||||||
|
post_experts_all_reduce,
|
||||||
should_skip_mlp_all_reduce,
|
should_skip_mlp_all_reduce,
|
||||||
should_skip_post_experts_all_reduce,
|
should_skip_post_experts_all_reduce,
|
||||||
should_use_dp_reduce_scatterv,
|
should_use_dp_reduce_scatterv,
|
||||||
@@ -28,6 +31,9 @@ __all__ = [
|
|||||||
"get_moe_runner_backend",
|
"get_moe_runner_backend",
|
||||||
"get_deepep_mode",
|
"get_deepep_mode",
|
||||||
"should_skip_mlp_all_reduce",
|
"should_skip_mlp_all_reduce",
|
||||||
|
"can_merge_post_experts_all_reduce",
|
||||||
|
"deferred_post_experts_all_reduce",
|
||||||
|
"post_experts_all_reduce",
|
||||||
"should_skip_post_experts_all_reduce",
|
"should_skip_post_experts_all_reduce",
|
||||||
"should_use_dp_reduce_scatterv",
|
"should_use_dp_reduce_scatterv",
|
||||||
"should_use_flashinfer_cutlass_moe_fp4_allgather",
|
"should_use_flashinfer_cutlass_moe_fp4_allgather",
|
||||||
|
|||||||
@@ -731,30 +731,9 @@ def should_skip_mlp_all_reduce() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def should_skip_post_experts_all_reduce(*, is_tp_path: bool) -> bool:
|
def should_skip_post_experts_all_reduce(*, is_tp_path: bool) -> bool:
|
||||||
"""Whether to skip the post-experts all-reduce (EP or TP) because a
|
"""Whether a downstream component will fuse, replace, or absorb the post-experts all-reduce.
|
||||||
downstream component will fuse, replace, or absorb it.
|
|
||||||
|
|
||||||
Skip reasons, in order:
|
Pass ``is_tp_path=True`` for the TP all-reduce, ``False`` for the EP one.
|
||||||
- ``get_forward().fuse_mlp_allreduce``: LayerCommunicator will fuse the
|
|
||||||
all-reduce with the next layer's residual all-reduce.
|
|
||||||
- ``get_forward().mlp_reduce_scatter``: LayerCommunicator's post-attention
|
|
||||||
scatter will do reduce-scatter, which would double-reduce on top of
|
|
||||||
an all-reduce.
|
|
||||||
- ``should_use_dp_reduce_scatterv()``: the standard dispatcher's combine
|
|
||||||
path replaces the all-reduce with a reduce-scatterv.
|
|
||||||
- ``should_use_flashinfer_cutlass_moe_fp4_allgather()`` (TP path only):
|
|
||||||
the flashinfer cutlass FP4 kernel performs an all-gather that absorbs
|
|
||||||
the post-experts TP all-reduce. Not relevant to the EP all-reduce.
|
|
||||||
- ``get_moe_a2a_backend().is_flashinfer()``: the flashinfer A2A
|
|
||||||
dispatcher's ``MoeAlltoAll.combine`` already alltoall-reduces partial
|
|
||||||
MoE outputs back to the source rank, so any further EP/TP all-reduce
|
|
||||||
would double-count and overflow BF16. Mirrors TRTLLM's
|
|
||||||
``not enable_alltoall`` gate
|
|
||||||
(``tensorrt_llm/_torch/modules/fused_moe/interface.py:879``).
|
|
||||||
|
|
||||||
The first two reasons come from per-layer ``ForwardFlags`` published by
|
|
||||||
the decoder via ``get_forward().scoped(...)``. Pass ``is_tp_path=True``
|
|
||||||
for the post-experts TP all-reduce, ``False`` for the EP all-reduce.
|
|
||||||
"""
|
"""
|
||||||
if should_skip_mlp_all_reduce():
|
if should_skip_mlp_all_reduce():
|
||||||
return True
|
return True
|
||||||
@@ -778,6 +757,70 @@ def should_skip_post_experts_all_reduce(*, is_tp_path: bool) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def can_merge_post_experts_all_reduce() -> bool:
|
||||||
|
"""Whether the EP and MoE-TP reductions can collapse into one _TP all-reduce.
|
||||||
|
|
||||||
|
True when moe_dp_size == 1: the two groups are an orthogonal decomposition
|
||||||
|
of _TP, so reducing over each in turn equals one _TP reduction.
|
||||||
|
"""
|
||||||
|
parallel = get_parallel()
|
||||||
|
return (
|
||||||
|
parallel.moe_ep_size > 1
|
||||||
|
and parallel.moe_tp_size > 1
|
||||||
|
and parallel.moe_dp_size == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def post_experts_all_reduce(hidden_states: torch.Tensor) -> torch.Tensor:
|
||||||
|
"""Reduce the post-experts MoE output across the EP and MoE-TP groups.
|
||||||
|
|
||||||
|
When both are live and mergeable, issues one _TP all-reduce instead of two
|
||||||
|
sequential ones, which also restores the invariant the fused residual+LN path
|
||||||
|
depends on.
|
||||||
|
"""
|
||||||
|
from sglang.srt.distributed.communication_op import (
|
||||||
|
moe_expert_parallel_all_reduce,
|
||||||
|
moe_tensor_model_parallel_all_reduce,
|
||||||
|
tensor_model_parallel_all_reduce,
|
||||||
|
)
|
||||||
|
|
||||||
|
parallel = get_parallel()
|
||||||
|
reduce_ep = parallel.moe_ep_size > 1 and not should_skip_post_experts_all_reduce(
|
||||||
|
is_tp_path=False
|
||||||
|
)
|
||||||
|
reduce_tp = parallel.moe_tp_size > 1 and not should_skip_post_experts_all_reduce(
|
||||||
|
is_tp_path=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if reduce_ep and reduce_tp and can_merge_post_experts_all_reduce():
|
||||||
|
return tensor_model_parallel_all_reduce(hidden_states)
|
||||||
|
|
||||||
|
if reduce_ep:
|
||||||
|
hidden_states = moe_expert_parallel_all_reduce(hidden_states)
|
||||||
|
if reduce_tp:
|
||||||
|
hidden_states = moe_tensor_model_parallel_all_reduce(hidden_states)
|
||||||
|
return hidden_states
|
||||||
|
|
||||||
|
|
||||||
|
def deferred_post_experts_all_reduce(hidden_states: torch.Tensor) -> torch.Tensor:
|
||||||
|
"""Run the post-experts reduction that was deferred to allreduce fusion.
|
||||||
|
|
||||||
|
Called when the fused residual+LN kernel cannot service the shape. Reduces
|
||||||
|
over the same group ``resolve_fusion_group`` builds the workspace on.
|
||||||
|
"""
|
||||||
|
from sglang.srt.distributed.communication_op import (
|
||||||
|
moe_expert_parallel_all_reduce,
|
||||||
|
moe_tensor_model_parallel_all_reduce,
|
||||||
|
tensor_model_parallel_all_reduce,
|
||||||
|
)
|
||||||
|
|
||||||
|
if can_merge_post_experts_all_reduce():
|
||||||
|
return tensor_model_parallel_all_reduce(hidden_states)
|
||||||
|
if get_parallel().moe_ep_size > 1:
|
||||||
|
return moe_expert_parallel_all_reduce(hidden_states)
|
||||||
|
return moe_tensor_model_parallel_all_reduce(hidden_states)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def speculative_moe_backend_context():
|
def speculative_moe_backend_context():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -51,11 +51,7 @@ from sglang.srt.configs.model_config import (
|
|||||||
is_deepseek_dsa,
|
is_deepseek_dsa,
|
||||||
is_glm_moe_dsa,
|
is_glm_moe_dsa,
|
||||||
)
|
)
|
||||||
from sglang.srt.distributed import (
|
from sglang.srt.distributed import divide, get_pp_group
|
||||||
divide,
|
|
||||||
get_pp_group,
|
|
||||||
tensor_model_parallel_all_reduce,
|
|
||||||
)
|
|
||||||
from sglang.srt.environ import envs
|
from sglang.srt.environ import envs
|
||||||
from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
|
from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
|
||||||
from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
|
from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
|
||||||
@@ -95,7 +91,7 @@ from sglang.srt.layers.logits_processor import LogitsProcessor
|
|||||||
from sglang.srt.layers.moe import (
|
from sglang.srt.layers.moe import (
|
||||||
get_moe_a2a_backend,
|
get_moe_a2a_backend,
|
||||||
get_moe_runner_backend,
|
get_moe_runner_backend,
|
||||||
should_skip_post_experts_all_reduce,
|
post_experts_all_reduce,
|
||||||
should_use_flashinfer_cutlass_moe_fp4_allgather,
|
should_use_flashinfer_cutlass_moe_fp4_allgather,
|
||||||
)
|
)
|
||||||
from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class
|
from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class
|
||||||
@@ -1037,10 +1033,7 @@ class DeepseekV2MoE(nn.Module):
|
|||||||
self.routed_scaling_factor,
|
self.routed_scaling_factor,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.tp_size > 1 and not should_skip_post_experts_all_reduce(
|
final_hidden_states = post_experts_all_reduce(final_hidden_states)
|
||||||
is_tp_path=True,
|
|
||||||
):
|
|
||||||
final_hidden_states = tensor_model_parallel_all_reduce(final_hidden_states)
|
|
||||||
# TP1 shared experts are replicated, so add them after all-reduce to
|
# TP1 shared experts are replicated, so add them after all-reduce to
|
||||||
# avoid summing the same shared output once per TP rank.
|
# avoid summing the same shared output once per TP rank.
|
||||||
if self._shared_expert_tp1:
|
if self._shared_expert_tp1:
|
||||||
@@ -1179,10 +1172,7 @@ class DeepseekV2MoE(nn.Module):
|
|||||||
self.routed_scaling_factor,
|
self.routed_scaling_factor,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.tp_size > 1 and not should_skip_post_experts_all_reduce(
|
final_hidden_states = post_experts_all_reduce(final_hidden_states)
|
||||||
is_tp_path=True,
|
|
||||||
):
|
|
||||||
final_hidden_states = tensor_model_parallel_all_reduce(final_hidden_states)
|
|
||||||
# TP1 shared experts are replicated, so add them after all-reduce to
|
# TP1 shared experts are replicated, so add them after all-reduce to
|
||||||
# avoid summing the same shared output once per TP rank.
|
# avoid summing the same shared output once per TP rank.
|
||||||
if shared_output is not None and self._shared_expert_tp1:
|
if shared_output is not None and self._shared_expert_tp1:
|
||||||
@@ -1240,10 +1230,7 @@ class DeepseekV2MoE(nn.Module):
|
|||||||
), # block_size
|
), # block_size
|
||||||
True, # is_vnni
|
True, # is_vnni
|
||||||
)
|
)
|
||||||
if self.tp_size > 1 and not should_skip_post_experts_all_reduce(
|
final_hidden_states = post_experts_all_reduce(final_hidden_states)
|
||||||
is_tp_path=True,
|
|
||||||
):
|
|
||||||
final_hidden_states = tensor_model_parallel_all_reduce(final_hidden_states)
|
|
||||||
return final_hidden_states
|
return final_hidden_states
|
||||||
|
|
||||||
def forward_deepep(
|
def forward_deepep(
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ import torch
|
|||||||
from torch import nn
|
from torch import nn
|
||||||
from transformers import PretrainedConfig
|
from transformers import PretrainedConfig
|
||||||
|
|
||||||
from sglang.srt.distributed import (
|
|
||||||
moe_expert_parallel_all_reduce,
|
|
||||||
moe_tensor_model_parallel_all_reduce,
|
|
||||||
)
|
|
||||||
from sglang.srt.layers.activation import SiluAndMul
|
from sglang.srt.layers.activation import SiluAndMul
|
||||||
from sglang.srt.layers.layernorm import RMSNorm
|
from sglang.srt.layers.layernorm import RMSNorm
|
||||||
from sglang.srt.layers.linear import (
|
from sglang.srt.layers.linear import (
|
||||||
@@ -31,7 +27,7 @@ from sglang.srt.layers.linear import (
|
|||||||
RowParallelLinear,
|
RowParallelLinear,
|
||||||
)
|
)
|
||||||
from sglang.srt.layers.logits_processor import LogitsProcessor
|
from sglang.srt.layers.logits_processor import LogitsProcessor
|
||||||
from sglang.srt.layers.moe import should_skip_post_experts_all_reduce
|
from sglang.srt.layers.moe import post_experts_all_reduce
|
||||||
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
||||||
from sglang.srt.layers.moe.topk import TopK
|
from sglang.srt.layers.moe.topk import TopK
|
||||||
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
||||||
@@ -191,17 +187,7 @@ class HYV3MoEFused(nn.Module):
|
|||||||
hidden_states=hidden_states, topk_output=topk_output
|
hidden_states=hidden_states, topk_output=topk_output
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.ep_size > 1 and not should_skip_post_experts_all_reduce(
|
final_hidden_states = post_experts_all_reduce(final_hidden_states)
|
||||||
is_tp_path=False,
|
|
||||||
):
|
|
||||||
final_hidden_states = moe_expert_parallel_all_reduce(final_hidden_states)
|
|
||||||
|
|
||||||
if self.tp_size > 1 and not should_skip_post_experts_all_reduce(
|
|
||||||
is_tp_path=True,
|
|
||||||
):
|
|
||||||
final_hidden_states = moe_tensor_model_parallel_all_reduce(
|
|
||||||
final_hidden_states
|
|
||||||
)
|
|
||||||
|
|
||||||
return final_hidden_states.view(orig_shape)
|
return final_hidden_states.view(orig_shape)
|
||||||
|
|
||||||
@@ -226,17 +212,7 @@ class HYV3MoEFused(nn.Module):
|
|||||||
current_stream.wait_stream(self.alt_stream)
|
current_stream.wait_stream(self.alt_stream)
|
||||||
final_hidden_states = final_hidden_states + shared_output
|
final_hidden_states = final_hidden_states + shared_output
|
||||||
|
|
||||||
if self.ep_size > 1 and not should_skip_post_experts_all_reduce(
|
final_hidden_states = post_experts_all_reduce(final_hidden_states)
|
||||||
is_tp_path=False,
|
|
||||||
):
|
|
||||||
final_hidden_states = moe_expert_parallel_all_reduce(final_hidden_states)
|
|
||||||
|
|
||||||
if self.tp_size > 1 and not should_skip_post_experts_all_reduce(
|
|
||||||
is_tp_path=True,
|
|
||||||
):
|
|
||||||
final_hidden_states = moe_tensor_model_parallel_all_reduce(
|
|
||||||
final_hidden_states
|
|
||||||
)
|
|
||||||
|
|
||||||
return final_hidden_states.view(orig_shape)
|
return final_hidden_states.view(orig_shape)
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ from sglang.srt.layers.linear import (
|
|||||||
)
|
)
|
||||||
from sglang.srt.layers.logits_processor import LogitsProcessor
|
from sglang.srt.layers.logits_processor import LogitsProcessor
|
||||||
from sglang.srt.layers.moe import (
|
from sglang.srt.layers.moe import (
|
||||||
|
can_merge_post_experts_all_reduce,
|
||||||
get_moe_a2a_backend,
|
get_moe_a2a_backend,
|
||||||
should_skip_post_experts_all_reduce,
|
should_skip_post_experts_all_reduce,
|
||||||
)
|
)
|
||||||
@@ -1161,10 +1162,20 @@ class Qwen2MoeModel(nn.Module):
|
|||||||
and hasattr(hidden_states, "_sglang_needs_allreduce_fusion")
|
and hasattr(hidden_states, "_sglang_needs_allreduce_fusion")
|
||||||
and hidden_states._sglang_needs_allreduce_fusion
|
and hidden_states._sglang_needs_allreduce_fusion
|
||||||
):
|
):
|
||||||
|
# The deferred reduction the next layer would have fused; no
|
||||||
|
# layer follows on this rank, so run it here. Unconditional --
|
||||||
|
# the skip flags that deferred it are what got us into this
|
||||||
|
# branch -- so it bypasses post_experts_all_reduce()'s guards
|
||||||
|
# while reusing its merge rule.
|
||||||
|
if can_merge_post_experts_all_reduce():
|
||||||
|
hidden_states = tensor_model_parallel_all_reduce(hidden_states)
|
||||||
|
else:
|
||||||
if get_parallel().moe_ep_size > 1:
|
if get_parallel().moe_ep_size > 1:
|
||||||
hidden_states = moe_expert_parallel_all_reduce(hidden_states)
|
hidden_states = moe_expert_parallel_all_reduce(hidden_states)
|
||||||
if get_parallel().moe_tp_size > 1:
|
if get_parallel().moe_tp_size > 1:
|
||||||
hidden_states = moe_tensor_model_parallel_all_reduce(hidden_states)
|
hidden_states = moe_tensor_model_parallel_all_reduce(
|
||||||
|
hidden_states
|
||||||
|
)
|
||||||
hidden_states._sglang_needs_allreduce_fusion = False
|
hidden_states._sglang_needs_allreduce_fusion = False
|
||||||
return PPProxyTensors(
|
return PPProxyTensors(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ from transformers import PretrainedConfig
|
|||||||
|
|
||||||
from sglang.srt.distributed import (
|
from sglang.srt.distributed import (
|
||||||
get_pp_group,
|
get_pp_group,
|
||||||
moe_expert_parallel_all_reduce,
|
|
||||||
moe_tensor_model_parallel_all_reduce,
|
|
||||||
)
|
)
|
||||||
from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
|
from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
|
||||||
from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
|
from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
|
||||||
@@ -44,7 +42,7 @@ from sglang.srt.layers.linear import (
|
|||||||
from sglang.srt.layers.logits_processor import LogitsProcessor
|
from sglang.srt.layers.logits_processor import LogitsProcessor
|
||||||
from sglang.srt.layers.moe import (
|
from sglang.srt.layers.moe import (
|
||||||
get_moe_a2a_backend,
|
get_moe_a2a_backend,
|
||||||
should_skip_post_experts_all_reduce,
|
post_experts_all_reduce,
|
||||||
)
|
)
|
||||||
from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class
|
from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class
|
||||||
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
||||||
@@ -335,17 +333,7 @@ class Qwen3MoeSparseMoeBlock(nn.Module):
|
|||||||
topk_output = self.topk.empty_topk_output(hidden_states.device)
|
topk_output = self.topk.empty_topk_output(hidden_states.device)
|
||||||
final_hidden_states = self.experts(hidden_states, topk_output)
|
final_hidden_states = self.experts(hidden_states, topk_output)
|
||||||
|
|
||||||
if self.ep_size > 1 and not should_skip_post_experts_all_reduce(
|
final_hidden_states = post_experts_all_reduce(final_hidden_states)
|
||||||
is_tp_path=False
|
|
||||||
):
|
|
||||||
final_hidden_states = moe_expert_parallel_all_reduce(final_hidden_states)
|
|
||||||
|
|
||||||
if self.tp_size > 1 and not should_skip_post_experts_all_reduce(
|
|
||||||
is_tp_path=True
|
|
||||||
):
|
|
||||||
final_hidden_states = moe_tensor_model_parallel_all_reduce(
|
|
||||||
final_hidden_states
|
|
||||||
)
|
|
||||||
|
|
||||||
return final_hidden_states.view(num_tokens, hidden_dim)
|
return final_hidden_states.view(num_tokens, hidden_dim)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
|
import contextlib
|
||||||
import types
|
import types
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
from sglang.srt.layers import communicator as comm
|
from sglang.srt.layers import communicator as comm
|
||||||
from sglang.srt.layers.communicator import LayerCommunicator, ScatterMode
|
from sglang.srt.layers.communicator import LayerCommunicator, ScatterMode
|
||||||
|
from sglang.srt.layers.moe import (
|
||||||
|
can_merge_post_experts_all_reduce,
|
||||||
|
deferred_post_experts_all_reduce,
|
||||||
|
post_experts_all_reduce,
|
||||||
|
)
|
||||||
|
from sglang.srt.layers.moe import utils as moe_utils
|
||||||
from sglang.srt.runtime_context import get_parallel
|
from sglang.srt.runtime_context import get_parallel
|
||||||
from sglang.test.ci.ci_register import register_cpu_ci
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
from sglang.test.test_utils import CustomTestCase
|
from sglang.test.test_utils import CustomTestCase
|
||||||
@@ -20,19 +29,218 @@ def _fake_communicator(mlp_mode=ScatterMode.TP_ATTN_FULL):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestFuseMlpAllReduceGate(CustomTestCase):
|
@contextlib.contextmanager
|
||||||
"""Hybrid EP+TP must not fuse the post-experts all-reduce away.
|
def _recorded_all_reduces(called, *, moe_ep_size, moe_tp_size, moe_dp_size):
|
||||||
|
"""Log which group each all-reduce helper reduces over, under a fixed topo."""
|
||||||
|
|
||||||
The fused residual+LN reduces over a single group, but with moe_ep_size > 1
|
def record(name):
|
||||||
and moe_tp_size > 1 the post-experts reduction spans two disjoint groups
|
return lambda x: called.append(name) or x
|
||||||
(_MOE_EP then _MOE_TP) and should_skip_post_experts_all_reduce() drops both
|
|
||||||
once fusion is published. The result is activations reduced over only half
|
with (
|
||||||
the peers -- wrong output, no crash. Observed as garbage completions on
|
patch(
|
||||||
Qwen3-30B-A3B with --tp-size 4 --ep-size 2.
|
"sglang.srt.distributed.communication_op.tensor_model_parallel_all_reduce",
|
||||||
|
side_effect=record("tp"),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"sglang.srt.distributed.communication_op.moe_expert_parallel_all_reduce",
|
||||||
|
side_effect=record("ep"),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"sglang.srt.distributed.communication_op.moe_tensor_model_parallel_all_reduce",
|
||||||
|
side_effect=record("moe_tp"),
|
||||||
|
),
|
||||||
|
get_parallel().override(
|
||||||
|
moe_ep_size=moe_ep_size,
|
||||||
|
moe_tp_size=moe_tp_size,
|
||||||
|
moe_dp_size=moe_dp_size,
|
||||||
|
tp_size=moe_ep_size * moe_tp_size * moe_dp_size,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostExpertsAllReduceMerge(CustomTestCase):
|
||||||
|
"""The two post-experts reductions collapse into one _TP reduction.
|
||||||
|
|
||||||
|
_MOE_EP and _MOE_TP are orthogonal subgroups of _TP, so with
|
||||||
|
moe_dp_size == 1 reducing over each in turn equals one _TP reduction --
|
||||||
|
one collective instead of two. With moe_dp_size > 1 they cover only part of
|
||||||
|
_TP and merging would sum across DP replicas, which hold different tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _calls(self, *, moe_ep_size, moe_tp_size, moe_dp_size=1, skip=False):
|
||||||
|
called = []
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
moe_utils, "should_skip_post_experts_all_reduce", return_value=skip
|
||||||
|
),
|
||||||
|
_recorded_all_reduces(
|
||||||
|
called,
|
||||||
|
moe_ep_size=moe_ep_size,
|
||||||
|
moe_tp_size=moe_tp_size,
|
||||||
|
moe_dp_size=moe_dp_size,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
post_experts_all_reduce(torch.zeros(2, 2))
|
||||||
|
return called
|
||||||
|
|
||||||
|
def test_hybrid_issues_one_tp_reduction(self):
|
||||||
|
self.assertEqual(self._calls(moe_ep_size=2, moe_tp_size=2), ["tp"])
|
||||||
|
|
||||||
|
def test_moe_dp_keeps_the_two_step_form(self):
|
||||||
|
# Server args reject moe_ep_size > 1 together with moe_tp_size > 1 and
|
||||||
|
# moe_dp_size > 1 (they force ep_size * moe_dp_size == tp_size), so this
|
||||||
|
# pins the guard rather than a topology that can be launched today.
|
||||||
|
self.assertEqual(
|
||||||
|
self._calls(moe_ep_size=2, moe_tp_size=2, moe_dp_size=2), ["ep", "moe_tp"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_dimension_issues_one_reduction(self):
|
||||||
|
self.assertEqual(self._calls(moe_ep_size=1, moe_tp_size=4), ["moe_tp"])
|
||||||
|
self.assertEqual(self._calls(moe_ep_size=4, moe_tp_size=1), ["ep"])
|
||||||
|
|
||||||
|
def test_skipped_when_deferred_to_fusion(self):
|
||||||
|
self.assertEqual(self._calls(moe_ep_size=2, moe_tp_size=2, skip=True), [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeferredPostExpertsAllReduce(CustomTestCase):
|
||||||
|
"""The inline fallback must reduce over the same peers the fused kernel would.
|
||||||
|
|
||||||
|
_MOE_TP holds a single rank under pure EP, so reducing over it there is a
|
||||||
|
no-op that drops the deferred reduction instead of performing it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _calls(self, *, moe_ep_size, moe_tp_size, moe_dp_size=1):
|
||||||
|
called = []
|
||||||
|
with _recorded_all_reduces(
|
||||||
|
called,
|
||||||
|
moe_ep_size=moe_ep_size,
|
||||||
|
moe_tp_size=moe_tp_size,
|
||||||
|
moe_dp_size=moe_dp_size,
|
||||||
|
):
|
||||||
|
deferred_post_experts_all_reduce(torch.zeros(2, 2))
|
||||||
|
return called
|
||||||
|
|
||||||
|
def test_hybrid_reduces_over_tp(self):
|
||||||
|
self.assertEqual(self._calls(moe_ep_size=2, moe_tp_size=2), ["tp"])
|
||||||
|
|
||||||
|
def test_pure_ep_reduces_over_ep(self):
|
||||||
|
self.assertEqual(self._calls(moe_ep_size=4, moe_tp_size=1), ["ep"])
|
||||||
|
|
||||||
|
def test_pure_tp_reduces_over_moe_tp(self):
|
||||||
|
self.assertEqual(self._calls(moe_ep_size=1, moe_tp_size=4), ["moe_tp"])
|
||||||
|
|
||||||
|
def test_moe_dp_reduces_over_moe_tp(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self._calls(moe_ep_size=1, moe_tp_size=2, moe_dp_size=2), ["moe_tp"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCanMergePostExpertsAllReduce(CustomTestCase):
|
||||||
|
def _can_merge(self, *, moe_ep_size, moe_tp_size, moe_dp_size=1):
|
||||||
|
with get_parallel().override(
|
||||||
|
moe_ep_size=moe_ep_size,
|
||||||
|
moe_tp_size=moe_tp_size,
|
||||||
|
moe_dp_size=moe_dp_size,
|
||||||
|
tp_size=moe_ep_size * moe_tp_size * moe_dp_size,
|
||||||
|
):
|
||||||
|
return can_merge_post_experts_all_reduce()
|
||||||
|
|
||||||
|
def test_hybrid_ep_tp_merges(self):
|
||||||
|
self.assertTrue(self._can_merge(moe_ep_size=2, moe_tp_size=2))
|
||||||
|
|
||||||
|
def test_single_dimension_does_not_merge(self):
|
||||||
|
self.assertFalse(self._can_merge(moe_ep_size=1, moe_tp_size=4))
|
||||||
|
self.assertFalse(self._can_merge(moe_ep_size=4, moe_tp_size=1))
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveFusionGroup(CustomTestCase):
|
||||||
|
"""EP2/MoE-TP2/DP1 (e.g. DeepSeek-V4-Flash with --tp-size 4 --ep-size 2) must
|
||||||
|
resolve to the _TP group with world_size=4 and the TP rank."""
|
||||||
|
|
||||||
|
def _resolve(self, *, moe_ep_size, moe_tp_size, moe_dp_size=1, tp_rank=0):
|
||||||
|
from sglang.srt.layers.flashinfer_comm_fusion import (
|
||||||
|
resolve_fusion_group,
|
||||||
|
resolve_fusion_world_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
fake_tp_group = MagicMock(name="tp_group")
|
||||||
|
fake_ep_group = MagicMock(name="ep_group")
|
||||||
|
fake_moe_tp_group = MagicMock(name="moe_tp_group")
|
||||||
|
tp_size = moe_ep_size * moe_tp_size * moe_dp_size
|
||||||
|
|
||||||
|
with (
|
||||||
|
get_parallel().override(
|
||||||
|
moe_ep_size=moe_ep_size,
|
||||||
|
moe_tp_size=moe_tp_size,
|
||||||
|
moe_dp_size=moe_dp_size,
|
||||||
|
tp_size=tp_size,
|
||||||
|
tp_rank=tp_rank,
|
||||||
|
moe_ep_rank=tp_rank % moe_ep_size,
|
||||||
|
moe_tp_rank=tp_rank % moe_tp_size,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"sglang.srt.layers.flashinfer_comm_fusion.get_tp_group",
|
||||||
|
return_value=fake_tp_group,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"sglang.srt.layers.flashinfer_comm_fusion.get_moe_ep_group",
|
||||||
|
return_value=fake_ep_group,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"sglang.srt.layers.flashinfer_comm_fusion.get_moe_tp_group",
|
||||||
|
return_value=fake_moe_tp_group,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
ws = resolve_fusion_world_size(use_attn_tp_group=False)
|
||||||
|
group_tuple = resolve_fusion_group(use_attn_tp_group=False)
|
||||||
|
return ws, group_tuple, (fake_tp_group, fake_ep_group, fake_moe_tp_group)
|
||||||
|
|
||||||
|
def test_hybrid_ep2_tp2_dp1_resolves_to_tp_ws4(self):
|
||||||
|
# EP2/MoE-TP2/DP1 (DeepSeek-V4-Flash on 4 GPUs): workspace must sit on
|
||||||
|
# _TP (ws=4) so the fused kernel reduces over all 4 peers.
|
||||||
|
ws, (size, rank, group), (tp_grp, ep_grp, moe_tp_grp) = self._resolve(
|
||||||
|
moe_ep_size=2, moe_tp_size=2, moe_dp_size=1, tp_rank=3
|
||||||
|
)
|
||||||
|
self.assertEqual(ws, 4)
|
||||||
|
self.assertEqual(size, 4)
|
||||||
|
self.assertEqual(rank, 3)
|
||||||
|
self.assertIs(group, tp_grp)
|
||||||
|
|
||||||
|
def test_pure_ep_resolves_to_ep_group(self):
|
||||||
|
ws, (size, rank, group), (tp_grp, ep_grp, moe_tp_grp) = self._resolve(
|
||||||
|
moe_ep_size=4, moe_tp_size=1, moe_dp_size=1, tp_rank=2
|
||||||
|
)
|
||||||
|
self.assertEqual(ws, 4)
|
||||||
|
self.assertEqual(size, 4)
|
||||||
|
self.assertIs(group, ep_grp)
|
||||||
|
|
||||||
|
def test_pure_tp_resolves_to_moe_tp_group(self):
|
||||||
|
ws, (size, rank, group), (tp_grp, ep_grp, moe_tp_grp) = self._resolve(
|
||||||
|
moe_ep_size=1, moe_tp_size=4, moe_dp_size=1, tp_rank=1
|
||||||
|
)
|
||||||
|
self.assertEqual(ws, 4)
|
||||||
|
self.assertEqual(size, 4)
|
||||||
|
self.assertIs(group, moe_tp_grp)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFuseMlpAllReduceGate(CustomTestCase):
|
||||||
|
"""Fusion is allowed only when one group covers the whole reduction.
|
||||||
|
|
||||||
|
The fused residual+LN reduces over a single group. Hybrid EP+TP produces two
|
||||||
|
reductions over disjoint groups; merging collapses them to one _TP reduction
|
||||||
|
that the fused kernel can absorb. When merging does not apply
|
||||||
|
(moe_dp_size > 1) there is no such group and fusion must stay off --
|
||||||
|
otherwise the fused reduce covers half the peers and silently under-reduces.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _should_fuse(
|
def _should_fuse(
|
||||||
self, *, moe_ep_size, moe_tp_size, mlp_mode=ScatterMode.TP_ATTN_FULL
|
self,
|
||||||
|
*,
|
||||||
|
moe_ep_size,
|
||||||
|
moe_tp_size,
|
||||||
|
moe_dp_size=1,
|
||||||
|
mlp_mode=ScatterMode.TP_ATTN_FULL,
|
||||||
):
|
):
|
||||||
forward_batch = types.SimpleNamespace(
|
forward_batch = types.SimpleNamespace(
|
||||||
input_ids=types.SimpleNamespace(shape=(8,))
|
input_ids=types.SimpleNamespace(shape=(8,))
|
||||||
@@ -46,15 +254,24 @@ class TestFuseMlpAllReduceGate(CustomTestCase):
|
|||||||
return_value=types.SimpleNamespace(input_scattered=False),
|
return_value=types.SimpleNamespace(input_scattered=False),
|
||||||
),
|
),
|
||||||
get_parallel().override(
|
get_parallel().override(
|
||||||
moe_ep_size=moe_ep_size, moe_tp_size=moe_tp_size, tp_size=4
|
moe_ep_size=moe_ep_size,
|
||||||
|
moe_tp_size=moe_tp_size,
|
||||||
|
moe_dp_size=moe_dp_size,
|
||||||
|
tp_size=moe_ep_size * moe_tp_size * moe_dp_size,
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
return LayerCommunicator.should_fuse_mlp_allreduce_with_next_layer(
|
return LayerCommunicator.should_fuse_mlp_allreduce_with_next_layer(
|
||||||
_fake_communicator(mlp_mode), forward_batch
|
_fake_communicator(mlp_mode), forward_batch
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_hybrid_ep_tp_does_not_fuse(self):
|
def test_hybrid_ep_tp_fuses_when_mergeable(self):
|
||||||
self.assertFalse(self._should_fuse(moe_ep_size=2, moe_tp_size=2))
|
self.assertTrue(self._should_fuse(moe_ep_size=2, moe_tp_size=2))
|
||||||
|
|
||||||
|
def test_hybrid_ep_tp_does_not_fuse_when_moe_dp_blocks_the_merge(self):
|
||||||
|
# Same caveat as test_moe_dp_keeps_the_two_step_form: unreachable today,
|
||||||
|
# kept so a future relaxation cannot silently re-enable fusion over a
|
||||||
|
# reduction that no single group covers.
|
||||||
|
self.assertFalse(self._should_fuse(moe_ep_size=2, moe_tp_size=2, moe_dp_size=2))
|
||||||
|
|
||||||
def test_pure_tp_still_fuses(self):
|
def test_pure_tp_still_fuses(self):
|
||||||
self.assertTrue(self._should_fuse(moe_ep_size=1, moe_tp_size=4))
|
self.assertTrue(self._should_fuse(moe_ep_size=1, moe_tp_size=4))
|
||||||
|
|||||||
Reference in New Issue
Block a user