[AMD] Feat/dsv4 aiter reduce scatter decode (#29103)
Co-authored-by: wunhuang <wunhuang@amd.com>
This commit is contained in:
@@ -798,9 +798,58 @@ class GroupCoordinator:
|
|||||||
def reduce_scatter_tensor(self, output: torch.Tensor, input: torch.Tensor):
|
def reduce_scatter_tensor(self, output: torch.Tensor, input: torch.Tensor):
|
||||||
if _is_npu:
|
if _is_npu:
|
||||||
self._reduce_scatter_tensor(output, input)
|
self._reduce_scatter_tensor(output, input)
|
||||||
|
elif self._maybe_aiter_reduce_scatter(output, input):
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
reg_reduce_scatter_tensor(output, input, group_name=self.unique_name)
|
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:
|
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)
|
torch.distributed.all_to_all_single(output, input, group=self.device_group)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import warnings
|
import warnings
|
||||||
@@ -6,6 +7,23 @@ from enum import IntEnum
|
|||||||
from typing import Any, Optional
|
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
|
@contextmanager
|
||||||
def temp_set_env(*, allow_sglang: bool = False, **env_vars: Any):
|
def temp_set_env(*, allow_sglang: bool = False, **env_vars: Any):
|
||||||
"""Temporarily set environment variables, restoring originals on exit.
|
"""Temporarily set environment variables, restoring originals on exit.
|
||||||
@@ -51,6 +69,11 @@ class EnvField:
|
|||||||
def parse(self, value: str) -> Any:
|
def parse(self, value: str) -> Any:
|
||||||
raise NotImplementedError()
|
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:
|
def get(self) -> Any:
|
||||||
value = os.getenv(self.name)
|
value = os.getenv(self.name)
|
||||||
|
|
||||||
@@ -61,15 +84,16 @@ class EnvField:
|
|||||||
|
|
||||||
# Not set, return default
|
# Not set, return default
|
||||||
if value is None:
|
if value is None:
|
||||||
return self.default
|
return self._resolve_default()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.parse(value)
|
return self.parse(value)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
default = self._resolve_default()
|
||||||
warnings.warn(
|
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):
|
def is_set(self):
|
||||||
return self.name in os.environ
|
return self.name in os.environ
|
||||||
@@ -443,6 +467,11 @@ class Envs:
|
|||||||
# AMD & ROCm
|
# AMD & ROCm
|
||||||
SGLANG_USE_AITER = EnvBool(False)
|
SGLANG_USE_AITER = EnvBool(False)
|
||||||
SGLANG_USE_AITER_AG = EnvBool(True)
|
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)
|
SGLANG_USE_AITER_UNIFIED_ATTN = EnvBool(False)
|
||||||
# Select the gate/up tile layout for AITER MoE: True -> interleave
|
# Select the gate/up tile layout for AITER MoE: True -> interleave
|
||||||
# (matches FlyDSL `gate_mode="interleave"` kernels), False -> separated
|
# (matches FlyDSL `gate_mode="interleave"` kernels), False -> separated
|
||||||
|
|||||||
@@ -533,12 +533,20 @@ _USE_DP_GATHERV = get_bool_env_var("SGLANG_DP_USE_GATHERV")
|
|||||||
|
|
||||||
def is_dp_gatherv_active() -> bool:
|
def is_dp_gatherv_active() -> bool:
|
||||||
"""Variable-length DP-MoE gather/scatter (all_gatherv + reduce_scatterv) is
|
"""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)
|
enabled and applicable to the CURRENT forward. Requires:
|
||||||
is supported. Env-gated by SGLANG_DP_USE_GATHERV; default off."""
|
- 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 (
|
return (
|
||||||
_USE_DP_GATHERV
|
_USE_DP_GATHERV
|
||||||
and get_attention_tp_size() == 1
|
and get_attention_tp_size() == 1
|
||||||
and get_tensor_model_parallel_world_size() == get_attention_dp_size()
|
and get_tensor_model_parallel_world_size() == get_attention_dp_size()
|
||||||
|
and not _DpGatheredBufferWrapper.is_dp_max_padding()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ from sglang.srt.layers.dp_attention import (
|
|||||||
attn_tp_all_reduce,
|
attn_tp_all_reduce,
|
||||||
dp_gather_partial,
|
dp_gather_partial,
|
||||||
dp_gather_replicate,
|
dp_gather_replicate,
|
||||||
|
dp_reduce_scatter_tensor,
|
||||||
dp_scatter,
|
dp_scatter,
|
||||||
get_dp_global_num_tokens,
|
get_dp_global_num_tokens,
|
||||||
get_global_dp_buffer,
|
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
|
# 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
|
# MUST tell the MoE to skip it (use_reduce_scatter=True) or it
|
||||||
# double-reduces. Env-gated via SGLANG_DP_USE_GATHERV, default OFF.
|
# double-reduces. Env-gated via SGLANG_DP_USE_GATHERV, default OFF.
|
||||||
_use_gatherv_pair = (
|
_use_reduce_scatterv = (
|
||||||
_use_tp_moe_gather
|
_use_tp_moe_gather
|
||||||
and is_dp_gatherv_active()
|
and is_dp_gatherv_active()
|
||||||
and forward_batch.dp_padding_mode is not None
|
and forward_batch.dp_padding_mode is not None
|
||||||
and not forward_batch.dp_padding_mode.is_max_len()
|
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
|
# PoC (SGLANG_DP_SHARED_EXPERT_LOCAL): compute the replicated shared expert
|
||||||
# on LOCAL hidden before the gather and add it back after the combine
|
# 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.
|
# (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=input_ids,
|
||||||
input_ids_global=input_ids_global,
|
input_ids_global=input_ids_global,
|
||||||
# Skip the MoE-internal post-experts all_reduce when we will do the
|
# Skip the MoE-internal post-experts all_reduce when we will do the
|
||||||
# reduce via reduce_scatterv at the combine below (else double-reduce).
|
# reduce via reduce_scatterv/reduce_scatter at the combine below
|
||||||
use_reduce_scatter=_use_cp or _use_gatherv_pair,
|
# (else double-reduce).
|
||||||
|
use_reduce_scatter=_use_cp or _use_reduce_scatterv or _use_reduce_scatter,
|
||||||
skip_shared_experts=_do_shared_local,
|
skip_shared_experts=_do_shared_local,
|
||||||
)
|
)
|
||||||
if _use_cp and get_moe_a2a_backend().is_none():
|
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()),
|
get_local_dp_buffer(get_tp_group()),
|
||||||
hidden_states,
|
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
|
# SUM the TP-sharded per-rank partial expert outputs AND scatter
|
||||||
# each rank its own token slice, in one op. Correct because the
|
# each rank its own token slice, in one op. Correct because the
|
||||||
# MoE-internal all_reduce was skipped (use_reduce_scatter above).
|
# MoE-internal all_reduce was skipped (use_reduce_scatter above).
|
||||||
@@ -1659,6 +1677,17 @@ class DeepseekV4DecoderLayer(nn.Module):
|
|||||||
output=hidden_states,
|
output=hidden_states,
|
||||||
sizes=get_dp_global_num_tokens(),
|
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:
|
else:
|
||||||
dp_scatter(hidden_states, global_hidden_states, forward_batch)
|
dp_scatter(hidden_states, global_hidden_states, forward_batch)
|
||||||
# PoC: add the locally-computed shared-expert output to this rank's
|
# PoC: add the locally-computed shared-expert output to this rank's
|
||||||
|
|||||||
Reference in New Issue
Block a user