[Feature] Megatron LayerNorm sequence parallelism (--enable-layernorm-sp) (#30915)

This commit is contained in:
Martin Hua
2026-08-31 19:27:28 -07:00
committed by GitHub
parent 783af667fb
commit 562b661e0e
11 changed files with 623 additions and 3 deletions
@@ -0,0 +1,63 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from sglang.srt.server_args import ServerArgs
from sglang.srt.arg_groups.overrides import model_config_of, resolving_view
logger = logging.getLogger(__name__)
def handle_layernorm_sp(server_args: ServerArgs) -> None:
"""Validate --enable-layernorm-sp against the resolved parallelism config.
Runs in the resolution pipeline rather than in the layers so a model that
never builds a LayerCommunicator rejects the flag instead of ignoring it.
"""
cfg = resolving_view(server_args)
if not cfg.enable_layernorm_sp:
return
architectures = model_config_of(server_args).hf_config.architectures
validate_layernorm_sp(
architecture=architectures[0] if architectures else None,
tp_size=cfg.tp_size,
enable_dp_attention=cfg.enable_dp_attention,
speculative_algorithm=cfg.speculative_algorithm,
)
def validate_layernorm_sp(
*,
architecture: Optional[str],
tp_size: int,
enable_dp_attention: bool,
speculative_algorithm: Optional[str],
) -> None:
"""Fail loud for unsupported / incompatible configs. Callers gate on the flag."""
from sglang.srt.layers.layernorm_sp import SP_SUPPORTED_ARCHITECTURES
if architecture not in SP_SUPPORTED_ARCHITECTURES:
raise ValueError(
"--enable-layernorm-sp is only supported for "
f"{sorted(SP_SUPPORTED_ARCHITECTURES)}; got {architecture}."
)
if tp_size <= 1:
raise ValueError(
"--enable-layernorm-sp requires tp_size > 1: there is no sequence to "
"shard across a single TP rank."
)
if enable_dp_attention:
raise ValueError(
"--enable-layernorm-sp is not compatible with --enable-dp-attention: "
"SP shards the sequence across the full TP group, which under DP "
"attention spans data-parallel groups holding different sequences."
)
if speculative_algorithm is not None:
raise ValueError(
"--enable-layernorm-sp is not compatible with speculative decoding "
"(EAGLE/EAGLE3): the captured aux hidden states would be "
"sequence-sharded."
)
+5
View File
@@ -318,6 +318,11 @@ def run_resolution_pipeline(server_args: Any) -> None:
handle_speculative_decoding(server_args)
# After the speculative hook so speculative_algorithm is final.
from sglang.srt.arg_groups.layernorm_sp_hook import handle_layernorm_sp
handle_layernorm_sp(server_args)
# Validate the CuteDSL A2A token budget now that num_tokens_per_req is final.
validate_cutedsl_a2a_token_budget(server_args)
@@ -27,6 +27,7 @@ from sglang.srt.distributed.parallel_state import (
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.environ import envs
from sglang.srt.layers.dp_attention import initialize_dp_attention
from sglang.srt.layers.layernorm_sp import initialize_layernorm_sp
from sglang.srt.platforms import current_platform
from sglang.srt.runtime_context import (
get_disagg,
@@ -280,6 +281,10 @@ def _init_parallel_groups(
server_args=server_args,
model_config=model_config,
)
initialize_layernorm_sp(
server_args=server_args,
model_config=model_config,
)
if is_npu():
register_sgl_tp_rank(gpu_id)
+69 -1
View File
@@ -31,6 +31,7 @@ from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.environ import envs
from sglang.srt.layers import layernorm_sp
from sglang.srt.layers.attention.dsa.utils import (
dsa_use_prefill_cp,
is_dsa_enable_prefill_cp,
@@ -72,7 +73,7 @@ from sglang.srt.model_executor.cuda_graph_config import (
Phase,
check_cuda_graph_backend,
)
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
from sglang.srt.runtime_context import (
get_exec,
get_forward,
@@ -483,6 +484,7 @@ class LayerCommunicator:
force_layernorm_before_dp_gather: bool = False,
enable_fused_ar_quant: bool = False,
fused_ar_quant_keep_bf16: bool = False,
_is_sp_variant: bool = False,
):
self.layer_scatter_modes = layer_scatter_modes
self.input_layernorm = input_layernorm
@@ -503,6 +505,30 @@ class LayerCommunicator:
get_spec().speculative_algorithm
)
# Under LayerNorm SP the norm/residual run on the sequence shard with no
# collectives, so delegate to an all-SCATTERED sibling while the region is
# active. _is_sp_variant stops the sibling from building its own.
self._sp_variant: Optional[LayerCommunicator] = None
if not _is_sp_variant and layernorm_sp.layernorm_sp_enabled():
self._sp_variant = LayerCommunicator(
layer_scatter_modes=LayerScatterModes(
layer_input_mode=ScatterMode.SCATTERED,
attn_mode=ScatterMode.SCATTERED,
mlp_mode=ScatterMode.SCATTERED,
middle_residual_mode=ScatterMode.SCATTERED,
layer_output_mode=ScatterMode.SCATTERED,
),
input_layernorm=input_layernorm,
post_attention_layernorm=post_attention_layernorm,
allow_reduce_scatter=allow_reduce_scatter,
is_last_layer=is_last_layer,
qkv_latent_func=qkv_latent_func,
force_layernorm_before_dp_gather=force_layernorm_before_dp_gather,
enable_fused_ar_quant=enable_fused_ar_quant,
fused_ar_quant_keep_bf16=fused_ar_quant_keep_bf16,
_is_sp_variant=True,
)
def _post_init_communicate(self):
self._communicate_simple_fn = CommunicateSimpleFn.get_fn(
input_mode=self.layer_scatter_modes.layer_input_mode,
@@ -592,6 +618,24 @@ class LayerCommunicator:
quant_format: str = "",
post_residual_addition: Optional[torch.Tensor] = None,
):
# residual is None marks the first decoder layer, where the SP region
# opens: re-evaluated per forward so a crash mid-loop cannot leak into
# the next one.
if self._sp_variant is not None:
if residual is None:
get_forward().set(
"sp_active", forward_batch.forward_mode == ForwardMode.EXTEND
)
if get_forward().sp_active:
hidden_states = layernorm_sp.sp_entry_scatter(hidden_states)
if get_forward().sp_active:
return self._sp_variant.prepare_attn(
hidden_states,
residual,
forward_batch,
quant_format,
post_residual_addition,
)
if get_attn_tp_context().input_scattered:
hidden_states, residual = self._tp_reduce_scatter(
hidden_states,
@@ -788,6 +832,10 @@ class LayerCommunicator:
forward_batch: ForwardBatch,
cache=None,
):
if self._sp_variant is not None and get_forward().sp_active:
return self._sp_variant.prepare_mlp(
hidden_states, residual, forward_batch, cache
)
if cache is not None:
self._context.cache = cache
@@ -805,6 +853,10 @@ class LayerCommunicator:
residual: torch.Tensor,
forward_batch: ForwardBatch,
):
if self._sp_variant is not None and get_forward().sp_active:
return self._sp_variant.postprocess_layer(
hidden_states, residual, forward_batch
)
return self._communicate_summable_tensor_pair_fn(
hidden_states=hidden_states,
residual=residual,
@@ -1028,6 +1080,22 @@ class CommunicateWithAllReduceAndLayerNormFn:
):
return CommunicateWithAllReduceAndLayerNormFn._simple
if (
hidden_states_input_mode == ScatterMode.SCATTERED
and residual_input_mode == ScatterMode.SCATTERED
and hidden_states_output_mode == ScatterMode.SCATTERED
and residual_output_mode == ScatterMode.SCATTERED
):
# Megatron LayerNorm sequence parallelism (layers/layernorm_sp.py):
# activations stay sequence-sharded across the attn->mlp boundary, so
# there is nothing to gather or scatter here -- just the residual add
# plus LayerNorm on the local shard. The row-parallel o_proj already
# issued the reduce-scatter (g-bar) that the all-reduce would have
# done, and the g all-gather is fused into the next column-parallel
# linear. Distinct from the branch above because under pure TP
# attn_tp_size == tp_size > 1, so that gate does not fire.
return CommunicateWithAllReduceAndLayerNormFn._simple
if (
(hidden_states_input_mode == ScatterMode.TP_ATTN_FULL)
and (
+245
View File
@@ -0,0 +1,245 @@
# Copyright 2023-2024 SGLang Team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Megatron-style LayerNorm sequence parallelism (SP, arXiv:2205.05198).
Under pure tensor parallelism the row-parallel ``all_reduce`` is algebraically a
``reduce_scatter`` (g-bar) followed by an ``all_gather`` (g). Splitting it that
way lets the LayerNorm / residual regions run on sequence-sharded activations --
each rank holds 1/tp of the tokens -- which cuts the transient activation memory
of long-context prefill with no extra communication volume (all_reduce and
reduce_scatter+all_gather move the same bytes).
Everything SP lives here so the feature stays decoupled from model code and from
``dp_attention.py``:
- which models opt in (the Qwen3-dense allowlist) and config validation,
- the per-forward ``sp_active`` flag (a ForwardFlags bool) read at depth by the
participant linears and the ``LayerCommunicator``,
- the entry-scatter / exit-gather collectives, and
- the fused matmul + collective fast-paths for the participant linears.
SP runs for prefill (EXTEND) only and is off by default; with the flag off,
nothing in this module executes.
"""
from __future__ import annotations
from typing import Optional
import torch
from sglang.srt.distributed import get_tp_group
from sglang.srt.runtime_context import get_flags, get_forward
from sglang.srt.utils.common import ceil_align
# Architectures whose decoder layers route attention/MLP through
# ``LayerCommunicator`` with the standard participant linears, and for which SP
# has been validated. Other models reject --enable-layernorm-sp at construction.
# The mechanism is generic; extend the allowlist as families are validated.
SP_SUPPORTED_ARCHITECTURES = frozenset({"Qwen3ForCausalLM"})
def initialize_layernorm_sp(*, server_args, model_config) -> None:
"""Materialize ``flags.sp.enabled``; runs once per worker after distributed
setup, alongside ``initialize_dp_attention``."""
architectures = model_config.hf_config.architectures
get_flags().sp.enabled = bool(
server_args.enable_layernorm_sp
and architectures
and architectures[0] in SP_SUPPORTED_ARCHITECTURES
)
def layernorm_sp_enabled() -> bool:
return get_flags().sp.enabled
def runs_sp(forward_mode) -> bool:
"""Whether this forward runs SP: an enabled model, prefill only.
Code outside the CUDA-graph-captured region must use this and not
``get_forward().sp_active``: Python writes made inside that region do not
re-execute on graph replay, so the flag is stale there.
"""
from sglang.srt.model_executor.forward_batch_info import ForwardMode
return layernorm_sp_enabled() and forward_mode == ForwardMode.EXTEND
class _SPForwardState:
"""Real (unpadded) token count of the current SP forward.
An instance attribute, not a ForwardFlags int slot: this is read inside
torch.compile-traced linear code, where dynamo gives an attribute-source int
automatic-dynamic, while a dict-slot int recompiles per sequence length.
"""
num_tokens: int = 0
_sp_state = _SPForwardState()
def set_sp_num_tokens(num_tokens: int) -> None:
_sp_state.num_tokens = num_tokens
def sp_num_tokens() -> int:
return _sp_state.num_tokens
# --- entry scatter / exit gather (once per forward, at the boundary) ----------
def sp_entry_scatter(hidden_states: torch.Tensor) -> torch.Tensor:
"""Shard the replicated ``[M, h]`` hidden states along the token dim.
Pads M up to a multiple of tp_size; the padding rows are dropped by the exit
gather. The input is replicated across the TP group, so this is a local slice.
"""
num_tokens = hidden_states.shape[0]
set_sp_num_tokens(num_tokens)
tp_group = get_tp_group()
tp_size = tp_group.world_size
if tp_size == 1:
return hidden_states
padded = ceil_align(num_tokens, tp_size)
if padded != num_tokens:
hidden_states = torch.nn.functional.pad(
hidden_states, (0, 0, 0, padded - num_tokens)
)
return hidden_states.tensor_split(tp_size)[tp_group.rank_in_group].contiguous()
def sp_exit_gather(hidden_states: torch.Tensor, num_tokens: int) -> torch.Tensor:
"""g: all-gather the per-rank shards back to the full sequence along dim 0,
then narrow to ``num_tokens`` (dropping the entry-scatter padding)."""
tp_group = get_tp_group()
tp_size = tp_group.world_size
if tp_size == 1:
return hidden_states[:num_tokens]
hidden_states = hidden_states.contiguous()
output = hidden_states.new_empty(
(hidden_states.shape[0] * tp_size, *hidden_states.shape[1:])
)
tp_group.all_gather_into_tensor(output, hidden_states)
return output[:num_tokens]
def maybe_exit_gather(
*,
hidden_states: torch.Tensor,
hidden_states_before_norm: Optional[torch.Tensor],
input_ids: Optional[torch.Tensor],
forward_mode,
) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
"""Undo the sequence sharding before the LM head, and leave the region.
No-op unless this forward runs SP. The token count comes from ``input_ids``
and the predicate from ``runs_sp``, so this stays correct on CUDA graph
replay, where the writes made inside the captured region do not re-execute.
"""
if not runs_sp(forward_mode) or input_ids is None:
return hidden_states, hidden_states_before_norm
num_tokens = input_ids.shape[0]
hidden_states = sp_exit_gather(hidden_states, num_tokens=num_tokens)
if hidden_states_before_norm is not None:
hidden_states_before_norm = sp_exit_gather(
hidden_states_before_norm, num_tokens=num_tokens
)
get_forward().set("sp_active", False)
return hidden_states, hidden_states_before_norm
# --- fused matmul + collective fast-paths for the participant linears ---------
# Fused matmul+reduce-scatter (g-bar) and all-gather+matmul (g) overlap the
# collective with the GEMM. Availability is probed once at import; TP groups are
# registered for symmetric memory lazily by the fused ops on first use (the old
# enable_symm_mem_for_group is a deprecated no-op), so we only import the module
# to register the torch.ops.symm_mem namespace the probe checks. NVLink/NVSwitch.
try:
import torch.distributed._symmetric_memory # noqa: F401
_HAS_TORCH_SYMM_MEM_FUSED = hasattr(
torch.ops.symm_mem, "fused_matmul_reduce_scatter"
) and hasattr(torch.ops.symm_mem, "fused_all_gather_matmul")
except Exception:
_HAS_TORCH_SYMM_MEM_FUSED = False
def sp_fused_matmul_eligible(linear) -> bool:
"""Whether the torch symm_mem fused matmul+collective fast-path applies: the
ops are available and ``linear`` is unquantized, bias-free, bf16/fp16 (the
case the fused ops support). Depends only on static layer properties, so the
decision is identical across TP ranks.
"""
if not _HAS_TORCH_SYMM_MEM_FUSED or linear.bias is not None:
return False
from sglang.srt.layers.quantization.unquant import UnquantizedLinearMethod
if not isinstance(linear.quant_method, UnquantizedLinearMethod):
return False
return linear.weight.dtype in (torch.bfloat16, torch.float16)
def column_parallel_g_matmul(
linear, input_parallel: torch.Tensor, bias
) -> torch.Tensor:
"""Megatron SP g for a ColumnParallelLinear participant (qkv / gate_up).
The input is this rank's sequence shard ``[M_pad/tp, K]``; all-gather it back
to the full sequence, matmul, and narrow to the real token count (recorded at
the entry scatter). Uses the fused symm-mem kernel when eligible (all-gather +
GEMM in one shot), else a plain all-gather + matmul.
"""
num_tokens = sp_num_tokens()
if sp_fused_matmul_eligible(linear):
group_name = get_tp_group().device_group.group_name
_, mm_outputs = torch.ops.symm_mem.fused_all_gather_matmul(
input_parallel.contiguous(),
[linear.weight.t()],
gather_dim=0,
group_name=group_name,
)
return mm_outputs[0][:num_tokens]
gathered = sp_exit_gather(input_parallel, num_tokens=num_tokens)
return linear.quant_method.apply(linear, gathered, bias)
def row_parallel_gbar_matmul(linear, input_: torch.Tensor, bias) -> torch.Tensor:
"""Megatron SP g-bar for a RowParallelLinear participant (o_proj / down).
Computes ``input_ @ weight.T`` and reduce-scatters (sum) the result across the
TP group along dim 0, leaving this rank's ``[M_pad/tp, h]`` shard. The token
dim is padded to a multiple of tp_size (padding rows are zeros, dropped by the
exit gather). Uses the fused symm-mem kernel when eligible, else matmul + a
plain reduce-scatter.
"""
tp_size = linear.tp_size
x = input_.contiguous()
num_tokens = x.shape[0]
padded = ceil_align(num_tokens, tp_size)
if padded != num_tokens:
x = torch.nn.functional.pad(x, (0, 0, 0, padded - num_tokens))
if sp_fused_matmul_eligible(linear):
group_name = get_tp_group().device_group.group_name
return torch.ops.symm_mem.fused_matmul_reduce_scatter(
x,
linear.weight.t(),
"sum",
scatter_dim=0,
group_name=group_name,
)
full = linear.quant_method.apply(linear, x, bias)
output = full.new_empty((padded // tp_size, *full.shape[1:]))
get_tp_group().reduce_scatter_tensor(output, full)
return output
+25 -1
View File
@@ -25,6 +25,7 @@ from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.environ import envs
from sglang.srt.layers import layernorm_sp
from sglang.srt.layers.dp_attention import (
is_allocation_symmetric,
)
@@ -39,7 +40,7 @@ from sglang.srt.layers.parameter import (
_ColumnvLLMParameter,
)
from sglang.srt.layers.utils import pad_or_narrow_weight
from sglang.srt.runtime_context import get_exec, get_parallel
from sglang.srt.runtime_context import get_exec, get_forward, get_parallel
from sglang.srt.utils import get_bool_env_var, is_cpu, is_hip, is_npu, set_weight_attrs
if TYPE_CHECKING:
@@ -492,6 +493,14 @@ class ColumnParallelLinear(LinearBase):
def forward(self, input_):
bias = self.bias if not self.skip_bias_add else None
# Megatron SP "g": the input is this rank's [M_pad/tp, K] sequence shard;
# all-gather to the full sequence and matmul. Participants (qkv/gate_up)
# have gather_output=False, so there is no output all-gather to reconcile.
if get_forward().sp_active and self.tp_size > 1:
output = layernorm_sp.column_parallel_g_matmul(self, input_, bias)
output_bias = self.bias if self.skip_bias_add else None
return output, output_bias
# Matrix multiply.
assert self.quant_method is not None
output_parallel = self.quant_method.apply(self, input_, bias)
@@ -1621,6 +1630,21 @@ class RowParallelLinear(LinearBase):
# Only fuse bias add into GEMM for rank 0 (this ensures that
# bias will not get added more than once in TP>1 case)
bias_ = None if (self.tp_rank > 0 or self.skip_bias_add) else self.bias
# Megatron SP "g-bar": reduce-scatter along the token dim instead of
# all-reduce, leaving the output sharded for the next SP LayerNorm region.
# Fires regardless of reduce_results: o_proj / down are built
# reduce_results=False, so under SP the linear owns the reduction.
if (
get_forward().sp_active
and self.tp_size > 1
and not skip_all_reduce
and output_tensor is None
):
output = layernorm_sp.row_parallel_gbar_matmul(self, input_parallel, bias_)
output_bias = self.bias if self.skip_bias_add else None
return output, output_bias
if self.use_dp_attention_reduce:
symm_ctx = use_symmetric_memory(get_parallel().attn_tp_group)
else:
@@ -28,6 +28,7 @@ from sglang.srt.beam_search.logits_capture import BeamLogitsCapture
from sglang.srt.distributed import get_tp_group
from sglang.srt.distributed.device_communicators import triton_symm_mem_ag
from sglang.srt.environ import envs
from sglang.srt.layers import layernorm_sp
from sglang.srt.layers.aux_hidden_states import (
AuxHiddenStates,
pack_aux_hidden_states,
@@ -439,6 +440,15 @@ class LogitsProcessor(nn.Module):
if _autotune_run_lm_head is False:
return LogitsProcessorOutput(next_token_logits=None)
# Under LayerNorm SP the decoder loop leaves these sequence-sharded; undo
# that before the LM head, which must not participate.
hidden_states, hidden_states_before_norm = layernorm_sp.maybe_exit_gather(
hidden_states=hidden_states,
hidden_states_before_norm=hidden_states_before_norm,
input_ids=input_ids,
forward_mode=logits_metadata.forward_mode,
)
# Multi-item scoring only for prefill-only requests with pre-computed indices.
if multi_item_delimiter_indices is not None and logits_metadata.is_prefill_only:
return self.compute_logprobs_for_multi_item_scoring(
+14 -1
View File
@@ -522,6 +522,15 @@ class DpFlags(_FlagGroupBase):
buffer_device: Any = None
@dataclasses.dataclass
class SpFlags(_FlagGroupBase):
"""LayerNorm sequence-parallelism flags, materialized by
``initialize_layernorm_sp`` (after distributed setup; reads the model
config). See ``layers.layernorm_sp``."""
enabled: bool = False
@dataclasses.dataclass
class Flags(_FlagGroupBase):
"""Root of the runtime-flags tier.
@@ -529,12 +538,13 @@ class Flags(_FlagGroupBase):
Resolved configuration lives in the config bags below (projected from the
declarations at publish) — this tier only carries genuine runtime
state whose value is not a function of the configuration alone, grouped
by lifecycle (``capture``) or subsystem (``moe`` / ``dp``).
by lifecycle (``capture``) or subsystem (``moe`` / ``dp`` / ``sp``).
"""
capture: CaptureFlags = dataclasses.field(default_factory=CaptureFlags)
moe: MoeFlags = dataclasses.field(default_factory=MoeFlags)
dp: DpFlags = dataclasses.field(default_factory=DpFlags)
sp: SpFlags = dataclasses.field(default_factory=SpFlags)
@dataclasses.dataclass
@@ -606,6 +616,8 @@ class ForwardFlags:
"fuse_mlp_allreduce": False,
"mlp_reduce_scatter": False,
"flashinfer_trtllm_bypass": False,
# LayerNorm sequence parallelism region; see layers/layernorm_sp.py.
"sp_active": False,
}
# Read/written inside compiled graphs (vocab embedding, communicator,
@@ -620,6 +632,7 @@ class ForwardFlags:
"fuse_mlp_allreduce",
"mlp_reduce_scatter",
"flashinfer_trtllm_bypass",
"sp_active",
}
)
+9
View File
@@ -1124,6 +1124,15 @@ class ServerArgs:
"Shard dense MLP weights across the attention TP group under DP attention.",
NS("parallel"),
] = False
enable_layernorm_sp: A[
bool,
"Enable Megatron-style sequence parallelism (arXiv:2205.05198) for the "
"LayerNorm/residual regions under pure tensor parallelism: the row-parallel "
"all-reduce becomes reduce-scatter + all-gather, so LayerNorm runs on "
"sequence-sharded activations with no extra communication volume. "
"Prefill only; Qwen3 dense; requires tp_size > 1 and NVLink/NVSwitch.",
NS("parallel"),
] = False
disable_attn_tp_gather: A[
bool,
"Disable scheduler-side attn_tp_gather (the upstream SP path "