Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

793 lines
28 KiB
Python

from __future__ import annotations
import functools
import logging
from contextlib import contextmanager
from enum import IntEnum, auto
from typing import TYPE_CHECKING, List, Optional, Tuple
import torch
import triton
import triton.language as tl
from sglang.srt.distributed import (
GroupCoordinator,
get_attn_cp_group,
get_attn_tensor_model_parallel_rank,
get_attn_tensor_model_parallel_world_size,
get_attn_tp_group,
)
from sglang.srt.distributed import get_moe_dp_group as _get_moe_dp_group
from sglang.srt.distributed import (
get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
get_tp_group,
tensor_model_parallel_all_reduce,
)
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.runtime_context import get_flags
from sglang.srt.utils import get_bool_env_var, is_hip
if TYPE_CHECKING:
from sglang.srt.configs.model_config import ModelConfig
from sglang.srt.server_args import ServerArgs
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
_ATTN_DP_RANK: Optional[int] = None
_ATTN_DP_SIZE: Optional[int] = None
_is_hip = is_hip()
_USE_ROCM700A_WA = _is_hip and get_bool_env_var("SGLANG_USE_ROCM700A")
class DpPaddingMode(IntEnum):
# Padding tokens to max length and then gather tokens using `all_gather_into_tensor`
MAX_LEN = auto()
# Padding tokens to sum length and then gather tokens using `all_reduce`
SUM_LEN = auto()
def is_max_len(self):
return self == DpPaddingMode.MAX_LEN
def is_sum_len(self):
return self == DpPaddingMode.SUM_LEN
@classmethod
def get_dp_padding_mode(
cls, is_extend_in_batch, global_num_tokens: List[int]
) -> DpPaddingMode:
dp_size = get_attention_dp_size()
# When is_extend_in_batch and dp_size > 1, use SUM_LEN to avoid padding
# overhead from uneven token distribution.
# For dp_size=1, max_len equals sum_len, so prefer MAX_LEN mode
# to enable symmetric memory optimization (needed for DSA CP, etc.).
if is_extend_in_batch and dp_size > 1:
# Hybrid-SSM models materialize idle ranks via the MAX_LEN
# fabricated-row conversion; other models keep mainline SUM_LEN.
if get_flags().dp.max_len_with_idle and min(global_num_tokens) == 0:
return DpPaddingMode.MAX_LEN
return DpPaddingMode.SUM_LEN
# we choose the mode that minimizes the communication cost
# prefer MAX_LEN when communication cost is equal to enable symmetric memory
max_len = max(global_num_tokens)
sum_len = sum(global_num_tokens)
if sum_len * 2 >= max_len * dp_size:
return cls.MAX_LEN
else:
return cls.SUM_LEN
@classmethod
def get_default_mode_in_cuda_graph(cls) -> DpPaddingMode:
# TODO(kkhuang-amd): noqa, temporary work-around for rocm 7.0.0 alpha
# it can be safely removed later, once RCCL fixed
if _USE_ROCM700A_WA:
return cls.SUM_LEN
else:
return cls.MAX_LEN
class _DpGatheredBufferWrapper:
"""Facade for the DP gathered-buffer state: allocation metadata lives on
``flags.dp`` (set once at initialize_dp_attention). The per-forward
sizing quartet stays as class attributes: the values are read inside
torch.compile-traced model code, and attribute-source ints get dynamo's
automatic-dynamic treatment, while contextvars are untraceable and dict
slots value-guard into the recompile limit (one recompile per distinct
size)."""
_global_dp_buffer_len: int
_local_dp_buffer_len: int
_dp_max_padding: bool
_global_num_tokens: Optional[List[int]]
@classmethod
def set_metadata(cls, hidden_size: int, dtype: torch.dtype, device: torch.device):
from sglang.srt.runtime_context import get_flags
dp = get_flags().dp
dp.buffer_hidden_size = hidden_size
dp.buffer_dtype = dtype
dp.buffer_device = device
@classmethod
def set_dp_buffer_len(
cls,
global_dp_buffer_len: int,
local_dp_buffer_len: int,
dp_max_padding: bool,
global_num_tokens: Optional[List[int]] = None,
):
cls._global_dp_buffer_len = global_dp_buffer_len
cls._local_dp_buffer_len = local_dp_buffer_len
cls._dp_max_padding = dp_max_padding
cls._global_num_tokens = global_num_tokens
@classmethod
def get_global_dp_buffer(cls, group: GroupCoordinator) -> torch.Tensor:
from sglang.srt.runtime_context import get_flags
dp = get_flags().dp
with use_symmetric_memory(group, disabled=not cls._dp_max_padding):
buffer = torch.empty(
(cls._global_dp_buffer_len, dp.buffer_hidden_size),
dtype=dp.buffer_dtype,
device=dp.buffer_device,
)
return buffer
@classmethod
def get_local_dp_buffer(cls, group: GroupCoordinator) -> torch.Tensor:
from sglang.srt.runtime_context import get_flags
dp = get_flags().dp
with use_symmetric_memory(group, disabled=not cls._dp_max_padding):
buffer = torch.empty(
(cls._local_dp_buffer_len, dp.buffer_hidden_size),
dtype=dp.buffer_dtype,
device=dp.buffer_device,
)
return buffer
@classmethod
def get_global_dp_buffer_len(cls) -> int:
return cls._global_dp_buffer_len
@classmethod
def get_local_dp_buffer_len(cls) -> int:
return cls._local_dp_buffer_len
@classmethod
def get_dp_global_num_tokens(cls) -> List[int]:
return cls._global_num_tokens
@classmethod
def get_dp_hidden_size(cls) -> int:
from sglang.srt.runtime_context import get_flags
return get_flags().dp.buffer_hidden_size
@classmethod
def get_dp_dtype(cls) -> torch.dtype:
from sglang.srt.runtime_context import get_flags
return get_flags().dp.buffer_dtype
@classmethod
def get_dp_device(cls) -> torch.device:
from sglang.srt.runtime_context import get_flags
return get_flags().dp.buffer_device
@classmethod
def is_dp_max_padding(cls) -> bool:
return cls._dp_max_padding
def set_dp_buffer_len(
global_dp_buffer_len: int,
local_dp_buffer_len: int,
dp_max_padding: bool,
global_num_tokens: Optional[List[int]] = None,
):
_DpGatheredBufferWrapper.set_dp_buffer_len(
global_dp_buffer_len, local_dp_buffer_len, dp_max_padding, global_num_tokens
)
def get_global_dp_buffer(group: GroupCoordinator) -> torch.Tensor:
return _DpGatheredBufferWrapper.get_global_dp_buffer(group=group)
def get_local_dp_buffer(group: GroupCoordinator) -> torch.Tensor:
return _DpGatheredBufferWrapper.get_local_dp_buffer(group=group)
def get_global_dp_buffer_len() -> int:
return _DpGatheredBufferWrapper.get_global_dp_buffer_len()
def get_local_dp_buffer_len() -> int:
return _DpGatheredBufferWrapper.get_local_dp_buffer_len()
def get_dp_global_num_tokens() -> List[int]:
return _DpGatheredBufferWrapper.get_dp_global_num_tokens()
def get_dp_hidden_size() -> int:
return _DpGatheredBufferWrapper.get_dp_hidden_size()
def get_dp_dtype() -> torch.dtype:
return _DpGatheredBufferWrapper.get_dp_dtype()
def get_dp_device() -> torch.device:
return _DpGatheredBufferWrapper.get_dp_device()
def set_is_extend_in_batch(is_extend_in_batch: bool):
# Sticky within the thread: every ForwardBatch construction writes it,
# graph runners force False around capture; readers are the EP
# dispatchers on the same (single) forward thread.
from sglang.srt.runtime_context import get_forward
get_forward().set("is_extend_in_batch", is_extend_in_batch)
def get_is_extend_in_batch() -> bool:
from sglang.srt.runtime_context import get_forward
return get_forward().is_extend_in_batch
def is_dp_max_padding() -> bool:
return _DpGatheredBufferWrapper.is_dp_max_padding()
def compute_dp_attention_world_info(
enable_dp_attention, tp_rank, tp_size, dp_size, attn_cp_size: int = 1
):
attn_dp_size = dp_size if enable_dp_attention else 1
attn_tp_size = tp_size // attn_dp_size // attn_cp_size
attn_tp_rank = tp_rank % attn_tp_size
if not enable_dp_attention:
attn_dp_rank = 0
else:
# Rank layout is (dp, cp, tp) where tp is the fastest-changing dim:
# tp_rank = (attn_dp_rank * attn_cp_size + attn_cp_rank) * attn_tp_size + attn_tp_rank
attn_dp_rank = tp_rank // (attn_tp_size * attn_cp_size)
return attn_tp_rank, attn_tp_size, attn_dp_rank, attn_dp_size
def initialize_dp_attention(
server_args: ServerArgs,
model_config: ModelConfig,
):
global _ATTN_DP_RANK, _ATTN_DP_SIZE
dp = get_flags().dp
dp.max_len_with_idle = (
getattr(model_config.hf_config, "hybrid_override_pattern", None) is not None
)
enable_dp_attention = server_args.enable_dp_attention
dp_size = server_args.dp_size
moe_dense_tp_size = server_args.moe_dense_tp_size
attn_cp_size = server_args.attn_cp_size
dp.enabled = enable_dp_attention
tp_rank = get_tensor_model_parallel_rank()
tp_size = get_tensor_model_parallel_world_size()
_, _, _ATTN_DP_RANK, _ = compute_dp_attention_world_info(
enable_dp_attention, tp_rank, tp_size, dp_size, attn_cp_size
)
_ATTN_DP_SIZE = dp_size if enable_dp_attention else 1
_DpGatheredBufferWrapper.set_metadata(
hidden_size=model_config.hidden_size,
dtype=model_config.dtype,
device=torch.device(server_args.device),
)
def is_dp_attention_enabled() -> bool:
return get_flags().dp.enabled
def is_allocation_symmetric() -> bool:
return not is_dp_attention_enabled() or is_dp_max_padding()
def get_attention_dp_rank() -> int:
assert _ATTN_DP_RANK is not None, "dp attention not initialized!"
return _ATTN_DP_RANK
def get_attention_dp_size() -> int:
assert _ATTN_DP_SIZE is not None, "dp attention not initialized!"
return _ATTN_DP_SIZE
@contextmanager
def disable_dp_size():
"""Patch the tp group temporarily until this function ends.
This method is for draft workers of speculative decoding to run draft model
with different tp degree from that of target model workers.
Args:
tp_group (GroupCoordinator): the tp group coordinator
"""
global _ATTN_DP_SIZE
assert _ATTN_DP_SIZE is not None, "dp attention not initialized!"
old_dp_size = _ATTN_DP_SIZE
_ATTN_DP_SIZE = 1
try:
yield
finally:
_ATTN_DP_SIZE = old_dp_size
def get_dp_local_info(forward_batch: ForwardBatch) -> Tuple[torch.Tensor, torch.Tensor]:
# `get_dp_local_info` is only called in global DP gather and scatter. We use global DP rank here.
dp_rank = get_attention_dp_rank()
if forward_batch.dp_local_start_pos is None:
cumtokens = torch.cumsum(forward_batch.global_num_tokens_gpu, dim=0)
if dp_rank == 0:
local_start_pos = torch.zeros_like(cumtokens[0])
else:
local_start_pos = cumtokens[dp_rank - 1]
local_num_tokens = forward_batch.global_num_tokens_gpu[dp_rank]
forward_batch.dp_local_start_pos = local_start_pos
forward_batch.dp_local_num_tokens = local_num_tokens
return forward_batch.dp_local_start_pos, forward_batch.dp_local_num_tokens
def get_dp_local_slice_cpu(
forward_batch: ForwardBatch,
can_run_graph: bool,
cuda_graph_batch: Optional[int],
) -> Tuple[int, int]:
# CPU (start, length) slice for DP-local data in a rank-padded buffer.
# Returns Python ints (no D2H sync) and handles the cuda-graph-padded layout.
global_num_tokens = forward_batch.global_num_tokens_cpu
dp_rank = get_attention_dp_rank()
local_num_tokens = global_num_tokens[dp_rank]
if can_run_graph:
local_start_pos = dp_rank * cuda_graph_batch
else:
local_start_pos = sum(global_num_tokens[:dp_rank])
return local_start_pos, local_num_tokens
@triton.jit
def memcpy_triton_kernel(
dst_ptr,
src_ptr,
offset_ptr,
sz_ptr,
offset_src: tl.constexpr,
chunk_size, # multiplied for offset and sz
BLOCK_SIZE: tl.constexpr,
):
pid = tl.program_id(axis=0).to(tl.int64)
offset = tl.load(offset_ptr).to(tl.int64) * chunk_size
sz = tl.load(sz_ptr).to(tl.int64) * chunk_size
start_index = pid * BLOCK_SIZE
offs = tl.arange(0, BLOCK_SIZE)
mask = start_index + offs < sz
if offset_src:
data = tl.load(src_ptr + offset + start_index + offs, mask=mask)
tl.store(dst_ptr + start_index + offs, data, mask=mask)
else:
data = tl.load(src_ptr + start_index + offs, mask=mask)
tl.store(dst_ptr + offset + start_index + offs, data, mask=mask)
def prod(x):
return functools.reduce(lambda a, b: a * b, x, 1)
def memcpy_triton(dst, src, dim, offset, sz, offset_src):
max_size = min(src.numel(), dst.numel())
assert dim == 0, "dim != 0 unsupported"
assert src.shape[1:] == dst.shape[1:], "src and dst must have same shape"
chunk_size = prod(src.shape[1:])
BLOCK_SIZE = 8192
grid = (triton.cdiv(max_size, BLOCK_SIZE),)
memcpy_triton_kernel[grid](dst, src, offset, sz, offset_src, chunk_size, BLOCK_SIZE)
def _dp_gather_via_all_reduce(
global_tokens: torch.Tensor,
local_tokens: torch.Tensor,
forward_batch: ForwardBatch,
is_partial: bool,
):
local_start_pos, local_num_tokens = get_dp_local_info(forward_batch)
global_tokens.fill_(0)
assert local_tokens.is_contiguous()
assert global_tokens.is_contiguous()
if local_tokens.shape[0] > 0 and (
is_partial or get_attn_tensor_model_parallel_rank() == 0
):
assert (
local_tokens.untyped_storage() is not global_tokens.untyped_storage()
), "aliasing between global_tokens and local_tokens not allowed"
memcpy_triton(
global_tokens, local_tokens, 0, local_start_pos, local_num_tokens, False
)
# Input IDs are in int 32. We should use inplace_all_reduce for local case because of custom all reduce.
NUM_GPUS_PER_NODE = 8
if (
not local_tokens.dtype.is_floating_point
and get_tensor_model_parallel_world_size() <= NUM_GPUS_PER_NODE
):
from sglang.srt.distributed.parallel_state import inplace_all_reduce
inplace_all_reduce(global_tokens, group_name=get_tp_group().unique_name)
else:
global_tokens[:] = tensor_model_parallel_all_reduce(global_tokens)
def _dp_gather_via_all_gather(
global_tokens: torch.Tensor,
local_tokens: torch.Tensor,
forward_batch: ForwardBatch,
is_partial: bool,
):
if get_attn_tensor_model_parallel_world_size() == 1:
get_tp_group().all_gather_into_tensor(global_tokens, local_tokens)
return
if not is_partial:
if get_attn_tensor_model_parallel_rank() != 0:
local_tokens.fill_(0)
scattered_local_tokens = local_tokens.tensor_split(
get_attn_tensor_model_parallel_world_size()
)[get_attn_tensor_model_parallel_rank()]
get_attn_tp_group().reduce_scatter_tensor(scattered_local_tokens, local_tokens)
get_tp_group().all_gather_into_tensor(global_tokens, scattered_local_tokens)
# Variable-length DP-MoE gather (reference https://github.com/ROCm/ATOM/pull/930): instead of padding every
# rank to max_len (all_gather) or all-reducing a sum_len zero-buffer (all_reduce),
# gather exactly sum(per-rank tokens) via all_gatherv. Env-gated; only the simple
# tp_size==dp_size (attn_tp_size==1) case is supported for now (e.g. tp8dp8).
_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 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_attn_tensor_model_parallel_world_size() == 1
and get_tensor_model_parallel_world_size() == get_attention_dp_size()
and not _DpGatheredBufferWrapper.is_dp_max_padding()
)
def _dp_gatherv_sizes(forward_batch) -> Optional[List[int]]:
"""Per-rank CPU token counts for the buffer being gathered. The MoE gather
passes a ForwardBatch (global_num_tokens_cpu); the logits gather passes a
LogitsMetadata (global_num_tokens_for_logprob_cpu). Return the sizes that
match the LOCAL tensor for this context, or None to fall back."""
sizes = getattr(forward_batch, "global_num_tokens_for_logprob_cpu", None)
if sizes is None:
sizes = getattr(forward_batch, "global_num_tokens_cpu", None)
if sizes is None:
return None
try:
return [int(x) for x in sizes]
except (TypeError, ValueError):
return None
def _dp_gather_via_all_gatherv(
global_tokens: torch.Tensor,
local_tokens: torch.Tensor,
forward_batch: ForwardBatch,
is_partial: bool,
sizes: List[int],
):
# attn_tp_size == 1: each DP rank contributes exactly `sizes[rank]` rows.
# CRITICAL: the MoE downstream runs on the WHOLE `global_tokens` buffer
# (M = global_tokens.shape[0]), so the gather MUST fill every row. We pad
# each rank's local tensor up to sizes[rank] with zeros (matching the
# buffer's reserved per-rank slot) so sum(sizes) == buffer rows and there
# is no uninitialized tail for the MoE to read.
rank = get_attention_dp_rank()
local_rows = sizes[rank]
if local_tokens.shape[0] == local_rows:
local_real = local_tokens
elif local_tokens.shape[0] > local_rows:
local_real = local_tokens[:local_rows]
else:
local_real = local_tokens.new_zeros((local_rows, *local_tokens.shape[1:]))
local_real[: local_tokens.shape[0]].copy_(local_tokens)
# sum(sizes) == global_tokens.shape[0] is guaranteed by the caller (else it
# falls back to all_reduce). Pass global_tokens as the NCCL output buffer so
# the gather writes directly into it -- avoids the previous extra full-buffer
# torch.cat + copy_ (two ~sum(sizes)*hidden DtoD copies, ~700us/layer at c512).
get_tp_group().all_gatherv(local_real, sizes=sizes, output=global_tokens)
def _dp_gather(
global_tokens: torch.Tensor,
local_tokens: torch.Tensor,
forward_batch: ForwardBatch,
is_partial: bool,
):
if (
is_dp_gatherv_active()
and forward_batch.dp_padding_mode is not None
and not forward_batch.dp_padding_mode.is_max_len()
):
# The gatherv per-rank sizes MUST sum to the pre-allocated global buffer
# (the MoE runs on the whole buffer, so any unfilled tail = garbage).
# The buffer was sized from the ceil_align'd global_num_tokens stored via
# set_dp_buffer_len (forward_batch_info), so the authoritative sizes are
# get_dp_global_num_tokens() — the SAME source the reduce_scatterv combine
# uses (symmetric). _dp_gatherv_sizes() reads the raw (un-aligned, and for
# the MoE-gather context the logprob-token) counts, which do NOT match the
# buffer for prefill steps -> would force an all_reduce fallback.
# Prefer the buffer-aligned sizes; fall back to the per-batch sizes only
# if they happen to match (e.g. the logits gather path).
_gatherv_sizes = get_dp_global_num_tokens()
if _gatherv_sizes is None or sum(_gatherv_sizes) != global_tokens.shape[0]:
_gatherv_sizes = _dp_gatherv_sizes(forward_batch)
if _gatherv_sizes is not None and sum(_gatherv_sizes) == global_tokens.shape[0]:
_dp_gather_via_all_gatherv(
global_tokens, local_tokens, forward_batch, is_partial, _gatherv_sizes
)
return
if forward_batch.dp_padding_mode.is_max_len():
_dp_gather_via_all_gather(
global_tokens, local_tokens, forward_batch, is_partial
)
else:
_dp_gather_via_all_reduce(
global_tokens, local_tokens, forward_batch, is_partial
)
def dp_gather_partial(
global_tokens: torch.Tensor,
local_tokens: torch.Tensor,
forward_batch: ForwardBatch,
):
_dp_gather(global_tokens, local_tokens, forward_batch, is_partial=True)
def dp_gather_replicate(
global_tokens: torch.Tensor,
local_tokens: torch.Tensor,
forward_batch: ForwardBatch,
):
_dp_gather(global_tokens, local_tokens, forward_batch, is_partial=False)
def dp_scatter(
local_tokens: torch.Tensor, # output
global_tokens: torch.Tensor, # input
forward_batch: ForwardBatch,
):
# local_num_tokens is not necessarily the same as local_tokens.shape[0],
# since local_tokens may be padded for cuda graph
local_start_pos, local_num_tokens = get_dp_local_info(forward_batch)
local_tokens.fill_(0)
assert local_tokens.is_contiguous()
assert global_tokens.is_contiguous()
if local_tokens.shape[0] > 0:
assert (
local_tokens.untyped_storage() is not global_tokens.untyped_storage()
), "aliasing between local_tokens and global_tokens not allowed"
memcpy_triton(
local_tokens, global_tokens, 0, local_start_pos, local_num_tokens, True
)
def dp_reduce_scatter_tensor(output: torch.Tensor, input: torch.Tensor):
if is_dp_gatherv_active():
# Variable-length combine matching all_gatherv dispatch: scatter the
# global (sum_len) tensor back to per-rank token counts. Fall through to
# the default reduce-scatter path if per-rank sizes are unavailable.
sizes = get_dp_global_num_tokens()
if sizes is not None:
get_tp_group().reduce_scatterv(input, output=output, sizes=sizes)
return
if get_tensor_model_parallel_world_size() == get_attention_dp_size():
get_tp_group().reduce_scatter_tensor(output, input)
else:
scattered_local_tokens = input.tensor_split(
get_tensor_model_parallel_world_size()
)[get_tensor_model_parallel_rank()]
get_tp_group().reduce_scatter_tensor(scattered_local_tokens, input)
get_attn_tp_group().all_gather_into_tensor(output, scattered_local_tokens)
# ---------------------------------------------------------------------------
# Two-batch-overlap (non-EP / DP TP-MoE) async gather + combine.
#
# The DP TP-MoE path (deepseek_v4) gathers local hidden -> a global buffer
# before the experts and reduce-scatters back after. For TBO we run those two
# collectives on a single shared comm stream (mirroring the mori dispatcher's
# _comm_stream) and return a CUDA event, so the op engine can yield and let the
# OTHER ubatch's attn+MoE compute run on the compute stream while this ubatch's
# gather/combine proceeds on the comm stream. Both ubatches share ONE comm
# stream -> their collectives serialize in-order (no concurrent-collective
# deadlock on the RCCL communicator), each overlapping the other's compute.
# ---------------------------------------------------------------------------
def get_dp_tbo_comm_stream() -> torch.cuda.Stream:
from sglang.srt.runtime_context import get_stream
return get_stream("dp_tbo_comm")
# Persistent reusable CUDA events for non-EP DP TBO, keyed by (kind, subbatch).
# CRITICAL: do NOT create a fresh event per gather/combine -- that is ~244 new
# torch.cuda.Event per forward (61 layers x 2 ubatches x 2), and the HSA signal
# pool is exhausted after a few hundred forwards -> HSA_STATUS_ERROR_OUT_OF_RESOURCES
# ("...create internal OS-specific events"). Reuse one event per (kind, subbatch)
# and just re-record it (mirrors the mori CommStreamPool event reuse).
def _tbo_event(key) -> torch.cuda.Event:
from sglang.srt.runtime_context import get_resources
pool = get_resources().tbo_event_pool
ev = pool.get(key)
if ev is None:
ev = torch.cuda.Event()
pool[key] = ev
return ev
def dp_gather_partial_async(
global_tokens: torch.Tensor,
local_tokens: torch.Tensor,
forward_batch: ForwardBatch,
event_key=("gather", 0),
) -> torch.cuda.Event:
"""Launch `dp_gather_partial` (all_gatherv) on the shared DP TBO comm stream;
re-record + return a PERSISTENT event (keyed by `event_key`) that fires when
the gather completes. Caller yields, then `compute_stream.wait_event(ev)`
before reading `global_tokens`."""
comm = get_dp_tbo_comm_stream()
compute = torch.cuda.current_stream()
# Keep buffers alive across streams (caching allocator).
local_tokens.record_stream(comm)
global_tokens.record_stream(comm)
ev = _tbo_event(event_key)
with torch.cuda.stream(comm):
comm.wait_stream(compute) # inputs were produced on the compute stream
dp_gather_partial(global_tokens, local_tokens, forward_batch)
ev.record(comm)
return ev
# Persistent grow-only buffers for non-EP DP TBO, keyed by (kind, tbo_subbatch).
# Reused across ALL layers (and forwards) so the caching allocator does not churn
# a fresh per-layer `torch.empty` for the 8x DP-gather / combine buffers. That
# churn (different sizes per forward x 2 ubatches x 61 layers, kept alive by the
# comm-stream record_stream) ballooned `reserved` to ~270GB and tripped
# HSA_STATUS_ERROR_OUT_OF_RESOURCES at large prefill chunks, even though the live
# (allocated) working set was only ~10GB.
_TBO_PERSIST_BUF: dict = {}
def get_tbo_persistent_buffer(
key, rows: int, hidden: int, dtype: torch.dtype, device
) -> torch.Tensor:
"""Return a [rows, hidden] view of a grow-only persistent buffer for `key`.
Reallocates only when the request exceeds the cached capacity / changes
dtype|hidden. Caller must treat the returned view as scratch (overwritten)."""
buf = _TBO_PERSIST_BUF.get(key)
cap = 0 if buf is None else buf.shape[0]
if buf is None or rows > cap or buf.shape[1] != hidden or buf.dtype != dtype:
new_rows = max(rows, cap)
buf = torch.empty((new_rows, hidden), dtype=dtype, device=device)
_TBO_PERSIST_BUF[key] = buf
return buf[:rows]
def dp_reduce_scatterv_async(
output_local: torch.Tensor,
global_tokens: torch.Tensor,
sizes: List[int],
event_key=("combine", 0),
) -> torch.cuda.Event:
"""Launch the variable-length reduce_scatterv (combine) on the shared DP TBO
comm stream; re-record + return a PERSISTENT event (keyed by `event_key`).
Matches the gatherv (SUM_LEN) path."""
comm = get_dp_tbo_comm_stream()
compute = torch.cuda.current_stream()
ev = _tbo_event(event_key)
with torch.cuda.stream(comm):
comm.wait_stream(compute)
get_tp_group().reduce_scatterv(global_tokens, output=output_local, sizes=sizes)
ev.record(comm)
return ev
def attn_tp_reduce_scatter_tensor(output: torch.Tensor, input: torch.Tensor):
return get_attn_tp_group().reduce_scatter_tensor(output, input)
def attn_cp_reduce_scatter_tensor(output: torch.Tensor, input: torch.Tensor):
return get_attn_cp_group().reduce_scatter_tensor(output, input)
def attn_tp_all_reduce(input: torch.Tensor):
return get_attn_tp_group().all_reduce(input)
def attn_tp_all_gather_into_tensor(output: torch.Tensor, input: torch.Tensor):
return get_attn_tp_group().all_gather_into_tensor(output, input)
def attn_cp_all_gather_into_tensor(output: torch.Tensor, input: torch.Tensor):
return get_attn_cp_group().all_gather_into_tensor(output, input)
def get_moe_cp_group() -> GroupCoordinator:
"""Returns the MOE_DP group, which includes CP partners when attn_cp_size > moe_dp_size."""
return _get_moe_dp_group()
def get_moe_cp_rank() -> int:
return _get_moe_dp_group().rank_in_group
def get_moe_cp_size() -> int:
return _get_moe_dp_group().world_size
def is_enable_moe_cp_allgather() -> bool:
"""True when moe_dp_size < attn_cp_size, requiring allgather across CP ranks before MoE."""
from sglang.srt.runtime_context import get_server_args
sa = get_server_args()
return sa.attn_cp_size > sa.moe_dp_size
def moe_cp_all_gather_into_tensor(output: torch.Tensor, input: torch.Tensor):
return _get_moe_dp_group().all_gather_into_tensor(output, input)
def attn_tp_all_gather(output_list: List[torch.Tensor], input: torch.Tensor):
return get_attn_tp_group().all_gather(input, output_tensor_list=output_list)