Fix silently wrong EPLB output with --moe-a2a-backend none (rank-invariant dispatch) (#32962)
This commit is contained in:
@@ -560,8 +560,11 @@ def _compute_logical_to_all_physical_map(
|
||||
physical_expert_id
|
||||
)
|
||||
|
||||
# Replace by the physical expert on local GPU or node if possible
|
||||
if moe_ep_rank is not None:
|
||||
# Replace by the physical expert on local GPU or node if possible. Skipped
|
||||
# without an a2a backend, where all EP ranks must agree on the pick: this
|
||||
# collapse is per-rank, and the full candidate list is what lets the dispatch
|
||||
# spread a hot expert over its replicas. See ExpertLocationDispatchInfo.
|
||||
if moe_ep_rank is not None and server_args.moe_a2a_backend != "none":
|
||||
num_local_gpu_physical_experts = num_physical_experts // ep_size
|
||||
prefer_same_node = _prefer_same_node_experts(server_args)
|
||||
num_gpus_per_node = (
|
||||
|
||||
@@ -23,7 +23,7 @@ from sglang.srt.runtime_context import get_server_args
|
||||
|
||||
@dataclass
|
||||
class ExpertLocationDispatchInfo:
|
||||
ep_dispatch_algorithm: Literal["static", "random"]
|
||||
ep_dispatch_algorithm: Literal["static", "dynamic", "fake", "lp"]
|
||||
# (num_logical_experts,)
|
||||
partial_logical_to_rank_dispatch_physical_map: Optional[torch.Tensor]
|
||||
# (num_logical_experts, X)
|
||||
@@ -31,10 +31,17 @@ class ExpertLocationDispatchInfo:
|
||||
# (num_logical_experts,)
|
||||
partial_logical_to_all_physical_map_num_valid: torch.Tensor
|
||||
num_physical_experts: int
|
||||
# Whether every rank must pick the same physical expert for a token. True
|
||||
# without an a2a backend: all EP ranks then run the MoE over the same tokens
|
||||
# and sum their partial outputs, so a rank-dependent pick counts a replicated
|
||||
# logical expert several times. With one, each rank dispatches only its own
|
||||
# tokens and is free to disagree.
|
||||
rank_invariant: bool = False
|
||||
|
||||
@classmethod
|
||||
def init_new(cls, layer_id: int):
|
||||
ep_dispatch_algorithm = get_server_args().ep_dispatch_algorithm
|
||||
server_args = get_server_args()
|
||||
ep_dispatch_algorithm = server_args.ep_dispatch_algorithm
|
||||
expert_location_metadata = get_global_expert_location_metadata()
|
||||
assert expert_location_metadata is not None
|
||||
|
||||
@@ -43,6 +50,7 @@ class ExpertLocationDispatchInfo:
|
||||
|
||||
return cls(
|
||||
ep_dispatch_algorithm=ep_dispatch_algorithm,
|
||||
rank_invariant=server_args.moe_a2a_backend == "none",
|
||||
partial_logical_to_rank_dispatch_physical_map=(
|
||||
expert_location_metadata.logical_to_rank_dispatch_physical_map[
|
||||
layer_id, :
|
||||
@@ -107,15 +115,37 @@ def _topk_ids_logical_to_physical_static(
|
||||
def _topk_ids_logical_to_physical_dynamic(
|
||||
topk_ids: torch.Tensor, info: Optional[ExpertLocationDispatchInfo]
|
||||
) -> torch.Tensor:
|
||||
"""Spread each (token, logical expert) over that logical expert's replicas.
|
||||
|
||||
Under ``rank_invariant`` the replica is picked by token row rather than at
|
||||
random: ``torch.randint`` reads the default CUDA generator, so agreement
|
||||
across ranks would rest on their philox offsets staying aligned, which
|
||||
nothing asserts, and it would make greedy requests non-reproducible. Both
|
||||
pick evenly, which is the load split EPLB's placement solver assumes.
|
||||
|
||||
Row indexing leaves replicas unused for a single-row batch, where there is no
|
||||
imbalance to fix anyway.
|
||||
"""
|
||||
topk_ids_original_shape = topk_ids.shape
|
||||
original_dtype = topk_ids.dtype
|
||||
device = topk_ids.device
|
||||
topk_ids = topk_ids.flatten()
|
||||
|
||||
chosen_dispatch_index = (
|
||||
torch.randint(0, 65536, topk_ids.shape, dtype=torch.int32, device=device)
|
||||
% info.partial_logical_to_all_physical_map_num_valid[topk_ids]
|
||||
)
|
||||
num_valid = info.partial_logical_to_all_physical_map_num_valid[topk_ids]
|
||||
if info.rank_invariant:
|
||||
slots_per_token = (
|
||||
topk_ids_original_shape[-1] if len(topk_ids_original_shape) > 1 else 1
|
||||
)
|
||||
row_index = (
|
||||
torch.arange(topk_ids.shape[0], dtype=num_valid.dtype, device=device)
|
||||
// slots_per_token
|
||||
)
|
||||
chosen_dispatch_index = row_index % num_valid
|
||||
else:
|
||||
chosen_dispatch_index = (
|
||||
torch.randint(0, 65536, topk_ids.shape, dtype=torch.int32, device=device)
|
||||
% num_valid
|
||||
)
|
||||
topk_ids = info.partial_logical_to_all_physical_map[topk_ids, chosen_dispatch_index]
|
||||
if topk_ids.dtype != original_dtype:
|
||||
topk_ids = topk_ids.to(original_dtype)
|
||||
|
||||
@@ -6710,10 +6710,29 @@ class ServerArgs:
|
||||
"EPLB is enabled. The expert_distribution_recorder_mode is automatically set."
|
||||
)
|
||||
|
||||
# Without an a2a backend all EP ranks run the MoE over the same tokens and
|
||||
# sum their partial outputs, so the pick has to agree across ranks.
|
||||
needs_rank_invariant_dispatch = self._resolved().moe_a2a_backend == "none"
|
||||
|
||||
if (self.enable_eplb or (self.init_expert_location != "trivial")) and (
|
||||
self.ep_dispatch_algorithm is None
|
||||
):
|
||||
self.ep_dispatch_algorithm = "static"
|
||||
self.ep_dispatch_algorithm = (
|
||||
"dynamic" if needs_rank_invariant_dispatch else "static"
|
||||
)
|
||||
|
||||
# `dynamic` / `fake` switch to the row-index pick; `static` reads a
|
||||
# per-rank table and `lp` samples inside its kernel.
|
||||
if needs_rank_invariant_dispatch and self.ep_dispatch_algorithm in (
|
||||
"static",
|
||||
"lp",
|
||||
):
|
||||
raise ValueError(
|
||||
f"--ep-dispatch-algorithm {self.ep_dispatch_algorithm} picks a "
|
||||
"different physical replica per rank, which only holds up when an "
|
||||
"a2a backend routes each token to a single rank. Use "
|
||||
"--ep-dispatch-algorithm dynamic with --moe-a2a-backend none."
|
||||
)
|
||||
|
||||
if self.enable_eplb and self.ep_join_mode != "scale":
|
||||
assert self._resolved().ep_size > 1
|
||||
|
||||
Reference in New Issue
Block a user