[AMD] Feat/dsv4 aiter reduce scatter decode (#29103)

Co-authored-by: wunhuang <wunhuang@amd.com>
This commit is contained in:
kk
2026-06-25 13:45:18 -07:00
committed by GitHub
co-authored by wunhuang
parent 212c30d008
commit b7d3c3016d
4 changed files with 124 additions and 9 deletions
@@ -798,9 +798,58 @@ class GroupCoordinator:
def reduce_scatter_tensor(self, output: torch.Tensor, input: torch.Tensor):
if _is_npu:
self._reduce_scatter_tensor(output, input)
elif self._maybe_aiter_reduce_scatter(output, input):
return
else:
reg_reduce_scatter_tensor(output, input, group_name=self.unique_name)
def _has_aiter_custom_reduce_scatter(self) -> bool:
ca_comm = self.ca_comm
return (
ca_comm is not None
and not getattr(ca_comm, "disabled", True)
and hasattr(ca_comm, "should_custom_ar")
and hasattr(ca_comm, "reduce_scatter")
)
def _maybe_aiter_reduce_scatter(
self, output: torch.Tensor, input: torch.Tensor
) -> bool:
# Aiter custom reduce-scatter (ROCm). Mirrors `_all_gather_into_tensor`'s
# custom all-gather path: an equal-chunk (no variable sizes) reduce-scatter
# using the registered symmetric-memory buffers, which is faster than the
# generic RCCL kernel for the small, latency-bound decode collective.
# Gated by SGLANG_DP_USE_REDUCE_SCATTER. Falls back (returns False)
# for non-ROCm / unsupported shape/size/topology so the caller uses RCCL.
if not (
is_hip()
and envs.SGLANG_DP_USE_REDUCE_SCATTER.get()
and self._has_aiter_custom_reduce_scatter()
and input.is_contiguous()
and output.is_contiguous()
and input.dtype in (torch.float32, torch.float16, torch.bfloat16)
):
return False
ca_comm = self.ca_comm
# input is the full (pre-reduce) buffer; should_custom_ar bounds its size.
if not ca_comm.should_custom_ar(input):
return False
# Equal-chunk only: input rows must split evenly into world_size chunks
# matching the per-rank output rows.
if input.shape[0] != output.shape[0] * self.world_size:
return False
if getattr(ca_comm, "_IS_CAPTURING", False):
if torch.cuda.is_current_stream_capturing():
ca_comm.reduce_scatter(input, output, registered=True)
elif is_in_tc_piecewise_cuda_graph():
ca_comm.reduce_scatter(input, output, registered=False)
else:
# True CUDA graph warmup: avoid a different host collective.
output.zero_()
return True
ca_comm.reduce_scatter(input, output, registered=False)
return True
def _all_to_all_single(self, output: torch.Tensor, input: torch.Tensor) -> None:
torch.distributed.all_to_all_single(output, input, group=self.device_group)
+32 -3
View File
@@ -1,3 +1,4 @@
import functools
import os
import subprocess
import warnings
@@ -6,6 +7,23 @@ from enum import IntEnum
from typing import Any, Optional
@functools.lru_cache(maxsize=1)
def _default_hip() -> bool:
"""Lazy ROCm/HIP detection for platform-conditional env defaults.
Avoids importing torch at environ import time (this module is intentionally
stdlib-only and loaded very early). Resolved on first EnvField.get() that uses
it as a default, by which point torch is already imported in any real run;
falls back to False if torch is unavailable.
"""
try:
import torch
return torch.version.hip is not None
except Exception:
return False
@contextmanager
def temp_set_env(*, allow_sglang: bool = False, **env_vars: Any):
"""Temporarily set environment variables, restoring originals on exit.
@@ -51,6 +69,11 @@ class EnvField:
def parse(self, value: str) -> Any:
raise NotImplementedError()
def _resolve_default(self) -> Any:
# Support a callable default for lazily/platform-computed defaults
# (e.g. EnvBool(_default_hip)); evaluated only when the env is unset.
return self.default() if callable(self.default) else self.default
def get(self) -> Any:
value = os.getenv(self.name)
@@ -61,15 +84,16 @@ class EnvField:
# Not set, return default
if value is None:
return self.default
return self._resolve_default()
try:
return self.parse(value)
except ValueError as e:
default = self._resolve_default()
warnings.warn(
f'Invalid value for {self.name}: {e}, using default "{self.default}"'
f'Invalid value for {self.name}: {e}, using default "{default}"'
)
return self.default
return default
def is_set(self):
return self.name in os.environ
@@ -443,6 +467,11 @@ class Envs:
# AMD & ROCm
SGLANG_USE_AITER = EnvBool(False)
SGLANG_USE_AITER_AG = EnvBool(True)
# Use reduce_scatter (instead of all_reduce + dp_scatter) for the equal-chunk
# MAX_LEN DP-MoE combine. Default ON for ROCm/HIP (uses the aiter custom
# symmetric-memory kernel), OFF elsewhere (would fall back to RCCL); override
# explicitly to force on/off on any platform.
SGLANG_DP_USE_REDUCE_SCATTER = EnvBool(_default_hip)
SGLANG_USE_AITER_UNIFIED_ATTN = EnvBool(False)
# Select the gate/up tile layout for AITER MoE: True -> interleave
# (matches FlyDSL `gate_mode="interleave"` kernels), False -> separated
+10 -2
View File
@@ -533,12 +533,20 @@ _USE_DP_GATHERV = get_bool_env_var("SGLANG_DP_USE_GATHERV")
def is_dp_gatherv_active() -> bool:
"""Variable-length DP-MoE gather/scatter (all_gatherv + reduce_scatterv) is
enabled and the current parallel layout (attn_tp_size==1, tp_size==dp_size)
is supported. Env-gated by SGLANG_DP_USE_GATHERV; default off."""
enabled and applicable to the CURRENT forward. Requires:
- env SGLANG_DP_USE_GATHERV (default off),
- supported layout (attn_tp_size==1, tp_size==dp_size),
- SUM_LEN padding mode. The gatherv pair (all_gatherv + reduce_scatterv) is
only valid under SUM_LEN; under MAX_LEN the buffer is equal-padded and the
gather/combine use all_gather / (aiter) reduce_scatter instead. Reading the
per-forward padding via _DpGatheredBufferWrapper.is_dp_max_padding() (set by
set_dp_buffer_len) keeps callers that lack a ForwardBatch (e.g.
dp_reduce_scatter_tensor) consistent."""
return (
_USE_DP_GATHERV
and get_attention_tp_size() == 1
and get_tensor_model_parallel_world_size() == get_attention_dp_size()
and not _DpGatheredBufferWrapper.is_dp_max_padding()
)
+33 -4
View File
@@ -57,6 +57,7 @@ from sglang.srt.layers.dp_attention import (
attn_tp_all_reduce,
dp_gather_partial,
dp_gather_replicate,
dp_reduce_scatter_tensor,
dp_scatter,
get_dp_global_num_tokens,
get_global_dp_buffer,
@@ -1587,12 +1588,28 @@ class DeepseekV4DecoderLayer(nn.Module):
# in ONE op, REPLACING the MoE-internal post-experts all_reduce — so we
# MUST tell the MoE to skip it (use_reduce_scatter=True) or it
# double-reduces. Env-gated via SGLANG_DP_USE_GATHERV, default OFF.
_use_gatherv_pair = (
_use_reduce_scatterv = (
_use_tp_moe_gather
and is_dp_gatherv_active()
and forward_batch.dp_padding_mode is not None
and not forward_batch.dp_padding_mode.is_max_len()
)
# SGLANG_DP_USE_REDUCE_SCATTER: in the MAX_LEN decode path (equal per-rank
# padding, gatherv inactive, no EP), replace the MoE-internal post-experts
# all_reduce + dp_scatter with an equal-chunk reduce_scatter. On ROCm this
# uses the aiter custom kernel (so BOTH gather and combine are aiter custom),
# elsewhere RCCL reduce_scatter; either way it cuts combine traffic ~2x vs
# all_reduce. tp_size==attn_dp_size required so the global buffer splits
# evenly into per-rank chunks.
_use_reduce_scatter = (
envs.SGLANG_DP_USE_REDUCE_SCATTER.get()
and _use_tp_moe_gather
and not _use_reduce_scatterv
and not should_use_dp_reduce_scatterv()
and forward_batch.dp_padding_mode is not None
and forward_batch.dp_padding_mode.is_max_len()
and get_parallel().tp_size == get_parallel().attn_dp_size
)
# PoC (SGLANG_DP_SHARED_EXPERT_LOCAL): compute the replicated shared expert
# on LOCAL hidden before the gather and add it back after the combine
# (reduce_scatterv OR dp_scatter), instead of on the gathered global buffer.
@@ -1638,8 +1655,9 @@ class DeepseekV4DecoderLayer(nn.Module):
input_ids=input_ids,
input_ids_global=input_ids_global,
# Skip the MoE-internal post-experts all_reduce when we will do the
# reduce via reduce_scatterv at the combine below (else double-reduce).
use_reduce_scatter=_use_cp or _use_gatherv_pair,
# reduce via reduce_scatterv/reduce_scatter at the combine below
# (else double-reduce).
use_reduce_scatter=_use_cp or _use_reduce_scatterv or _use_reduce_scatter,
skip_shared_experts=_do_shared_local,
)
if _use_cp and get_moe_a2a_backend().is_none():
@@ -1649,7 +1667,7 @@ class DeepseekV4DecoderLayer(nn.Module):
get_local_dp_buffer(get_tp_group()),
hidden_states,
)
if should_use_dp_reduce_scatterv() or _use_gatherv_pair:
if should_use_dp_reduce_scatterv() or _use_reduce_scatterv:
# SUM the TP-sharded per-rank partial expert outputs AND scatter
# each rank its own token slice, in one op. Correct because the
# MoE-internal all_reduce was skipped (use_reduce_scatter above).
@@ -1659,6 +1677,17 @@ class DeepseekV4DecoderLayer(nn.Module):
output=hidden_states,
sizes=get_dp_global_num_tokens(),
)
elif _use_reduce_scatter:
# Equal-chunk reduce_scatter: SUM the TP-sharded per-rank partial
# expert outputs AND scatter each rank its own (MAX_LEN-padded)
# token chunk in one op (symmetric inverse of the MAX_LEN
# all_gather). Correct because the MoE-internal all_reduce was
# skipped (use_reduce_scatter above). dp_reduce_scatter_tensor
# routes to the equal-chunk reduce_scatter_tensor here (its
# variable-length reduce_scatterv branch is gated by
# is_dp_gatherv_active(), which is False under MAX_LEN), which in
# turn uses the aiter custom kernel when it fits (else RCCL).
dp_reduce_scatter_tensor(hidden_states, global_hidden_states)
else:
dp_scatter(hidden_states, global_hidden_states, forward_batch)
# PoC: add the locally-computed shared-expert output to this rank's