qwen 3.8 rebase (#35758)

Co-authored-by: cherichy <cherichy@outlook.com>
Co-authored-by: guangyunh-nv <guangyunh@nvidia.com>
Co-authored-by: jiahanc <jiahanc@nvidia.com>
Co-authored-by: jinyangyuan-nvidia <joyuan@nvidia.com>
Co-authored-by: Cheng Hang <chang@nvidia.com>
Co-authored-by: Yicheng Qiang <yqiang@nvidia.com>
Co-authored-by: Sam Li <lsam@nvidia.com>
Co-authored-by: Tom-Zheng <tizheng@nvidia.com>
Co-authored-by: Yangmin Li <yangminl@nvidia.com>
Co-authored-by: xiaoweiw-nv <xiaoweiw@nvidia.com>
Co-authored-by: Zheng Li <lizheng.cs@zju.edu.cn>
Co-authored-by: yizhang2077 <1109276519@qq.com>
Co-authored-by: Ke Bao <ispobaoke@gmail.com>
Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com>
Co-authored-by: Zijie Xia <zijie.xia@radixark.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Qiaolin Yu
2026-08-28 20:41:34 -07:00
committed by GitHub
co-authored by cherichy guangyunh-nv jiahanc jinyangyuan-nvidia Cheng Hang Yicheng Qiang Sam Li Tom-Zheng Yangmin Li xiaoweiw-nv Zheng Li yizhang2077 Ke Bao Xinyuan Tong Yuhao Yang Zijie Xia github-actions[bot]
parent ca8cc101b8
commit 5f216fc33f
97 changed files with 14483 additions and 373 deletions
@@ -151,11 +151,10 @@ __global__ __launch_bounds__(1024, 2) void //
// Read this token's kTopK destinations once (fully unrolled).
const auto* src2dst_row = params.src2dst + static_cast<uint64_t>(token_id) * kTopK;
const auto* topk_ids_row = params.topk_ids + static_cast<uint64_t>(token_id) * kTopK;
int32_t dst_rows[kTopK];
#pragma unroll
for (uint32_t i = 0; i < kTopK; ++i) {
dst_rows[i] = (topk_ids_row[i] >= 0) ? src2dst_row[i] : -1;
dst_rows[i] = src2dst_row[i];
}
const uint32_t group_id = tid / kThreadsPerGroup;
@@ -330,6 +330,312 @@ def fused_qkvzba_split_reshape_cat_contiguous(
return mixed_qkv, z, b, a
# Fusion begins after the quantized GEMMs: qkvz=[q|k|v|z], ba=[b|a].
# This tail unpacks both projections and updates the causal Conv1D state.
@triton.jit
def _fused_qkvzba_causal_conv1d_update_contiguous_kernel(
mixed_qkv,
z,
b,
a,
mixed_qkvz,
mixed_ba,
conv_state,
conv_weight,
conv_bias,
conv_state_indices,
stride_qkvz_batch: tl.constexpr,
stride_qkvz_dim: tl.constexpr,
stride_ba_batch: tl.constexpr,
stride_ba_dim: tl.constexpr,
stride_state_batch: tl.constexpr,
stride_state_dim: tl.constexpr,
stride_state_pos: tl.constexpr,
stride_weight_dim: tl.constexpr,
stride_weight_width: tl.constexpr,
stride_state_indices: tl.constexpr,
QKV_DIM: tl.constexpr,
V_DIM: tl.constexpr,
NUM_V_HEADS: tl.constexpr,
NUM_STATE_SLOTS: tl.constexpr,
STATE_LEN: tl.constexpr,
KERNEL_WIDTH: tl.constexpr,
HAS_BIAS: tl.constexpr,
SILU_ACTIVATION: tl.constexpr,
PAD_SLOT_ID: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
batch_idx = tl.program_id(0)
dim_idx = tl.program_id(1) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
qkv_mask = dim_idx < QKV_DIM
x = tl.load(
mixed_qkvz + batch_idx * stride_qkvz_batch + dim_idx * stride_qkvz_dim,
mask=qkv_mask,
other=0.0,
)
state_slot = tl.load(conv_state_indices + batch_idx * stride_state_indices).to(
tl.int64
)
# Treat every out-of-range index as padding so stale replay metadata cannot
# turn an indexed state update into an OOB access.
valid_slot = (
(state_slot != PAD_SLOT_ID) & (state_slot >= 0) & (state_slot < NUM_STATE_SLOTS)
)
state_base = (
conv_state + state_slot * stride_state_batch + dim_idx * stride_state_dim
)
acc = tl.zeros((BLOCK_SIZE,), dtype=tl.float32)
if HAS_BIAS:
acc += tl.load(conv_bias + dim_idx, mask=qkv_mask, other=0.0).to(tl.float32)
# Match the deployed direct-Triton update exactly. Its effective decode
# state length is width-1 even when the physical cache tensor is wider.
for pos in tl.static_range(KERNEL_WIDTH - 1):
state_value = tl.load(
state_base + pos * stride_state_pos,
mask=qkv_mask & valid_slot,
other=0.0,
)
weight_value = tl.load(
conv_weight + dim_idx * stride_weight_dim + pos * stride_weight_width,
mask=qkv_mask,
other=0.0,
)
# Do not force an FP32 multiply here. This expression deliberately
# retains the operand types/order of causal_conv1d_triton.py.
acc += state_value * weight_value
last_weight = tl.load(
conv_weight
+ dim_idx * stride_weight_dim
+ (KERNEL_WIDTH - 1) * stride_weight_width,
mask=qkv_mask,
other=0.0,
)
acc += x * last_weight
if SILU_ACTIVATION:
conv_out = acc / (1.0 + tl.exp(-acc))
else:
conv_out = acc
# The legacy kernel leaves padded rows' input unchanged.
conv_out = tl.where(valid_slot, conv_out, x)
tl.store(
mixed_qkv + batch_idx * QKV_DIM + dim_idx,
conv_out,
mask=qkv_mask,
)
# The direct-Triton wrapper sets effective state_len=width-1 for decode.
for pos in tl.static_range(KERNEL_WIDTH - 2):
next_value = tl.load(
state_base + (pos + 1) * stride_state_pos,
mask=qkv_mask & valid_slot,
other=0.0,
)
tl.store(
state_base + pos * stride_state_pos,
next_value,
mask=qkv_mask & valid_slot,
)
tl.store(
state_base + (KERNEL_WIDTH - 2) * stride_state_pos,
x,
mask=qkv_mask & valid_slot,
)
# The first feature lanes also materialize the smaller downstream tensors.
z_mask = dim_idx < V_DIM
z_value = tl.load(
mixed_qkvz
+ batch_idx * stride_qkvz_batch
+ (QKV_DIM + dim_idx) * stride_qkvz_dim,
mask=z_mask,
other=0.0,
)
tl.store(z + batch_idx * V_DIM + dim_idx, z_value, mask=z_mask)
gate_mask = dim_idx < NUM_V_HEADS
b_value = tl.load(
mixed_ba + batch_idx * stride_ba_batch + dim_idx * stride_ba_dim,
mask=gate_mask,
other=0.0,
)
a_value = tl.load(
mixed_ba
+ batch_idx * stride_ba_batch
+ (NUM_V_HEADS + dim_idx) * stride_ba_dim,
mask=gate_mask,
other=0.0,
)
tl.store(b + batch_idx * NUM_V_HEADS + dim_idx, b_value, mask=gate_mask)
tl.store(a + batch_idx * NUM_V_HEADS + dim_idx, a_value, mask=gate_mask)
def can_use_fused_qkvzba_causal_conv1d_update_contiguous(
mixed_qkvz: torch.Tensor,
mixed_ba: torch.Tensor,
conv_state: torch.Tensor,
conv_weight: torch.Tensor,
conv_bias: torch.Tensor | None,
conv_state_indices: torch.Tensor,
*,
qkv_dim: int,
v_dim: int,
num_v_heads: int,
activation: str | None,
) -> tuple[bool, str]:
"""Return an explicit eligibility decision for the decode fusion."""
tensors = (mixed_qkvz, mixed_ba, conv_state, conv_weight, conv_state_indices)
if not all(isinstance(tensor, torch.Tensor) for tensor in tensors):
return False, "all inputs must be torch.Tensor instances"
if not all(tensor.is_cuda for tensor in tensors):
return False, "CUDA tensors are required"
if mixed_qkvz.ndim != 2 or mixed_ba.ndim != 2:
return False, "projection outputs must be rank-2"
if conv_state.ndim != 3 or conv_weight.ndim != 2:
return False, "Conv1D state/weight ranks must be 3/2"
if conv_state_indices.ndim != 1:
return False, "conv_state_indices must be rank-1"
batch = mixed_qkvz.shape[0]
if mixed_ba.shape[0] != batch or conv_state_indices.shape[0] != batch:
return False, "batch dimensions must match"
if qkv_dim <= 0 or v_dim <= 0 or num_v_heads <= 0:
return False, "TP-local dimensions must be positive"
if mixed_qkvz.shape[1] != qkv_dim + v_dim:
return False, "qkvz layout is not contiguous [Q|K|V|Z]"
if mixed_ba.shape[1] != 2 * num_v_heads:
return False, "ba layout is not contiguous [B|A]"
if conv_state.shape[1] != qkv_dim or conv_weight.shape[0] != qkv_dim:
return False, "Conv1D feature dimension does not match packed QKV"
width = conv_weight.shape[1]
if width < 2 or width > 4:
return False, "only Conv1D widths 2 through 4 are supported"
if conv_state.shape[2] < width - 1:
return False, "Conv1D state is shorter than width - 1"
supported_dtypes = (torch.float16, torch.bfloat16, torch.float32)
if mixed_qkvz.dtype not in supported_dtypes:
return False, "QKVZ activation dtype must be FP16, BF16, or FP32"
if conv_state.dtype != mixed_qkvz.dtype or conv_weight.dtype != mixed_qkvz.dtype:
return False, "QKVZ, Conv1D state, and weight dtypes must match"
if mixed_ba.dtype not in supported_dtypes:
return False, "BA activation dtype must be FP16, BF16, or FP32"
if conv_bias is not None:
if (
not isinstance(conv_bias, torch.Tensor)
or not conv_bias.is_cuda
or conv_bias.ndim != 1
or conv_bias.shape[0] != qkv_dim
or conv_bias.dtype != mixed_qkvz.dtype
):
return False, "Conv1D bias contract is incompatible"
if activation not in (None, "silu", "swish"):
return False, "activation must be None, silu, or swish"
if mixed_qkvz.stride(1) != 1 or mixed_ba.stride(1) != 1:
return False, "projection feature dimensions must be contiguous"
if conv_weight.stride(1) != 1:
return False, "Conv1D weight width dimension must be contiguous"
if conv_state_indices.dtype not in (torch.int32, torch.int64):
return False, "conv_state_indices must be int32 or int64"
return True, "eligible"
def fused_qkvzba_causal_conv1d_update_contiguous(
mixed_qkvz: torch.Tensor,
mixed_ba: torch.Tensor,
conv_state: torch.Tensor,
conv_weight: torch.Tensor,
conv_bias: torch.Tensor | None,
conv_state_indices: torch.Tensor,
*,
qkv_dim: int,
v_dim: int,
num_v_heads: int,
head_v_dim: int,
activation: str | None,
pad_slot_id: int = -1,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Decode-only fused Qwen3.5 projection unpack and Conv1D state update."""
eligible, reason = can_use_fused_qkvzba_causal_conv1d_update_contiguous(
mixed_qkvz,
mixed_ba,
conv_state,
conv_weight,
conv_bias,
conv_state_indices,
qkv_dim=qkv_dim,
v_dim=v_dim,
num_v_heads=num_v_heads,
activation=activation,
)
if not eligible:
raise ValueError(f"Ineligible fused GDN decode projection/Conv1D: {reason}")
if v_dim != num_v_heads * head_v_dim:
raise ValueError(
"Ineligible fused GDN decode projection/Conv1D: "
"v_dim must equal num_v_heads * head_v_dim"
)
batch = mixed_qkvz.shape[0]
mixed_qkv = torch.empty(
(batch, qkv_dim), dtype=mixed_qkvz.dtype, device=mixed_qkvz.device
)
z = torch.empty(
(batch, num_v_heads, head_v_dim),
dtype=mixed_qkvz.dtype,
device=mixed_qkvz.device,
)
b = torch.empty(
(batch, num_v_heads),
dtype=mixed_ba.dtype,
device=mixed_ba.device,
)
a = torch.empty_like(b)
block_size = 256
grid = (batch, triton.cdiv(qkv_dim, block_size))
_fused_qkvzba_causal_conv1d_update_contiguous_kernel[grid](
mixed_qkv,
z,
b,
a,
mixed_qkvz,
mixed_ba,
conv_state,
conv_weight,
conv_bias,
conv_state_indices,
mixed_qkvz.stride(0),
mixed_qkvz.stride(1),
mixed_ba.stride(0),
mixed_ba.stride(1),
conv_state.stride(0),
conv_state.stride(1),
conv_state.stride(2),
conv_weight.stride(0),
conv_weight.stride(1),
conv_state_indices.stride(0),
QKV_DIM=qkv_dim,
V_DIM=v_dim,
NUM_V_HEADS=num_v_heads,
NUM_STATE_SLOTS=conv_state.shape[0],
STATE_LEN=conv_state.shape[2],
KERNEL_WIDTH=conv_weight.shape[1],
HAS_BIAS=conv_bias is not None,
SILU_ACTIVATION=activation in ("silu", "swish"),
PAD_SLOT_ID=pad_slot_id,
BLOCK_SIZE=block_size,
num_warps=8,
num_stages=2,
)
return mixed_qkv, z, b, a
@triton.jit
def fused_qkv_split_gdn_prefill_kernel(
q,
@@ -0,0 +1,52 @@
# Copyright (c) 2026 by FlashInfer 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.
"""MNNVL CuTe DSL AllReduce fusion backend internals."""
from importlib import import_module
from .config import (
KernelTarget,
MNNVLCuteDSLConfig,
MRangeDispatch,
ProtocolKind,
StaticProfile,
)
def __getattr__(name: str):
if name in {
"BT_ONLY_CONFIG",
"DEFAULT_CONFIG",
"HT_ONLY_CONFIG",
"LL_ONLY_CONFIG",
}:
presets = import_module(f"{__name__}.presets")
value = getattr(presets, name)
globals()[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"BT_ONLY_CONFIG",
"DEFAULT_CONFIG",
"HT_ONLY_CONFIG",
"LL_ONLY_CONFIG",
"KernelTarget",
"MNNVLCuteDSLConfig",
"MRangeDispatch",
"ProtocolKind",
"StaticProfile",
]
@@ -0,0 +1,231 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Configuration and token-count routing for the MNNVL CuTe DSL backend."""
from __future__ import annotations
from bisect import bisect_left
from dataclasses import dataclass
from enum import Enum
from typing import Generic, TypeVar
import torch
__all__ = [
"KernelTarget",
"MNNVLCuteDSLConfig",
"MRangeDispatch",
"ProtocolKind",
"StaticProfile",
]
class ProtocolKind(Enum):
LL = "ll"
BT = "bt"
HT = "ht"
PresetT = TypeVar("PresetT")
@dataclass(frozen=True, slots=True)
class KernelTarget(Generic[PresetT]):
protocol: ProtocolKind
preset: PresetT
TargetT = TypeVar("TargetT")
@dataclass(frozen=True, slots=True)
class MRangeDispatch(Generic[TargetT]):
"""Map contiguous positive token-count ranges to kernel targets."""
upper_bounds: tuple[int | None, ...]
targets: tuple[TargetT, ...]
def __post_init__(self) -> None:
if not self.upper_bounds:
raise ValueError("M range dispatch must contain at least one range")
if len(self.upper_bounds) != len(self.targets):
raise ValueError("M range upper bounds and targets must have equal length")
previous = 0
for index, upper_bound in enumerate(self.upper_bounds):
if upper_bound is None:
if index != len(self.upper_bounds) - 1:
raise ValueError("An unbounded M range must be the final range")
continue
if upper_bound <= previous:
raise ValueError("M range upper bounds must be strictly increasing")
previous = upper_bound
@property
def is_unbounded(self) -> bool:
return self.upper_bounds[-1] is None
@property
def finite_upper_bound(self) -> int | None:
return None if self.is_unbounded else self.upper_bounds[-1]
def supports(self, m: int) -> bool:
if m <= 0:
return False
upper_bound = self.finite_upper_bound
return upper_bound is None or m <= upper_bound
def select(self, m: int) -> TargetT:
if not self.supports(m):
raise ValueError(f"No kernel route supports M={m}")
finite_bounds = tuple(
upper_bound for upper_bound in self.upper_bounds if upper_bound is not None
)
index = bisect_left(finite_bounds, m)
return self.targets[index]
def referenced_protocols(self) -> frozenset[ProtocolKind]:
protocols = {
target.protocol
for target in self.targets
if isinstance(target, KernelTarget)
}
return frozenset(protocols)
def targets_for_capacity(self, capacity_m: int) -> tuple[TargetT, ...]:
if capacity_m <= 0:
return ()
selected = []
lower_bound = 1
for upper_bound, target in zip(self.upper_bounds, self.targets, strict=True):
if lower_bound > capacity_m:
break
selected.append(target)
if upper_bound is None:
break
lower_bound = upper_bound + 1
return tuple(selected)
def max_m_for_protocol(
self, protocol: ProtocolKind, *, capacity_m: int
) -> int | None:
lower_bound = 1
maximum = None
for upper_bound, target in zip(self.upper_bounds, self.targets, strict=True):
effective_upper_bound = capacity_m if upper_bound is None else upper_bound
if (
isinstance(target, KernelTarget)
and target.protocol is protocol
and lower_bound <= capacity_m
):
maximum = min(effective_upper_bound, capacity_m)
lower_bound = effective_upper_bound + 1
return maximum
@dataclass(frozen=True, slots=True)
class StaticProfile:
tp_size: int
hidden_size: int
top_k: int
dtype: torch.dtype
finalize_routes: MRangeDispatch[KernelTarget[object]]
all_reduce_routes: MRangeDispatch[KernelTarget[object]]
def __post_init__(self) -> None:
if self.hidden_size <= 0 or self.hidden_size % 8:
raise ValueError("hidden_size must be a positive multiple of 8")
def matches(
self,
*,
tp_size: int,
hidden_size: int,
top_k: int,
dtype: torch.dtype,
) -> bool:
return (
self.tp_size == tp_size
and self.hidden_size == hidden_size
and self.top_k == top_k
and self.dtype == dtype
)
def validate_capacity(self, capacity_m: int) -> None:
if capacity_m <= 0:
raise ValueError("capacity_m must be positive")
if not self.finalize_routes.supports(capacity_m):
raise ValueError(
"Finalize routes do not cover the requested workspace capacity"
)
if not self.all_reduce_routes.supports(capacity_m):
raise ValueError(
"AllReduce routes do not cover the requested workspace capacity"
)
@property
def referenced_protocols(self) -> frozenset[ProtocolKind]:
return (
self.finalize_routes.referenced_protocols()
| self.all_reduce_routes.referenced_protocols()
)
def protocol_capacity(
self, protocol: ProtocolKind, *, capacity_m: int
) -> int | None:
maxima = (
self.finalize_routes.max_m_for_protocol(protocol, capacity_m=capacity_m),
self.all_reduce_routes.max_m_for_protocol(protocol, capacity_m=capacity_m),
)
present = tuple(value for value in maxima if value is not None)
return max(present) if present else None
@dataclass(frozen=True, slots=True)
class MNNVLCuteDSLConfig:
"""Static profiles and routing policy for one backend configuration."""
profiles: tuple[StaticProfile, ...]
def __post_init__(self) -> None:
keys = [
(profile.tp_size, profile.hidden_size, profile.top_k, profile.dtype)
for profile in self.profiles
]
if not keys:
raise ValueError("A backend config must contain at least one profile")
if len(keys) != len(set(keys)):
raise ValueError("Backend config profiles must have unique static shapes")
def resolve(
self,
*,
tp_size: int,
hidden_size: int,
top_k: int,
dtype: torch.dtype,
capacity_m: int,
) -> StaticProfile:
for profile in self.profiles:
if profile.matches(
tp_size=tp_size,
hidden_size=hidden_size,
top_k=top_k,
dtype=dtype,
):
profile.validate_capacity(capacity_m)
return profile
raise ValueError("No MNNVL CuTe DSL profile supports this static shape")
@@ -0,0 +1,876 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Small standalone CuTe DSL and PTX primitives shared by Kernel backends."""
import cutlass
import cutlass.cute as cute
from cutlass import BFloat16, Float32, Int32, Int64, Uint16, Uint32
from cutlass._mlir import ir
from cutlass._mlir.dialects import llvm, vector
from cutlass.cutlass_dsl import T, dsl_user_op
WARP_SIZE = 32
VEC_BF16 = 8
QUAD_BF16 = 4
NEGATIVE_ZERO_BF16_BITS = 0x8000
NEGATIVE_ZERO_BF16_PAIR = 0x80008000
# CUTLASS cute::TMA::CacheHintSm100::EVICT_FIRST policy descriptor.
L2_EVICT_FIRST = 0x12F0000000000000
@dsl_user_op
def load_global_u32x4(
pointer: cute.Pointer,
*,
volatile: cutlass.Constexpr[bool] = False,
loc=None,
ip=None,
):
address = pointer.toint(loc=loc, ip=ip)
if volatile:
opcode = "ld.volatile.global.v4.u32"
else:
opcode = "ld.global.v4.u32"
loaded = llvm.inline_asm(
llvm.StructType.get_literal([T.i32()] * 4),
[address.ir_value(loc=loc, ip=ip)],
f"{opcode} {{$0, $1, $2, $3}}, [$4];",
"=r,=r,=r,=r,l",
has_side_effects=volatile,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([4], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(4)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@dsl_user_op
def load_global_u32x4_predicated(
pointer: cute.Pointer,
predicate: Int32,
*,
loc=None,
ip=None,
):
address = pointer.toint(loc=loc, ip=ip)
loaded = llvm.inline_asm(
llvm.StructType.get_literal([T.i32()] * 4),
[
address.ir_value(loc=loc, ip=ip),
Int32(predicate).ir_value(loc=loc, ip=ip),
],
(
"{\n\t"
".reg .pred p;\n\t"
"setp.ne.s32 p, $5, 0;\n\t"
"@!p mov.u32 $0, 0;\n\t"
"@!p mov.u32 $1, 0;\n\t"
"@!p mov.u32 $2, 0;\n\t"
"@!p mov.u32 $3, 0;\n\t"
"@p ld.global.v4.u32 {$0, $1, $2, $3}, [$4];\n\t"
"}"
),
"=r,=r,=r,=r,l,r",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([4], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(4)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@dsl_user_op
def load_global_u32(pointer: cute.Pointer, *, loc=None, ip=None) -> Uint32:
address = pointer.toint(loc=loc, ip=ip)
return Uint32(
llvm.inline_asm(
T.i32(),
[address.ir_value(loc=loc, ip=ip)],
"ld.global.u32 $0, [$1];",
"=r,l",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def load_global_u32_predicated(
pointer: cute.Pointer,
predicate: Int32,
*,
loc=None,
ip=None,
) -> Uint32:
address = pointer.toint(loc=loc, ip=ip)
return Uint32(
llvm.inline_asm(
T.i32(),
[
address.ir_value(loc=loc, ip=ip),
Int32(predicate).ir_value(loc=loc, ip=ip),
],
(
"{\n\t"
".reg .pred p;\n\t"
"setp.ne.s32 p, $2, 0;\n\t"
"@!p mov.u32 $0, 0;\n\t"
"@p ld.global.u32 $0, [$1];\n\t"
"}"
),
"=r,l,r",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def load_global_u32x2(pointer: cute.Pointer, *, loc=None, ip=None):
address = pointer.toint(loc=loc, ip=ip)
loaded = llvm.inline_asm(
llvm.StructType.get_literal([T.i32()] * 2),
[address.ir_value(loc=loc, ip=ip)],
"ld.global.v2.u32 {$0, $1}, [$2];",
"=r,=r,l",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([2], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(2)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 2, Uint32)
@dsl_user_op
def load_global_u32x2_predicated(
pointer: cute.Pointer,
predicate: Int32,
*,
loc=None,
ip=None,
):
address = pointer.toint(loc=loc, ip=ip)
loaded = llvm.inline_asm(
llvm.StructType.get_literal([T.i32()] * 2),
[
address.ir_value(loc=loc, ip=ip),
Int32(predicate).ir_value(loc=loc, ip=ip),
],
(
"{\n\t"
".reg .pred p;\n\t"
"setp.ne.s32 p, $3, 0;\n\t"
"@!p mov.u32 $0, 0;\n\t"
"@!p mov.u32 $1, 0;\n\t"
"@p ld.global.v2.u32 {$0, $1}, [$2];\n\t"
"}"
),
"=r,=r,l,r",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([2], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(2)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 2, Uint32)
@dsl_user_op
def store_global_u32x4(address: Int64, packed, *, loc=None, ip=None) -> None:
words = [packed[index].ir_value(loc=loc, ip=ip) for index in range(4)]
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), *words],
"st.global.v4.u32 [$0], {$1, $2, $3, $4};",
"l,r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def store_global_u32_address(
address: Int64,
value: Uint32,
*,
loc=None,
ip=None,
) -> None:
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), value.ir_value(loc=loc, ip=ip)],
"st.global.u32 [$0], $1;",
"l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def store_global_u32x2(address: Int64, packed, *, loc=None, ip=None) -> None:
words = [packed[index].ir_value(loc=loc, ip=ip) for index in range(2)]
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), *words],
"st.global.v2.u32 [$0], {$1, $2};",
"l,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def store_global_u16_bits(
address: Int64,
value: Uint32,
*,
loc=None,
ip=None,
) -> None:
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), value.ir_value(loc=loc, ip=ip)],
(
"{\n\t"
".reg .b16 bits;\n\t"
"cvt.u16.u32 bits, $1;\n\t"
"st.global.u16 [$0], bits;\n\t"
"}"
),
"l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def store_lamport_sentinel_u32x4(
address: Int64,
*,
loc=None,
ip=None,
) -> None:
sentinel = Uint32(NEGATIVE_ZERO_BF16_PAIR).ir_value(loc=loc, ip=ip)
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), sentinel, sentinel, sentinel, sentinel],
"st.global.v4.u32 [$0], {$1, $2, $3, $4};",
"l,r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def load_global_bf16_as_f32(
address: Int64,
*,
loc=None,
ip=None,
) -> Float32:
return Float32(
llvm.inline_asm(
T.f32(),
[address.ir_value(loc=loc, ip=ip)],
(
"{\n\t"
".reg .b16 bits;\n\t"
"ld.global.b16 bits, [$1];\n\t"
"cvt.f32.bf16 $0, bits;\n\t"
"}"
),
"=f,l",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def load_global_bf16_as_f32_predicated(
address: Int64,
predicate: Int32,
*,
loc=None,
ip=None,
) -> Float32:
return Float32(
llvm.inline_asm(
T.f32(),
[
address.ir_value(loc=loc, ip=ip),
Int32(predicate).ir_value(loc=loc, ip=ip),
],
(
"{\n\t"
".reg .pred p;\n\t"
".reg .b16 bits;\n\t"
"setp.ne.s32 p, $2, 0;\n\t"
"@!p mov.b16 bits, 0;\n\t"
"@p ld.global.b16 bits, [$1];\n\t"
"cvt.f32.bf16 $0, bits;\n\t"
"}"
),
"=f,l,r",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def f32_to_bf16_bits(value: Float32, *, loc=None, ip=None) -> Uint32:
return Uint32(
llvm.inline_asm(
T.i32(),
[value.ir_value(loc=loc, ip=ip)],
(
"{\n\t"
".reg .b16 bits;\n\t"
"cvt.rn.bf16.f32 bits, $1;\n\t"
"cvt.u32.u16 $0, bits;\n\t"
"}"
),
"=r,f",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def shuffle_sync_idx_u32(
value: Uint32,
source_lane: Int32,
*,
loc=None,
ip=None,
) -> Uint32:
return Uint32(
llvm.inline_asm(
T.i32(),
[
value.ir_value(loc=loc, ip=ip),
source_lane.ir_value(loc=loc, ip=ip),
],
"shfl.sync.idx.b32 $0, $1, $2, 0x1f, 0xffffffff;",
"=r,r,r",
# Preserve full-warp execution across later divergent consumers.
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def load_volatile_u32(pointer: cute.Pointer, *, loc=None, ip=None) -> Uint32:
address = pointer.toint(loc=loc, ip=ip)
return Uint32(
llvm.inline_asm(
T.i32(),
[address.ir_value(loc=loc, ip=ip)],
"ld.volatile.global.u32 $0, [$1];",
"=r,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def store_global_u32(
pointer: cute.Pointer,
value: Uint32,
*,
loc=None,
ip=None,
) -> None:
address = pointer.toint(loc=loc, ip=ip)
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), value.ir_value(loc=loc, ip=ip)],
"st.global.u32 [$0], $1;",
"l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def packed_u32x4_to_bf16x8(packed, *, loc=None, ip=None):
values = llvm.bitcast(
ir.VectorType.get([VEC_BF16], BFloat16.mlir_type, loc=loc),
packed.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(values, VEC_BF16, BFloat16)
@dsl_user_op
def packed_u32_to_bf16x2(packed: Uint32, *, loc=None, ip=None):
values = llvm.bitcast(
ir.VectorType.get([2], BFloat16.mlir_type, loc=loc),
packed.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(values, 2, BFloat16)
@dsl_user_op
def packed_u32x2_to_bf16x4(packed, *, loc=None, ip=None):
values = llvm.bitcast(
ir.VectorType.get([QUAD_BF16], BFloat16.mlir_type, loc=loc),
packed.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(values, QUAD_BF16, BFloat16)
@dsl_user_op
def bf16x8_to_packed_u32x4(values, *, loc=None, ip=None):
packed = llvm.bitcast(
ir.VectorType.get([4], T.i32(), loc=loc),
values.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@dsl_user_op
def bf16x2_to_packed_u32(values, *, loc=None, ip=None) -> Uint32:
return Uint32(
llvm.bitcast(
T.i32(),
values.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
)
@dsl_user_op
def bf16x4_to_packed_u32x2(values, *, loc=None, ip=None):
packed = llvm.bitcast(
ir.VectorType.get([2], T.i32(), loc=loc),
values.ir_value(loc=loc, ip=ip),
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 2, Uint32)
@cute.jit
def sanitize_negative_zero_u32x4(packed):
sanitized = cute.make_rmem_tensor(cute.make_layout((4,)), Uint32)
for index in cutlass.range_constexpr(4):
sanitized[index] = sanitize_negative_zero_u32(packed[index])
return sanitized.load()
@cute.jit
def sanitize_negative_zero_u32(word: Uint32) -> Uint32:
low = Uint16(word & Uint32(0xFFFF))
high = Uint16(word >> Uint32(16))
if low == Uint16(NEGATIVE_ZERO_BF16_BITS):
word = word & Uint32(0xFFFF0000)
if high == Uint16(NEGATIVE_ZERO_BF16_BITS):
word = word & Uint32(0x0000FFFF)
return word
@cute.jit
def sanitize_negative_zero_u32x2(packed):
sanitized = cute.make_rmem_tensor(cute.make_layout((2,)), Uint32)
for index in cutlass.range_constexpr(2):
sanitized[index] = sanitize_negative_zero_u32(packed[index])
return sanitized.load()
@cute.jit
def fragment_has_negative_zero(packed):
dirty = False
for index in cutlass.range_constexpr(4):
word = packed[index]
dirty = (
dirty
| (Uint16(word & Uint32(0xFFFF)) == Uint16(NEGATIVE_ZERO_BF16_BITS))
| (Uint16(word >> Uint32(16)) == Uint16(NEGATIVE_ZERO_BF16_BITS))
)
return dirty
@dsl_user_op
def map_shared_to_peer(
smem_pointer: cute.Pointer,
peer_rank: Int32,
*,
loc=None,
ip=None,
) -> Int32:
address = smem_pointer.toint(loc=loc, ip=ip).ir_value(loc=loc, ip=ip)
return Int32(
llvm.inline_asm(
T.i32(),
[address, peer_rank.ir_value(loc=loc, ip=ip)],
"mapa.shared::cluster.u32 $0, $1, $2;",
"=r,r,r",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
)
@dsl_user_op
def store_shared_cluster_f32(
remote_address: Int32,
value: Float32,
*,
loc=None,
ip=None,
) -> None:
llvm.inline_asm(
None,
[
remote_address.ir_value(loc=loc, ip=ip),
value.ir_value(loc=loc, ip=ip),
],
"st.shared::cluster.f32 [$0], $1;",
"r,f",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def load_shared_u32x4(pointer: cute.Pointer, *, loc=None, ip=None):
address = pointer.toint(loc=loc, ip=ip)
# Prevent motion across the named-barrier pipeline protocol.
loaded = llvm.inline_asm(
llvm.StructType.get_literal([T.i32()] * 4),
[Int32(address).ir_value(loc=loc, ip=ip)],
"ld.shared.v4.u32 {$0, $1, $2, $3}, [$4];",
"=r,=r,=r,=r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([4], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(4)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@dsl_user_op
def store_shared_u32x4(
pointer: cute.Pointer,
packed,
*,
loc=None,
ip=None,
) -> None:
address = pointer.toint(loc=loc, ip=ip)
words = [packed[index].ir_value(loc=loc, ip=ip) for index in range(4)]
llvm.inline_asm(
None,
[Int32(address).ir_value(loc=loc, ip=ip), *words],
"st.shared.v4.u32 [$0], {$1, $2, $3, $4};",
"r,r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def load_global_u32x4_address(
address: Int64,
*,
volatile: cutlass.Constexpr[bool] = False,
loc=None,
ip=None,
):
opcode = "ld.volatile.global.v4.u32" if volatile else "ld.global.v4.u32"
loaded = llvm.inline_asm(
llvm.StructType.get_literal([T.i32()] * 4),
[address.ir_value(loc=loc, ip=ip)],
f"{opcode} {{$0, $1, $2, $3}}, [$4];",
"=r,=r,=r,=r,l",
has_side_effects=volatile,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([4], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(4)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@dsl_user_op
def packed_negative_zero_bf16x8(*, loc=None, ip=None):
word = Uint32(NEGATIVE_ZERO_BF16_PAIR).ir_value(loc=loc, ip=ip)
packed = vector.from_elements(
ir.VectorType.get([4], T.i32(), loc=loc),
[word, word, word, word],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@dsl_user_op
def cpasync_bulk_g2s(
gmem_ptr: cute.Pointer,
smem_ptr: cute.Pointer,
barrier_ptr: cute.Pointer,
size_bytes: Int32,
*,
loc=None,
ip=None,
) -> None:
operands = [
gmem_ptr.toint(loc=loc, ip=ip).ir_value(),
smem_ptr.toint(loc=loc, ip=ip).ir_value(),
barrier_ptr.toint(loc=loc, ip=ip).ir_value(),
size_bytes.ir_value(loc=loc, ip=ip),
Int64(L2_EVICT_FIRST).ir_value(),
]
llvm.inline_asm(
None,
operands,
(
"cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes"
".L2::cache_hint [$1], [$0], $3, [$2], $4;"
),
"l,r,r,r,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def fence_proxy_async_shared_cta(*, loc=None, ip=None) -> None:
llvm.inline_asm(
None,
[],
"fence.proxy.async.shared::cta;",
"",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def remote_release_add1_u32(address: Int64, *, loc=None, ip=None) -> None:
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip)],
"red.release.sys.global.add.u32 [$0], 1;",
"l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def ldmc_bf16x8(address: Int64, *, loc=None, ip=None):
loaded = llvm.inline_asm(
llvm.StructType.get_literal([T.i32()] * 4),
[address.ir_value(loc=loc, ip=ip)],
"multimem.ld_reduce.relaxed.sys.global.add.acc::f32.v4.bf16x2 {$0, $1, $2, $3}, [$4];",
"=r,=r,=r,=r,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
packed = vector.from_elements(
ir.VectorType.get([4], T.i32(), loc=loc),
[
llvm.extractvalue(T.i32(), loaded, [index], loc=loc, ip=ip)
for index in range(4)
],
loc=loc,
ip=ip,
)
return cute.TensorSSA(packed, 4, Uint32)
@dsl_user_op
def stmc_bf16x2(
address: Int64,
packed: Uint32,
*,
loc=None,
ip=None,
) -> None:
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), packed.ir_value(loc=loc, ip=ip)],
"multimem.st.relaxed.sys.global.bf16x2 [$0], $1;",
"l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def stmc_bf16x4(address: Int64, values, *, loc=None, ip=None) -> None:
words = [values[index].ir_value(loc=loc, ip=ip) for index in range(2)]
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), *words],
"multimem.st.relaxed.sys.global.v2.bf16x2 [$0], {$1, $2};",
"l,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@dsl_user_op
def stmc_bf16x8(address: Int64, values, *, loc=None, ip=None) -> None:
words = [values[index].ir_value(loc=loc, ip=ip) for index in range(4)]
llvm.inline_asm(
None,
[address.ir_value(loc=loc, ip=ip), *words],
"multimem.st.relaxed.sys.global.v4.bf16x2 [$0], {$1, $2, $3, $4};",
"l,r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
loc=loc,
ip=ip,
)
@@ -0,0 +1,43 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Balanced MNNVL protocol."""
from .protocol import (
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_0,
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_1,
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_0,
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_1,
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_0,
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_1,
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_0,
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_1,
BTAllReduceTuning,
BTCollectiveTuning,
BTFinalizeTuning,
)
__all__ = [
"BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_0",
"BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_1",
"BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_0",
"BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_1",
"BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_0",
"BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_1",
"BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_0",
"BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_1",
"BTAllReduceTuning",
"BTCollectiveTuning",
"BTFinalizeTuning",
]
@@ -0,0 +1,511 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Balanced MNNVL protocol and its two operation paths."""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Any, TypedDict, cast
import cutlass.cute as cute
import torch
import torch.distributed as dist
from cutlass import BFloat16, Int32, Int64
from cutlass.cute.runtime import make_fake_compact_tensor
from ..cute_dsl_primitives import VEC_BF16
from ..runtime import (
current_cu_stream,
make_fake_dynamic_compact_tensor,
to_cute,
to_cute_dynamic,
)
from ..symmetric_buffer import SymmetricBuffer
from .device_kernels import (
LAMPORT_GENERATIONS,
_MaterializeRMSNormDeviceKernel,
_NarrowVectorFinalizeUnicastDeviceKernel,
_OwnerReduceMulticastDeviceKernel,
_ScalarFinalizeUnicastDeviceKernel,
_SharedOnlyPublishDeviceKernel,
_VectorFinalizeUnicastDeviceKernel,
)
@dataclass(frozen=True, slots=True)
class BTCollectiveTuning:
reduction_threads: int = 128
rms_threads: int = 1024
enable_pdl: bool = True
@dataclass(frozen=True, slots=True)
class BTFinalizeTuning:
elements_per_thread: int = VEC_BF16
threads: int = 128
prefetch_group: int = 1
load_shared_expert_before_pdl: bool = False
collective: BTCollectiveTuning = BTCollectiveTuning()
@dataclass(frozen=True, slots=True)
class BTAllReduceTuning:
publish_threads: int = 128
publish_vectors_per_thread: int = 1
collective: BTCollectiveTuning = BTCollectiveTuning(reduction_threads=32)
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_0 = BTFinalizeTuning(
elements_per_thread=2, threads=256
)
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_1 = BTFinalizeTuning()
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_0 = BTFinalizeTuning(
elements_per_thread=2, threads=256
)
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_1 = BTFinalizeTuning()
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_0 = BTAllReduceTuning()
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_1 = BTAllReduceTuning(
collective=BTCollectiveTuning(reduction_threads=320)
)
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_0 = BTAllReduceTuning()
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_1 = BTAllReduceTuning(
collective=BTCollectiveTuning(reduction_threads=320)
)
@dataclass(slots=True)
class BTProtocolState:
contribution_mailbox: SymmetricBuffer
prenorm_mailbox: SymmetricBuffer
stage_state: torch.Tensor
@dataclass(frozen=True, slots=True)
class _CompiledTail:
reduce: Any
rms_norm: Any
@dataclass(frozen=True, slots=True)
class _CompiledFinalize:
publish: Any
tail: _CompiledTail
@dataclass(frozen=True, slots=True)
class _CompiledAllReduce:
publish: Any
tail: _CompiledTail
class _PathKwargs(TypedDict):
hidden_size: int
top_k: int
capacity_m: int
write_residual_output: bool
class _BTPath:
def __init__(
self,
*,
hidden_size: int,
top_k: int,
capacity_m: int,
write_residual_output: bool,
) -> None:
self.hidden_size = hidden_size
self.top_k = top_k
self.capacity_m = capacity_m
self.write_residual_output = write_residual_output
def _outputs(
self,
m: int,
norm_output: torch.Tensor | None,
residual_output: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
shape = (m, self.hidden_size)
device = torch.device("cuda", torch.cuda.current_device())
if norm_output is None:
norm_output = torch.empty(shape, dtype=torch.bfloat16, device=device)
if self.write_residual_output and residual_output is None:
residual_output = torch.empty(shape, dtype=torch.bfloat16, device=device)
return norm_output, residual_output
def _validate_state(self, state: BTProtocolState, m: int) -> None:
if not 1 <= m <= self.capacity_m:
raise ValueError(f"m must be in [1, {self.capacity_m}]")
if state.contribution_mailbox.peer_addresses is None:
raise ValueError("BT contribution mailbox requires peer addresses")
address = state.prenorm_mailbox.multicast_address
if address is None or address % 16:
raise ValueError(
"BT prenorm mailbox requires a 16-byte-aligned multicast address"
)
def _launch_tail(
self,
tail: _CompiledTail,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
state: BTProtocolState,
norm_output: torch.Tensor,
residual_output: torch.Tensor | None,
m: int,
) -> None:
residual_arg = residual_source if residual_source is not None else norm_output
residual_output_arg = (
residual_output if residual_output is not None else norm_output
)
stream = current_cu_stream()
tail.reduce(
to_cute(state.contribution_mailbox.tensor.flatten(), 16),
to_cute_dynamic(residual_arg.flatten(), 16, divisibility=self.hidden_size),
to_cute(state.stage_state, 4),
Int64(cast(int, state.prenorm_mailbox.multicast_address)),
Int32(m),
stream,
)
tail.rms_norm(
to_cute(state.prenorm_mailbox.tensor.flatten(), 16),
to_cute_dynamic(
residual_output_arg.flatten(),
16,
divisibility=self.hidden_size,
),
to_cute_dynamic(norm_output.flatten(), 16, divisibility=self.hidden_size),
to_cute(gamma, 16),
to_cute(state.stage_state, 4),
Int32(m),
stream,
)
class FinalizeAllReduceRMSNormBTKernel(_BTPath):
def __init__(self, *, compiled: _CompiledFinalize, **kwargs) -> None:
super().__init__(**kwargs)
self._compiled = compiled
def __call__(
self,
routed_output: torch.Tensor,
expert_weights: torch.Tensor,
permuted_indices: torch.Tensor,
shared_output: torch.Tensor | None,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
m: int,
*,
state: BTProtocolState,
norm_output: torch.Tensor | None = None,
residual_output: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
self._validate_state(state, m)
norm_output, residual_output = self._outputs(m, norm_output, residual_output)
shared_arg = shared_output if shared_output is not None else norm_output
peers = cast(torch.Tensor, state.contribution_mailbox.peer_addresses)
self._compiled.publish(
to_cute_dynamic(routed_output.flatten(), 16, divisibility=self.hidden_size),
to_cute_dynamic(expert_weights.flatten(), 2, divisibility=self.top_k),
to_cute_dynamic(permuted_indices.flatten(), 4, divisibility=self.top_k),
to_cute_dynamic(shared_arg.flatten(), 16, divisibility=self.hidden_size),
to_cute(state.stage_state, 4),
to_cute(peers, 8),
Int32(m),
current_cu_stream(),
)
self._launch_tail(
self._compiled.tail,
residual_source,
gamma,
state,
norm_output,
residual_output,
m,
)
return norm_output, residual_output
class AllReduceRMSNormBTKernel(_BTPath):
def __init__(self, *, compiled: _CompiledAllReduce, **kwargs) -> None:
super().__init__(**kwargs)
self._compiled = compiled
def __call__(
self,
local_contribution: torch.Tensor,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
m: int,
*,
state: BTProtocolState,
norm_output: torch.Tensor | None = None,
residual_output: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
self._validate_state(state, m)
norm_output, residual_output = self._outputs(m, norm_output, residual_output)
peers = cast(torch.Tensor, state.contribution_mailbox.peer_addresses)
self._compiled.publish(
to_cute_dynamic(
local_contribution.flatten(),
16,
divisibility=self.hidden_size,
),
to_cute(state.stage_state, 4),
to_cute(peers, 8),
Int32(m),
current_cu_stream(),
)
self._launch_tail(
self._compiled.tail,
residual_source,
gamma,
state,
norm_output,
residual_output,
m,
)
return norm_output, residual_output
class BTProtocol:
"""Own BT State and protocol-local compiled variants for both paths."""
def __init__(
self,
hidden_size: int,
top_k: int,
tp_size: int,
rank: int,
capacity_m: int,
rms_epsilon: float,
routed_scaling_factor: float,
weight_bias: float,
*,
include_shared_expert: bool,
add_residual: bool,
write_residual_output: bool,
finalize_tunings: tuple[BTFinalizeTuning, ...],
all_reduce_tunings: tuple[BTAllReduceTuning, ...],
group: dist.ProcessGroup,
) -> None:
self.hidden_size = hidden_size
self.top_k = top_k
self.tp_size = tp_size
self.rank = rank
self.capacity_m = capacity_m
self.local_capacity = math.ceil(capacity_m / tp_size)
self.rms_epsilon = rms_epsilon
self.routed_scaling_factor = routed_scaling_factor
self.weight_bias = weight_bias
self.include_shared_expert = include_shared_expert
self.add_residual = add_residual
self.write_residual_output = write_residual_output
tail_cache = {
tuning: self._compile_tail(tuning)
for tuning in {
*(item.collective for item in finalize_tunings),
*(item.collective for item in all_reduce_tunings),
}
}
self.finalize_kernels = {
tuning: FinalizeAllReduceRMSNormBTKernel(
compiled=_CompiledFinalize(
publish=self._compile_finalize(tuning),
tail=tail_cache[tuning.collective],
),
**self._path_kwargs(),
)
for tuning in dict.fromkeys(finalize_tunings)
}
self.all_reduce_kernels = {
tuning: AllReduceRMSNormBTKernel(
compiled=_CompiledAllReduce(
publish=self._compile_all_reduce_publish(tuning),
tail=tail_cache[tuning.collective],
),
**self._path_kwargs(),
)
for tuning in dict.fromkeys(all_reduce_tunings)
}
self.state = self._create_state(group)
def _path_kwargs(self) -> _PathKwargs:
return {
"hidden_size": self.hidden_size,
"top_k": self.top_k,
"capacity_m": self.capacity_m,
"write_residual_output": self.write_residual_output,
}
def _compile_finalize(self, tuning: BTFinalizeTuning):
if tuning.elements_per_thread not in (1, 2, 4, VEC_BF16):
raise ValueError("BT finalize elements_per_thread must be 1, 2, 4, or 8")
kwargs: dict[str, Any] = {
"hidden_size": self.hidden_size,
"top_k": self.top_k,
"tp_size": self.tp_size,
"rank": self.rank,
"local_capacity": self.local_capacity,
"threads": tuning.threads,
"routed_scaling_factor": self.routed_scaling_factor,
"include_shared_expert": self.include_shared_expert,
"load_shared_expert_before_pdl": tuning.load_shared_expert_before_pdl,
"enable_pdl": tuning.collective.enable_pdl,
"prefetch_group": tuning.prefetch_group,
}
device_kernel: Any
if tuning.elements_per_thread == 1:
device_kernel = _ScalarFinalizeUnicastDeviceKernel(**kwargs)
elif tuning.elements_per_thread == VEC_BF16:
device_kernel = _VectorFinalizeUnicastDeviceKernel(**kwargs)
else:
device_kernel = _NarrowVectorFinalizeUnicastDeviceKernel(
**kwargs, elements_per_thread=tuning.elements_per_thread
)
return cute.compile(
device_kernel,
*self._publish_compile_args(include_routed=True),
)
def _compile_all_reduce_publish(self, tuning: BTAllReduceTuning):
device_kernel = _SharedOnlyPublishDeviceKernel(
hidden_size=self.hidden_size,
tp_size=self.tp_size,
rank=self.rank,
local_capacity=self.local_capacity,
threads=tuning.publish_threads,
vectors_per_thread=tuning.publish_vectors_per_thread,
enable_pdl=tuning.collective.enable_pdl,
)
return cute.compile(
device_kernel,
*self._publish_compile_args(include_routed=False),
)
def _publish_compile_args(self, *, include_routed: bool) -> tuple:
activation = make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
)
common = (
make_fake_compact_tensor(Int32, (2,), assumed_align=4),
make_fake_compact_tensor(Int64, (self.tp_size,), assumed_align=8),
Int32(self.capacity_m),
current_cu_stream(),
)
if not include_routed:
return (activation, *common)
return (
activation,
make_fake_dynamic_compact_tensor(
BFloat16, alignment=2, divisibility=self.top_k
),
make_fake_dynamic_compact_tensor(
Int32, alignment=4, divisibility=self.top_k
),
activation,
*common,
)
def _compile_tail(self, tuning: BTCollectiveTuning) -> _CompiledTail:
reduce_kernel = _OwnerReduceMulticastDeviceKernel(
hidden_size=self.hidden_size,
tp_size=self.tp_size,
rank=self.rank,
capacity_m=self.capacity_m,
local_capacity=self.local_capacity,
threads=tuning.reduction_threads,
add_residual=self.add_residual,
enable_pdl=tuning.enable_pdl,
)
reduce_elements = (
LAMPORT_GENERATIONS * self.tp_size * self.local_capacity * self.hidden_size
)
reduce = cute.compile(
reduce_kernel,
make_fake_compact_tensor(BFloat16, (reduce_elements,), assumed_align=16),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_compact_tensor(Int32, (2,), assumed_align=4),
Int64(0),
Int32(self.capacity_m),
current_cu_stream(),
)
rms_kernel = _MaterializeRMSNormDeviceKernel(
hidden_size=self.hidden_size,
capacity_m=self.capacity_m,
threads=tuning.rms_threads,
rms_epsilon=self.rms_epsilon,
weight_bias=self.weight_bias,
write_residual_output=self.write_residual_output,
enable_pdl=tuning.enable_pdl,
)
prenorm_elements = LAMPORT_GENERATIONS * self.capacity_m * self.hidden_size
rms_norm = cute.compile(
rms_kernel,
make_fake_compact_tensor(BFloat16, (prenorm_elements,), assumed_align=16),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_compact_tensor(BFloat16, (self.hidden_size,), assumed_align=16),
make_fake_compact_tensor(Int32, (2,), assumed_align=4),
Int32(self.capacity_m),
current_cu_stream(),
)
return _CompiledTail(reduce=reduce, rms_norm=rms_norm)
def _create_state(self, group: dist.ProcessGroup) -> BTProtocolState:
if dist.get_world_size(group) != self.tp_size:
raise ValueError("ProcessGroup size does not match tp_size")
if dist.get_rank(group) != self.rank:
raise ValueError("ProcessGroup rank does not match rank")
device = torch.device("cuda", torch.cuda.current_device())
contribution = SymmetricBuffer.allocate(
(
LAMPORT_GENERATIONS,
self.tp_size,
self.local_capacity,
self.hidden_size,
),
torch.bfloat16,
device,
group,
materialize_peer_addresses=True,
)
contribution.tensor.view(torch.int16).fill_(-32768)
prenorm = SymmetricBuffer.allocate(
(
LAMPORT_GENERATIONS,
self.capacity_m,
self.hidden_size,
),
torch.bfloat16,
device,
group,
require_multicast=True,
)
prenorm.tensor.view(torch.int16).fill_(-32768)
return BTProtocolState(
contribution_mailbox=contribution,
prenorm_mailbox=prenorm,
stage_state=torch.zeros((2,), dtype=torch.int32, device=device),
)
@@ -0,0 +1,37 @@
# Copyright (c) 2026 by FlashInfer 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.
"""High-throughput MNNVL protocol."""
from .protocol import (
HT_ALL_REDUCE_GB300_TP8_H8192,
HT_ALL_REDUCE_GB300_TP16_H8192,
HT_FINALIZE_GB300_TP8_H8192_K10,
HT_FINALIZE_GB300_TP8_H8192_K10_M_GE_2049,
HT_FINALIZE_GB300_TP8_H8192_K10_M_LE_2048,
HT_FINALIZE_GB300_TP16_H8192_K10,
HTAllReduceTuning,
HTFinalizeTuning,
)
__all__ = [
"HT_ALL_REDUCE_GB300_TP8_H8192",
"HT_ALL_REDUCE_GB300_TP16_H8192",
"HT_FINALIZE_GB300_TP8_H8192_K10",
"HT_FINALIZE_GB300_TP8_H8192_K10_M_GE_2049",
"HT_FINALIZE_GB300_TP8_H8192_K10_M_LE_2048",
"HT_FINALIZE_GB300_TP16_H8192_K10",
"HTAllReduceTuning",
"HTFinalizeTuning",
]
@@ -0,0 +1,978 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Persistent BF16 MoE finalize, TP reduction, and RMSNorm for SM100."""
from __future__ import annotations
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
import cutlass.pipeline as pipeline
import cutlass.utils as utils
from cutlass import BFloat16, Float32, Int32, Int64, Uint32
from ..cute_dsl_primitives import (
VEC_BF16,
WARP_SIZE,
bf16x8_to_packed_u32x4,
cpasync_bulk_g2s,
fence_proxy_async_shared_cta,
fragment_has_negative_zero,
ldmc_bf16x8,
load_global_bf16_as_f32,
load_global_u32x4_address,
load_shared_u32x4,
packed_negative_zero_bf16x8,
packed_u32x4_to_bf16x8,
remote_release_add1_u32,
sanitize_negative_zero_u32x4,
stmc_bf16x8,
store_global_u32x4,
store_shared_u32x4,
)
SMEM_ALIGNMENT = 1024
class _MoeFinalizeAllReduceRMSNormHTDeviceKernel:
def __init__(
self,
*,
hidden: int,
top_k: int,
tp: int,
rank: int,
active_ctas: int,
stages: int,
consumer_threads: int,
vectors_per_thread: int,
reduction_warps: int,
reduction_cta_groups: int | None,
rms_token_groups: int,
rms_pipeline_stages: int,
rms_shard_major: bool,
rms_epsilon: float,
routed_scaling_factor: float,
weight_bias: float,
include_shared_expert: bool,
add_residual: bool,
write_residual_output: bool,
enable_pdl: bool,
) -> None:
if tp not in (2, 4, 8, 16):
raise ValueError("tp must be 2, 4, 8, or 16")
if rank < 0 or rank >= tp:
raise ValueError("rank must be in [0, tp)")
if hidden <= 0 or hidden % VEC_BF16:
raise ValueError("hidden must be a positive multiple of 8")
if top_k < 0:
raise ValueError("top_k must be nonnegative")
if active_ctas <= 0 or active_ctas % tp:
raise ValueError("active_ctas must be positive and divisible by tp")
if stages < 2:
raise ValueError("stages must be at least 2")
if consumer_threads <= 0 or consumer_threads % WARP_SIZE:
raise ValueError("consumer_threads must be a positive warp multiple")
if vectors_per_thread <= 0:
raise ValueError("vectors_per_thread must be positive")
if reduction_warps not in (1, 2, 4, 8):
raise ValueError("reduction_warps must be 1, 2, 4, or 8")
if rms_token_groups not in (1, 2, 4):
raise ValueError("rms_token_groups must be 1, 2, or 4")
if consumer_threads % rms_token_groups:
raise ValueError("consumer threads must divide across RMS token groups")
if rms_pipeline_stages not in (1, 2, 3):
raise ValueError("rms_pipeline_stages must be 1, 2, or 3")
block_threads = consumer_threads + (2 + reduction_warps) * WARP_SIZE
if block_threads > 1024:
raise ValueError("warp roles exceed the CUDA block limit")
shard_elements = consumer_threads * VEC_BF16 * vectors_per_thread
if hidden <= 0 or hidden % shard_elements:
raise ValueError(f"hidden must be divisible by {shard_elements}")
cta_groups = active_ctas // tp
if reduction_cta_groups is None:
reduction_cta_groups = active_ctas // tp
if reduction_cta_groups <= 0 or reduction_cta_groups * tp > active_ctas:
raise ValueError("reduction CTA groups and shards must fit the grid")
contributions = top_k + int(include_shared_expert)
if contributions <= 0:
raise ValueError("at least one local contribution is required")
self.hidden = hidden
self.top_k = top_k
self.tp = tp
self.rank = rank
self.active_ctas = active_ctas
self.stages = stages
self.vectors_per_thread = vectors_per_thread
self.consumer_threads = consumer_threads
self.reduction_warps = reduction_warps
self.reduction_cta_groups = reduction_cta_groups
self.reduction_ctas = reduction_cta_groups * tp
self.rms_token_groups = rms_token_groups
self.rms_pipeline_stages = rms_pipeline_stages
self.rms_shard_major = rms_shard_major
self.rms_epsilon = rms_epsilon
self.routed_scaling_factor = routed_scaling_factor
self.weight_bias = weight_bias
self.include_shared_expert = include_shared_expert
self.add_residual = add_residual
self.write_residual_output = write_residual_output
self.enable_pdl = enable_pdl
self.metadata_chunks = (top_k + WARP_SIZE - 1) // WARP_SIZE
self.metadata_slots = max(top_k, 1)
self.consumer_warps = consumer_threads // WARP_SIZE
self.rms_threads_per_token = consumer_threads // rms_token_groups
self.rms_warps_per_token = self.rms_threads_per_token // WARP_SIZE
self.rms_stage_slots = rms_token_groups * rms_pipeline_stages
self.rms_warp_sum_slots = self.rms_stage_slots * self.rms_warps_per_token
self.publisher_warp = 1 + self.consumer_warps
self.reduction_warp_begin = self.publisher_warp + 1
self.reduction_threads = reduction_warps * WARP_SIZE
self.block_threads = block_threads
self.shard_elements = shard_elements
self.shard_bytes = shard_elements * 2
self.hidden_shards = hidden // shard_elements
self.contributions = contributions
if (
rms_pipeline_stages > 1
and self.rms_stage_slots * hidden > self.shard_elements * stages
):
raise ValueError("finalize stage storage cannot hold the RMS pipeline")
self.cta_groups = cta_groups
self.packs_per_token = hidden // VEC_BF16
if self.packs_per_token % tp:
raise ValueError("hidden vector count must be divisible by tp")
if self.packs_per_token % consumer_threads:
raise ValueError("token vectors must divide evenly across consumers")
self.clear_vectors_per_thread = self.packs_per_token // consumer_threads
self.copy_threads = self.rms_threads_per_token
if self.packs_per_token % self.copy_threads:
raise ValueError("token vectors must divide evenly across finalize threads")
self.rms_vectors_per_thread = self.packs_per_token // self.copy_threads
self.packs_per_reduction_shard = self.packs_per_token // tp
if rms_shard_major:
if tp < self.rms_warps_per_token or tp % self.rms_warps_per_token:
raise ValueError(
"shard-major RMS requires an integer number of reduction "
"shards per RMS warp"
)
self.reduction_shards_per_rms_warp = tp // self.rms_warps_per_token
if (
self.rms_vectors_per_thread * WARP_SIZE
!= self.packs_per_reduction_shard * self.reduction_shards_per_rms_warp
):
raise ValueError(
"shard-major RMS warp coverage must match its reduction shards"
)
else:
self.reduction_shards_per_rms_warp = 0
if self.packs_per_reduction_shard % self.reduction_threads:
raise ValueError("the reduction shard must divide evenly across threads")
self.reduction_vectors_per_thread = (
self.packs_per_reduction_shard // self.reduction_threads
)
@cute.jit
def _rms_arrive_and_wait(self, rms_group: Int32) -> None:
barrier_0 = pipeline.NamedBarrier(
barrier_id=2, num_threads=self.rms_threads_per_token
)
if cutlass.const_expr(self.rms_token_groups > 1):
barrier_1 = pipeline.NamedBarrier(
barrier_id=3, num_threads=self.rms_threads_per_token
)
if cutlass.const_expr(self.rms_token_groups == 4):
barrier_2 = pipeline.NamedBarrier(
barrier_id=4, num_threads=self.rms_threads_per_token
)
barrier_3 = pipeline.NamedBarrier(
barrier_id=5, num_threads=self.rms_threads_per_token
)
if rms_group == 0:
barrier_0.arrive_and_wait()
elif cutlass.const_expr(self.rms_token_groups == 2): # noqa: SIM114
barrier_1.arrive_and_wait()
elif rms_group == 1:
barrier_1.arrive_and_wait()
elif rms_group == 2:
barrier_2.arrive_and_wait()
else:
barrier_3.arrive_and_wait()
else:
barrier_0.arrive_and_wait()
@cute.jit
def __call__(
self,
routed_output: cute.Tensor,
expert_weights: cute.Tensor,
permuted_indices: cute.Tensor,
shared_output: cute.Tensor,
residual_source: cute.Tensor,
gamma: cute.Tensor,
local_contributions: cute.Tensor,
prenorm_mailbox: cute.Tensor,
residual_output: cute.Tensor,
norm_output: cute.Tensor,
ready_counter_peer_addresses: cute.Tensor,
ready_counters: cute.Tensor,
processed_counters: cute.Tensor,
local_contributions_multicast_address: Int64,
prenorm_mailbox_multicast_address: Int64,
m: Int32,
stream: cuda.CUstream,
) -> None:
smem_layout = cute.make_layout(
(self.shard_elements * self.stages,), stride=(1,)
)
@cute.struct
class SharedStorage:
barriers: cute.struct.MemRange[Int64, 2 * self.stages]
stage_probs: cute.struct.MemRange[Float32, self.stages]
cached_rows: cute.struct.MemRange[Int32, self.metadata_slots]
cached_probs: cute.struct.MemRange[Float32, self.metadata_slots]
consumer_progress: cute.struct.MemRange[Int32, self.consumer_warps]
norm_warp_sums: cute.struct.MemRange[Float32, self.rms_warp_sum_slots]
norm_inv_rms: cute.struct.MemRange[Float32, self.rms_stage_slots]
rows: cute.struct.Align[
cute.struct.MemRange[BFloat16, cute.cosize(smem_layout)],
SMEM_ALIGNMENT,
]
self.shared_storage: type[cute.struct.Struct] = SharedStorage
self.kernel(
routed_output,
shared_output,
residual_source,
gamma,
expert_weights,
permuted_indices,
local_contributions,
prenorm_mailbox,
residual_output,
norm_output,
ready_counter_peer_addresses,
ready_counters,
processed_counters,
local_contributions_multicast_address,
prenorm_mailbox_multicast_address,
m,
smem_layout,
).launch(
grid=(self.active_ctas, 1, 1),
block=(self.block_threads, 1, 1),
min_blocks_per_mp=1,
use_pdl=self.enable_pdl,
stream=stream,
)
@cute.kernel
def kernel(
self,
routed_source: cute.Tensor,
shared_source: cute.Tensor,
residual_source: cute.Tensor,
gamma: cute.Tensor,
expert_weights: cute.Tensor,
permuted_indices: cute.Tensor,
local_contributions: cute.Tensor,
prenorm_mailbox: cute.Tensor,
residual_output: cute.Tensor,
norm_output: cute.Tensor,
ready_counter_peer_addresses: cute.Tensor,
ready_counters: cute.Tensor,
processed_counters: cute.Tensor,
local_contributions_multicast_address: Int64,
prenorm_mailbox_multicast_address: Int64,
m: Int32,
smem_layout: cute.Layout,
) -> None:
block = cute.arch.block_idx()[0]
tidx = cute.arch.thread_idx()[0]
warp = cute.arch.make_warp_uniform(cute.arch.warp_idx())
lane = cute.arch.lane_idx()
cta_group = block // self.tp
cta_slot = block % self.tp
wave = Int64(cta_group)
token = wave * self.tp + cta_slot
smem = utils.SmemAllocator()
storage = smem.allocate(self.shared_storage)
rows = storage.rows.get_tensor(smem_layout)
barrier_storage = storage.barriers.data_ptr()
stage_probs = storage.stage_probs.data_ptr()
cached_rows = storage.cached_rows.data_ptr()
cached_probs = storage.cached_probs.data_ptr()
consumer_progress = storage.consumer_progress.data_ptr()
norm_warp_sums = storage.norm_warp_sums.data_ptr()
norm_inv_rms = storage.norm_inv_rms.data_ptr()
if tidx < self.consumer_warps:
cute.arch.store((consumer_progress + tidx).llvm_ptr, Int32(0))
cute.arch.sync_threads()
if cutlass.const_expr(self.enable_pdl):
cute.arch.griddepcontrol_wait()
load_pipeline = pipeline.PipelineTmaAsync.create(
barrier_storage=barrier_storage,
num_stages=self.stages,
producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, 1),
consumer_group=pipeline.CooperativeGroup(
pipeline.Agent.Thread, self.consumer_warps
),
tx_count=self.shard_bytes,
)
if warp == 0:
producer_state = pipeline.make_pipeline_state(
pipeline.PipelineUserType.Producer, self.stages
)
peek_empty = cutlass.Boolean(1)
if token < Int64(m):
peek_empty = load_pipeline.producer_try_acquire(producer_state)
while token < Int64(m):
for metadata_chunk in cutlass.range_constexpr(self.metadata_chunks):
metadata_slot = metadata_chunk * WARP_SIZE + lane
if metadata_slot < self.top_k:
item = Int64(token) * self.top_k + metadata_slot
row = cute.arch.load(
(permuted_indices.iterator + item).llvm_ptr, Int32
)
prob = load_global_bf16_as_f32(
Int64((expert_weights.iterator + item).toint())
)
if cutlass.const_expr(self.routed_scaling_factor != 1.0):
prob = prob * Float32(self.routed_scaling_factor)
if row == Int32(-1):
row = Int32(0)
prob = Float32(0.0)
cute.arch.store((cached_rows + metadata_slot).llvm_ptr, row)
cute.arch.store((cached_probs + metadata_slot).llvm_ptr, prob)
cute.arch.sync_warp()
for shard in cutlass.range_constexpr(self.hidden_shards):
for contribution in cutlass.range_constexpr(self.contributions):
load_pipeline.producer_acquire(producer_state, peek_empty)
if lane == 0:
if cutlass.const_expr(contribution < self.top_k):
prob = cute.arch.load(
(cached_probs + contribution).llvm_ptr,
Float32,
)
row = cute.arch.load(
(cached_rows + contribution).llvm_ptr,
Int32,
)
source_element = (
Int64(row) * self.hidden
+ shard * self.shard_elements
)
source = routed_source.iterator + source_element
else:
prob = Float32(1.0)
source_element = (
Int64(token) * self.hidden
+ shard * self.shard_elements
)
source = shared_source.iterator + source_element
cute.arch.store(
(stage_probs + producer_state.index).llvm_ptr,
prob,
)
fence_proxy_async_shared_cta()
cpasync_bulk_g2s(
source,
rows.iterator
+ producer_state.index * self.shard_elements,
load_pipeline.producer_get_barrier(producer_state),
Int32(self.shard_bytes),
)
producer_state.advance()
peek_empty = load_pipeline.producer_try_acquire(producer_state)
wave += self.cta_groups
token = wave * self.tp + cta_slot
load_pipeline.producer_tail(producer_state)
elif warp > 0 and warp <= self.consumer_warps:
finalize_join = pipeline.NamedBarrier(
barrier_id=6, num_threads=self.consumer_threads
)
consumer_state = pipeline.make_pipeline_state(
pipeline.PipelineUserType.Consumer, self.stages
)
consumer_tid = tidx - WARP_SIZE
consumer_wave = Int64(cta_group)
token = consumer_wave * self.tp + cta_slot
consumer_token_progress = Int32(0)
while token < Int64(m):
for shard in cutlass.range_constexpr(self.hidden_shards):
if cutlass.const_expr(self.top_k == 0):
load_pipeline.consumer_wait(consumer_state)
for trip in cutlass.range_constexpr(self.vectors_per_thread):
output_element = (
Int64(token) * self.hidden
+ shard * self.shard_elements
+ trip * self.consumer_threads * VEC_BF16
+ consumer_tid * VEC_BF16
)
store_global_u32x4(
Int64(
(
local_contributions.iterator + output_element
).toint()
),
load_shared_u32x4(
rows.iterator
+ consumer_state.index * self.shard_elements
+ trip * self.consumer_threads * VEC_BF16
+ consumer_tid * VEC_BF16
),
)
load_pipeline.consumer_release(consumer_state)
consumer_state.advance()
accum = cute.make_rmem_tensor(
cute.make_layout(
(self.vectors_per_thread, VEC_BF16),
stride=(VEC_BF16, 1),
),
Float32,
)
accum.fill(Float32(0.0))
for _ in cutlass.range_constexpr(
self.contributions if self.top_k > 0 else 0
):
load_pipeline.consumer_wait(consumer_state)
prob = cute.arch.load(
(stage_probs + consumer_state.index).llvm_ptr,
Float32,
)
if prob != Float32(0.0):
for trip in cutlass.range_constexpr(
self.vectors_per_thread
):
stage_ptr = (
rows.iterator
+ consumer_state.index * self.shard_elements
+ trip * self.consumer_threads * VEC_BF16
+ consumer_tid * VEC_BF16
)
values = packed_u32x4_to_bf16x8(
load_shared_u32x4(stage_ptr)
).to(Float32)
accum[trip, None].store(
accum[trip, None].load() + values * prob
)
load_pipeline.consumer_release(consumer_state)
consumer_state.advance()
for trip in cutlass.range_constexpr(
self.vectors_per_thread if self.top_k > 0 else 0
):
output_element = (
Int64(token) * self.hidden
+ shard * self.shard_elements
+ trip * self.consumer_threads * VEC_BF16
+ consumer_tid * VEC_BF16
)
store_global_u32x4(
Int64(
(local_contributions.iterator + output_element).toint()
),
bf16x8_to_packed_u32x4(
accum[trip, None].load().to(BFloat16)
),
)
clear_value = packed_negative_zero_bf16x8()
token_pack = token * self.packs_per_token
for clear_item in cutlass.range_constexpr(
self.clear_vectors_per_thread
):
clear_pack = consumer_tid + clear_item * self.consumer_threads
clear_element = (token_pack + clear_pack) * VEC_BF16
store_global_u32x4(
Int64((prenorm_mailbox.iterator + clear_element).toint()),
clear_value,
)
cute.arch.sync_warp()
consumer_token_progress += 1
if lane == 0:
cute.arch.store(
(consumer_progress + warp - 1).llvm_ptr,
consumer_token_progress,
sem="release",
scope="cta",
)
consumer_wave += self.cta_groups
token = consumer_wave * self.tp + cta_slot
finalize_join.arrive_and_wait()
if cutlass.const_expr(self.rms_token_groups > 1):
rms_group = (warp - 1) // self.rms_warps_per_token
rms_group_warp = warp - 1 - rms_group * self.rms_warps_per_token
copy_tid = rms_group_warp * WARP_SIZE + lane
else:
rms_group = Int32(0)
rms_group_warp = warp - 1
copy_tid = consumer_tid
rms_pack_base = copy_tid
rms_pack_stride = self.copy_threads
if cutlass.const_expr(self.rms_shard_major):
rms_pack_base = (
rms_group_warp
* self.reduction_shards_per_rms_warp
* self.packs_per_reduction_shard
+ lane
)
rms_pack_stride = WARP_SIZE
copy_wave = Int64(cta_group) + Int64(rms_group) * self.cta_groups
copy_token = copy_wave * self.tp + cta_slot
if cutlass.const_expr(self.rms_pipeline_stages > 1):
rms_wave_stride = self.cta_groups * self.rms_token_groups
while copy_token < Int64(m):
for rms_stage in cutlass.range_constexpr(self.rms_pipeline_stages):
stage_wave = copy_wave + rms_stage * rms_wave_stride
stage_token = stage_wave * self.tp + cta_slot
if stage_token < Int64(m):
stage_slot = (
rms_group * self.rms_pipeline_stages + rms_stage
)
token_pack = stage_token * self.packs_per_token
copy_fragments = cute.make_rmem_tensor(
cute.make_layout(
(self.rms_vectors_per_thread, 4),
stride=(4, 1),
),
Uint32,
)
all_ready = cutlass.Boolean(0)
while not all_ready:
all_ready = cutlass.Boolean(1)
for item in cutlass.range_constexpr(
self.rms_vectors_per_thread
):
pack = rms_pack_base + item * rms_pack_stride
linear_pack = token_pack + pack
packed = load_global_u32x4_address(
Int64(
(
prenorm_mailbox.iterator
+ linear_pack * VEC_BF16
).toint()
),
volatile=True,
)
copy_fragments[item, None].store(packed)
all_ready = all_ready and (
not fragment_has_negative_zero(packed)
)
thread_sum = Float32(0.0)
if cutlass.const_expr(self.write_residual_output):
prenorm_packed = []
for item in cutlass.range_constexpr(
self.rms_vectors_per_thread
):
pack = rms_pack_base + item * rms_pack_stride
prenorm = packed_u32x4_to_bf16x8(
copy_fragments[item, None].load()
)
packed_prenorm = bf16x8_to_packed_u32x4(prenorm)
if cutlass.const_expr(self.write_residual_output):
prenorm_packed.append(packed_prenorm)
# Finalize has drained `rows`; __init__ verifies the RMS layout fits.
store_shared_u32x4(
rows.iterator
+ stage_slot * self.hidden
+ pack * VEC_BF16,
packed_prenorm,
)
prenorm_f32 = prenorm.to(Float32)
thread_sum = thread_sum + (
prenorm_f32 * prenorm_f32
).reduce(
cute.ReductionOp.ADD,
init_val=Float32(0.0),
reduction_profile=0,
)
if cutlass.const_expr(self.write_residual_output):
for item in cutlass.range_constexpr(
self.rms_vectors_per_thread
):
pack = rms_pack_base + item * rms_pack_stride
linear_pack = token_pack + pack
residual_address = Int64(
(
residual_output.iterator
+ linear_pack * VEC_BF16
).toint()
)
store_global_u32x4(
residual_address, prenorm_packed[item]
)
warp_sum = cute.arch.warp_reduction_sum(thread_sum)
if lane == 0:
cute.arch.store(
(
norm_warp_sums
+ stage_slot * self.rms_warps_per_token
+ rms_group_warp
).llvm_ptr,
warp_sum,
)
self._rms_arrive_and_wait(rms_group)
if warp == 1 + rms_group * self.rms_warps_per_token:
for rms_stage in cutlass.range_constexpr(
self.rms_pipeline_stages
):
stage_wave = copy_wave + rms_stage * rms_wave_stride
stage_token = stage_wave * self.tp + cta_slot
if stage_token < Int64(m):
stage_slot = (
rms_group * self.rms_pipeline_stages + rms_stage
)
cta_sum = Float32(0.0)
if lane < self.rms_warps_per_token:
cta_sum = cute.arch.load(
(
norm_warp_sums
+ stage_slot * self.rms_warps_per_token
+ lane
).llvm_ptr,
Float32,
)
cta_sum = cute.arch.warp_reduction_sum(cta_sum)
if lane == 0:
inv_rms = cute.math.rsqrt(
cta_sum / Float32(self.hidden)
+ Float32(self.rms_epsilon),
fastmath=True,
)
cute.arch.store(
(norm_inv_rms + stage_slot).llvm_ptr,
inv_rms,
)
self._rms_arrive_and_wait(rms_group)
gamma_values = []
for item in cutlass.range_constexpr(self.rms_vectors_per_thread):
pack = rms_pack_base + item * rms_pack_stride
gamma_value = packed_u32x4_to_bf16x8(
load_global_u32x4_address(
Int64((gamma.iterator + pack * VEC_BF16).toint())
)
).to(Float32)
if cutlass.const_expr(self.weight_bias != 0.0):
gamma_value = gamma_value + Float32(self.weight_bias)
gamma_values.append(gamma_value)
for rms_stage in cutlass.range_constexpr(self.rms_pipeline_stages):
stage_wave = copy_wave + rms_stage * rms_wave_stride
stage_token = stage_wave * self.tp + cta_slot
if stage_token < Int64(m):
stage_slot = (
rms_group * self.rms_pipeline_stages + rms_stage
)
token_pack = stage_token * self.packs_per_token
inv_rms = cute.arch.load(
(norm_inv_rms + stage_slot).llvm_ptr, Float32
)
norm_packed = []
for item in cutlass.range_constexpr(
self.rms_vectors_per_thread
):
pack = rms_pack_base + item * rms_pack_stride
prenorm = packed_u32x4_to_bf16x8(
load_shared_u32x4(
rows.iterator
+ stage_slot * self.hidden
+ pack * VEC_BF16
)
).to(Float32)
result = (prenorm * inv_rms * gamma_values[item]).to(
BFloat16
)
norm_packed.append(bf16x8_to_packed_u32x4(result))
for item in cutlass.range_constexpr(
self.rms_vectors_per_thread
):
pack = rms_pack_base + item * rms_pack_stride
linear_pack = token_pack + pack
store_global_u32x4(
Int64(
(
norm_output.iterator
+ linear_pack * VEC_BF16
).toint()
),
norm_packed[item],
)
copy_wave += rms_wave_stride * self.rms_pipeline_stages
copy_token = copy_wave * self.tp + cta_slot
if cutlass.const_expr(self.rms_pipeline_stages == 1):
while copy_token < Int64(m):
token_pack = copy_token * self.packs_per_token
copy_values = []
copy_sources = []
if cutlass.const_expr(self.write_residual_output):
copy_destinations = []
for item in cutlass.range_constexpr(self.rms_vectors_per_thread):
pack = rms_pack_base + item * rms_pack_stride
linear_pack = token_pack + pack
source_address = Int64(
(prenorm_mailbox.iterator + linear_pack * VEC_BF16).toint()
)
copy_sources.append(source_address)
if cutlass.const_expr(self.write_residual_output):
copy_destinations.append(
Int64(
(
residual_output.iterator
+ linear_pack * VEC_BF16
).toint()
)
)
copy_fragments = cute.make_rmem_tensor(
cute.make_layout(
(self.rms_vectors_per_thread, 4),
stride=(4, 1),
),
Uint32,
)
all_ready = cutlass.Boolean(0)
while not all_ready:
all_ready = cutlass.Boolean(1)
for item in cutlass.range_constexpr(
self.rms_vectors_per_thread
):
packed = load_global_u32x4_address(
copy_sources[item],
volatile=True,
)
copy_fragments[item, None].store(packed)
all_ready = all_ready and (
not fragment_has_negative_zero(packed)
)
for item in cutlass.range_constexpr(self.rms_vectors_per_thread):
copy_values.append(copy_fragments[item, None].load())
prenorm_fragments = cute.make_rmem_tensor(
cute.make_layout(
(self.rms_vectors_per_thread, VEC_BF16),
stride=(VEC_BF16, 1),
),
BFloat16,
)
thread_sum = Float32(0.0)
if cutlass.const_expr(self.write_residual_output):
prenorm_packed = []
for item in cutlass.range_constexpr(self.rms_vectors_per_thread):
pack = rms_pack_base + item * rms_pack_stride
prenorm = packed_u32x4_to_bf16x8(copy_values[item])
prenorm_fragments[item, None].store(prenorm)
packed_prenorm = bf16x8_to_packed_u32x4(prenorm)
if cutlass.const_expr(self.write_residual_output):
prenorm_packed.append(packed_prenorm)
prenorm_f32 = prenorm.to(Float32)
thread_sum = thread_sum + (prenorm_f32 * prenorm_f32).reduce(
cute.ReductionOp.ADD,
init_val=Float32(0.0),
reduction_profile=0,
)
if cutlass.const_expr(self.write_residual_output):
for item in cutlass.range_constexpr(
self.rms_vectors_per_thread
):
store_global_u32x4(
copy_destinations[item], prenorm_packed[item]
)
warp_sum = cute.arch.warp_reduction_sum(thread_sum)
if lane == 0:
cute.arch.store((norm_warp_sums + warp - 1).llvm_ptr, warp_sum)
self._rms_arrive_and_wait(rms_group)
if warp == 1 + rms_group * self.rms_warps_per_token:
cta_sum = Float32(0.0)
if lane < self.rms_warps_per_token:
cta_sum = cute.arch.load(
(
norm_warp_sums
+ rms_group * self.rms_warps_per_token
+ lane
).llvm_ptr,
Float32,
)
cta_sum = cute.arch.warp_reduction_sum(cta_sum)
if lane == 0:
inv_rms = cute.math.rsqrt(
cta_sum / Float32(self.hidden)
+ Float32(self.rms_epsilon),
fastmath=True,
)
cute.arch.store(
(norm_inv_rms + rms_group).llvm_ptr, inv_rms
)
self._rms_arrive_and_wait(rms_group)
inv_rms = cute.arch.load(
(norm_inv_rms + rms_group).llvm_ptr, Float32
)
gamma_values = []
for item in cutlass.range_constexpr(self.rms_vectors_per_thread):
pack = rms_pack_base + item * rms_pack_stride
gamma_value = packed_u32x4_to_bf16x8(
load_global_u32x4_address(
Int64((gamma.iterator + pack * VEC_BF16).toint())
)
).to(Float32)
if cutlass.const_expr(self.weight_bias != 0.0):
gamma_value = gamma_value + Float32(self.weight_bias)
gamma_values.append(gamma_value)
norm_packed = []
for item in cutlass.range_constexpr(self.rms_vectors_per_thread):
pack = rms_pack_base + item * rms_pack_stride
linear_pack = token_pack + pack
prenorm_for_norm = (
prenorm_fragments[item, None].load().to(Float32)
)
result = (prenorm_for_norm * inv_rms * gamma_values[item]).to(
BFloat16
)
norm_packed.append(bf16x8_to_packed_u32x4(result))
for item in cutlass.range_constexpr(self.rms_vectors_per_thread):
pack = rms_pack_base + item * rms_pack_stride
linear_pack = token_pack + pack
store_global_u32x4(
Int64(
(norm_output.iterator + linear_pack * VEC_BF16).toint()
),
norm_packed[item],
)
copy_wave += self.cta_groups * self.rms_token_groups
copy_token = copy_wave * self.tp + cta_slot
elif warp == self.publisher_warp:
owner_ready_address = cute.arch.load(
(ready_counter_peer_addresses.iterator + cta_slot).llvm_ptr,
Int64,
)
first_token = Int64(cta_group) * self.tp + cta_slot
token_count = Int32(0)
if first_token < Int64(m):
token_count = Int32(
(Int64(m) + self.active_ctas - 1 - first_token) // self.active_ctas
)
published = Int32(0)
while published < token_count:
observed = token_count
if lane < self.consumer_warps:
observed = cute.arch.load(
(consumer_progress + lane).llvm_ptr,
Int32,
sem="relaxed",
scope="cta",
)
frontier = cute.arch.warp_reduction(
observed, lambda x, y: cutlass.min(x, y)
)
if frontier > published:
acquired = token_count
if lane < self.consumer_warps:
acquired = cute.arch.load(
(consumer_progress + lane).llvm_ptr,
Int32,
sem="acquire",
scope="cta",
)
frontier = cute.arch.warp_reduction(
acquired, lambda x, y: cutlass.min(x, y)
)
cute.arch.sync_warp()
batch = cutlass.min(frontier - published, Int32(WARP_SIZE))
if lane < batch:
sequence = Int64(published + lane)
publish_token = (
Int64(cta_group) + sequence * self.cta_groups
) * self.tp + cta_slot
owner_token = publish_token // self.tp
remote_release_add1_u32(owner_ready_address + owner_token * 4)
published += batch
elif (
block < self.reduction_ctas
and warp >= self.reduction_warp_begin
and (warp < self.reduction_warp_begin + self.reduction_warps)
):
reduction_warp = warp - self.reduction_warp_begin
reduction_tid = reduction_warp * WARP_SIZE + lane
reduction_barrier = pipeline.NamedBarrier(
barrier_id=1, num_threads=self.reduction_threads
)
reduction_shard = block % self.tp
local_token = Int64(block // self.tp)
token = local_token * self.tp + self.rank
while token < Int64(m):
processed_index = local_token * self.tp + reduction_shard
target = Uint32(0)
if reduction_tid == 0:
ready_counter_address = (
ready_counters.iterator + local_token
).llvm_ptr
processed_counter_address = (
processed_counters.iterator + processed_index
).llvm_ptr
target = cute.arch.load(processed_counter_address, Uint32) + Uint32(
self.tp
)
observed = Uint32(0)
while observed != target:
observed = cute.arch.load(
ready_counter_address,
Uint32,
sem="relaxed",
scope="sys",
)
cute.arch.load(
ready_counter_address,
Uint32,
sem="acquire",
scope="sys",
)
reduction_barrier.arrive_and_wait()
values = []
addresses = []
token_pack = token * self.packs_per_token
shard_pack = reduction_shard * self.packs_per_reduction_shard
for item in cutlass.range_constexpr(self.reduction_vectors_per_thread):
pack = shard_pack + reduction_tid + item * self.reduction_threads
input_address = (
local_contributions_multicast_address + (token_pack + pack) * 16
)
output_address = (
prenorm_mailbox_multicast_address + (token_pack + pack) * 16
)
reduced_packed = ldmc_bf16x8(input_address)
reduced_values = packed_u32x4_to_bf16x8(reduced_packed).to(Float32)
if cutlass.const_expr(self.add_residual):
residual_values = packed_u32x4_to_bf16x8(
load_global_u32x4_address(
Int64(
(
residual_source.iterator
+ (token_pack + pack) * VEC_BF16
).toint()
)
)
).to(Float32)
reduced_values = reduced_values + residual_values
reduced_packed = bf16x8_to_packed_u32x4(reduced_values.to(BFloat16))
values.append(sanitize_negative_zero_u32x4(reduced_packed))
addresses.append(output_address)
for item in cutlass.range_constexpr(self.reduction_vectors_per_thread):
stmc_bf16x8(addresses[item], values[item])
if reduction_tid == 0:
cute.arch.store(
(processed_counters.iterator + processed_index).llvm_ptr,
target,
)
local_token += self.reduction_cta_groups
token = local_token * self.tp + self.rank
cute.arch.sync_threads()
if cutlass.const_expr(self.enable_pdl):
cute.arch.griddepcontrol_launch_dependents()
@@ -0,0 +1,469 @@
# Copyright (c) 2026 by FlashInfer 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.
"""High-throughput MNNVL protocol and its two operation paths."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, TypedDict, cast
import cutlass.cute as cute
import torch
import torch.distributed as dist
from cutlass import BFloat16, Int32, Int64, Uint32
from cutlass.cute.runtime import make_fake_compact_tensor
from ..runtime import (
current_cu_stream,
make_fake_dynamic_compact_tensor,
to_cute,
to_cute_dynamic,
)
from ..symmetric_buffer import SymmetricBuffer
from .device_kernel import _MoeFinalizeAllReduceRMSNormHTDeviceKernel
@dataclass(frozen=True, slots=True)
class HTFinalizeTuning:
persistent_ctas: int | None = None
consumer_threads: int = 512
vectors_per_thread: int = 2
stages: int = 6
reduction_warps: int = 1
reduction_cta_groups: int | None = None
rms_token_groups: int = 2
rms_pipeline_stages: int = 2
rms_shard_major: bool = False
enable_pdl: bool = True
@dataclass(frozen=True, slots=True)
class HTAllReduceTuning:
persistent_ctas: int | None = None
consumer_threads: int = 512
vectors_per_thread: int = 2
stages: int = 2
reduction_warps: int = 2
reduction_cta_groups: int | None = None
rms_token_groups: int = 2
rms_pipeline_stages: int = 1
rms_shard_major: bool = False
enable_pdl: bool = True
HT_FINALIZE_GB300_TP8_H8192_K10_M_LE_2048 = HTFinalizeTuning(
stages=7,
reduction_warps=2,
rms_pipeline_stages=3,
rms_shard_major=True,
)
HT_FINALIZE_GB300_TP8_H8192_K10_M_GE_2049 = HTFinalizeTuning()
HT_FINALIZE_GB300_TP8_H8192_K10 = HT_FINALIZE_GB300_TP8_H8192_K10_M_GE_2049
HT_FINALIZE_GB300_TP16_H8192_K10 = HTFinalizeTuning(
stages=7,
reduction_warps=2,
rms_pipeline_stages=3,
rms_shard_major=True,
)
HT_ALL_REDUCE_GB300_TP8_H8192 = HTAllReduceTuning()
HT_ALL_REDUCE_GB300_TP16_H8192 = HTAllReduceTuning()
@dataclass(slots=True)
class HTProtocolState:
local_contributions: SymmetricBuffer
prenorm_mailbox: SymmetricBuffer
routed_ready_counters: SymmetricBuffer
routed_processed_counters: torch.Tensor
all_reduce_ready_counters: SymmetricBuffer
all_reduce_processed_counters: torch.Tensor
class _PathKwargs(TypedDict):
hidden_size: int
top_k: int
capacity_m: int
write_residual_output: bool
class _HTPath:
def __init__(
self,
*,
compiled: Any,
hidden_size: int,
top_k: int,
capacity_m: int,
write_residual_output: bool,
) -> None:
self._compiled = compiled
self.hidden_size = hidden_size
self.top_k = top_k
self.capacity_m = capacity_m
self.write_residual_output = write_residual_output
def _outputs(
self,
m: int,
norm_output: torch.Tensor | None,
residual_output: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
shape = (m, self.hidden_size)
device = torch.device("cuda", torch.cuda.current_device())
if norm_output is None:
norm_output = torch.empty(shape, dtype=torch.bfloat16, device=device)
if self.write_residual_output and residual_output is None:
residual_output = torch.empty(shape, dtype=torch.bfloat16, device=device)
return norm_output, residual_output
def _state_buffers(
self, state: HTProtocolState
) -> tuple[SymmetricBuffer, SymmetricBuffer]:
return state.local_contributions, state.prenorm_mailbox
def _validate_m(self, m: int) -> None:
if not 1 <= m <= self.capacity_m:
raise ValueError(f"m must be in [1, {self.capacity_m}]")
class FinalizeAllReduceRMSNormHTKernel(_HTPath):
def __call__(
self,
routed_output: torch.Tensor,
expert_weights: torch.Tensor,
permuted_indices: torch.Tensor,
shared_output: torch.Tensor | None,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
m: int,
*,
state: HTProtocolState,
norm_output: torch.Tensor | None = None,
residual_output: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
self._validate_m(m)
norm_output, residual_output = self._outputs(m, norm_output, residual_output)
local, prenorm = self._state_buffers(state)
peers = cast(torch.Tensor, state.routed_ready_counters.peer_addresses)
shared_arg = shared_output if shared_output is not None else norm_output
residual_arg = residual_source if residual_source is not None else norm_output
residual_output_arg = (
residual_output if residual_output is not None else norm_output
)
self._compiled(
to_cute_dynamic(routed_output.flatten(), 16, divisibility=self.hidden_size),
to_cute_dynamic(expert_weights.flatten(), 2, divisibility=self.top_k),
to_cute_dynamic(permuted_indices.flatten(), 4, divisibility=self.top_k),
to_cute_dynamic(shared_arg.flatten(), 16, divisibility=self.hidden_size),
to_cute_dynamic(residual_arg.flatten(), 16, divisibility=self.hidden_size),
to_cute(gamma, 16),
to_cute(local.tensor.flatten(), 16),
to_cute(prenorm.tensor.flatten(), 16),
to_cute_dynamic(
residual_output_arg.flatten(),
16,
divisibility=self.hidden_size,
),
to_cute_dynamic(norm_output.flatten(), 16, divisibility=self.hidden_size),
to_cute(peers, 8),
to_cute(state.routed_ready_counters.tensor, 4),
to_cute(state.routed_processed_counters.flatten(), 4),
Int64(cast(int, local.multicast_address)),
Int64(cast(int, prenorm.multicast_address)),
Int32(m),
current_cu_stream(),
)
return norm_output, residual_output
class AllReduceRMSNormHTKernel(_HTPath):
def __call__(
self,
local_contribution: torch.Tensor,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
m: int,
*,
state: HTProtocolState,
norm_output: torch.Tensor | None = None,
residual_output: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
self._validate_m(m)
norm_output, residual_output = self._outputs(m, norm_output, residual_output)
local, prenorm = self._state_buffers(state)
peers = cast(torch.Tensor, state.all_reduce_ready_counters.peer_addresses)
residual_arg = residual_source if residual_source is not None else norm_output
residual_output_arg = (
residual_output if residual_output is not None else norm_output
)
index_arg = state.all_reduce_processed_counters.view(torch.int32)
# top_k=0 disables metadata reads, so the aliased placeholders stay unused.
self._compiled(
to_cute_dynamic(
local_contribution.flatten(),
16,
divisibility=self.hidden_size,
),
to_cute_dynamic(local_contribution.flatten(), 2, divisibility=1),
to_cute_dynamic(index_arg.flatten(), 4, divisibility=1),
to_cute_dynamic(
local_contribution.flatten(),
16,
divisibility=self.hidden_size,
),
to_cute_dynamic(residual_arg.flatten(), 16, divisibility=self.hidden_size),
to_cute(gamma, 16),
to_cute(local.tensor.flatten(), 16),
to_cute(prenorm.tensor.flatten(), 16),
to_cute_dynamic(
residual_output_arg.flatten(),
16,
divisibility=self.hidden_size,
),
to_cute_dynamic(norm_output.flatten(), 16, divisibility=self.hidden_size),
to_cute(peers, 8),
to_cute(state.all_reduce_ready_counters.tensor, 4),
to_cute(state.all_reduce_processed_counters.flatten(), 4),
Int64(cast(int, local.multicast_address)),
Int64(cast(int, prenorm.multicast_address)),
Int32(m),
current_cu_stream(),
)
return norm_output, residual_output
class HTProtocol:
"""Own tuning-independent HT State and both persistent path variants."""
def __init__(
self,
hidden_size: int,
top_k: int,
tp_size: int,
rank: int,
capacity_m: int,
rms_epsilon: float,
routed_scaling_factor: float,
weight_bias: float,
*,
include_shared_expert: bool,
add_residual: bool,
write_residual_output: bool,
finalize_tunings: tuple[HTFinalizeTuning, ...],
all_reduce_tunings: tuple[HTAllReduceTuning, ...],
group: dist.ProcessGroup,
) -> None:
self.hidden_size = hidden_size
self.top_k = top_k
self.tp_size = tp_size
self.rank = rank
self.capacity_m = capacity_m
self.rms_epsilon = rms_epsilon
self.routed_scaling_factor = routed_scaling_factor
self.weight_bias = weight_bias
self.include_shared_expert = include_shared_expert
self.add_residual = add_residual
self.write_residual_output = write_residual_output
self.finalize_kernels = {
tuning: FinalizeAllReduceRMSNormHTKernel(
compiled=self._compile_finalize(tuning),
**self._path_kwargs(),
)
for tuning in dict.fromkeys(finalize_tunings)
}
self.all_reduce_kernels = {
tuning: AllReduceRMSNormHTKernel(
compiled=self._compile_all_reduce(tuning),
**self._path_kwargs(),
)
for tuning in dict.fromkeys(all_reduce_tunings)
}
self.state = self._create_state(group)
def _path_kwargs(self) -> _PathKwargs:
return {
"hidden_size": self.hidden_size,
"top_k": self.top_k,
"capacity_m": self.capacity_m,
"write_residual_output": self.write_residual_output,
}
def _resolve_ctas(self, persistent_ctas: int | None) -> int:
sm_count = torch.cuda.get_device_properties(
torch.cuda.current_device()
).multi_processor_count
# min_blocks_per_mp=1 guarantees one resident CTA per SM for this kernel.
resident_ctas = (sm_count // self.tp_size) * self.tp_size
if resident_ctas == 0:
raise ValueError("tp_size exceeds the available SM count")
if persistent_ctas is None:
return resident_ctas
if persistent_ctas <= 0 or persistent_ctas % self.tp_size:
raise ValueError(
"persistent_ctas must be positive and divisible by tp_size"
)
return min(persistent_ctas, resident_ctas)
def _compile_finalize(self, tuning: HTFinalizeTuning):
active_ctas = self._resolve_ctas(tuning.persistent_ctas)
groups = tuning.reduction_cta_groups or active_ctas // self.tp_size
kernel = _MoeFinalizeAllReduceRMSNormHTDeviceKernel(
hidden=self.hidden_size,
top_k=self.top_k,
tp=self.tp_size,
rank=self.rank,
active_ctas=active_ctas,
stages=tuning.stages,
consumer_threads=tuning.consumer_threads,
vectors_per_thread=tuning.vectors_per_thread,
reduction_warps=tuning.reduction_warps,
reduction_cta_groups=groups,
rms_token_groups=tuning.rms_token_groups,
rms_pipeline_stages=tuning.rms_pipeline_stages,
rms_shard_major=tuning.rms_shard_major,
rms_epsilon=self.rms_epsilon,
routed_scaling_factor=self.routed_scaling_factor,
weight_bias=self.weight_bias,
include_shared_expert=self.include_shared_expert,
add_residual=self.add_residual,
write_residual_output=self.write_residual_output,
enable_pdl=tuning.enable_pdl,
)
return self._compile(kernel, top_k=self.top_k)
def _compile_all_reduce(self, tuning: HTAllReduceTuning):
active_ctas = self._resolve_ctas(tuning.persistent_ctas)
groups = tuning.reduction_cta_groups or active_ctas // self.tp_size
kernel = _MoeFinalizeAllReduceRMSNormHTDeviceKernel(
hidden=self.hidden_size,
top_k=0,
tp=self.tp_size,
rank=self.rank,
active_ctas=active_ctas,
stages=tuning.stages,
consumer_threads=tuning.consumer_threads,
vectors_per_thread=tuning.vectors_per_thread,
reduction_warps=tuning.reduction_warps,
reduction_cta_groups=groups,
rms_token_groups=tuning.rms_token_groups,
rms_pipeline_stages=tuning.rms_pipeline_stages,
rms_shard_major=tuning.rms_shard_major,
rms_epsilon=self.rms_epsilon,
routed_scaling_factor=1.0,
weight_bias=self.weight_bias,
include_shared_expert=True,
add_residual=self.add_residual,
write_residual_output=self.write_residual_output,
enable_pdl=tuning.enable_pdl,
)
return self._compile(kernel, top_k=0)
def _compile(
self,
kernel: _MoeFinalizeAllReduceRMSNormHTDeviceKernel,
*,
top_k: int,
):
activation = self.capacity_m * self.hidden_size
token_slots = (self.capacity_m + self.tp_size - 1) // self.tp_size
args = (
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=2, divisibility=max(top_k, 1)
),
make_fake_dynamic_compact_tensor(
Int32, alignment=4, divisibility=max(top_k, 1)
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_compact_tensor(BFloat16, (self.hidden_size,), assumed_align=16),
make_fake_compact_tensor(BFloat16, (activation,), assumed_align=16),
make_fake_compact_tensor(BFloat16, (activation,), assumed_align=16),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_compact_tensor(Int64, (self.tp_size,), assumed_align=8),
make_fake_compact_tensor(Uint32, (token_slots,), assumed_align=4),
make_fake_compact_tensor(
Uint32, (token_slots * self.tp_size,), assumed_align=4
),
Int64(0),
Int64(0),
Int32(self.capacity_m),
current_cu_stream(),
)
return cute.compile(kernel, *args)
def _allocate_large_buffers(
self, group: dist.ProcessGroup
) -> tuple[SymmetricBuffer, SymmetricBuffer]:
shape = (self.capacity_m, self.hidden_size)
device = torch.device("cuda", torch.cuda.current_device())
return (
SymmetricBuffer.allocate(
shape,
torch.bfloat16,
device,
group,
require_multicast=True,
),
SymmetricBuffer.allocate(
shape,
torch.bfloat16,
device,
group,
require_multicast=True,
),
)
def _create_state(self, group: dist.ProcessGroup) -> HTProtocolState:
device = torch.device("cuda", torch.cuda.current_device())
token_slots = (self.capacity_m + self.tp_size - 1) // self.tp_size
def counters() -> tuple[SymmetricBuffer, torch.Tensor]:
ready = SymmetricBuffer.allocate(
(token_slots,),
torch.uint32,
device,
group,
materialize_peer_addresses=True,
)
ready.tensor.zero_()
processed = torch.zeros(
(token_slots, self.tp_size), dtype=torch.uint32, device=device
)
return ready, processed
routed_ready, routed_processed = counters()
all_reduce_ready, all_reduce_processed = counters()
local, prenorm = self._allocate_large_buffers(group)
return HTProtocolState(
local_contributions=local,
prenorm_mailbox=prenorm,
routed_ready_counters=routed_ready,
routed_processed_counters=routed_processed,
all_reduce_ready_counters=all_reduce_ready,
all_reduce_processed_counters=all_reduce_processed,
)
@@ -0,0 +1,47 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Low-latency MNNVL protocol."""
from .protocol import (
LL_ALL_REDUCE_GB300_TP8_H8192,
LL_ALL_REDUCE_GB300_TP8_H8192_M_GE_5,
LL_ALL_REDUCE_GB300_TP8_H8192_M_LE_4,
LL_ALL_REDUCE_GB300_TP16_H8192,
LL_ALL_REDUCE_GB300_TP16_H8192_M_11_TO_17,
LL_ALL_REDUCE_GB300_TP16_H8192_M_GE_18,
LL_ALL_REDUCE_GB300_TP16_H8192_M_LE_10,
LL_FINALIZE_GB300_TP8_H8192_K10,
LL_FINALIZE_GB300_TP8_H8192_K10_M_GE_20,
LL_FINALIZE_GB300_TP16_H8192_K10,
LLAllReduceTuning,
LLCollectiveTuning,
LLFinalizeTuning,
)
__all__ = [
"LL_ALL_REDUCE_GB300_TP8_H8192",
"LL_ALL_REDUCE_GB300_TP8_H8192_M_GE_5",
"LL_ALL_REDUCE_GB300_TP8_H8192_M_LE_4",
"LL_ALL_REDUCE_GB300_TP16_H8192",
"LL_ALL_REDUCE_GB300_TP16_H8192_M_11_TO_17",
"LL_ALL_REDUCE_GB300_TP16_H8192_M_GE_18",
"LL_ALL_REDUCE_GB300_TP16_H8192_M_LE_10",
"LL_FINALIZE_GB300_TP8_H8192_K10",
"LL_FINALIZE_GB300_TP8_H8192_K10_M_GE_20",
"LL_FINALIZE_GB300_TP16_H8192_K10",
"LLAllReduceTuning",
"LLCollectiveTuning",
"LLFinalizeTuning",
]
@@ -0,0 +1,461 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Low-latency MNNVL protocol and its two operation paths."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, TypedDict, cast
import cutlass.cute as cute
import torch
import torch.distributed as dist
from cutlass import BFloat16, Int32, Int64
from cutlass.cute.runtime import make_fake_compact_tensor
from ..cute_dsl_primitives import QUAD_BF16
from ..runtime import (
current_cu_stream,
make_fake_dynamic_compact_tensor,
to_cute,
to_cute_dynamic,
)
from ..symmetric_buffer import SymmetricBuffer
from .device_kernels import (
LAMPORT_GENERATIONS,
_LamportResidualRMSNormDeviceKernel,
_QuadFinalizePublishDeviceKernel,
_ScalarFinalizePublishDeviceKernel,
_SharedOnlyPublishDeviceKernel,
)
@dataclass(frozen=True, slots=True)
class LLCollectiveTuning:
cluster_size: int = 8
rank_lanes: int = 1
threads: int = 128
enable_pdl: bool = True
@dataclass(frozen=True, slots=True)
class LLFinalizeTuning:
elements_per_thread: int = 4
threads: int = 128
prefetch_group: int = 10
load_shared_expert_before_pdl: bool = False
collective: LLCollectiveTuning = LLCollectiveTuning()
@dataclass(frozen=True, slots=True)
class LLAllReduceTuning:
publish_elements_per_thread: int = 8
publish_threads: int = 128
publish_release_before_store: bool = False
collective: LLCollectiveTuning = LLCollectiveTuning()
LL_FINALIZE_GB300_TP8_H8192_K10 = LLFinalizeTuning()
LL_FINALIZE_GB300_TP8_H8192_K10_M_GE_20 = LLFinalizeTuning(
collective=LLCollectiveTuning(cluster_size=16, rank_lanes=2)
)
LL_FINALIZE_GB300_TP16_H8192_K10 = LLFinalizeTuning(
collective=LLCollectiveTuning(cluster_size=16, rank_lanes=2)
)
LL_ALL_REDUCE_GB300_TP8_H8192_M_LE_4 = LLAllReduceTuning(
collective=LLCollectiveTuning(cluster_size=16, threads=64)
)
LL_ALL_REDUCE_GB300_TP8_H8192_M_GE_5 = LLAllReduceTuning()
LL_ALL_REDUCE_GB300_TP16_H8192_M_LE_10 = LLAllReduceTuning(
collective=LLCollectiveTuning(cluster_size=16, threads=64)
)
LL_ALL_REDUCE_GB300_TP16_H8192_M_11_TO_17 = LLAllReduceTuning()
LL_ALL_REDUCE_GB300_TP16_H8192_M_GE_18 = LLAllReduceTuning(
collective=LLCollectiveTuning(cluster_size=16, rank_lanes=2)
)
LL_ALL_REDUCE_GB300_TP8_H8192 = LL_ALL_REDUCE_GB300_TP8_H8192_M_GE_5
LL_ALL_REDUCE_GB300_TP16_H8192 = LL_ALL_REDUCE_GB300_TP16_H8192_M_LE_10
@dataclass(slots=True)
class LLProtocolState:
contribution_mailbox: SymmetricBuffer
stage_state: torch.Tensor
@dataclass(frozen=True, slots=True)
class _CompiledFinalize:
publish: Any
collective: Any
@dataclass(frozen=True, slots=True)
class _CompiledAllReduce:
publish: Any
collective: Any
class _PathKwargs(TypedDict):
hidden_size: int
top_k: int
capacity_m: int
write_residual_output: bool
class _LLPath:
def __init__(
self,
*,
hidden_size: int,
top_k: int,
capacity_m: int,
write_residual_output: bool,
) -> None:
self.hidden_size = hidden_size
self.top_k = top_k
self.capacity_m = capacity_m
self.write_residual_output = write_residual_output
def _outputs(
self,
m: int,
norm_output: torch.Tensor | None,
residual_output: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
shape = (m, self.hidden_size)
device = torch.device("cuda", torch.cuda.current_device())
if norm_output is None:
norm_output = torch.empty(shape, dtype=torch.bfloat16, device=device)
if self.write_residual_output and residual_output is None:
residual_output = torch.empty(shape, dtype=torch.bfloat16, device=device)
return norm_output, residual_output
def _validate_state(self, state: LLProtocolState, m: int) -> None:
if not 1 <= m <= self.capacity_m:
raise ValueError(f"m must be in [1, {self.capacity_m}]")
address = state.contribution_mailbox.multicast_address
if address is None or address % 16:
raise ValueError(
"LL contribution mailbox requires a 16-byte-aligned multicast address"
)
def _launch_collective(
self,
collective,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
state: LLProtocolState,
norm_output: torch.Tensor,
residual_output: torch.Tensor | None,
m: int,
) -> None:
residual_arg = residual_source if residual_source is not None else norm_output
residual_output_arg = (
residual_output if residual_output is not None else norm_output
)
collective(
to_cute(state.contribution_mailbox.tensor.flatten(), 16),
to_cute_dynamic(residual_arg.flatten(), 16, divisibility=self.hidden_size),
to_cute(gamma, 16),
to_cute_dynamic(
residual_output_arg.flatten(),
16,
divisibility=self.hidden_size,
),
to_cute_dynamic(norm_output.flatten(), 16, divisibility=self.hidden_size),
to_cute(state.stage_state, 4),
Int32(m),
current_cu_stream(),
)
class FinalizeAllReduceRMSNormLLKernel(_LLPath):
def __init__(self, *, compiled: _CompiledFinalize, **kwargs) -> None:
super().__init__(**kwargs)
self._compiled = compiled
def __call__(
self,
routed_output: torch.Tensor,
expert_weights: torch.Tensor,
permuted_indices: torch.Tensor,
shared_output: torch.Tensor | None,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
m: int,
*,
state: LLProtocolState,
norm_output: torch.Tensor | None = None,
residual_output: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
self._validate_state(state, m)
norm_output, residual_output = self._outputs(m, norm_output, residual_output)
shared_arg = shared_output if shared_output is not None else norm_output
self._compiled.publish(
to_cute_dynamic(routed_output.flatten(), 16, divisibility=self.hidden_size),
to_cute_dynamic(expert_weights.flatten(), 2, divisibility=self.top_k),
to_cute_dynamic(permuted_indices.flatten(), 4, divisibility=self.top_k),
to_cute_dynamic(shared_arg.flatten(), 16, divisibility=self.hidden_size),
to_cute(state.stage_state, 4),
Int64(cast(int, state.contribution_mailbox.multicast_address)),
Int32(m),
current_cu_stream(),
)
self._launch_collective(
self._compiled.collective,
residual_source,
gamma,
state,
norm_output,
residual_output,
m,
)
return norm_output, residual_output
class AllReduceRMSNormLLKernel(_LLPath):
def __init__(self, *, compiled: _CompiledAllReduce, **kwargs) -> None:
super().__init__(**kwargs)
self._compiled = compiled
def __call__(
self,
local_contribution: torch.Tensor,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
m: int,
*,
state: LLProtocolState,
norm_output: torch.Tensor | None = None,
residual_output: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
self._validate_state(state, m)
norm_output, residual_output = self._outputs(m, norm_output, residual_output)
self._compiled.publish(
to_cute_dynamic(
local_contribution.flatten(),
16,
divisibility=self.hidden_size,
),
to_cute(state.stage_state, 4),
Int64(cast(int, state.contribution_mailbox.multicast_address)),
Int32(m),
current_cu_stream(),
)
self._launch_collective(
self._compiled.collective,
residual_source,
gamma,
state,
norm_output,
residual_output,
m,
)
return norm_output, residual_output
class LLProtocol:
"""Own LL State and protocol-local compiled variants for both paths."""
def __init__(
self,
hidden_size: int,
top_k: int,
tp_size: int,
rank: int,
capacity_m: int,
rms_epsilon: float,
routed_scaling_factor: float,
weight_bias: float,
*,
include_shared_expert: bool,
add_residual: bool,
write_residual_output: bool,
finalize_tunings: tuple[LLFinalizeTuning, ...],
all_reduce_tunings: tuple[LLAllReduceTuning, ...],
group: dist.ProcessGroup,
) -> None:
self.hidden_size = hidden_size
self.top_k = top_k
self.tp_size = tp_size
self.rank = rank
self.capacity_m = capacity_m
self.rms_epsilon = rms_epsilon
self.routed_scaling_factor = routed_scaling_factor
self.weight_bias = weight_bias
self.include_shared_expert = include_shared_expert
self.add_residual = add_residual
self.write_residual_output = write_residual_output
collective_cache = {
tuning: self._compile_collective(tuning)
for tuning in {
*(item.collective for item in finalize_tunings),
*(item.collective for item in all_reduce_tunings),
}
}
self.finalize_kernels = {
tuning: FinalizeAllReduceRMSNormLLKernel(
compiled=_CompiledFinalize(
publish=self._compile_finalize(tuning),
collective=collective_cache[tuning.collective],
),
**self._path_kwargs(),
)
for tuning in dict.fromkeys(finalize_tunings)
}
self.all_reduce_kernels = {
tuning: AllReduceRMSNormLLKernel(
compiled=_CompiledAllReduce(
publish=self._compile_all_reduce_publish(tuning),
collective=collective_cache[tuning.collective],
),
**self._path_kwargs(),
)
for tuning in dict.fromkeys(all_reduce_tunings)
}
self.state = self._create_state(group)
def _path_kwargs(self) -> _PathKwargs:
return {
"hidden_size": self.hidden_size,
"top_k": self.top_k,
"capacity_m": self.capacity_m,
"write_residual_output": self.write_residual_output,
}
def _compile_finalize(self, tuning: LLFinalizeTuning):
if tuning.elements_per_thread not in (1, QUAD_BF16):
raise ValueError("LL finalize elements_per_thread must be 1 or 4")
kwargs: dict[str, Any] = {
"hidden": self.hidden_size,
"top_k": self.top_k,
"tp": self.tp_size,
"rank": self.rank,
"capacity_m": self.capacity_m,
"threads": tuning.threads,
"routed_scaling_factor": self.routed_scaling_factor,
"include_shared_expert": self.include_shared_expert,
"load_shared_expert_before_pdl": tuning.load_shared_expert_before_pdl,
"enable_pdl": tuning.collective.enable_pdl,
"prefetch_group": tuning.prefetch_group,
}
device_kernel = (
_ScalarFinalizePublishDeviceKernel(**kwargs)
if tuning.elements_per_thread == 1
else _QuadFinalizePublishDeviceKernel(**kwargs)
)
args = (
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=2, divisibility=self.top_k
),
make_fake_dynamic_compact_tensor(
Int32, alignment=4, divisibility=self.top_k
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_compact_tensor(Int32, (2,), assumed_align=4),
Int64(0),
Int32(self.capacity_m),
current_cu_stream(),
)
return cute.compile(device_kernel, *args)
def _compile_all_reduce_publish(self, tuning: LLAllReduceTuning):
device_kernel = _SharedOnlyPublishDeviceKernel(
hidden=self.hidden_size,
tp=self.tp_size,
rank=self.rank,
capacity_m=self.capacity_m,
elements_per_thread=tuning.publish_elements_per_thread,
threads=tuning.publish_threads,
release_before_store=tuning.publish_release_before_store,
enable_pdl=tuning.collective.enable_pdl,
)
args = (
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_compact_tensor(Int32, (2,), assumed_align=4),
Int64(0),
Int32(self.capacity_m),
current_cu_stream(),
)
return cute.compile(device_kernel, *args)
def _compile_collective(self, tuning: LLCollectiveTuning):
device_kernel = _LamportResidualRMSNormDeviceKernel(
hidden=self.hidden_size,
tp=self.tp_size,
capacity_m=self.capacity_m,
cluster_size=tuning.cluster_size,
rank_lanes=tuning.rank_lanes,
threads=tuning.threads,
rms_epsilon=self.rms_epsilon,
weight_bias=self.weight_bias,
add_residual=self.add_residual,
write_residual_output=self.write_residual_output,
enable_pdl=tuning.enable_pdl,
)
activation = self.capacity_m * self.hidden_size
args = (
make_fake_compact_tensor(
BFloat16,
(LAMPORT_GENERATIONS * self.tp_size * activation,),
assumed_align=16,
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_compact_tensor(BFloat16, (self.hidden_size,), assumed_align=16),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_dynamic_compact_tensor(
BFloat16, alignment=16, divisibility=self.hidden_size
),
make_fake_compact_tensor(Int32, (2,), assumed_align=4),
Int32(self.capacity_m),
current_cu_stream(),
)
return cute.compile(device_kernel, *args)
def _create_state(self, group: dist.ProcessGroup) -> LLProtocolState:
if dist.get_world_size(group) != self.tp_size:
raise ValueError("ProcessGroup size does not match tp_size")
if dist.get_rank(group) != self.rank:
raise ValueError("ProcessGroup rank does not match rank")
device = torch.device("cuda", torch.cuda.current_device())
mailbox = SymmetricBuffer.allocate(
(
LAMPORT_GENERATIONS,
self.tp_size,
self.capacity_m,
self.hidden_size,
),
torch.bfloat16,
device,
group,
require_multicast=True,
)
mailbox.tensor.view(torch.int16).fill_(-32768)
return LLProtocolState(
contribution_mailbox=mailbox,
stage_state=torch.zeros((2,), dtype=torch.int32, device=device),
)
@@ -0,0 +1,338 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Built-in routing configurations for the MNNVL CuTe DSL backend."""
import torch
from .config import (
KernelTarget,
MNNVLCuteDSLConfig,
MRangeDispatch,
ProtocolKind,
StaticProfile,
)
from .kernel_bt import (
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_0,
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_1,
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_0,
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_1,
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_0,
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_1,
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_0,
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_1,
)
from .kernel_ht import (
HT_ALL_REDUCE_GB300_TP8_H8192,
HT_ALL_REDUCE_GB300_TP16_H8192,
HT_FINALIZE_GB300_TP8_H8192_K10,
HT_FINALIZE_GB300_TP16_H8192_K10,
)
from .kernel_ll import (
LL_ALL_REDUCE_GB300_TP8_H8192,
LL_ALL_REDUCE_GB300_TP16_H8192,
LL_FINALIZE_GB300_TP8_H8192_K10,
LL_FINALIZE_GB300_TP16_H8192_K10,
)
__all__ = [
"BT_ONLY_CONFIG",
"DEFAULT_CONFIG",
"HT_ONLY_CONFIG",
"LL_ONLY_CONFIG",
]
def _target(protocol: ProtocolKind, preset: object) -> KernelTarget[object]:
return KernelTarget(protocol=protocol, preset=preset)
LL_ONLY_CONFIG = MNNVLCuteDSLConfig(
profiles=(
StaticProfile(
tp_size=8,
hidden_size=8192,
top_k=10,
dtype=torch.bfloat16,
finalize_routes=MRangeDispatch(
upper_bounds=(None,),
targets=(
_target(
ProtocolKind.LL,
LL_FINALIZE_GB300_TP8_H8192_K10,
),
),
),
all_reduce_routes=MRangeDispatch(
upper_bounds=(None,),
targets=(
_target(
ProtocolKind.LL,
LL_ALL_REDUCE_GB300_TP8_H8192,
),
),
),
),
StaticProfile(
tp_size=16,
hidden_size=8192,
top_k=10,
dtype=torch.bfloat16,
finalize_routes=MRangeDispatch(
upper_bounds=(None,),
targets=(
_target(
ProtocolKind.LL,
LL_FINALIZE_GB300_TP16_H8192_K10,
),
),
),
all_reduce_routes=MRangeDispatch(
upper_bounds=(None,),
targets=(
_target(
ProtocolKind.LL,
LL_ALL_REDUCE_GB300_TP16_H8192,
),
),
),
),
)
)
BT_ONLY_CONFIG = MNNVLCuteDSLConfig(
profiles=(
StaticProfile(
tp_size=8,
hidden_size=8192,
top_k=10,
dtype=torch.bfloat16,
finalize_routes=MRangeDispatch(
upper_bounds=(48, 1024),
targets=(
_target(
ProtocolKind.BT,
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_0,
),
_target(
ProtocolKind.BT,
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_1,
),
),
),
all_reduce_routes=MRangeDispatch(
upper_bounds=(256, 1024),
targets=(
_target(
ProtocolKind.BT,
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_0,
),
_target(
ProtocolKind.BT,
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_1,
),
),
),
),
StaticProfile(
tp_size=16,
hidden_size=8192,
top_k=10,
dtype=torch.bfloat16,
finalize_routes=MRangeDispatch(
upper_bounds=(52, 1024),
targets=(
_target(
ProtocolKind.BT,
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_0,
),
_target(
ProtocolKind.BT,
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_1,
),
),
),
all_reduce_routes=MRangeDispatch(
upper_bounds=(512, 1024),
targets=(
_target(
ProtocolKind.BT,
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_0,
),
_target(
ProtocolKind.BT,
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_1,
),
),
),
),
)
)
HT_ONLY_CONFIG = MNNVLCuteDSLConfig(
profiles=(
StaticProfile(
tp_size=8,
hidden_size=8192,
top_k=10,
dtype=torch.bfloat16,
finalize_routes=MRangeDispatch(
upper_bounds=(None,),
targets=(
_target(
ProtocolKind.HT,
HT_FINALIZE_GB300_TP8_H8192_K10,
),
),
),
all_reduce_routes=MRangeDispatch(
upper_bounds=(None,),
targets=(
_target(
ProtocolKind.HT,
HT_ALL_REDUCE_GB300_TP8_H8192,
),
),
),
),
StaticProfile(
tp_size=16,
hidden_size=8192,
top_k=10,
dtype=torch.bfloat16,
finalize_routes=MRangeDispatch(
upper_bounds=(None,),
targets=(
_target(
ProtocolKind.HT,
HT_FINALIZE_GB300_TP16_H8192_K10,
),
),
),
all_reduce_routes=MRangeDispatch(
upper_bounds=(None,),
targets=(
_target(
ProtocolKind.HT,
HT_ALL_REDUCE_GB300_TP16_H8192,
),
),
),
),
)
)
DEFAULT_CONFIG = MNNVLCuteDSLConfig(
profiles=(
StaticProfile(
tp_size=8,
hidden_size=8192,
top_k=10,
dtype=torch.bfloat16,
finalize_routes=MRangeDispatch(
upper_bounds=(23, 48, 703, None),
targets=(
_target(
ProtocolKind.LL,
LL_FINALIZE_GB300_TP8_H8192_K10,
),
_target(
ProtocolKind.BT,
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_0,
),
_target(
ProtocolKind.BT,
BT_FINALIZE_GB300_TP8_H8192_K10_PRESET_1,
),
_target(
ProtocolKind.HT,
HT_FINALIZE_GB300_TP8_H8192_K10,
),
),
),
all_reduce_routes=MRangeDispatch(
upper_bounds=(15, 256, 1024, None),
targets=(
_target(
ProtocolKind.LL,
LL_ALL_REDUCE_GB300_TP8_H8192,
),
_target(
ProtocolKind.BT,
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_0,
),
_target(
ProtocolKind.BT,
BT_ALL_REDUCE_GB300_TP8_H8192_PRESET_1,
),
_target(
ProtocolKind.HT,
HT_ALL_REDUCE_GB300_TP8_H8192,
),
),
),
),
StaticProfile(
tp_size=16,
hidden_size=8192,
top_k=10,
dtype=torch.bfloat16,
finalize_routes=MRangeDispatch(
upper_bounds=(7, 52, 703, None),
targets=(
_target(
ProtocolKind.LL,
LL_FINALIZE_GB300_TP16_H8192_K10,
),
_target(
ProtocolKind.BT,
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_0,
),
_target(
ProtocolKind.BT,
BT_FINALIZE_GB300_TP16_H8192_K10_PRESET_1,
),
_target(
ProtocolKind.HT,
HT_FINALIZE_GB300_TP16_H8192_K10,
),
),
),
all_reduce_routes=MRangeDispatch(
upper_bounds=(5, 512, 959, None),
targets=(
_target(
ProtocolKind.LL,
LL_ALL_REDUCE_GB300_TP16_H8192,
),
_target(
ProtocolKind.BT,
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_0,
),
_target(
ProtocolKind.BT,
BT_ALL_REDUCE_GB300_TP16_H8192_PRESET_1,
),
_target(
ProtocolKind.HT,
HT_ALL_REDUCE_GB300_TP16_H8192,
),
),
),
),
)
)
@@ -0,0 +1,70 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Framework-side facilities shared by production Kernel wrappers."""
import cuda.bindings.driver as cuda
import cutlass.cute as cute
import torch
from cutlass.cute.runtime import from_dlpack, make_fake_compact_tensor
class _GraphSafeDLPack:
__slots__ = ("tensor",)
def __init__(self, tensor: torch.Tensor) -> None:
self.tensor = tensor
def __dlpack__(self, stream=None):
# stream=-1 skips producer sync; CuTe launches on the current captured stream.
return self.tensor.__dlpack__(stream=-1)
def __dlpack_device__(self):
return self.tensor.__dlpack_device__()
def to_cute(tensor: torch.Tensor, alignment: int) -> cute.Tensor:
return from_dlpack(
_GraphSafeDLPack(tensor.detach()),
assumed_align=alignment,
)
def to_cute_dynamic(
tensor: torch.Tensor,
alignment: int,
*,
divisibility: int,
) -> cute.Tensor:
return to_cute(tensor, alignment).mark_compact_shape_dynamic(
mode=0,
divisibility=divisibility,
)
def make_fake_dynamic_compact_tensor(
dtype,
*,
alignment: int,
divisibility: int,
) -> cute.Tensor:
return make_fake_compact_tensor(
dtype,
(cute.sym_int32(divisibility=divisibility),),
assumed_align=alignment,
)
def current_cu_stream() -> cuda.CUstream:
return cuda.CUstream(torch.cuda.current_stream().cuda_stream)
@@ -0,0 +1,107 @@
# Copyright (c) 2026 by FlashInfer 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.
"""Typed ownership for one rendezvoused symmetric Tensor."""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
import torch
import torch.distributed as dist
import torch.distributed._symmetric_memory as symm_mem
# Upstream reads this helper through a package-relative import; every
# FlashInfer this tree supports ships it, so only the spelling differs here.
from flashinfer.comm.torch_symmetric_memory import _enable_symm_mem_for_group
@dataclass(frozen=True, slots=True)
class SymmetricBuffer:
"""A symmetric Tensor and the mapping resources derived at rendezvous."""
tensor: torch.Tensor
# Keep the rendezvous mapping alive without exposing the backend handle as
# part of a Kernel State's public surface.
_handle: object = field(repr=False)
multicast_address: int | None = field(default=None, repr=False)
peer_addresses: torch.Tensor | None = field(default=None, repr=False)
@classmethod
def allocate(
cls,
shape: Sequence[int],
dtype: torch.dtype,
device: torch.device,
group: dist.ProcessGroup,
*,
require_multicast: bool = False,
materialize_peer_addresses: bool = False,
) -> SymmetricBuffer:
"""Allocate with the current SymmMem backend and verify requested mappings."""
if symm_mem.get_backend(device) is None:
raise RuntimeError(
"PyTorch Symmetric Memory has no backend for the current device"
)
_enable_symm_mem_for_group(group.group_name)
return cls.rendezvous(
symm_mem.empty(shape, dtype=dtype, device=device),
group,
require_multicast=require_multicast,
materialize_peer_addresses=materialize_peer_addresses,
)
@classmethod
def rendezvous(
cls,
tensor: torch.Tensor,
group: dist.ProcessGroup,
*,
require_multicast: bool = False,
materialize_peer_addresses: bool = False,
) -> SymmetricBuffer:
_enable_symm_mem_for_group(group.group_name)
handle = symm_mem.rendezvous(tensor, group)
multicast_address = None
if require_multicast:
multicast_address = int(handle.multicast_ptr or 0)
if not multicast_address:
raise RuntimeError("NVLink multicast mapping is unavailable")
peer_addresses = None
if materialize_peer_addresses:
# Preserve the rendezvous offset for SymmMem Pool suballocations.
addresses = [
handle.get_remote_tensor(
peer,
tensor.shape,
tensor.dtype,
).data_ptr()
for peer in range(dist.get_world_size(group))
]
if any(not address for address in addresses):
raise RuntimeError("Symmetric peer mapping is unavailable")
peer_addresses = torch.tensor(
addresses,
dtype=torch.int64,
device=tensor.device,
)
return cls(
tensor=tensor,
_handle=handle,
multicast_address=multicast_address,
peer_addresses=peer_addresses,
)
@@ -0,0 +1,584 @@
# Copyright (c) 2026 by FlashInfer 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.
"""MNNVL AllReduce fusion backend implemented with CuTe DSL.
Ported from flashinfer-ai/flashinfer#4358 at main commit 906181e (with
sibling package mnnvl_cutedsl/), pending an installable FlashInfer release
that ships flashinfer.comm.mnnvl_cutedsl. The base communication
infrastructure (mnnvl probing, pattern enum, workspace ABC) still comes
from the installed FlashInfer; the pinned release provides it.
"""
from __future__ import annotations
import logging
from typing import Any, Optional, cast
import torch
import torch.distributed as dist
import torch.distributed._symmetric_memory as symm_mem
# Keep the copied backend and kernel package self-contained while reusing the
# stable communication infrastructure already supplied by the serving image.
from flashinfer.comm.mnnvl import is_multicast_supported
from flashinfer.comm.trtllm_ar import AllReduceFusionPattern
from flashinfer.comm.workspace_base import AllReduceFusionWorkspace
from torch.distributed import ProcessGroup
from .mnnvl_cutedsl import DEFAULT_CONFIG, MNNVLCuteDSLConfig, ProtocolKind
from .mnnvl_cutedsl.config import StaticProfile
from .mnnvl_cutedsl.kernel_bt import BTAllReduceTuning, BTFinalizeTuning
from .mnnvl_cutedsl.kernel_bt.protocol import BTProtocol
from .mnnvl_cutedsl.kernel_ht import HTAllReduceTuning, HTFinalizeTuning
from .mnnvl_cutedsl.kernel_ht.protocol import HTProtocol
from .mnnvl_cutedsl.kernel_ll import LLAllReduceTuning, LLFinalizeTuning
from .mnnvl_cutedsl.kernel_ll.protocol import LLProtocol
logger = logging.getLogger(__name__)
__all__ = [
"MNNVLCuteDSLAllReduceFusionWorkspace",
"mnnvl_cutedsl_allreduce_fusion",
]
def _check_tensor(
tensor: torch.Tensor,
name: str,
*,
shape: tuple[int | None, ...],
dtype: torch.dtype,
device: torch.device,
alignment: int,
) -> None:
if tensor.device != device:
raise ValueError(f"{name} must be on {device}")
if tensor.dtype != dtype:
raise ValueError(f"{name} must have dtype {dtype}")
if tensor.ndim != len(shape) or any(
expected is not None and actual != expected
for actual, expected in zip(tensor.shape, shape, strict=True)
):
raise ValueError(f"{name} has an unsupported shape")
if not tensor.is_contiguous():
raise ValueError(f"{name} must be contiguous")
if tensor.data_ptr() % alignment:
raise ValueError(f"{name} must be {alignment}-byte aligned")
def _warn_pdl_mismatch(
workspace: MNNVLCuteDSLAllReduceFusionWorkspace,
pattern: int,
m: int,
launch_with_pdl: bool,
) -> None:
preset_pdl = workspace._uses_pdl(pattern, m)
if launch_with_pdl != preset_pdl:
logger.warning(
"launch_with_pdl does not match the selected MNNVL CuTe DSL "
"preset; using enable_pdl=%s",
preset_pdl,
)
class MNNVLCuteDSLAllReduceFusionWorkspace(AllReduceFusionWorkspace):
"""Compiled LL, BT, and HT protocols for one static problem shape.
Workspace construction compiles the selected kernels and must finish before
the first invocation. Calls using the same workspace must not overlap.
Feature-disabled tensor slots use internal placeholders that are not read.
"""
_destroyed: bool
def __init__(
self,
tp_size: int,
tp_rank: int,
max_token_num: int,
hidden_dim: int,
dtype: torch.dtype,
*,
group: Optional[ProcessGroup] = None,
top_k: int = 10,
rms_eps: float = 1e-6,
routed_scaling_factor: float = 1.0,
weight_bias: float = 0.0,
include_shared_expert: bool = True,
add_residual: bool = True,
write_residual_output: bool = True,
config: MNNVLCuteDSLConfig = DEFAULT_CONFIG,
) -> None:
if tp_size not in (2, 4, 8, 16):
raise ValueError("tp_size must be 2, 4, 8, or 16")
if not 0 <= tp_rank < tp_size:
raise ValueError("tp_rank must be in [0, tp_size)")
if max_token_num <= 0:
raise ValueError("max_token_num must be positive")
if dtype != torch.bfloat16:
raise ValueError("MNNVL CuTe DSL kernels only support torch.bfloat16")
if not torch.cuda.is_available():
raise RuntimeError("MNNVL CuTe DSL kernels require CUDA")
device = torch.device("cuda", torch.cuda.current_device())
if torch.cuda.get_device_capability(device)[0] < 10:
raise RuntimeError("MNNVL CuTe DSL kernels require a Blackwell GPU")
if symm_mem.get_backend(device) is None:
raise RuntimeError("PyTorch Symmetric Memory is unavailable")
if not is_multicast_supported(device.index):
raise RuntimeError("NVLink multicast is unavailable")
if group is None:
if not dist.is_initialized():
raise ValueError("A ProcessGroup is required before initialization")
group = dist.group.WORLD
if dist.get_world_size(group) != tp_size:
raise ValueError("ProcessGroup size does not match tp_size")
if dist.get_rank(group) != tp_rank:
raise ValueError("ProcessGroup rank does not match tp_rank")
super().__init__(tp_size, tp_rank)
self._protocols: dict[ProtocolKind, LLProtocol | BTProtocol | HTProtocol] = {}
self.max_token_num = max_token_num
self.hidden_dim = hidden_dim
self.top_k = top_k
self.dtype = dtype
self.group = group
self.rms_eps = rms_eps
self.routed_scaling_factor = routed_scaling_factor
self.weight_bias = weight_bias
self.include_shared_expert = include_shared_expert
self.add_residual = add_residual
self.write_residual_output = write_residual_output
self.config = config
self.profile = config.resolve(
tp_size=tp_size,
hidden_size=hidden_dim,
top_k=top_k,
dtype=dtype,
capacity_m=max_token_num,
)
for protocol in (ProtocolKind.LL, ProtocolKind.BT, ProtocolKind.HT):
capacity = self.profile.protocol_capacity(
protocol, capacity_m=max_token_num
)
if capacity is None:
continue
finalize_tunings = self._tunings(
self.profile, protocol, finalize=True, capacity_m=capacity
)
all_reduce_tunings = self._tunings(
self.profile, protocol, finalize=False, capacity_m=capacity
)
common = dict(
hidden_size=hidden_dim,
top_k=top_k,
tp_size=tp_size,
rank=tp_rank,
capacity_m=capacity,
rms_epsilon=rms_eps,
routed_scaling_factor=routed_scaling_factor,
weight_bias=weight_bias,
include_shared_expert=include_shared_expert,
add_residual=add_residual,
write_residual_output=write_residual_output,
group=group,
)
instance: LLProtocol | BTProtocol | HTProtocol
if protocol is ProtocolKind.LL:
instance = LLProtocol(
**common,
finalize_tunings=finalize_tunings,
all_reduce_tunings=all_reduce_tunings,
)
elif protocol is ProtocolKind.BT:
instance = BTProtocol(
**common,
finalize_tunings=finalize_tunings,
all_reduce_tunings=all_reduce_tunings,
)
else:
instance = HTProtocol(
**common,
finalize_tunings=finalize_tunings,
all_reduce_tunings=all_reduce_tunings,
)
self._protocols[protocol] = instance
torch.cuda.synchronize(device)
dist.barrier(group=group)
@staticmethod
def _tunings(
profile: StaticProfile,
protocol: ProtocolKind,
*,
finalize: bool,
capacity_m: int,
) -> tuple:
routes = profile.finalize_routes if finalize else profile.all_reduce_routes
tunings = tuple(
dict.fromkeys(
target.preset
for target in routes.targets_for_capacity(capacity_m)
if target.protocol is protocol
)
)
expected_type = {
(ProtocolKind.LL, True): LLFinalizeTuning,
(ProtocolKind.LL, False): LLAllReduceTuning,
(ProtocolKind.BT, True): BTFinalizeTuning,
(ProtocolKind.BT, False): BTAllReduceTuning,
(ProtocolKind.HT, True): HTFinalizeTuning,
(ProtocolKind.HT, False): HTAllReduceTuning,
}[(protocol, finalize)]
if not all(isinstance(tuning, expected_type) for tuning in tunings):
path = "finalize" if finalize else "all-reduce"
raise TypeError(f"Invalid {protocol.value} {path} preset")
return tunings
def _uses_pdl(self, pattern: int, m: int) -> bool:
if pattern == AllReduceFusionPattern.kMoEFinalizeARResidualRMSNorm:
target = self.profile.finalize_routes.select(m)
elif pattern == AllReduceFusionPattern.kARResidualRMSNorm:
target = self.profile.all_reduce_routes.select(m)
else:
raise NotImplementedError("Unsupported MNNVL CuTe DSL fusion pattern")
preset = cast(Any, target.preset)
enabled = getattr(preset, "enable_pdl", None)
if enabled is None:
enabled = preset.collective.enable_pdl
return bool(enabled)
@property
def backend(self) -> str:
return "mnnvl-cutedsl"
def is_buffer_size_sufficient(
self,
tp_size: int,
num_tokens: int,
hidden_dim: int,
dtype: torch.dtype,
use_oneshot=None,
) -> bool:
del use_oneshot
return (
tp_size == self.world_size
and num_tokens <= self.max_token_num
and hidden_dim == self.hidden_dim
and dtype == self.dtype
)
def _finalize_all_reduce_rms_norm(
self,
routed_output: torch.Tensor,
expert_weights: torch.Tensor,
permuted_indices: torch.Tensor,
shared_output: torch.Tensor | None,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
m: int,
*,
norm_output: torch.Tensor | None,
residual_output: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
target = self.profile.finalize_routes.select(m)
protocol = cast(Any, self._protocols[target.protocol])
kernel = protocol.finalize_kernels[target.preset]
return kernel(
routed_output,
expert_weights,
permuted_indices,
shared_output,
residual_source,
gamma,
m,
state=protocol.state,
norm_output=norm_output,
residual_output=residual_output,
)
def _all_reduce_rms_norm(
self,
local_contribution: torch.Tensor,
residual_source: torch.Tensor | None,
gamma: torch.Tensor,
m: int,
*,
norm_output: torch.Tensor | None,
residual_output: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
target = self.profile.all_reduce_routes.select(m)
protocol = cast(Any, self._protocols[target.protocol])
kernel = protocol.all_reduce_kernels[target.preset]
return kernel(
local_contribution,
residual_source,
gamma,
m,
state=protocol.state,
norm_output=norm_output,
residual_output=residual_output,
)
def destroy(self) -> None:
if self._destroyed:
return
self._protocols.clear()
self._destroyed = True
def _mnnvl_cutedsl_allreduce_fusion(
input: torch.Tensor,
workspace: MNNVLCuteDSLAllReduceFusionWorkspace,
pattern: int,
*,
launch_with_pdl: bool,
output: Optional[torch.Tensor] = None,
residual_in: Optional[torch.Tensor] = None,
residual_out: Optional[torch.Tensor] = None,
norm_out: Optional[torch.Tensor] = None,
quant_out: Optional[torch.Tensor] = None,
scale_out: Optional[torch.Tensor] = None,
rms_gamma: Optional[torch.Tensor] = None,
rms_eps: float = 1e-6,
scale_factor: Optional[torch.Tensor | float] = None,
layout_code: Optional[int] = None,
use_oneshot: Optional[bool] = None,
fp32_acc: bool = False,
moe_reduction_device_num_experts: Optional[int] = None,
moe_reduction_scale_input: Optional[torch.Tensor] = None,
moe_reduction_active_experts_token_input: Optional[torch.Tensor] = None,
moe_reduction_token_input: Optional[torch.Tensor] = None,
weight_bias: float = 0.0,
expanded_idx_to_permuted_idx: Optional[torch.Tensor] = None,
expert_scale_factor: Optional[torch.Tensor] = None,
shared_expert_output: Optional[torch.Tensor] = None,
block_quant_group_size: Optional[int] = None,
) -> torch.Tensor:
if workspace._destroyed:
raise RuntimeError(
"The MNNVLCuteDSLAllReduceFusionWorkspace has been destroyed"
)
if pattern not in (
AllReduceFusionPattern.kARResidualRMSNorm,
AllReduceFusionPattern.kMoEFinalizeARResidualRMSNorm,
):
raise NotImplementedError("Unsupported MNNVL CuTe DSL fusion pattern")
unsupported = [
name
for name, value in (
("output", output),
("quant_out", quant_out),
("scale_out", scale_out),
("scale_factor", scale_factor),
("layout_code", layout_code),
("use_oneshot", use_oneshot),
("block_quant_group_size", block_quant_group_size),
("moe_reduction_scale_input", moe_reduction_scale_input),
(
"moe_reduction_active_experts_token_input",
moe_reduction_active_experts_token_input,
),
("moe_reduction_token_input", moe_reduction_token_input),
)
if value is not None
]
if fp32_acc:
unsupported.append("fp32_acc")
if moe_reduction_device_num_experts is not None:
unsupported.append("moe_reduction_device_num_experts")
if unsupported:
raise ValueError("MNNVL CuTe DSL does not support: " + ", ".join(unsupported))
if rms_eps != workspace.rms_eps:
raise ValueError("rms_eps does not match the compiled workspace")
if weight_bias != workspace.weight_bias:
raise ValueError("weight_bias does not match the compiled workspace")
if rms_gamma is None:
raise ValueError("rms_gamma is required")
if workspace.add_residual and residual_in is None:
raise ValueError("residual_in is required by the compiled workspace")
if not workspace.add_residual and residual_in is not None:
raise ValueError("residual_in must be None for this compiled workspace")
if not workspace.write_residual_output and residual_out is not None:
raise ValueError("residual_out must be None for this compiled workspace")
device = torch.device("cuda", torch.cuda.current_device())
hidden = workspace.hidden_dim
_check_tensor(
rms_gamma,
"rms_gamma",
shape=(hidden,),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
if pattern == AllReduceFusionPattern.kARResidualRMSNorm:
if any(
value is not None
for value in (
expanded_idx_to_permuted_idx,
expert_scale_factor,
shared_expert_output,
)
):
raise ValueError("MoE finalize operands require the finalize pattern")
_check_tensor(
input,
"input",
shape=(None, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
m = input.shape[0]
if not 1 <= m <= workspace.max_token_num:
raise ValueError("input token count exceeds workspace capacity")
_warn_pdl_mismatch(workspace, pattern, m, launch_with_pdl)
if residual_in is not None:
_check_tensor(
residual_in,
"residual_in",
shape=(m, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
if norm_out is not None:
_check_tensor(
norm_out,
"norm_out",
shape=(m, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
if residual_out is not None:
_check_tensor(
residual_out,
"residual_out",
shape=(m, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
norm_out, _ = workspace._all_reduce_rms_norm(
input,
residual_in,
rms_gamma,
input.shape[0],
norm_output=norm_out,
residual_output=residual_out,
)
return norm_out
if pattern == AllReduceFusionPattern.kMoEFinalizeARResidualRMSNorm:
if expanded_idx_to_permuted_idx is None:
raise ValueError("expanded_idx_to_permuted_idx is required")
if expert_scale_factor is None:
raise ValueError("expert_scale_factor is required")
if workspace.include_shared_expert and shared_expert_output is None:
raise ValueError(
"shared_expert_output is required by the compiled workspace"
)
if not workspace.include_shared_expert and shared_expert_output is not None:
raise ValueError(
"shared_expert_output must be None for this compiled workspace"
)
m = expanded_idx_to_permuted_idx.shape[0]
if not 1 <= m <= workspace.max_token_num:
raise ValueError("input token count exceeds workspace capacity")
_warn_pdl_mismatch(workspace, pattern, m, launch_with_pdl)
_check_tensor(
input,
"input",
shape=(None, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
_check_tensor(
expert_scale_factor,
"expert_scale_factor",
shape=(m, workspace.top_k),
dtype=torch.bfloat16,
device=device,
alignment=2,
)
_check_tensor(
expanded_idx_to_permuted_idx,
"expanded_idx_to_permuted_idx",
shape=(m, workspace.top_k),
dtype=torch.int32,
device=device,
alignment=4,
)
if shared_expert_output is not None:
_check_tensor(
shared_expert_output,
"shared_expert_output",
shape=(m, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
if residual_in is not None:
_check_tensor(
residual_in,
"residual_in",
shape=(m, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
if norm_out is not None:
_check_tensor(
norm_out,
"norm_out",
shape=(m, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
if residual_out is not None:
_check_tensor(
residual_out,
"residual_out",
shape=(m, hidden),
dtype=torch.bfloat16,
device=device,
alignment=16,
)
norm_out, _ = workspace._finalize_all_reduce_rms_norm(
input,
expert_scale_factor,
expanded_idx_to_permuted_idx,
shared_expert_output,
residual_in,
rms_gamma,
m,
norm_output=norm_out,
residual_output=residual_out,
)
return norm_out
raise AssertionError("unreachable")
# Keep the upstream private name intact while exposing SGLang's backend entry point;
# upstream exports it through flashinfer.comm.allreduce_fusion.
mnnvl_cutedsl_allreduce_fusion = _mnnvl_cutedsl_allreduce_fusion
@@ -465,9 +465,10 @@ def _fused_gate_sigmoid_mul_add_kernel(
hidden_states_ptr, # [num_tokens, hidden_dim]
gate_weight_ptr, # [hidden_dim]
shared_output_ptr, # [num_tokens, hidden_dim]
final_hidden_states_ptr, # [num_tokens, hidden_dim]
output_ptr, # [num_tokens, hidden_dim], optionally also the addend
hidden_dim: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
DO_ADD: tl.constexpr = True,
USE_PDL: tl.constexpr = False,
):
pid = tl.program_id(axis=0).to(tl.int64)
@@ -487,41 +488,39 @@ def _fused_gate_sigmoid_mul_add_kernel(
s = tl.load(shared_output_ptr + row_offset + offsets, mask=mask, other=0.0).to(
tl.float32
)
f = tl.load(
final_hidden_states_ptr + row_offset + offsets, mask=mask, other=0.0
).to(tl.float32)
if DO_ADD:
f = tl.load(output_ptr + row_offset + offsets, mask=mask, other=0.0).to(
tl.float32
)
if USE_PDL:
tl.extra.cuda.gdc_launch_dependents()
gate_val = tl.sigmoid(tl.sum(h * w, axis=0))
result = f + gate_val * s
result = gate_val * s
if DO_ADD:
result += f
tl.store(final_hidden_states_ptr + row_offset + offsets, result, mask=mask)
tl.store(output_ptr + row_offset + offsets, result, mask=mask)
def fused_gate_sigmoid_mul_add(
def _launch_fused_gate_sigmoid_mul(
hidden_states: torch.Tensor,
gate_weight: torch.Tensor,
shared_output: torch.Tensor,
final_hidden_states: torch.Tensor,
output: torch.Tensor,
*,
do_add: bool,
) -> None:
"""
Fused gate-sigmoid-mul-add for MoE shared expert gating.
Equivalent to:
gate = hidden_states @ gate_weight
final_hidden_states += sigmoid(gate).unsqueeze(1) * shared_output
"""
assert hidden_states.is_contiguous(), "hidden_states must be contiguous"
assert gate_weight.is_contiguous(), "gate_weight must be contiguous"
assert shared_output.is_contiguous(), "shared_output must be contiguous"
assert final_hidden_states.is_contiguous(), "final_hidden_states must be contiguous"
assert output.is_contiguous(), "output must be contiguous"
num_tokens, hidden_dim = hidden_states.shape
assert gate_weight.shape == (hidden_dim,)
assert shared_output.shape == (num_tokens, hidden_dim)
assert final_hidden_states.shape == (num_tokens, hidden_dim)
assert output.shape == (num_tokens, hidden_dim)
max_warps = 16 if _is_hip else 32
config = {
@@ -541,9 +540,43 @@ def fused_gate_sigmoid_mul_add(
hidden_states,
gate_weight,
shared_output,
final_hidden_states,
output,
hidden_dim=hidden_dim,
DO_ADD=do_add,
USE_PDL=use_pdl,
**config,
**pdl_kwargs,
)
def fused_gate_sigmoid_mul(
hidden_states: torch.Tensor,
gate_weight: torch.Tensor,
shared_output: torch.Tensor,
) -> torch.Tensor:
"""Materialize the gated shared-expert contribution without an add/copy."""
output = torch.empty_like(shared_output)
_launch_fused_gate_sigmoid_mul(
hidden_states,
gate_weight,
shared_output,
output,
do_add=False,
)
return output
def fused_gate_sigmoid_mul_add(
hidden_states: torch.Tensor,
gate_weight: torch.Tensor,
shared_output: torch.Tensor,
final_hidden_states: torch.Tensor,
) -> None:
"""Add the gated shared-expert contribution to routed-expert output."""
_launch_fused_gate_sigmoid_mul(
hidden_states,
gate_weight,
shared_output,
final_hidden_states,
do_add=True,
)
@@ -0,0 +1,367 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
# Vendored from flashinfer-ai/flashinfer@629147317d4149a12e53bcef27808bac380c283f.
"""Register-prefetch BF16 GEMM for low-M, long-K decode shapes.
The kernel keeps a complete output dot product inside one CTA and reuses each
prefetched B value across several public-M rows. It is intentionally a
separate autotuner runner from the Blackwell tensor-core split-K kernel: the
two algorithms have different useful shape regions and tactic spaces.
"""
from __future__ import annotations
import dataclasses
import functools
import cuda.bindings.driver as _cuda
import cutlass
import cutlass.cute as cute
import torch as _torch
from cutlass import const_expr
from cutlass.cute import experimental as cute_ext
from cutlass.cute.runtime import from_dlpack
_VECTOR_WIDTH = 8
_SUPPORTED_BLOCK_SIZES = (32, 64, 96, 128, 192, 256, 384)
_SUPPORTED_OUTPUTS_PER_BLOCK = (1, 2, 4)
_MAX_M = 32
_COMPILE_OPTIONS = "--ptxas-options -maxrregcount=64"
@dataclasses.dataclass(frozen=True)
class DirectTactic:
"""One direct-kernel specialization."""
block_size: int
outputs_per_block: int
rows_per_block: int
def _default_rows_per_block(m: int) -> int:
if m <= 8:
return m
return next(rows for rows in (8, 4, 2, 1) if m % rows == 0)
def validate_tactic(tactic: DirectTactic, m: int, n: int, k: int) -> None:
"""Reject a direct tactic that cannot serve ``(m, n, k)``."""
if tactic.block_size not in _SUPPORTED_BLOCK_SIZES:
raise ValueError(f"unsupported block_size={tactic.block_size}")
if tactic.outputs_per_block not in _SUPPORTED_OUTPUTS_PER_BLOCK:
raise ValueError(f"unsupported outputs_per_block={tactic.outputs_per_block}")
if not 1 <= m <= _MAX_M:
raise ValueError(f"direct GEMM requires 1 <= M <= {_MAX_M}, got {m}")
if not 1 <= tactic.rows_per_block <= m or m % tactic.rows_per_block:
raise ValueError(f"rows_per_block={tactic.rows_per_block} must divide M={m}")
if n <= 0 or n % tactic.outputs_per_block:
raise ValueError(
f"N={n} must be divisible by outputs_per_block={tactic.outputs_per_block}"
)
k_tile = tactic.block_size * _VECTOR_WIDTH
if k <= 0 or k % k_tile:
raise ValueError(f"K={k} must be divisible by {k_tile}")
def default_tactic(m: int, n: int, k: int) -> DirectTactic:
"""Choose the measured register-prefetch fallback tactic."""
block_size = next(
(
block
for block in (256, 192, 128, 96, 64, 32)
if k % (block * _VECTOR_WIDTH) == 0
),
None,
)
if block_size is None:
raise ValueError("direct GEMM requires a supported 16-byte K tiling")
outputs_per_block = next(outputs for outputs in (2, 1) if n % outputs == 0)
tactic = DirectTactic(
block_size,
outputs_per_block,
_default_rows_per_block(m),
)
validate_tactic(tactic, m, n, k)
return tactic
def autotune_tactics(m: int, n: int, k: int) -> list[DirectTactic]:
"""Enumerate the compact tactic space used by FlashInfer autotuning.
Block sizes cover every configuration exercised in the H100/B200 sweep;
output grouping spans the measured 1/2/4-column choices. Row tiling stays
at the occupancy-oriented default to keep JIT cost bounded.
"""
try:
default = default_tactic(m, n, k)
except ValueError:
return []
tactics = [default]
for block_size in _SUPPORTED_BLOCK_SIZES:
for outputs_per_block in _SUPPORTED_OUTPUTS_PER_BLOCK:
tactic = DirectTactic(
block_size,
outputs_per_block,
default.rows_per_block,
)
try:
validate_tactic(tactic, m, n, k)
except ValueError:
continue
tactics.append(tactic)
return list(dict.fromkeys(tactics))
def prefer_direct_bf16_gemm_sm100(m: int, n: int, k: int) -> bool:
"""Return the conservative B200 no-autotune crossover heuristic.
The three bands are a compact fit to a warm/cold sweep over M=1..16,24,32,
18 N values, and 11 K values. This is deliberately not a blanket rule for
K=8192: direct wins only where public M and N leave the tensor-core path
with too little independent output work.
"""
return k == 8192 and (
(m == 1 and n <= 4608) or (m <= 4 and n <= 512) or (m <= 8 and n <= 256)
)
class DirectDenseGemmKernel:
"""K-specialized direct GEMM with whole-mainloop vector prefetch."""
def __init__(
self,
*,
element_type,
num_rows: int,
k_extent: int,
tactic: DirectTactic,
use_pdl: bool,
) -> None:
validate_tactic(tactic, num_rows, tactic.outputs_per_block, k_extent)
self.element_type = element_type
self.num_rows = num_rows
self.rows_per_block = tactic.rows_per_block
self.k_extent = k_extent
self.block_size = tactic.block_size
self.outputs_per_block = tactic.outputs_per_block
self.vector_width = _VECTOR_WIDTH
self.use_pdl = use_pdl
self.num_warps = tactic.block_size // cute.arch.WARP_SIZE
self.num_k_tiles = k_extent // (tactic.block_size * _VECTOR_WIDTH)
@cute.jit
def __call__(
self,
gA: cute.Tensor,
gB: cute.Tensor,
gC: cute.Tensor,
stream: _cuda.CUstream,
) -> None:
n = cute.size(gB, mode=[0])
copy_a = cute.make_copy_atom(
cute.nvgpu.CopyG2ROp(),
self.element_type,
num_bits_per_copy=self.vector_width * self.element_type.width,
load_cache_mode=cute.nvgpu.LoadCacheMode.ALWAYS,
)
copy_b = cute.make_copy_atom(
cute.nvgpu.CopyG2ROp(),
self.element_type,
num_bits_per_copy=self.vector_width * self.element_type.width,
load_cache_mode=cute.nvgpu.LoadCacheMode.STREAMING,
)
self.kernel(gA, gB, gC, copy_a, copy_b).launch(
grid=[
cute.ceil_div(n, self.outputs_per_block),
self.num_rows // self.rows_per_block,
1,
],
block=[self.block_size, 1, 1],
smem=self.rows_per_block * self.outputs_per_block * self.num_warps * 4,
stream=stream,
use_pdl=self.use_pdl,
min_blocks_per_mp=1,
)
@cute.kernel
def kernel(
self,
gA: cute.Tensor,
gB: cute.Tensor,
gC: cute.Tensor,
copy_a: cute.CopyAtom,
copy_b: cute.CopyAtom,
) -> None:
tidx, _, _ = cute.arch.thread_idx()
block_idx, block_m, _ = cute.arch.block_idx()
warp_idx = cute.arch.warp_idx()
num_rows: cutlass.Constexpr = self.rows_per_block
outputs_per_block: cutlass.Constexpr = self.outputs_per_block
vector_width: cutlass.Constexpr = self.vector_width
block_size: cutlass.Constexpr = self.block_size
num_warps: cutlass.Constexpr = self.num_warps
num_k_tiles: cutlass.Constexpr = self.num_k_tiles
acc = cute.make_rmem_tensor(
cute.make_layout(
(num_rows, outputs_per_block), stride=(outputs_per_block, 1)
),
cutlass.Float32,
)
acc.fill(0.0)
if const_expr(self.use_pdl):
cute.arch.griddepcontrol_wait()
n_base = block_idx * outputs_per_block
m_base = block_m * num_rows
gA_vec = cute.logical_divide(gA, (None, vector_width))
gB_vec = cute.logical_divide(gB, (None, vector_width))
tA_all = cute.logical_divide(gA_vec, (None, (None, block_size)))
tB_all = cute.logical_divide(gB_vec, (None, (None, block_size)))
tA = tA_all[None, (None, (tidx, None))]
b_regs = cute.make_rmem_tensor(
cute.make_layout(
(outputs_per_block, num_k_tiles, vector_width),
stride=(num_k_tiles * vector_width, vector_width, 1),
),
self.element_type,
)
for ni in cutlass.range_constexpr(outputs_per_block):
tB = tB_all[n_base + ni, (None, (tidx, None))]
for k_tile in cutlass.range_constexpr(num_k_tiles):
cute.copy(copy_b, tB[None, k_tile], b_regs[ni, k_tile, None])
a_regs = cute.make_rmem_tensor(
cute.make_layout((num_k_tiles, vector_width), stride=(vector_width, 1)),
self.element_type,
)
for mi in cutlass.range_constexpr(num_rows):
for k_tile in cutlass.range_constexpr(num_k_tiles):
cute.copy(
copy_a,
tA[m_base + mi, None, k_tile],
a_regs[k_tile, None],
)
for k_tile in cutlass.range_constexpr(num_k_tiles):
for vi in cutlass.range_constexpr(vector_width):
a_value = a_regs[k_tile, vi].to(cutlass.Float32)
for ni in cutlass.range_constexpr(outputs_per_block):
acc[mi, ni] = acc[mi, ni] + a_value * b_regs[ni, k_tile, vi].to(
cutlass.Float32
)
for mi in cutlass.range_constexpr(num_rows):
for ni in cutlass.range_constexpr(outputs_per_block):
acc[mi, ni] = cute.arch.warp_reduction_sum(acc[mi, ni])
smem_layout = cute.make_layout(
(num_rows, outputs_per_block, num_warps),
stride=(outputs_per_block * num_warps, num_warps, 1),
)
smem = cutlass.utils.SmemAllocator()
partials = smem.allocate_tensor(cutlass.Float32, smem_layout, byte_alignment=16)
with cute.arch.elect_one():
for mi in cutlass.range_constexpr(num_rows):
for ni in cutlass.range_constexpr(outputs_per_block):
partials[mi, ni, warp_idx] = acc[mi, ni]
cute.arch.sync_threads()
if tidx == 0:
for mi in cutlass.range_constexpr(num_rows):
for ni in cutlass.range_constexpr(outputs_per_block):
total = cutlass.Float32(0.0)
for warp in cutlass.range_constexpr(num_warps):
total = total + partials[mi, ni, warp]
gC[m_base + mi, n_base + ni] = total.to(self.element_type)
if const_expr(self.use_pdl):
cute.arch.griddepcontrol_launch_dependents()
def _from_dlpack_static(tensor: _torch.Tensor):
# K is specialized and the row stride must retain its 16-byte divisibility
# for the verifier to accept vectorized G2R copies.
return from_dlpack(tensor, assumed_align=32)
def _make_compile_repr_tensors(dtype, m: int, n: int, k: int):
return tuple(
_from_dlpack_static(tensor)
for tensor in (
_torch.empty((m, k), dtype=dtype, device="cuda"),
_torch.empty((n, k), dtype=dtype, device="cuda"),
_torch.empty((m, n), dtype=dtype, device="cuda"),
)
)
@functools.cache
def _get_compiled_direct_kernel(
dtype,
m: int,
n: int,
k: int,
tactic: DirectTactic,
use_pdl: bool,
):
if dtype != _torch.bfloat16:
raise ValueError(f"direct GEMM supports BF16; got {dtype}")
kernel = DirectDenseGemmKernel(
element_type=cutlass.BFloat16,
num_rows=m,
k_extent=k,
tactic=tactic,
use_pdl=use_pdl,
)
tensors = _make_compile_repr_tensors(dtype, m, n, k)
stream = _cuda.CUstream(_torch.cuda.current_stream().cuda_stream)
return cute_ext.compile(kernel, *tensors, stream, options=_COMPILE_OPTIONS)
def _validate_runtime_tensors(a, b, out, tactic: DirectTactic):
if any(not isinstance(tensor, _torch.Tensor) for tensor in (a, b, out)):
raise ValueError("a, b, and out must be torch tensors")
if a.ndim != 2 or b.ndim != 2 or out.ndim != 2:
raise ValueError("direct GEMM accepts only 2D tensors")
if a.device.type != "cuda" or b.device != a.device or out.device != a.device:
raise ValueError("a, b, and out must be on the same CUDA device")
if a.dtype != _torch.bfloat16 or b.dtype != a.dtype or out.dtype != a.dtype:
raise ValueError("a, b, and out must share BF16 dtype")
if not a.is_contiguous() or not b.T.is_contiguous() or not out.is_contiguous():
raise ValueError("direct GEMM requires row-major A/out and column-major B")
if any(tensor.data_ptr() % 32 for tensor in (a, b, out)):
raise ValueError("a, b, and out must be 32-byte aligned")
m, k = a.shape
if b.shape[0] != k:
raise ValueError(
f"incompatible shapes: a is {tuple(a.shape)}, b is {tuple(b.shape)}"
)
n = b.shape[1]
if out.shape != (m, n):
raise ValueError(f"out must have shape {(m, n)}, got {tuple(out.shape)}")
validate_tactic(tactic, m, n, k)
return m, n, k
def run_direct_dense(a, b, out, pdl: bool, tactic: DirectTactic):
"""Run direct ``A[M,K] @ B[K,N]`` with the ``mm_bf16`` layouts."""
m, n, k = _validate_runtime_tensors(a, b, out, tactic)
compiled = _get_compiled_direct_kernel(a.dtype, m, n, k, tactic, pdl)
tensors = tuple(_from_dlpack_static(tensor) for tensor in (a, b.T, out))
stream = _cuda.CUstream(_torch.cuda.current_stream(a.device).cuda_stream)
compiled(*tensors, stream)
return out
__all__ = [
"DirectTactic",
"autotune_tactics",
"default_tactic",
"prefer_direct_bf16_gemm_sm100",
"run_direct_dense",
]
File diff suppressed because it is too large Load Diff
+40 -21
View File
@@ -911,9 +911,7 @@ def post_reorder_deepgemm_triton_kernel(
BLOCK_SIZE: tl.constexpr,
NUM_STAGES: tl.constexpr,
):
"""`expert_id >= 0` includes the shared expert at num_experts (padding=-1); don't
switch to the cutlass `!= num_local_experts` gate. routed_scaling_factor is folded into the store.
"""
"""Accumulate valid permuted rows; routed_scaling_factor is folded into the store."""
OutDtype = output_ptr.dtype.element_ty
offset = BLOCK_SIZE * tl.program_id(1) + tl.arange(0, BLOCK_SIZE)
@@ -935,9 +933,8 @@ def post_reorder_deepgemm_triton_kernel(
sum_vec = tl.zeros([BLOCK_SIZE], dtype=tl.float32)
for idx in range(topk):
expert_id = tl.load(token_topk_ids_ptr + idx)
if expert_id >= 0:
dst_idx = tl.load(token_src2dst_ptr + idx).to(tl.int64)
dst_idx = tl.load(token_src2dst_ptr + idx).to(tl.int64)
if dst_idx >= 0:
weight_scale = tl.load(token_topk_weights_ptr + idx).to(tl.float32)
load_ptr_offs = down_output_ptr_offs + dst_idx * hidden_size
in_data = tl.load(load_ptr_offs, mask=mask).to(tl.float32)
@@ -1074,6 +1071,8 @@ def _fwd_kernel_ep_scatter_2(
output_index,
output_index_stride0,
output_index_stride1,
expert_start,
num_experts,
topk_num: tl.constexpr,
HIDDEN_SIZE: tl.constexpr,
HIDDEN_SIZE_PAD: tl.constexpr,
@@ -1105,17 +1104,24 @@ def _fwd_kernel_ep_scatter_2(
for topk_idx_int32 in tl.range(0, topk_num, 1, num_stages=4):
topk_index = topk_idx_int32.to(tl.int64)
expert_id = tl.load(recv_topk + token_id * recv_topk_stride0 + topk_index)
if expert_id >= 0:
global_expert_id = tl.load(
recv_topk + token_id * recv_topk_stride0 + topk_index
)
expert_id = global_expert_id - expert_start
output_index_ptr = (
output_index + token_id * output_index_stride0 + topk_index
)
valid = (expert_id >= 0) & (expert_id < num_experts)
# The post-permute path uses this index as the validity sentinel.
# Initialize non-local/padding lanes in this same kernel.
tl.store(output_index_ptr, -1)
if valid:
dest_token_index_int32 = tl.atomic_add(
expert_start_loc + expert_id, 1, sem=ATOMIC_ADD_SEM
)
dest_token_index = dest_token_index_int32.to(tl.int64)
tl.store(
output_index + token_id * output_index_stride0 + topk_index,
dest_token_index_int32,
)
tl.store(output_index_ptr, dest_token_index_int32)
output_tensor_ptr = (
output_tensor + dest_token_index * output_tensor_stride0
)
@@ -1148,6 +1154,7 @@ def ep_scatter(
output_index: torch.Tensor,
scale_ue8m0: bool = False,
quant_block_size: int = 128,
expert_start: int = 0,
):
BLOCK_E = 128 # token num of per expert is aligned to 128
BLOCK_D = quant_block_size # block size of quantization
@@ -1208,6 +1215,8 @@ def ep_scatter(
output_index,
output_index.stride(0),
output_index.stride(1),
expert_start,
num_experts,
topk_num=recv_topk.shape[1],
num_warps=num_warps,
HIDDEN_SIZE=hidden_size,
@@ -1540,12 +1549,13 @@ def tma_align_input_scale(input_scale: torch.Tensor):
@triton.jit
def fused_moe_dispatch_index_triton_kernel(
topk_ids_ptr, # flat (num_toks,) int32; -1 = padding (drives the `expert >= 0` gate)
topk_ids_ptr, # flat (num_toks,) int32; global or already-local expert IDs
src2dst_ptr,
masked_m_ptr,
m_max,
num_toks,
num_experts,
expert_start,
BLOCK_SIZE: tl.constexpr,
ZERO_INIT: tl.constexpr,
):
@@ -1563,19 +1573,23 @@ def fused_moe_dispatch_index_triton_kernel(
tl.debug_barrier()
offs = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
mask = offs < num_toks
expert = tl.load(topk_ids_ptr + offs, mask=mask, other=-1)
valid = mask & (expert >= 0)
global_expert = tl.load(topk_ids_ptr + offs, mask=mask, other=-1)
expert = global_expert - expert_start
valid = mask & (expert >= 0) & (expert < num_experts)
# Clamp masked lanes to bin 0 so the masked atomic's pointer stays in-bounds.
expert_safe = tl.where(valid, expert, 0)
offset = tl.atomic_add(masked_m_ptr + expert_safe, 1, mask=valid)
dst = expert_safe * m_max + offset
tl.store(src2dst_ptr + offs, dst, mask=valid)
# post_reorder checks src2dst, so mark padding/non-local lanes -1 here to
# prevent uninitialized offsets from adding bogus expert contributions.
tl.store(src2dst_ptr + offs, tl.where(valid, dst, -1), mask=mask)
def fused_moe_dispatch_index(
topk_ids: torch.Tensor,
num_local_experts: int,
m_max: int,
expert_start: int = 0,
) -> Tuple[torch.Tensor, torch.Tensor]:
num_toks = topk_ids.numel()
src2dst = torch.empty(num_toks, device=topk_ids.device, dtype=torch.int32)
@@ -1600,6 +1614,7 @@ def fused_moe_dispatch_index(
m_max,
num_toks,
num_local_experts,
expert_start,
BLOCK_SIZE=BLOCK_SIZE,
ZERO_INIT=single_block,
)
@@ -1635,9 +1650,8 @@ def fill_gateup_input_triton_kernel(
vec = tl.arange(0, BLOCK_SIZE)
for idx in range(topk):
expert_id = tl.load(topk_ids_ptr + idx)
if expert_id >= 0:
dst_idx_int32 = tl.load(src2dst_ptr + idx)
dst_idx_int32 = tl.load(src2dst_ptr + idx)
if dst_idx_int32 >= 0:
dst_idx = dst_idx_int32.to(tl.int64)
dst_ptr = gateup_input_ptr + dst_idx * hidden_size
for start_offset in tl.range(0, hidden_size, BLOCK_SIZE):
@@ -1679,6 +1693,7 @@ def moe_ep_deepgemm_preprocess(
block_shape,
output_dtype: torch.dtype = torch.float8_e4m3fn,
use_mxfp8: bool = False,
expert_start: int = 0,
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
# For masked grouped GEMM, shape M should be multiple of the block M (current block M: {block_m}) https://github.com/deepseek-ai/DeepGEMM/blob/main/deep_gemm/jit_kernels/m_grouped_gemm.py#L165
m_max = (hidden_states.size(0) // 256 + 1) * 256
@@ -1698,12 +1713,16 @@ def moe_ep_deepgemm_preprocess(
# correctness is unconditional (m_cap >= max(masked_m) by
# construction, and the final src2dst below is built with the same
# capped stride).
masked_m_probe, _ = fused_moe_dispatch_index(topk_ids, num_local_experts, m_max)
masked_m_probe, _ = fused_moe_dispatch_index(
topk_ids, num_local_experts, m_max, expert_start=expert_start
)
m_cap = (int(masked_m_probe.max().item()) + 255) // 256 * 256
m_max = min(m_max, max(m_cap, 256))
expected_m = (topk_ids.numel() - 1) // num_local_experts + 1
masked_m, src2dst = fused_moe_dispatch_index(topk_ids, num_local_experts, m_max)
masked_m, src2dst = fused_moe_dispatch_index(
topk_ids, num_local_experts, m_max, expert_start=expert_start
)
gateup_input = torch.empty(
(num_local_experts, m_max, hidden_states.size(1)),
@@ -89,8 +89,9 @@ def per_token_quant_fp8_ue8m0_scatter(
then writes them to each of the token's ``topk`` destination rows:
``gateup_input`` fp8 ``[E, m_max, hidden]`` (row ``src2dst[token, i]``)
``gateup_input_scale`` int32 ``[E, hidden//group//4, m_max]`` (MN-major; byte-scattered)
Slots with ``topk_ids[token, i] < 0`` are skipped. Byte-identical to the
two-kernel path on every written row.
Slots with ``src2dst[token, i] < 0`` are skipped. Byte-identical to the
two-kernel path on every written row. ``topk_ids`` remains in the ABI for
compatibility with already-compiled JIT modules.
"""
assert x.is_cuda and x.dtype == torch.bfloat16 and x.dim() == 2
assert x.is_contiguous()
+7 -1
View File
@@ -268,8 +268,14 @@ def handle_a2a_moe(server_args: Any):
assert resolved_view(server_args).moe_runner_backend in [
"flashinfer_cutlass",
"flashinfer_cutedsl",
"flashinfer_trtllm",
"flashinfer_trtllm_routed",
], "Flashinfer MoE A2A is only supported with flashinfer_cutlass, flashinfer_cutedsl or flashinfer_trtllm_routed moe runner backend"
"deep_gemm",
], (
"FlashInfer MoE A2A is supported with flashinfer_cutlass, "
"flashinfer_cutedsl, flashinfer_trtllm, "
"flashinfer_trtllm_routed, or deep_gemm."
)
if a2a_backend == "mori":
if cfg.deepep_mode == "auto":
+15
View File
@@ -2272,6 +2272,20 @@ def _flashinfer_allreduce_fusion_auto_enable(view: Any) -> dict:
moe_a2a_backend (after the DeepSeek CP and a2a declarations), exactly
like the legacy tail block."""
model_arch = view.get_model_config().hf_config.architectures[0]
if envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION.get() and model_arch in {
"Qwen3_5MoeForCausalLM",
"Qwen3_5MoeForConditionalGeneration",
}:
# The Qwen backend owns one workspace for ordinary AR and MoE finalize;
# do not allocate or fall back to the legacy TRTLLM/MNNVL workspace.
if view.flashinfer_allreduce_fusion_backend is not None:
logger.warning(
"SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION owns both Qwen3.5 "
"AllReduce fusion patterns; suppressing the separately configured "
"--flashinfer-allreduce-fusion-backend=%s",
view.flashinfer_allreduce_fusion_backend,
)
return {"flashinfer_allreduce_fusion_backend": None}
if (
view.flashinfer_allreduce_fusion_backend is None
and model_arch in _FLASHINFER_ALLREDUCE_FUSION_ARCHS
@@ -2862,6 +2876,7 @@ _A2A_EP_SPANNING_BACKENDS = frozenset(
"flashinfer",
"mori",
"pplx",
"deepep_v2",
}
)
@@ -10,6 +10,7 @@ from __future__ import annotations
import dataclasses
import logging
import struct
import threading
import time
from typing import TYPE_CHECKING, List, Optional, Tuple
@@ -536,6 +537,44 @@ class StagingTransferInfo:
self.ends[idx] = end
@dataclasses.dataclass
class StagingRegisterInfo:
"""Staging buffer registration info attached to a KVArgsRegisterInfo."""
base_ptr: int = 0
total_size: int = 0
# Staging slots stay [all K, all V] after draft buffers alter kv_data_ptrs order;
# older peers leave this empty and callers fall back to kv_layer_ids.
slot_layer_ids: List[int] = dataclasses.field(default_factory=list)
@classmethod
def from_zmq_fields(
cls, msg: list, msg_start_offset: int, slot_ids_index: Optional[int] = None
) -> Optional[StagingRegisterInfo]:
i = msg_start_offset
base_ptr = (
struct.unpack("Q", msg[i])[0] if len(msg) > i and len(msg[i]) == 8 else 0
)
total_size = (
int(msg[i + 1].decode("ascii"))
if len(msg) > i + 1 and len(msg[i + 1]) > 0
else 0
)
if base_ptr == 0 and total_size == 0:
return None
slot_layer_ids: List[int] = []
if (
slot_ids_index is not None
and len(msg) > slot_ids_index
and len(msg[slot_ids_index]) > 0
):
raw = msg[slot_ids_index]
slot_layer_ids = list(struct.unpack(f"{len(raw) // 8}Q", raw))
return cls(
base_ptr=base_ptr, total_size=total_size, slot_layer_ids=slot_layer_ids
)
class PrefillStagingStrategy:
"""Prefill-side staging transfer: readiness check + gather-RDMA execution.
@@ -627,6 +666,11 @@ class PrefillStagingStrategy:
target_info.dst_kv_item_len,
target_info.dst_kv_layer_ids,
staging_buffer=self.staging_buffer,
dst_slot_layer_ids=(
target_info.staging.slot_layer_ids
if target_info.staging is not None
else None
),
)
except Exception as e:
raise RuntimeError(
+21 -7
View File
@@ -51,6 +51,8 @@ from sglang.srt.disaggregation.utils import (
ReqToMetadataIdxAllocator,
TransferBackend,
_is_fake_transfer,
build_kv_layer_ids,
build_staging_slot_metadata,
get_dsv4_c128_state_indices,
get_kv_class,
is_dsv4_c128_online_enabled,
@@ -541,6 +543,7 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
kv_data_lens += device_kv_data_lens[c4_layer_num:]
kv_item_lens += device_kv_item_lens[c4_layer_num:]
kv_data_mem_kinds += ["VRAM"] * len(device_kv_data_ptrs[c4_layer_num:])
num_draft_entries = 0
if self.draft_token_to_kv_pool is not None:
# We should also transfer draft model kv cache. The indices are
# always shared with a target model.
@@ -551,15 +554,16 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
kv_data_lens += draft_kv_data_lens
kv_item_lens += draft_kv_item_lens
kv_data_mem_kinds += ["VRAM"] * len(draft_kv_data_ptrs)
num_draft_entries = len(draft_kv_data_ptrs)
kv_args.kv_data_ptrs = kv_data_ptrs
kv_args.kv_data_lens = kv_data_lens
kv_args.kv_item_lens = kv_item_lens
kv_args.kv_layer_ids = (
self.token_to_kv_pool.get_kv_layer_ids()
if self.draft_token_to_kv_pool is None
and hasattr(self.token_to_kv_pool, "get_kv_layer_ids")
else []
kv_args.kv_layer_ids = build_kv_layer_ids(
token_to_kv_pool=self.token_to_kv_pool,
draft_token_to_kv_pool=self.draft_token_to_kv_pool,
num_draft_entries=num_draft_entries,
num_hidden_layers=self.scheduler.model_config.num_hidden_layers,
)
if self.transfer_backend == TransferBackend.NIXL:
kv_args.kv_data_mem_kinds = kv_data_mem_kinds
@@ -599,9 +603,19 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
)
if hasattr(kv_manager, "set_kv_buffer_tensors"):
kv_pool = kv_pool_for_heads
if hasattr(kv_pool, "k_buffer") and hasattr(kv_pool, "v_buffer"):
staging_slots = build_staging_slot_metadata(
kv_layer_ids=kv_args.kv_layer_ids,
num_draft_entries=num_draft_entries,
kv_pool=kv_pool,
draft_kv_pool=self.draft_token_to_kv_pool,
)
if staging_slots is not None:
k_buffers, v_buffers, slot_layer_ids = staging_slots
kv_manager.set_kv_buffer_tensors(
kv_pool.k_buffer, kv_pool.v_buffer, kv_pool.page_size
k_buffers,
v_buffers,
kv_pool.page_size,
slot_layer_ids=slot_layer_ids,
)
return kv_manager
@@ -28,6 +28,7 @@ from sglang.srt.disaggregation.common.staging_handler import (
DecodeStagingContext,
PrefillStagingContext,
StagingManagerMixin,
StagingRegisterInfo,
StagingTransferInfo,
handle_staging_rsp,
handle_watermark_msg,
@@ -148,6 +149,7 @@ class KVArgsRegisterInfo:
dcp_token_item_lens: Optional[List[int]] = None
staging_base_ptr: int = 0
staging_total_size: int = 0
staging: Optional[StagingRegisterInfo] = None
@classmethod
def from_zmq(cls, msg: List[bytes]):
@@ -192,6 +194,8 @@ class KVArgsRegisterInfo:
dst_dcp_rank=(
int(msg[17].decode("ascii")) if len(msg) > 17 and msg[17] != b"" else 0
),
# Note: always put the staging field at the final
staging=StagingRegisterInfo.from_zmq_fields(msg, 14, slot_ids_index=18),
)
@@ -333,11 +337,20 @@ class MooncakeKVManager(StagingManagerMixin, CommonKVManager):
self._staging_ctx.room_bootstrap[room] = bootstrap_infos
self._staging_ctx.room_receivers[room] = receiver
def set_kv_buffer_tensors(self, k_buffers: list, v_buffers: list, page_size: int):
def set_kv_buffer_tensors(
self,
k_buffers: list,
v_buffers: list,
page_size: int,
slot_layer_ids: Optional[List[int]] = None,
):
# slot_layer_ids follows the staging slot order (every k_buffer, then
# every v_buffer), which is not kv_args.kv_layer_ids once a draft exists.
self.kv_buffer_tensors = {
"k_buffers": k_buffers,
"v_buffers": v_buffers,
"page_size": page_size,
"slot_layer_ids": list(slot_layer_ids or []),
}
def _init_staging_buffers(self, count: int):
@@ -494,6 +507,7 @@ class MooncakeKVManager(StagingManagerMixin, CommonKVManager):
dst_kv_item_len: int,
dst_layer_ids: List[int],
staging_buffer=None,
dst_slot_layer_ids: Optional[List[int]] = None,
) -> int:
"""Transfer KV cache via staging buffers (gather -> bulk RDMA -> scatter on decode)."""
from sglang.srt.disaggregation.common.staging_buffer import (
@@ -528,13 +542,20 @@ class MooncakeKVManager(StagingManagerMixin, CommonKVManager):
local_bytes = per_layer_bytes * num_layers * 2
if self.pp_size > 1:
pairs = build_transfer_entry_pairs(
self.kv_args.kv_layer_ids,
dst_layer_ids,
num_layers * 2,
len(dst_layer_ids),
# Pair staging slots in [all K, all V] order; draft buffers make
# kv_data_ptrs diverge from this layout.
src_slot_ids = (
self.kv_buffer_tensors.get("slot_layer_ids")
or self.kv_args.kv_layer_ids
)
dst_num_layers = len(dst_layer_ids) // 2
dst_slot_ids = dst_slot_layer_ids or dst_layer_ids
pairs = build_transfer_entry_pairs(
src_slot_ids,
dst_slot_ids,
num_layers * 2,
len(dst_slot_ids),
)
dst_num_layers = len(dst_slot_ids) // 2
else:
pairs = None
dst_num_layers = num_layers
@@ -654,10 +675,18 @@ class MooncakeKVManager(StagingManagerMixin, CommonKVManager):
layers_params = None
# Decode pp size should be equal to prefill pp size or 1
if self.is_mla_backend or self.is_hybrid_mla_backend or force_flat:
# Published layer IDs give exact pairing; plain-MHA peers publish none
# and keep positional slicing.
has_layer_ids = bool(src_layer_ids or dst_layer_ids)
if (
self.is_mla_backend
or self.is_hybrid_mla_backend
or force_flat
or has_layer_ids
):
# Layer IDs map PP-local buffers to global decode entries.
# Registrations without them retain the existing PP mapping.
if src_layer_ids or dst_layer_ids:
if has_layer_ids:
pairs = build_transfer_entry_pairs(
src_layer_ids,
dst_layer_ids,
@@ -970,6 +999,7 @@ class MooncakeKVManager(StagingManagerMixin, CommonKVManager):
dst_attn_tp_size: int,
dst_kv_item_len: int,
executor: concurrent.futures.ThreadPoolExecutor,
dst_layer_ids: Optional[List[int]] = None,
):
"""
Sends KV cache slices from this Prefill rank to a target Decode rank,
@@ -1022,9 +1052,34 @@ class MooncakeKVManager(StagingManagerMixin, CommonKVManager):
num_heads_to_send = dst_heads_per_rank
dst_head_start_offset = 0
src_k_ptrs, src_v_ptrs, dst_k_ptrs, dst_v_ptrs, layers_current_pp_stage = (
self.get_mha_kv_ptrs_with_pp(self.kv_args.kv_data_ptrs, dst_kv_ptrs)
)
src_data_ptrs = self.kv_args.kv_data_ptrs
src_layer_ids = self.kv_args.kv_layer_ids
if src_layer_ids or dst_layer_ids:
# Draft buffers break the flat [K block, V block] layout, so pair by
# layer ID instead of the half-split used by get_mha_kv_ptrs_with_pp.
if any(l != src_kv_item_len for l in self.kv_args.kv_item_lens):
logger.error(
f"[{mooncake_session_id}] head-sliced transfer assumes one item "
f"length for every KV entry, got {set(self.kv_args.kv_item_lens)}"
)
return -1
layer_ptr_pairs = [
(src_data_ptrs[i], dst_kv_ptrs[j])
for i, j in build_transfer_entry_pairs(
src_layer_ids,
dst_layer_ids or [],
len(src_data_ptrs),
len(dst_kv_ptrs),
allow_positional_fallback=self.pp_size == 1,
)
]
else:
src_k_ptrs, src_v_ptrs, dst_k_ptrs, dst_v_ptrs, layers_current_pp_stage = (
self.get_mha_kv_ptrs_with_pp(src_data_ptrs, dst_kv_ptrs)
)
layer_ptr_pairs = [
(src_k_ptrs[i], dst_k_ptrs[i]) for i in range(layers_current_pp_stage)
] + [(src_v_ptrs[i], dst_v_ptrs[i]) for i in range(layers_current_pp_stage)]
# Calculate precise byte offset and length for the sub-slice within the token
src_head_slice_offset = src_head_start_offset * bytes_per_head_slice_to_send
@@ -1069,15 +1124,10 @@ class MooncakeKVManager(StagingManagerMixin, CommonKVManager):
mooncake_session_id, src_addr_list, dst_addr_list, length_list
)
futures = []
for i in range(layers_current_pp_stage):
futures.append(
executor.submit(process_layer_tp_aware, src_k_ptrs[i], dst_k_ptrs[i])
)
for i in range(layers_current_pp_stage):
futures.append(
executor.submit(process_layer_tp_aware, src_v_ptrs[i], dst_v_ptrs[i])
)
futures = [
executor.submit(process_layer_tp_aware, src_layer_ptr, dst_layer_ptr)
for src_layer_ptr, dst_layer_ptr in layer_ptr_pairs
]
return self._await_transfer_futures(futures)
@@ -1786,6 +1836,7 @@ class MooncakeKVManager(StagingManagerMixin, CommonKVManager):
target_rank_registration_info.dst_attn_tp_size,
target_rank_registration_info.dst_kv_item_len,
executor,
target_rank_registration_info.dst_kv_layer_ids,
)
if ret != 0:
with self.session_lock:
@@ -2418,6 +2469,11 @@ class MooncakeKVReceiver(MooncakeFailureExceptionMixin, CommonKVReceiver):
else:
packed_staging_base_ptr = b""
staging_total_size_str = b""
staging_slots = getattr(self.kv_mgr, "kv_buffer_tensors", None) or {}
packed_staging_slot_layer_ids = b"".join(
struct.pack("Q", layer_id)
for layer_id in (staging_slots.get("slot_layer_ids") or [])
)
try:
sock, lock = self._connect_to_bootstrap_server(bootstrap_info)
@@ -2442,6 +2498,7 @@ class MooncakeKVReceiver(MooncakeFailureExceptionMixin, CommonKVReceiver):
staging_total_size_str,
dst_dcp_size,
dst_dcp_rank,
packed_staging_slot_layer_ids,
]
)
except zmq.ZMQError:
+53 -17
View File
@@ -43,6 +43,8 @@ from sglang.srt.disaggregation.utils import (
MetadataBuffers,
ReqToMetadataIdxAllocator,
TransferBackend,
build_kv_layer_ids,
build_staging_slot_metadata,
get_dsv4_c128_state_indices,
get_kv_class,
is_aborted,
@@ -68,6 +70,7 @@ from sglang.srt.mem_cache.common import (
release_kv_cache,
)
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool
from sglang.srt.observability.req_time_stats import set_schedule_time_batch
from sglang.srt.runtime_context import (
get_disagg,
@@ -99,6 +102,16 @@ def should_force_retry(req: Req) -> bool:
return int.from_bytes(digest[:8], "big") < retry_prob * 2**64
def _transfer_start_layer(*, pool, hf_text_config) -> int:
# Hybrid pools count all layers in start_layer, but peer KV lists contain only
# full-attention layers, so translate to a full-attention-relative offset.
if not isinstance(pool, HybridLinearKVPool):
return pool.start_layer
return sum(
1 for lid in hf_text_config.full_attention_layer_ids if lid < pool.start_layer
)
def maybe_release_metadata_buffer(
req: Req, allocator: ReqToMetadataIdxAllocator
) -> None:
@@ -212,7 +225,10 @@ class PrefillBootstrapQueue:
self.token_to_kv_pool.start_layer,
)
if layer_shard_enabled
else self.token_to_kv_pool.start_layer
else _transfer_start_layer(
pool=self.token_to_kv_pool,
hf_text_config=self.scheduler.model_config.hf_text_config,
)
)
kv_args.mla_compression_ratios = None
kv_data_ptrs, kv_data_lens, kv_item_lens = (
@@ -224,24 +240,27 @@ class PrefillBootstrapQueue:
else getattr(self.token_to_kv_pool, "end_layer", None)
)
if self.draft_token_to_kv_pool is not None and transfer_draft_cache:
draft_kv_pool = self.draft_token_to_kv_pool if transfer_draft_cache else None
num_draft_entries = 0
if draft_kv_pool is not None:
# We should also transfer draft model kv cache. The indices are
# always shared with a target model.
draft_kv_data_ptrs, draft_kv_data_lens, draft_kv_item_lens = (
self.draft_token_to_kv_pool.get_contiguous_buf_infos()
draft_kv_pool.get_contiguous_buf_infos()
)
kv_data_ptrs += draft_kv_data_ptrs
kv_data_lens += draft_kv_data_lens
kv_item_lens += draft_kv_item_lens
num_draft_entries = len(draft_kv_data_ptrs)
kv_args.kv_data_ptrs = kv_data_ptrs
kv_args.kv_data_lens = kv_data_lens
kv_args.kv_item_lens = kv_item_lens
kv_args.kv_layer_ids = (
self.token_to_kv_pool.get_kv_layer_ids()
if self.draft_token_to_kv_pool is None
and hasattr(self.token_to_kv_pool, "get_kv_layer_ids")
else []
kv_args.kv_layer_ids = build_kv_layer_ids(
token_to_kv_pool=self.token_to_kv_pool,
draft_token_to_kv_pool=draft_kv_pool,
num_draft_entries=num_draft_entries,
num_hidden_layers=self.scheduler.model_config.num_hidden_layers,
)
if not self.is_mla_backend:
kv_args.kv_head_num = self.token_to_kv_pool.head_num
@@ -288,11 +307,19 @@ class PrefillBootstrapQueue:
kv_pool = self.token_to_kv_pool
if hasattr(kv_pool, "full_kv_pool"):
kv_pool = kv_pool.full_kv_pool
if hasattr(kv_pool, "k_buffer") and hasattr(kv_pool, "v_buffer"):
staging_slots = build_staging_slot_metadata(
kv_layer_ids=kv_args.kv_layer_ids,
num_draft_entries=num_draft_entries,
kv_pool=kv_pool,
draft_kv_pool=draft_kv_pool,
)
if staging_slots is not None:
k_buffers, v_buffers, slot_layer_ids = staging_slots
kv_manager.set_kv_buffer_tensors(
kv_pool.k_buffer,
kv_pool.v_buffer,
k_buffers,
v_buffers,
kv_pool.page_size,
slot_layer_ids=slot_layer_ids,
)
return kv_manager
@@ -693,6 +720,16 @@ class SchedulerDisaggregationPrefillMixin:
logprob_pt = 0
assert batch.spec_info is result.next_draft_input
draft_input = result.next_draft_input
draft_hidden_states_cpu = None
draft_dsa_topk_indices_cpu = None
if self.spec_algorithm.is_eagle() and draft_input is not None:
draft_hidden_states_cpu = draft_input.hidden_states.to(
"cpu", non_blocking=False
)
if batch.spec_info.dsa_topk_indices is not None:
draft_dsa_topk_indices_cpu = batch.spec_info.dsa_topk_indices.to(
"cpu", non_blocking=False
)
# Transfer kv for prefill completed requests and add it into disagg_prefill_inflight_queue
next_token_ids = result.next_token_ids.tolist()
self.batch_result_processor.move_logprobs_to_cpu(
@@ -727,12 +764,11 @@ class SchedulerDisaggregationPrefillMixin:
if self.spec_algorithm.is_eagle() and draft_input is not None:
req.output_topk_p = draft_input.topk_p[i]
req.output_topk_index = draft_input.topk_index[i]
req.hidden_states_tensor = (
draft_input.hidden_states[i].cpu().clone()
)
dsa_topk_indices = batch.spec_info.dsa_topk_indices
if dsa_topk_indices is not None:
req.output_dsa_topk_indices = dsa_topk_indices[i].cpu().clone()
req.hidden_states_tensor = draft_hidden_states_cpu[i].clone()
if draft_dsa_topk_indices_cpu is not None:
req.output_dsa_topk_indices = draft_dsa_topk_indices_cpu[
i
].clone()
else:
req.output_dsa_topk_indices = None
else:
+104
View File
@@ -915,6 +915,64 @@ def build_transfer_entry_pairs(
return [(i, i) for i in range(n_src)]
def build_kv_layer_ids(
*,
token_to_kv_pool,
draft_token_to_kv_pool,
num_draft_entries: int,
num_hidden_layers: int,
) -> List[int]:
"""Global layer id for every entry in ``kv_args.kv_data_ptrs``.
Draft KV buffers are appended after the target's, so they need ids of their
own: build_transfer_entry_pairs requires the id list to cover every entry,
and a target-only list would be rejected. The draft numbers its layers from
zero, which would collide with the target's, so its entries are remapped
into a reserved band above the target's layer range. Both PD peers run this
against the same draft config and so agree on the band.
Returns [] for pools that cannot report ids, leaving the peers on positional
pairing.
"""
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool
if not isinstance(token_to_kv_pool, HybridLinearKVPool):
return []
layer_ids = token_to_kv_pool.get_kv_layer_ids()
if draft_token_to_kv_pool is None:
return layer_ids
draft_ids = _draft_entry_layer_ids(
pool=draft_token_to_kv_pool, num_entries=num_draft_entries
)
# Rank the draft's own ids by first appearance, so the band stays dense and
# contiguous whatever the draft config numbers its layers.
band_index = {lid: i for i, lid in enumerate(dict.fromkeys(draft_ids))}
return layer_ids + [num_hidden_layers + band_index[lid] for lid in draft_ids]
def _draft_entry_layer_ids(*, pool, num_entries: int) -> List[int]:
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool
if isinstance(pool, HybridLinearKVPool):
ids = pool.get_kv_layer_ids()
else:
# Pools register k0..k(L-1) then v0..v(L-1), so ids repeat once per
# group; derive the group count rather than assuming MHA vs MLA.
if pool.layer_num <= 0 or num_entries % pool.layer_num != 0:
raise RuntimeError(
"Draft KV buffers must register a whole number of per-layer "
f"groups: entries={num_entries}, layers={pool.layer_num}"
)
ids = list(range(pool.layer_num)) * (num_entries // pool.layer_num)
if len(ids) != num_entries:
raise RuntimeError(
"Draft KV layer ids must cover every registered entry: "
f"ids={len(ids)}, entries={num_entries}"
)
return ids
def resolve_dcp_dst_entry_indices(
src_layer_ids: List[int],
dst_layer_ids: List[int],
@@ -940,6 +998,52 @@ def resolve_dcp_dst_entry_indices(
]
def build_staging_slot_metadata(
*,
kv_layer_ids: List[int],
num_draft_entries: int,
kv_pool,
draft_kv_pool,
):
"""Buffers and per-slot layer ids for the staging gather.
The gather writes every k_buffer and then every v_buffer, while kv_layer_ids
follows kv_data_ptrs ([K target, V target, K draft, V draft]), so the two
orders diverge as soon as a draft pool is registered.
Returns (k_buffers, v_buffers, slot_layer_ids), or None for a pool that has
no contiguous K/V tensors to stage.
"""
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool, MHATokenToKVPool
# A hybrid pool keeps its contiguous K/V tensors on the inner full-attention
# pool, and the draft pool is wrapped the same way.
if isinstance(kv_pool, HybridLinearKVPool):
kv_pool = kv_pool.full_kv_pool
if isinstance(draft_kv_pool, HybridLinearKVPool):
draft_kv_pool = draft_kv_pool.full_kv_pool
if not isinstance(kv_pool, MHATokenToKVPool):
return None
ids = list(kv_layer_ids or [])
num_target = len(ids) - num_draft_entries
half = num_target // 2
k_buffers, k_ids = list(kv_pool.k_buffer), ids[:half]
v_buffers, v_ids = list(kv_pool.v_buffer), ids[half:num_target]
draft_half = num_draft_entries // 2
if draft_half:
if not isinstance(draft_kv_pool, MHATokenToKVPool):
# An empty id list puts the sender back on kv_data_ptrs order, which
# is what staging did before draft KV existed.
return k_buffers, v_buffers, []
k_buffers += list(draft_kv_pool.k_buffer)
v_buffers += list(draft_kv_pool.v_buffer)
k_ids += ids[num_target : num_target + draft_half]
v_ids += ids[num_target + draft_half :]
return k_buffers, v_buffers, k_ids + v_ids
def append_state_component(
kv_args: KVArgs,
state_type: StateType,
+3 -1
View File
@@ -138,10 +138,12 @@ def init_torch_distributed(
host=server_args.host, port=server_args.gated_launch_port
)
# Draft workers reuse the target pool config and may exist on only one PP stage;
# including them in this WORLD reduction would deadlock on absent peers.
pre_model_load_memory = get_available_gpu_memory(
device,
ps.gpu_id,
distributed=get_world_group().world_size > 1,
distributed=get_world_group().world_size > 1 and not is_draft_worker,
cpu_group=get_world_group().cpu_group,
)
tp_group = get_tp_group()
@@ -2052,6 +2052,12 @@ def get_moe_tp_group() -> GroupCoordinator:
get_tensor_model_parallel_group = get_tp_group
_PP: Optional[GroupCoordinator] = None
_SELF_PP: Optional[GroupCoordinator] = None
def get_self_pp_group() -> GroupCoordinator:
assert _SELF_PP is not None, "self pipeline group is not initialized"
return _SELF_PP
def get_pp_group() -> GroupCoordinator:
@@ -2716,6 +2722,18 @@ def initialize_model_parallel(
max_world_size=max_world_size,
)
# The one-layer draft uses a singleton PP group; every rank creates all groups
# because new_group is collective.
global _SELF_PP
if _SELF_PP is None:
_SELF_PP = init_model_parallel_group(
[[r] for r in range(world_size)],
get_world_group().local_rank,
backend,
use_custom_allreduce=False,
group_name="self_pp",
)
get_parallel().stamp_derived_widths(**derived_widths)
@@ -2814,6 +2832,28 @@ def model_parallel_is_initialized():
_TP_STATE_PATCHED = False
_PP_STATE_PATCHED = False
@contextmanager
def patch_pipeline_parallel_group(pp_group: GroupCoordinator):
"""Patch the pp group temporarily until this function ends.
This method is for draft workers of speculative decoding, whose model does not
span pipeline stages and must not read the target's pp topology.
"""
global _PP_STATE_PATCHED
assert not _PP_STATE_PATCHED, "Should not call when it's already patched"
_PP_STATE_PATCHED = True
old_pp_group = get_pp_group()
global _PP
_PP = pp_group
try:
yield
finally:
_PP_STATE_PATCHED = False
_PP = old_pp_group
@contextmanager
+4
View File
@@ -101,6 +101,10 @@ def get_pp_indices(
"""
# partition_list_str can be set to None in sglang
partition_list_str = os.getenv("SGLANG_PP_LAYER_PARTITION", None)
if pp_size == 1:
# A singleton draft PP group owns every layer and must ignore the target's
# process-global pipeline partition list.
partition_list_str = None
if partition_list_str is not None:
try:
partitions = [int(layer) for layer in partition_list_str.split(",")]
@@ -674,6 +674,13 @@ async def health_generate(request: Request) -> Response:
if _global_state.tokenizer_manager.server_status == ServerStatus.Starting:
return Response(status_code=503)
# Let an external E2E driver establish a balanced DP batch before health traffic.
if envs.SGLANG_DIAG_BYPASS_HEALTH_GENERATE.get() and request.url.path in (
"/health",
"/health_generate",
):
return Response(status_code=200)
if (
not envs.SGLANG_ENABLE_HEALTH_ENDPOINT_GENERATION.get()
and request.url.path == "/health"
+29
View File
@@ -323,6 +323,7 @@ class Envs:
SGLANG_UVICORN_WORKER_HEALTHCHECK_TIMEOUT = EnvInt(10)
SGLANG_ENABLE_HEALTH_ENDPOINT_GENERATION = EnvBool(True)
SGLANG_EXPOSE_OWN_ENV_VARS = EnvBool(False)
SGLANG_DIAG_BYPASS_HEALTH_GENERATE = EnvBool(False)
# ===================================================================
# Logging
@@ -441,12 +442,21 @@ class Envs:
SGLANG_TRACE_ASYNC_FLUSH_THRESHOLD = EnvInt(100)
SGLANG_ENABLE_METRICS_DEVICE_TIMER = EnvBool(False)
SGLANG_ENABLE_METRICS_DP_ATTENTION = EnvBool(False)
SGLANG_TRACE_LOGITS_E2E = EnvBool(False)
SGLANG_TRACE_LOGITS_E2E_SYNC = EnvBool(False)
SGLANG_TRACE_SAMPLER_E2E = EnvBool(False)
SGLANG_TRACE_QWEN_MOE_DEEPEP_E2E = EnvBool(False)
SGLANG_DEEPEP_V2_TRACE_CONTIG = EnvBool(False)
SGLANG_DEEPEP_V2_TRACE_MASKED = EnvBool(False)
# ===================================================================
# Debugging and invariant checks
# ===================================================================
SGLANG_DETECT_SLOW_RANK = EnvBool(False)
SGLANG_DEBUG_MEMORY_POOL = EnvBool(False)
SGLANG_VALIDATE_MAMBA_REPLAY_STATE_INDICES = EnvBool(False)
SGLANG_GDN_DECODE_FUSION_LOG_LAYER_HITS = EnvBool(False)
SGLANG_GDN_DECODE_FUSION_VERIFY_REAL_TENSORS = EnvBool(False)
# NaN-fill the unified memory pool at boot (debug repro switch).
SGLANG_DEBUG_POISON_POOL = EnvBool(False)
SGLANG_DEBUG_REVERT_PR = EnvInt(0)
@@ -746,6 +756,7 @@ class Envs:
# ===================================================================
# MoRI transport and expert dispatch
# ===================================================================
SGLANG_DEEPEP_V2_FORCE_MAX_LEN = EnvBool(False)
# Send CPU-resident AUX data via RDMA instead of ZMQ TCP (default: TCP).
SGLANG_MORI_SEND_AUX_RDMA = EnvBool(False)
# Number of RDMA Queue Pairs (QPs) used per transfer operation. Higher
@@ -1009,6 +1020,9 @@ class Envs:
# DeepGEMM
# ===================================================================
SGLANG_ENABLE_JIT_DEEPGEMM = EnvBool(True)
# Enable the allowlisted low-M BF16 Split-K GEMM path on Blackwell. Shapes
# outside the measured allowlist continue to use CuTe DSL/cuBLAS.
SGLANG_ENABLE_BF16_SPLITK_GEMM = EnvBool(True)
SGLANG_DEEPGEMM_STANDARD_LAYOUT = EnvStr("auto")
SGLANG_DEEPGEMM_MASKED_MEMORY_BUDGET_FRACTION = EnvFloat(0.25)
# Cap the DeepGEMM masked grouped-GEMM per-expert padded capacity at
@@ -1017,6 +1031,9 @@ class Envs:
# load imbalance (they otherwise OOM saturated --moe-runner-backend
# deep_gemm serving). Costs one D2H sync per MoE layer.
SGLANG_OPT_DG_MASKED_M_CAP = EnvBool(False)
# Wide-DP eager prefill uses compact routing storage; masked storage scales
# with num_local_experts and can OOM on skewed batches.
SGLANG_OPT_DG_COMPACT_EAGER = EnvBool(False)
# Drop dp-attention MAX_LEN pad rows from MoE dispatch (StandardDispatcher
# post-translation topk_ids -> -1): pad rows otherwise run the router on
# stale hidden values and burn expert compute whose outputs are discarded;
@@ -1532,6 +1549,16 @@ class Envs:
SGLANG_SYMM_MEM_PREALLOC_GB_SIZE = EnvInt(-1)
SGLANG_DEBUG_SYMM_MEM = EnvBool(False)
# Qwen3.5 and GDN
SGLANG_ENABLE_GDN_DECODE_FUSED_PROJ_CONV = EnvBool(True)
SGLANG_TRACE_QWEN35_FINAL_NORM = EnvBool(False)
SGLANG_QWEN35_NATIVE_FINAL_NORM = EnvBool(False)
# One switch enables deferred MoE finalize and AR + residual + RMSNorm.
SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION = EnvBool(False)
# Distinct workspace configurations allowed in one process. Production
# uses one model/configuration per rank, so fail closed on accidental reuse.
SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION_MAX_INSTANCES = EnvInt(1)
# ===================================================================
# Plugin system
# ===================================================================
@@ -1685,6 +1712,8 @@ _DEPRECATED_ENVS: Dict[str, _DeprecatedEnv] = {
"SGLANG_OPT_SWA_EVICT_DROP_PAGE_MARGIN": _DeprecatedEnv(),
# sconv-family kernels always use the CUDA-JIT ports when supported; no toggle.
"SGLANG_OPT_USE_CUDA_SCONV": _DeprecatedEnv(),
# The direct dense BF16 GEMM source is vendored in-tree.
"SGLANG_FLASHINFER_PR4266_SOURCE": _DeprecatedEnv(),
# DSV4 compressor V2 is always used.
"SGLANG_OPT_USE_COMPRESSOR_V2": _DeprecatedEnv(),
# Replaced by CLI flags.
@@ -14,6 +14,7 @@ from sglang.kernels.ops.mamba.mamba_state_scatter_triton import (
track_mamba_states_all_layers,
track_mamba_states_if_needed,
)
from sglang.srt.environ import envs
from sglang.srt.layers.attention.base_attn_backend import (
AttentionBackend,
SharedReadEnds,
@@ -23,6 +24,9 @@ from sglang.srt.layers.attention.mamba.mamba2_metadata import (
ForwardMetadata,
Mamba2Metadata,
)
from sglang.srt.layers.attention.mamba.replay_state_indices_validator import (
validate_replay_state_indices_cpu,
)
from sglang.srt.layers.radix_attention import RadixAttention
from sglang.srt.mem_cache.memory_pool import HybridReqToTokenPool
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
@@ -40,6 +44,9 @@ if TYPE_CHECKING:
from sglang.srt.layers.attention.verify_mask import VerifyMask
logger = logging.getLogger(__name__)
_validate_mamba_replay_state_indices = (
envs.SGLANG_VALIDATE_MAMBA_REPLAY_STATE_INDICES.get()
)
class MambaAttnBackendBase(AttentionBackend):
@@ -598,6 +605,17 @@ class MambaAttnBackendBase(AttentionBackend):
mamba_indices = self._translate_mamba_indices(mamba_indices)
mamba_indices[bs - num_padding :] = -1
self.state_indices_list[bs - 1][: len(mamba_indices)].copy_(mamba_indices)
if _validate_mamba_replay_state_indices and not in_capture:
# This pre-replay diagnostic intentionally syncs to reject malformed
# live or padded indices before a captured state update uses them.
valid_bs = bs - int(num_padding)
validate_replay_state_indices_cpu(
mamba_indices.detach().cpu(),
valid_bs=valid_bs,
total_bs=bs,
num_state_slots=self.req_to_token_pool.mamba_pool.size + 1,
pad_slot_id=self.pad_slot_id,
)
# Refresh the static track-dest buffer in-place (translated); the captured
# track-save reads it, leaving the handed-in InputBuffer slot read-only.
# Hand out only the refreshed [:bs] prefix — Mamba2's track-save slices
@@ -8,6 +8,7 @@ from sglang.kernels.ops.mamba.causal_conv1d_triton import (
causal_conv1d_update,
)
from sglang.srt.configs.hybrid_arch import hybrid_gdn_config
from sglang.srt.environ import envs
from sglang.srt.layers.attention.hybrid_linear_attn_backend import MambaAttnBackendBase
from sglang.srt.layers.attention.linear.kernels.gdn_triton import TritonGDNKernel
from sglang.srt.layers.attention.linear.utils import (
@@ -32,10 +33,21 @@ if not is_cpu():
if is_cuda() or is_hip() or is_xpu():
from sglang.kernels.ops.attention.triton_gdn_fused_proj import (
can_use_fused_qkvzba_causal_conv1d_update_contiguous,
fused_qkv_split_gdn_prefill,
fused_qkvzba_causal_conv1d_update_contiguous,
fused_qkvzba_split_reshape_cat_contiguous,
)
MAX_FUSED_QKV_SPLIT_DIM = 8192
_fused_decode_proj_conv_logged = False
_fused_decode_proj_conv_fallback_logged = False
_fused_decode_proj_conv_layers_logged: set[int] = set()
_fused_decode_real_tensor_verified_layers: set[int] = set()
_fused_decode_log_layer_hits = envs.SGLANG_GDN_DECODE_FUSION_LOG_LAYER_HITS.get()
_fused_decode_verify_real_tensors = (
envs.SGLANG_GDN_DECODE_FUSION_VERIFY_REAL_TENSORS.get()
)
if is_cuda():
from sglang.srt.layers.attention.mamba.causal_conv1d import (
@@ -433,6 +445,11 @@ class GDNAttnBackend(MambaAttnBackendBase):
b: torch.Tensor,
**kwargs,
):
global _fused_decode_proj_conv_fallback_logged
global _fused_decode_proj_conv_logged
global _fused_decode_proj_conv_layers_logged
global _fused_decode_real_tensor_verified_layers
if _is_hip and isinstance(mixed_qkv, torch.Tensor) and mixed_qkv.shape[0] == 0:
return mixed_qkv.new_zeros((1, 0, layer.num_v_heads, layer.head_v_dim))
@@ -453,15 +470,163 @@ class GDNAttnBackend(MambaAttnBackendBase):
replayssm_k = layer_cache.replayssm_k
replayssm_g = layer_cache.replayssm_g
assert isinstance(mixed_qkv, torch.Tensor)
mixed_qkv = causal_conv1d_update(
mixed_qkv,
conv_states,
layer.conv_weights,
layer.bias,
layer.activation,
conv_state_indices=cache_indices,
)
return_z = False
conv_already_applied = False
if isinstance(mixed_qkv, tuple):
if len(mixed_qkv) != 2:
raise ValueError(
"Fused GDN decode projection input must be "
"(projected_qkvz, projected_ba)"
)
projected_qkvz, projected_ba = mixed_qkv
eligible, eligibility_reason = (
can_use_fused_qkvzba_causal_conv1d_update_contiguous(
projected_qkvz,
projected_ba,
conv_states,
layer.conv_weights,
layer.bias,
cache_indices,
qkv_dim=layer.q_dim + layer.k_dim + layer.v_dim,
v_dim=layer.v_dim,
num_v_heads=layer.num_v_heads,
activation=layer.activation,
)
)
if eligible:
qkv_dim = layer.q_dim + layer.k_dim + layer.v_dim
fused_backend = "triton_direct_oracle_exact"
if not _fused_decode_proj_conv_logged:
rank0_log("Using fused GDN decode QKVZ/BA unpack + indexed Conv1D.")
_fused_decode_proj_conv_logged = True
if (
_fused_decode_log_layer_hits or _fused_decode_verify_real_tensors
) and layer.layer_id not in _fused_decode_proj_conv_layers_logged:
rank0_log(
"GDN_FUSED_DECODE_BACKEND "
f"layer_id={layer.layer_id} backend={fused_backend} "
f"batch={projected_qkvz.shape[0]} "
f"qkv_dim={qkv_dim} state_shape={tuple(conv_states.shape)} "
f"state_indices_dtype={cache_indices.dtype}"
)
_fused_decode_proj_conv_layers_logged.add(layer.layer_id)
# Compare real activations against the direct-Triton update on a
# compact state copy, leaving the live cache to the candidate.
verify_real_tensors = (
_fused_decode_verify_real_tensors
and layer.layer_id not in _fused_decode_real_tensor_verified_layers
)
if verify_real_tensors:
if bool(torch.any(cache_indices < 0).item()):
raise AssertionError(
"Real-tensor GDN fusion verification requires "
"non-padding cache indices"
)
ref_indices = torch.arange(
cache_indices.numel(),
device=cache_indices.device,
dtype=torch.int32,
)
ref_state = torch.index_select(
conv_states, 0, cache_indices.to(torch.int64)
)
ref_mixed_qkv, ref_z, ref_b, ref_a = (
fused_qkvzba_split_reshape_cat_contiguous(
projected_qkvz,
projected_ba,
layer.num_q_heads,
layer.num_v_heads,
layer.head_q_dim,
layer.head_v_dim,
)
)
ref_mixed_qkv = causal_conv1d_update(
ref_mixed_qkv,
ref_state,
layer.conv_weights,
layer.bias,
layer.activation,
conv_state_indices=ref_indices,
)
mixed_qkv, z, b, a = fused_qkvzba_causal_conv1d_update_contiguous(
projected_qkvz,
projected_ba,
conv_states,
layer.conv_weights,
layer.bias,
cache_indices,
qkv_dim=qkv_dim,
v_dim=layer.v_dim,
num_v_heads=layer.num_v_heads,
head_v_dim=layer.head_v_dim,
activation=layer.activation,
)
if verify_real_tensors:
candidate_state = torch.index_select(
conv_states, 0, cache_indices.to(torch.int64)
)
named_pairs = (
("qkv", mixed_qkv, ref_mixed_qkv),
("z", z, ref_z),
("b", b, ref_b),
("a", a, ref_a),
("state", candidate_state, ref_state),
)
report = []
mismatch = False
for tensor_name, candidate, reference in named_pairs:
diff = (candidate.float() - reference.float()).abs()
nonzero = int(torch.count_nonzero(diff).item())
mismatch |= nonzero != 0
report.append(
f"{tensor_name}_nonzero={nonzero}/"
f"{diff.numel()} {tensor_name}_max="
f"{diff.max().item()}"
)
rank0_log(
"GDN_FUSED_REAL_TENSOR_PARITY "
f"layer_id={layer.layer_id} backend={fused_backend} "
+ " ".join(report)
)
_fused_decode_real_tensor_verified_layers.add(layer.layer_id)
if mismatch:
raise AssertionError(
"GDN fused real-tensor parity failed at "
f"layer_id={layer.layer_id}; " + " ".join(report)
)
conv_already_applied = True
else:
# Explicit correctness fallback for an unexpected runtime
# tensor/state contract. This still returns Z to the model.
if not _fused_decode_proj_conv_fallback_logged:
rank0_log(
"Falling back from fused GDN decode projection/Conv1D: "
f"{eligibility_reason}"
)
_fused_decode_proj_conv_fallback_logged = True
mixed_qkv, z, b, a = fused_qkvzba_split_reshape_cat_contiguous(
projected_qkvz,
projected_ba,
layer.num_q_heads,
layer.num_v_heads,
layer.head_q_dim,
layer.head_v_dim,
)
return_z = True
else:
assert isinstance(mixed_qkv, torch.Tensor)
if not conv_already_applied:
mixed_qkv = causal_conv1d_update(
mixed_qkv,
conv_states,
layer.conv_weights,
layer.bias,
layer.activation,
conv_state_indices=cache_indices,
)
# Skip split + reshape + separate gating kernel by consuming
# the packed mixed_qkv directly in a single fused Triton kernel.
@@ -486,7 +651,7 @@ class GDNAttnBackend(MambaAttnBackendBase):
self._track_mamba_state_decode(
forward_batch, conv_states, ssm_states, cache_indices, layer.layer_id
)
return core_attn_out
return (core_attn_out, z) if return_z else core_attn_out
query, key, value = torch.split(
mixed_qkv,
@@ -516,7 +681,7 @@ class GDNAttnBackend(MambaAttnBackendBase):
forward_batch, conv_states, ssm_states, cache_indices, layer.layer_id
)
return core_attn_out
return (core_attn_out, z) if return_z else core_attn_out
def forward_extend(
self,
@@ -735,6 +900,7 @@ class GDNAttnBackend(MambaAttnBackendBase):
state_checkpoint_every_n_tokens=(
forward_metadata.state_checkpoint_every_n_tokens
),
output=kwargs.get("linear_attn_output"),
)
if is_npu() and last_recurrent_state is not None:
@@ -30,6 +30,29 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
_FLASHINFER_GDN_ALIGNMENT = 32
def _empty_aligned_like(
tensor: torch.Tensor, alignment: int = _FLASHINFER_GDN_ALIGNMENT
) -> torch.Tensor:
element_size = tensor.dtype.itemsize
alignment_elements = max(1, alignment // element_size)
storage = torch.empty(
tensor.numel() + alignment_elements - 1,
dtype=tensor.dtype,
device=tensor.device,
)
start_bytes = (-storage.data_ptr()) % alignment
if start_bytes % element_size:
raise RuntimeError(
f"Cannot align {tensor.dtype} storage at {storage.data_ptr()} "
f"to {alignment} bytes"
)
start = start_bytes // element_size
return storage[start : start + tensor.numel()].view(tensor.shape)
# ---------------------------------------------------------------------------
# Lazy import for FlashInfer GDN kernels
# ---------------------------------------------------------------------------
@@ -167,6 +190,19 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
# states; SM100 accepts the state-pool dtype directly.
self._prefill_needs_fp32_state = sm_major >= 12
self.supports_target_verify = sm_major in (9, 10)
self._aligned_input_buffers: dict[tuple, torch.Tensor] = {}
self._aligned_parameter_cache: dict[
tuple, tuple[torch.Tensor, torch.Tensor]
] = {}
self._verify_intermediate_buffers: dict[tuple, torch.Tensor] = {}
self._alignment_fallback_warned = False
# FlashInfer writes through mutable state/workspace pointers, so misaligned
# inputs must fall back to Triton rather than use temporary copies.
from sglang.srt.layers.attention.linear.kernels.gdn_triton import (
TritonGDNKernel,
)
self._alignment_fallback_kernel = TritonGDNKernel()
if sm_major == 9 and self._prefill_fn is None:
raise RuntimeError("FlashInfer GDN prefill kernel is unavailable.")
@@ -209,6 +245,122 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
logger.info("Using FlashInfer GDN kernels")
def _prepare_dynamic_input(self, name: str, tensor: torch.Tensor) -> torch.Tensor:
# Reuse per-stream aligned scratch for uncommon read-only views; stable
# addresses avoid allocator churn and remain safe for CUDA graph capture.
if tensor.data_ptr() % _FLASHINFER_GDN_ALIGNMENT == 0:
return tensor
stream_key = (
torch.cuda.current_stream(tensor.device).cuda_stream
if tensor.device.type == "cuda"
else None
)
key = (
name,
tensor.device,
tensor.dtype,
tuple(tensor.shape),
stream_key,
)
aligned = self._aligned_input_buffers.get(key)
if aligned is None:
aligned = _empty_aligned_like(tensor)
self._aligned_input_buffers[key] = aligned
aligned.copy_(tensor)
return aligned
def _prepare_parameter(
self,
name: str,
tensor: torch.Tensor,
*,
dtype: Optional[torch.dtype] = None,
) -> torch.Tensor:
key = (
name,
id(tensor),
dtype,
)
cached_entry = self._aligned_parameter_cache.get(key)
if cached_entry is not None and cached_entry[0] is tensor:
return cached_entry[1]
prepared = tensor.detach().reshape(-1)
if dtype is not None:
prepared = prepared.to(dtype=dtype, copy=False)
if (
not prepared.is_contiguous()
or prepared.data_ptr() % _FLASHINFER_GDN_ALIGNMENT
):
aligned = _empty_aligned_like(prepared)
aligned.copy_(prepared)
prepared = aligned
# Retaining the source also prevents a recycled Python id from
# colliding with an older cache entry.
self._aligned_parameter_cache[key] = (tensor, prepared)
return prepared
def _prepare_gate_parameters(
self,
A_log: torch.Tensor,
dt_bias: torch.Tensor,
*,
A_log_dtype: Optional[torch.dtype] = None,
) -> tuple[torch.Tensor, torch.Tensor]:
return (
self._prepare_parameter("A_log", A_log, dtype=A_log_dtype),
self._prepare_parameter("dt_bias", dt_bias),
)
def _mutable_inputs_are_aligned(
self, *named_tensors: tuple[str, Optional[torch.Tensor]]
) -> bool:
for name, tensor in named_tensors:
if tensor is None or tensor.data_ptr() % _FLASHINFER_GDN_ALIGNMENT == 0:
continue
if not self._alignment_fallback_warned:
logger.warning(
"FlashInfer GDN mutable buffer %r has data_ptr %d "
"(mod 32 = %d); falling back to Triton for this call.",
name,
tensor.data_ptr(),
tensor.data_ptr() % _FLASHINFER_GDN_ALIGNMENT,
)
self._alignment_fallback_warned = True
return False
return True
def _prepare_verify_intermediate_buffer(
self,
intermediate_states_buffer: torch.Tensor,
batch_size: int,
) -> tuple[torch.Tensor, bool]:
# FlashInfer requires exact capture B, which may exceed the pool-scoped
# buffer; padded tiers use stable scratch and copy owned rows back.
direct = intermediate_states_buffer[:batch_size]
if direct.shape[0] == batch_size:
return direct, False
stream_key = (
torch.cuda.current_stream(intermediate_states_buffer.device).cuda_stream
if intermediate_states_buffer.device.type == "cuda"
else None
)
shape = (batch_size, *intermediate_states_buffer.shape[1:])
key = (
intermediate_states_buffer.device,
intermediate_states_buffer.dtype,
shape,
stream_key,
)
scratch = self._verify_intermediate_buffers.get(key)
if scratch is None:
template = intermediate_states_buffer.new_empty(shape)
scratch = _empty_aligned_like(template)
self._verify_intermediate_buffers[key] = scratch
return scratch, True
# ---- decode ----
def decode(
@@ -226,6 +378,21 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
query_start_loc: torch.Tensor,
**kwargs,
) -> torch.Tensor:
if not self._mutable_inputs_are_aligned(("ssm_states", ssm_states)):
return self._alignment_fallback_kernel.decode(
q,
k,
v,
a,
b,
A_log=A_log,
dt_bias=dt_bias,
ssm_states=ssm_states,
cache_indices=cache_indices,
query_start_loc=query_start_loc,
**kwargs,
)
batch_size = cache_indices.shape[0]
num_heads = q.shape[2]
head_k_dim = q.shape[3]
@@ -237,20 +404,35 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
value_fi = v.view(batch_size, 1, num_v_heads, head_v_dim)
a_fi = a.view(batch_size, 1, num_v_heads)
b_fi = b.view(batch_size, 1, num_v_heads)
query_fi = self._prepare_dynamic_input("decode_q", query_fi)
key_fi = self._prepare_dynamic_input("decode_k", key_fi)
value_fi = self._prepare_dynamic_input("decode_v", value_fi)
a_fi = self._prepare_dynamic_input("decode_a", a_fi)
b_fi = self._prepare_dynamic_input("decode_b", b_fi)
A_log_fi, dt_bias_fi = self._prepare_gate_parameters(
A_log,
dt_bias,
# Preserve the original backend contract: the SM100 state-pool
# kernel consumes float32 A_log, while SM90 uses the source dtype.
A_log_dtype=torch.float32 if self.use_state_pool else None,
)
if self.use_state_pool:
cache_indices_fi = self._prepare_dynamic_input(
"decode_cache_indices", cache_indices
)
output_fi, _ = self._decode_fn(
q=query_fi,
k=key_fi,
v=value_fi,
state=None,
A_log=A_log.detach().float(),
A_log=A_log_fi,
a=a_fi,
dt_bias=dt_bias.detach(),
dt_bias=dt_bias_fi,
b=b_fi,
use_qk_l2norm=True,
initial_state=ssm_states,
initial_state_indices=cache_indices,
initial_state_indices=cache_indices_fi,
)
else:
# TODO: Once FlashInfer PR#2521 is merged for SM90, gather/scatter
@@ -261,9 +443,9 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
k=key_fi,
v=value_fi,
state=state_batch,
A_log=A_log.detach(),
A_log=A_log_fi,
a=a_fi,
dt_bias=dt_bias.detach(),
dt_bias=dt_bias_fi,
b=b_fi,
scale=None,
output=None,
@@ -301,6 +483,30 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
k_fi = l2norm_fwd(k[0].contiguous())
v_fi = v[0].contiguous()
output = kwargs.get("output")
output_fi = None
if output is not None:
expected_output_shape = (
1,
total_seq_len,
num_v_heads,
head_v_dim,
)
if tuple(output.shape) != expected_output_shape:
raise ValueError(
"FlashInfer GDN prefill output shape mismatch: "
f"expected {expected_output_shape}, got {tuple(output.shape)}"
)
if output.dtype != v.dtype or output.device != v.device:
raise ValueError(
"FlashInfer GDN prefill output must match v dtype/device, "
f"got output=({output.dtype}, {output.device}) and "
f"v=({v.dtype}, {v.device})"
)
output_fi = output[0]
if not output_fi.is_contiguous():
raise ValueError("FlashInfer GDN prefill output must be contiguous")
# g (alpha) and beta: [1, seq, HV] -> [seq, HV], float32 for FlashInfer
alpha_fi = torch.exp(g[0].to(torch.float32))
beta_fi = beta[0].to(torch.float32)
@@ -347,6 +553,7 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
output_final_state=True,
cu_seqlens=cu_seqlens,
use_qk_l2norm_in_kernel=False,
output=output_fi,
output_state=output_state_fi,
state_checkpoints=state_checkpoints,
checkpoint_cu_starts=state_checkpoint_cu_starts,
@@ -404,34 +611,79 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
num_v_heads = v.shape[2]
head_v_dim = v.shape[3]
query_mtp = q.view(batch_size, draft_token_num, num_heads, head_k_dim)
key_mtp = k.view(batch_size, draft_token_num, num_heads, head_k_dim)
value_mtp = v.view(batch_size, draft_token_num, num_v_heads, head_v_dim)
if a is None or b is None or A_log is None or dt_bias is None:
raise RuntimeError(
"FlashInfer GDN MTP kernel requires a, b, A_log, dt_bias."
)
a_mtp = a.view(batch_size, draft_token_num, num_v_heads)
b_mtp = b.view(batch_size, draft_token_num, num_v_heads)
intermediate_states_buffer_mtp = intermediate_states_buffer
copy_verify_intermediate_back = False
if self.use_state_pool and intermediate_states_buffer is not None:
# The SM100 bf16 MTP kernel indexes this scratch buffer by the
# per-call batch id, while SGLang's speculative state cache is
# pool-scoped and may include an extra dummy slot.
intermediate_states_buffer_mtp = intermediate_states_buffer[:batch_size]
# Graph padding can exceed the pool-scoped scratch; use exact-B storage
# and copy owned rows back before post-verify commit reads the pool.
(
intermediate_states_buffer_mtp,
copy_verify_intermediate_back,
) = self._prepare_verify_intermediate_buffer(
intermediate_states_buffer, batch_size
)
if not self._mutable_inputs_are_aligned(
("ssm_states", ssm_states),
("intermediate_states_buffer", intermediate_states_buffer_mtp),
):
return self._alignment_fallback_kernel.target_verify(
A_log=A_log,
dt_bias=dt_bias,
q=q,
k=k,
v=v,
a=a,
b=b,
ssm_states=ssm_states,
cache_indices=cache_indices,
query_start_loc=query_start_loc,
intermediate_states_buffer=intermediate_states_buffer,
intermediate_state_indices=intermediate_state_indices,
cache_steps=cache_steps,
retrieve_parent_token=retrieve_parent_token,
**kwargs,
)
query_mtp = self._prepare_dynamic_input(
"verify_q",
q.view(batch_size, draft_token_num, num_heads, head_k_dim),
)
key_mtp = self._prepare_dynamic_input(
"verify_k",
k.view(batch_size, draft_token_num, num_heads, head_k_dim),
)
value_mtp = self._prepare_dynamic_input(
"verify_v",
v.view(batch_size, draft_token_num, num_v_heads, head_v_dim),
)
a_mtp = self._prepare_dynamic_input(
"verify_a", a.view(batch_size, draft_token_num, num_v_heads)
)
b_mtp = self._prepare_dynamic_input(
"verify_b", b.view(batch_size, draft_token_num, num_v_heads)
)
A_log_fi, dt_bias_fi = self._prepare_gate_parameters(A_log, dt_bias)
cache_indices_fi = self._prepare_dynamic_input(
"verify_cache_indices", cache_indices
)
output_fi, _ = self._mtp_fn(
q=query_mtp,
k=key_mtp,
v=value_mtp,
initial_state=ssm_states,
initial_state_indices=cache_indices,
A_log=A_log.detach(),
initial_state_indices=cache_indices_fi,
A_log=A_log_fi,
a=a_mtp,
dt_bias=dt_bias.detach(),
dt_bias=dt_bias_fi,
b=b_mtp,
scale=None,
output=None,
@@ -440,4 +692,9 @@ class FlashInferGDNKernel(LinearAttnKernelBase):
use_qk_l2norm=True,
)
if copy_verify_intermediate_back:
intermediate_states_buffer.copy_(
intermediate_states_buffer_mtp[: intermediate_states_buffer.shape[0]]
)
return output_fi.view(1, seq_len, num_v_heads, head_v_dim)
@@ -0,0 +1,82 @@
"""Debug validation for Mamba CUDA-graph replay state indices.
The validator is intentionally CPU-only. Replay metadata is prepared outside
the captured graph, so a diagnostic run may synchronize once here without
putting host reads or dynamic assertions into CUDA graph capture/replay.
"""
from __future__ import annotations
import torch
def validate_replay_state_indices_cpu(
state_indices: torch.Tensor,
*,
valid_bs: int,
total_bs: int,
num_state_slots: int,
pad_slot_id: int = -1,
) -> None:
"""Validate live and padded rows of a replay state-index buffer.
Live rows must own distinct in-range slots in ``[0, num_state_slots)``.
Slot zero is reserved for CUDA-graph dummy/idle traffic but is still a
valid storage row. All padded rows must carry exactly ``pad_slot_id`` so
indexed state kernels skip them.
"""
if state_indices.device.type != "cpu":
raise ValueError("state_indices must be copied to CPU before validation")
if state_indices.ndim != 1:
raise ValueError("state_indices must be rank-1")
if not 0 <= valid_bs <= total_bs <= state_indices.numel():
raise ValueError(
"expected 0 <= valid_bs <= total_bs <= state_indices.numel(), got "
f"valid_bs={valid_bs} total_bs={total_bs} "
f"numel={state_indices.numel()}"
)
if num_state_slots <= 1:
raise ValueError(
f"num_state_slots must include real slots, got {num_state_slots}"
)
indices = state_indices[:total_bs].to(dtype=torch.int64)
live = indices[:valid_bs]
padded = indices[valid_bs:]
errors: list[str] = []
live_in_range = (live >= 0) & (live < num_state_slots)
if not bool(torch.all(live_in_range)):
bad_rows = torch.nonzero(~live_in_range, as_tuple=False).flatten()
errors.append(
"live rows must contain in-range slots in "
f"[0, {num_state_slots}); bad_rows={bad_rows.tolist()} "
f"bad_values={live[bad_rows].tolist()}"
)
if live.numel() > 1:
unique_live, counts = torch.unique(live, sorted=True, return_counts=True)
duplicate_mask = counts > 1
if bool(torch.any(duplicate_mask)):
errors.append(
"live rows must own unique slots; "
f"duplicate_slots={unique_live[duplicate_mask].tolist()} "
f"counts={counts[duplicate_mask].tolist()}"
)
bad_padding = padded != pad_slot_id
if bool(torch.any(bad_padding)):
bad_offsets = torch.nonzero(bad_padding, as_tuple=False).flatten()
errors.append(
f"padded rows must equal pad_slot_id={pad_slot_id}; "
f"bad_rows={(bad_offsets + valid_bs).tolist()} "
f"bad_values={padded[bad_offsets].tolist()}"
)
if errors:
raise AssertionError(
"Invalid Mamba replay state indices: "
+ "; ".join(errors)
+ f"; valid_bs={valid_bs} total_bs={total_bs} "
f"indices={indices.tolist()}"
)
+6 -3
View File
@@ -28,6 +28,7 @@ from sglang.srt.distributed import (
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.environ import envs
from sglang.srt.runtime_context import (
derive_attention_widths,
get_device,
@@ -103,7 +104,11 @@ class DpPaddingMode(IntEnum):
# Force MAX_LEN so all ranks are padded to equal token counts.
from sglang.srt.layers.moe.utils import get_moe_a2a_backend
if get_moe_a2a_backend().is_pplx():
moe_a2a_backend = get_moe_a2a_backend()
if moe_a2a_backend.is_pplx():
return DpPaddingMode.MAX_LEN
if moe_a2a_backend.is_deepep_v2() and envs.SGLANG_DEEPEP_V2_FORCE_MAX_LEN.get():
return DpPaddingMode.MAX_LEN
# When is_extend_in_batch and dp_size > 1, use SUM_LEN to avoid padding
@@ -588,8 +593,6 @@ _dp_gather_fp8_bufs: dict = {}
@functools.lru_cache(maxsize=1)
def _use_dp_gather_fp8() -> bool:
from sglang.srt.environ import envs
return envs.SGLANG_ENABLE_DP_GATHER_FP8.get()
@@ -0,0 +1,392 @@
"""Process-local access to FlashInfer's MNNVL CuTe DSL fusion workspace."""
from __future__ import annotations
import importlib.util
import logging
import threading
from dataclasses import dataclass, replace
from functools import lru_cache
from typing import TYPE_CHECKING
import torch
import torch.distributed as dist
if TYPE_CHECKING:
from torch.distributed import ProcessGroup
logger = logging.getLogger(__name__)
def _import_kernel_backend():
try:
from flashinfer.comm import AllReduceFusionPattern
except ImportError as error:
raise RuntimeError(
"MNNVL CuTe DSL fusion requires FlashInfer's communication "
"infrastructure (flashinfer >= 0.6.16)"
) from error
try:
from sglang.kernels.ops.communication.mnnvl_cutedsl import DEFAULT_CONFIG
from sglang.kernels.ops.communication.mnnvl_cutedsl_ar import (
MNNVLCuteDSLAllReduceFusionWorkspace,
mnnvl_cutedsl_allreduce_fusion,
)
except ImportError as error:
raise RuntimeError(
"SGLang's in-tree MNNVL CuTe DSL kernels failed to import; check "
"their dependencies, including nvidia-cutlass-dsl and cuda-python"
) from error
if importlib.util.find_spec("flashinfer.comm.mnnvl_cutedsl") is not None:
logger.warning(
"The installed FlashInfer now ships flashinfer.comm.mnnvl_cutedsl; "
"SGLang is still running its in-tree port "
"(sglang.kernels.ops.communication.mnnvl_cutedsl), which can now "
"be retired in favor of the upstream backend."
)
return (
MNNVLCuteDSLAllReduceFusionWorkspace,
mnnvl_cutedsl_allreduce_fusion,
AllReduceFusionPattern,
DEFAULT_CONFIG,
)
def _with_early_finalize_shared_load(config):
profiles = []
updated_presets = 0
for profile in config.profiles:
targets = []
for target in profile.finalize_routes.targets:
preset = target.preset
if hasattr(preset, "load_shared_expert_before_pdl"):
preset = replace(preset, load_shared_expert_before_pdl=True)
target = replace(target, preset=preset)
updated_presets += 1
targets.append(target)
profiles.append(
replace(
profile,
finalize_routes=replace(
profile.finalize_routes,
targets=tuple(targets),
),
)
)
if updated_presets == 0:
raise RuntimeError(
"FlashInfer MNNVL config does not expose the finalize shared-load "
"PDL ordering option"
)
return replace(config, profiles=tuple(profiles))
@dataclass(frozen=True, slots=True)
class _WorkspaceSignature:
hidden_size: int
top_k: int
rms_epsilon: float
weight_bias: float
max_m: int
device_index: int
process_group_identity: int
class FlashInferMNNVLCuteDSLARFusion:
"""One graph-stable workspace serving both supported fusion patterns."""
def __init__(
self,
*,
hidden_size: int,
top_k: int,
max_m: int,
rms_epsilon: float,
weight_bias: float,
process_group: ProcessGroup,
device: torch.device,
) -> None:
if hidden_size <= 0 or top_k <= 0 or max_m <= 0:
raise ValueError("hidden_size, top_k, and max_m must be positive")
if device.type != "cuda":
raise ValueError(f"MNNVL CuTe DSL fusion requires CUDA, got {device}")
self.hidden_size = int(hidden_size)
self.top_k = int(top_k)
self.max_m = int(max_m)
self.rms_epsilon = float(rms_epsilon)
self.weight_bias = float(weight_bias)
self.process_group = process_group
self.device = torch.device(device)
self._destroyed = False
with torch.cuda.device(self.device):
self.device = torch.device("cuda", torch.cuda.current_device())
# CuTe DSL obtains NVLS storage through PyTorch symmetric memory, whose
# process-local backend must be selected before workspace construction.
import torch.distributed._symmetric_memory as symm_mem
symmetric_memory_backend = symm_mem.get_backend(self.device)
if symmetric_memory_backend is None:
symm_mem.set_backend("NCCL")
symmetric_memory_backend = symm_mem.get_backend(self.device)
if symmetric_memory_backend is None:
raise RuntimeError(
"PyTorch symmetric memory has no backend for the current device"
)
logger.info(
"Using PyTorch symmetric-memory backend %s for %s",
symmetric_memory_backend,
self.device,
)
(
workspace_type,
self._allreduce_fusion,
self._patterns,
default_config,
) = _import_kernel_backend()
# Only fused finalize launches have a completed shared-expert handoff;
# standalone AllReduce kernels retain the safe load ordering.
from sglang.srt.runtime_context import get_spec
if get_spec().speculative_algorithm is None:
self.workspace_config = _with_early_finalize_shared_load(default_config)
else:
# Early shared load is safe only for a single looping decode graph;
# alternating draft/verify replays can read an unfinished buffer.
logger.info(
"Speculative decoding active: keeping the FlashInfer MNNVL "
"CuTe DSL finalize presets on the safe (non-early-load) "
"ordering."
)
self.workspace_config = default_config
self.workspace = workspace_type(
tp_size=dist.get_world_size(process_group),
tp_rank=dist.get_rank(process_group),
max_token_num=self.max_m,
hidden_dim=self.hidden_size,
dtype=torch.bfloat16,
group=process_group,
top_k=self.top_k,
rms_eps=self.rms_epsilon,
routed_scaling_factor=1.0,
weight_bias=self.weight_bias,
include_shared_expert=True,
add_residual=True,
write_residual_output=True,
config=self.workspace_config,
)
# Publish only after the mailbox barrier; older FlashInfer workspace
# classes may not provide it and would desynchronize Lamport stages.
torch.cuda.synchronize(self.device)
dist.barrier(group=process_group)
def supports(self, m: int) -> bool:
if self._destroyed or not 1 <= int(m) <= self.max_m:
return False
return self.workspace.is_buffer_size_sufficient(
tp_size=dist.get_world_size(self.process_group),
num_tokens=int(m),
hidden_dim=self.hidden_size,
dtype=torch.bfloat16,
)
def moe_finalize_all_reduce_rms_norm(
self,
*,
routed_output: torch.Tensor,
expert_weights: torch.Tensor,
permuted_indices: torch.Tensor,
gated_shared_output: torch.Tensor,
residual: torch.Tensor,
gamma: torch.Tensor,
norm_output: torch.Tensor | None = None,
residual_output: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
m = int(permuted_indices.shape[0])
if not self.supports(m):
raise ValueError(f"workspace does not support M={m}")
shape = (m, self.hidden_size)
if norm_output is None:
norm_output = torch.empty(shape, dtype=torch.bfloat16, device=self.device)
if residual_output is None:
residual_output = torch.empty(
shape, dtype=torch.bfloat16, device=self.device
)
pattern = self._patterns.kMoEFinalizeARResidualRMSNorm
self._allreduce_fusion(
input=routed_output,
workspace=self.workspace,
pattern=pattern,
# The public API carries the caller's PDL intent. The backend's
# routing profile owns the compiled choice and validates it.
launch_with_pdl=True,
residual_in=residual,
residual_out=residual_output,
norm_out=norm_output,
rms_gamma=gamma,
rms_eps=self.rms_epsilon,
weight_bias=self.weight_bias,
expanded_idx_to_permuted_idx=permuted_indices,
expert_scale_factor=expert_weights,
shared_expert_output=gated_shared_output,
)
return norm_output, residual_output
def all_reduce_residual_rms_norm(
self,
*,
local_contribution: torch.Tensor,
residual: torch.Tensor,
gamma: torch.Tensor,
norm_output: torch.Tensor | None = None,
residual_output: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
m = int(local_contribution.shape[0])
if not self.supports(m):
raise ValueError(f"workspace does not support M={m}")
if norm_output is None:
norm_output = torch.empty_like(local_contribution)
if residual_output is None:
residual_output = torch.empty_like(local_contribution)
pattern = self._patterns.kARResidualRMSNorm
self._allreduce_fusion(
input=local_contribution,
workspace=self.workspace,
pattern=pattern,
launch_with_pdl=True,
residual_in=residual,
residual_out=residual_output,
norm_out=norm_output,
rms_gamma=gamma,
rms_eps=self.rms_epsilon,
weight_bias=self.weight_bias,
)
return norm_output, residual_output
def destroy(self) -> None:
if self._destroyed:
return
self.workspace.destroy()
self._destroyed = True
_WORKSPACES: dict[_WorkspaceSignature, FlashInferMNNVLCuteDSLARFusion] = {}
_WORKSPACES_LOCK = threading.RLock()
@lru_cache(maxsize=1)
def _max_workspace_instances() -> int:
from sglang.srt.environ import envs
value = int(envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION_MAX_INSTANCES.get())
if value < 1:
raise ValueError(
"SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION_MAX_INSTANCES must be positive"
)
return value
def get_flashinfer_mnnvl_cutedsl_ar_fusion(
*,
hidden_size: int | None = None,
top_k: int | None = None,
max_m: int | None = None,
rms_epsilon: float | None = None,
weight_bias: float | None = None,
) -> FlashInferMNNVLCuteDSLARFusion:
"""Lookup, or before graph capture create, the process-local workspace."""
supplied = (hidden_size, top_k, max_m, rms_epsilon, weight_bias)
if all(value is None for value in supplied):
with _WORKSPACES_LOCK:
if len(_WORKSPACES) == 1:
return next(iter(_WORKSPACES.values()))
if not _WORKSPACES:
raise RuntimeError(
"MNNVL CuTe DSL fusion workspace was not initialized before use"
)
raise RuntimeError(
"multiple MNNVL CuTe DSL fusion workspaces exist; configuration "
"arguments are required"
)
if any(value is None for value in supplied):
raise TypeError(
"hidden_size, top_k, max_m, rms_epsilon, and weight_bias must be "
"supplied together"
)
if not torch.cuda.is_available():
raise RuntimeError("MNNVL CuTe DSL fusion requires CUDA")
assert hidden_size is not None
assert top_k is not None
assert max_m is not None
assert rms_epsilon is not None
assert weight_bias is not None
from sglang.srt.distributed.parallel_state import get_tp_group
device = torch.device("cuda", torch.cuda.current_device())
process_group = get_tp_group().device_group
domain = (
int(hidden_size),
int(top_k),
float(rms_epsilon),
float(weight_bias),
int(device.index),
id(process_group),
)
with _WORKSPACES_LOCK:
compatible = [
(signature.max_m, instance)
for signature, instance in _WORKSPACES.items()
if (
signature.hidden_size,
signature.top_k,
signature.rms_epsilon,
signature.weight_bias,
signature.device_index,
signature.process_group_identity,
)
== domain
and signature.max_m >= int(max_m)
]
if compatible:
return min(compatible, key=lambda item: item[0])[1]
if torch.cuda.is_current_stream_capturing():
raise RuntimeError(
"creating an MNNVL CuTe DSL fusion workspace during CUDA Graph "
"capture is forbidden"
)
if len(_WORKSPACES) >= _max_workspace_instances():
raise RuntimeError(
"MNNVL CuTe DSL fusion workspace instance limit exceeded; "
"increase SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION_MAX_INSTANCES "
"only when multiple model configurations intentionally coexist"
)
signature = _WorkspaceSignature(
hidden_size=int(hidden_size),
top_k=int(top_k),
rms_epsilon=float(rms_epsilon),
weight_bias=float(weight_bias),
max_m=int(max_m),
device_index=int(device.index),
process_group_identity=id(process_group),
)
instance = FlashInferMNNVLCuteDSLARFusion(
hidden_size=hidden_size,
top_k=top_k,
max_m=max_m,
rms_epsilon=rms_epsilon,
weight_bias=weight_bias,
process_group=process_group,
device=device,
)
_WORKSPACES[signature] = instance
return instance
@@ -27,6 +27,7 @@ from sglang.kernels.ops.activation.softcap import (
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.aux_hidden_states import (
AuxHiddenStates,
pack_aux_hidden_states,
@@ -81,6 +82,83 @@ _UNQUANTIZED_LM_HEAD_METHODS = {
_autotune_run_lm_head: Optional[bool] = None
def _trace_e2e_logits(stage: str, **fields) -> None:
if not envs.SGLANG_TRACE_LOGITS_E2E.get():
return
try:
parallel = get_parallel()
rank = f"dp={parallel.attn_dp_rank} " f"tp={parallel.tp_rank}"
except Exception:
rank = "rank=unknown"
details = " ".join(f"{key}={value}" for key, value in fields.items())
print(f"SGLANG_TRACE_LOGITS_E2E {rank} stage={stage} {details}", flush=True)
def _has_lm_head_runtime_attrs(lm_head, attr_names: Tuple[str, ...]) -> bool:
return all(hasattr(lm_head, attr_name) for attr_name in attr_names)
def should_apply_lm_head_quant_method(lm_head, quant_method) -> bool:
if (
quant_method is None
or not hasattr(lm_head, "weight")
or not callable(getattr(quant_method, "apply", None))
):
return False
method_name = type(quant_method).__name__
if method_name in _UNQUANTIZED_LM_HEAD_METHODS:
return False
# A shared target lm_head can retain the draft's stale ModelOpt method; use it
# only when the runtime tensor layout matches that method.
if method_name == "ModelOptFp4LinearMethod":
if lm_head.weight.dtype == torch.int32 and _has_lm_head_runtime_attrs(
lm_head,
(
"weight_scale",
"weight_global_scale",
"workspace",
"input_size_per_partition",
"output_size_per_partition",
),
):
return True
return lm_head.weight.dtype == torch.uint8 and _has_lm_head_runtime_attrs(
lm_head,
(
"weight_scale_interleaved",
"alpha",
"input_scale_inv",
"input_size_per_partition",
"output_size_per_partition",
),
)
if method_name == "ModelOptNvFp4A16LinearMethod":
return lm_head.weight.dtype == torch.int32 and _has_lm_head_runtime_attrs(
lm_head,
(
"weight_scale",
"weight_global_scale",
"workspace",
"input_size_per_partition",
"output_size_per_partition",
),
)
if method_name == "ModelOptFp8LinearMethod":
return (
lm_head.weight.dtype == torch.float8_e4m3fn
and _has_lm_head_runtime_attrs(lm_head, ("weight_scale", "input_scale"))
)
return True
# FlashInfer autotune skips the unprofiled LM-head all-gather; its
# [batch * dp_size, vocab] output can OOM under tight DP-attention memory.
_in_autotune_dummy_run = False
def get_in_autotune_dummy_run() -> bool:
return _autotune_run_lm_head is not None
@@ -668,17 +746,42 @@ class LogitsProcessor(nn.Module):
last position (e.g., extend without input logprobs). The caller should
guarantee the given hidden_states follow this constraint.
"""
_trace_e2e_logits(
"get_logits_enter",
hidden_shape=tuple(hidden_states.shape),
dp_gather=self.do_tensor_parallel_all_gather_dp_attn,
tp_gather=self.do_tensor_parallel_all_gather,
)
hidden_states, local_hidden_states = self._gather_dp_attn_hidden_states(
hidden_states, logits_metadata
)
_trace_e2e_logits(
"dp_hidden_gather_returned",
global_shape=tuple(hidden_states.shape),
local_shape=tuple(local_hidden_states.shape),
)
if envs.SGLANG_TRACE_LOGITS_E2E_SYNC.get():
_trace_e2e_logits("pre_lm_head_sync_enter")
torch.cuda.synchronize()
_trace_e2e_logits("pre_lm_head_sync_returned")
_trace_e2e_logits("lm_head_enter", hidden_shape=tuple(hidden_states.shape))
logits = self._compute_lm_head(hidden_states, lm_head, embedding_bias)
_trace_e2e_logits("lm_head_returned", logits_shape=tuple(logits.shape))
if envs.SGLANG_TRACE_LOGITS_E2E_SYNC.get():
_trace_e2e_logits("post_lm_head_sync_enter")
torch.cuda.synchronize()
_trace_e2e_logits("post_lm_head_sync_returned")
if self.logit_scale is not None:
logits.mul_(self.logit_scale)
used_tp_lm_head_all_to_all = False
if self.do_tensor_parallel_all_gather:
_trace_e2e_logits(
"tp_logits_gather_enter", logits_shape=tuple(logits.shape)
)
if self.use_attn_tp_group:
logits = self._gather_attn_tp_logits(logits)
elif self._can_use_tp_lm_head_all_to_all(
@@ -688,11 +791,20 @@ class LogitsProcessor(nn.Module):
used_tp_lm_head_all_to_all = True
else:
logits = self._logits_gatherer(logits)
_trace_e2e_logits(
"tp_logits_gather_returned", logits_shape=tuple(logits.shape)
)
if not used_tp_lm_head_all_to_all:
_trace_e2e_logits(
"dp_logits_scatter_enter", logits_shape=tuple(logits.shape)
)
logits = self._scatter_dp_attn_logits(
logits, local_hidden_states, logits_metadata
)
_trace_e2e_logits(
"dp_logits_scatter_returned", logits_shape=tuple(logits.shape)
)
logits = self._copy_logits_to_buffer(
logits, logits_metadata, use_buffer=use_logits_buffer
@@ -776,10 +888,27 @@ class LogitsProcessor(nn.Module):
self, hidden_states: torch.Tensor, logits_metadata: LogitsMetadata
) -> Tuple[torch.Tensor, torch.Tensor]:
if self.do_tensor_parallel_all_gather_dp_attn:
_trace_e2e_logits(
"dp_metadata_enter",
local_shape=tuple(hidden_states.shape),
global_counts_cpu=logits_metadata.global_num_tokens_for_logprob_cpu,
)
logits_metadata.compute_dp_attention_metadata()
_trace_e2e_logits(
"dp_metadata_returned",
buffer_shape=tuple(logits_metadata.gathered_buffer.shape),
local_start=logits_metadata.dp_local_start_pos,
local_tokens=logits_metadata.dp_local_num_tokens,
)
local_hidden_states = hidden_states
hidden_states = logits_metadata.gathered_buffer
_trace_e2e_logits(
"dp_hidden_gather_enter",
global_shape=tuple(hidden_states.shape),
local_shape=tuple(local_hidden_states.shape),
)
dp_gather_replicate(hidden_states, local_hidden_states, logits_metadata)
_trace_e2e_logits("dp_hidden_gather_collective_returned")
return hidden_states, local_hidden_states
return hidden_states, hidden_states
@@ -206,6 +206,7 @@ def create_moe_dispatcher(moe_runner_config: MoeRunnerConfig) -> BaseDispatcher:
num_experts=moe_runner_config.num_experts,
num_local_experts=moe_runner_config.num_local_experts,
hidden_size=moe_runner_config.hidden_size,
moe_runner_config=moe_runner_config,
)
else:
raise NotImplementedError(f"Unsupported a2a backend: {a2a_backend}")
@@ -283,6 +284,8 @@ class FusedMoE(torch.nn.Module):
reduce_results: Whether to apply all_reduce on the output of the layer
quant_config: Quantization configuration.
inplace: suggestion to compute inplace (modify input activation).
enable_qwen35_fp8_deferred_finalize: Whether this concrete Qwen3.5
layer may expose FlashInfer's block-FP8 deferred MoE output.
"""
# True on shared-expert FusedMoE subclasses (e.g. Inkling's sink); lets
@@ -321,6 +324,7 @@ class FusedMoE(torch.nn.Module):
routing_method_type: Optional[RoutingMethodType] = None,
is_gated: bool = True,
gate_up_interleaved: bool = True,
enable_qwen35_fp8_deferred_finalize: bool = False,
):
super().__init__()
if params_dtype is None:
@@ -446,10 +450,17 @@ class FusedMoE(torch.nn.Module):
)
_validate_hpc_ops_quant_method(self.quant_method)
_validate_deepep_v2_quant_method(self.quant_method)
nvfp4_deferred = envs.SGLANG_ENABLE_MOE_DEFERRED_FINALIZE.get() and isinstance(
self.quant_method, ModelOptNvFp4FusedMoEMethod
)
qwen35_fp8_deferred = (
enable_qwen35_fp8_deferred_finalize
and isinstance(self.quant_method, Fp8MoEMethod)
and self.quant_method.block_quant
)
self.supports_deferred_finalize = (
envs.SGLANG_ENABLE_MOE_DEFERRED_FINALIZE.get()
and get_moe_runner_backend().is_flashinfer_trtllm()
and isinstance(self.quant_method, ModelOptNvFp4FusedMoEMethod)
get_moe_runner_backend().is_flashinfer_trtllm()
and (nvfp4_deferred or qwen35_fp8_deferred)
)
global _deferred_finalize_info_logged
if not _deferred_finalize_info_logged:
@@ -30,7 +30,7 @@ from sglang.srt.layers.moe.moe_runner.base import (
register_pre_permute,
)
from sglang.srt.layers.moe.utils import MoeRunnerBackend, get_moe_a2a_backend
from sglang.srt.runtime_context import get_exec
from sglang.srt.runtime_context import get_exec, get_flags
from sglang.srt.utils import (
ceil_div,
dispose_tensor,
@@ -53,6 +53,9 @@ if TYPE_CHECKING:
DeepEPv2CombineInput,
DeepEPv2DispatchOutput,
)
from sglang.srt.layers.moe.token_dispatcher.flashinfer import (
FlashinferDispatchOutput,
)
from sglang.srt.layers.moe.token_dispatcher.standard import (
StandardCombineInput,
StandardDispatchOutput,
@@ -161,6 +164,14 @@ def _should_use_masked_standard_layout(
quant_info: DeepGemmMoeQuantInfo,
hidden_states: torch.Tensor,
) -> bool:
# Preserve the Oakhaven WideEP escape hatch while adopting upstream's
# memory-budget-based auto policy. CUDA graph capture remains masked.
if (
envs.SGLANG_OPT_DG_COMPACT_EAGER.get()
and not get_flags().capture.disable_dispose_tensor
):
return False
mode = envs.SGLANG_DEEPGEMM_STANDARD_LAYOUT.get().lower()
if mode not in ("auto", "masked", "compact"):
raise ValueError(
@@ -306,11 +317,24 @@ class DeepGemmRunnerCore(MoeRunnerCore):
hidden_states_dtype = running_state["hidden_states_dtype"]
hidden_states_shape = running_state["hidden_states_shape"]
m_indices = runner_input.m_indices
trace_deepep_v2_contig = (
envs.SGLANG_DEEPEP_V2_TRACE_CONTIG.get()
and running_state.get("deepep_v2_expanded", False)
)
N = quant_info.w13_weight.size(1)
K = hidden_states_shape[1]
scale_block_size = 128
if all_tokens == 0:
if trace_deepep_v2_contig:
logger.warning("DeepEP v2 expanded contig runner empty return")
dispose_tensor(hidden_states)
dispose_tensor(hidden_states_scale)
return torch.empty(
(0, K), device=hidden_states_device, dtype=torch.bfloat16
)
recipe_a, recipe_b = (
((1, 128), (1, 32)) if quant_info.is_fp4_experts else (None, None)
)
@@ -340,6 +364,9 @@ class DeepGemmRunnerCore(MoeRunnerCore):
recipe_a=recipe_a,
recipe_b=recipe_b,
)
if trace_deepep_v2_contig:
torch.cuda.synchronize()
logger.warning("DeepEP v2 expanded contig gateup GEMM returned")
dispose_tensor(hidden_states)
dispose_tensor(hidden_states_scale)
@@ -400,6 +427,9 @@ class DeepGemmRunnerCore(MoeRunnerCore):
del down_input
elif self.use_swizzle:
swiglu_limit_arg: Optional[float] = self.swiglu_limit
use_contig_swizzle = self.use_swizzle and not running_state.get(
"deepep_v2_disable_contig_swizzle", False
)
down_input_fp8 = torch.empty(
(all_tokens, N // 2),
@@ -422,7 +452,7 @@ class DeepGemmRunnerCore(MoeRunnerCore):
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
transposed=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
swiglu_limit=swiglu_limit_arg,
swizzle=self.use_swizzle,
swizzle=use_contig_swizzle,
)
del gateup_output
else:
@@ -454,6 +484,9 @@ class DeepGemmRunnerCore(MoeRunnerCore):
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
)
del down_input
if trace_deepep_v2_contig:
torch.cuda.synchronize()
logger.warning("DeepEP v2 expanded contig activation returned")
# Allocate the MoE output in the NCCL symmetric memory pool when symmetric
# allocation is required, so the downstream all-reduce takes the low-latency
@@ -478,6 +511,9 @@ class DeepGemmRunnerCore(MoeRunnerCore):
recipe_a=recipe_a,
recipe_b=recipe_b,
)
if trace_deepep_v2_contig:
torch.cuda.synchronize()
logger.warning("DeepEP v2 expanded contig down GEMM returned")
return down_output
@@ -568,6 +604,22 @@ class DeepGemmRunnerCore(MoeRunnerCore):
w2_scale = quant_info.w2_scale
hidden_states_device = running_state["hidden_states_device"]
trace_deepep_v2_masked = envs.SGLANG_DEEPEP_V2_TRACE_MASKED.get()
if trace_deepep_v2_masked:
logger.warning(
"DeepEP v2 masked runner enter: hidden=%s hidden_stride=%s "
"scale=%s scale_stride=%s masked_m=%s expected_m=%s",
tuple(hidden_states.shape),
hidden_states.stride(),
(
None
if hidden_states_scale is None
else tuple(hidden_states_scale.shape)
),
None if hidden_states_scale is None else hidden_states_scale.stride(),
masked_m.detach().cpu().tolist(),
expected_m,
)
use_mxfp8 = quant_info.use_mxfp8
scale_block_size = quant_info.block_shape[1] if quant_info.block_shape else 128
@@ -632,6 +684,9 @@ class DeepGemmRunnerCore(MoeRunnerCore):
recipe_a=recipe_a,
recipe_b=recipe_b,
)
if trace_deepep_v2_masked:
torch.cuda.synchronize()
logger.warning("DeepEP v2 masked runner gateup GEMM returned")
dispose_tensor(hidden_states)
dispose_tensor(hidden_states_scale)
@@ -672,6 +727,9 @@ class DeepGemmRunnerCore(MoeRunnerCore):
gemm1_clamp_limit=self.config.gemm1_clamp_limit,
num_real_tokens=num_real_tokens,
)
if trace_deepep_v2_masked:
torch.cuda.synchronize()
logger.warning("DeepEP v2 masked runner activation returned")
del gateup_output
# Down activation is quantised locally at scale_block_size (never DeepEP-LL),
@@ -730,6 +788,9 @@ class DeepGemmRunnerCore(MoeRunnerCore):
recipe_b=recipe_b,
**gemm_overlap_args_dict,
)
if trace_deepep_v2_masked:
torch.cuda.synchronize()
logger.warning("DeepEP v2 masked runner down GEMM returned")
meta_overlap_args = running_state.get("meta_overlap_args", None)
# Returns (block_m, threshold) only with down-gemm overlap, else None;
# meta_overlap_args may be set without overlap, so guard the unpack.
@@ -818,6 +879,7 @@ def pre_permute_standard_to_deep_gemm(
quant_info: DeepGemmMoeQuantInfo,
runner_config: MoeRunnerConfig,
running_state: dict,
expert_start: int = 0,
) -> DeepGemmRunnerInput:
from sglang.kernels.ops.moe.ep_moe_kernels import (
ep_scatter,
@@ -853,6 +915,7 @@ def pre_permute_standard_to_deep_gemm(
quant_info.block_shape,
output_dtype=output_dtype,
use_mxfp8=quant_info.use_mxfp8,
expert_start=expert_start,
)
)
# Use the global expert count because expected_m is a tuning hint, not
@@ -894,7 +957,7 @@ def pre_permute_standard_to_deep_gemm(
all_tokens = _get_compact_all_tokens(num_assignments, num_experts, block_e)
tokens_per_expert, unused_masked_dst = fused_moe_dispatch_index(
topk_ids, num_experts, 1
topk_ids, num_experts, 1, expert_start=expert_start
)
dispose_tensor(unused_masked_dst)
valid_tokens_per_expert = tokens_per_expert
@@ -974,6 +1037,7 @@ def pre_permute_standard_to_deep_gemm(
src2dst,
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
quant_block_size=(quant_info.block_shape[1] if quant_info.block_shape else 128),
expert_start=expert_start,
)
if packed_input_source is not hidden_states:
dispose_tensor(packed_input_source)
@@ -1003,6 +1067,49 @@ def pre_permute_standard_to_deep_gemm(
)
@register_pre_permute("flashinfer", "deep_gemm")
def pre_permute_flashinfer_to_deep_gemm(
dispatch_output: FlashinferDispatchOutput,
quant_info: DeepGemmMoeQuantInfo,
runner_config: MoeRunnerConfig,
running_state: dict,
) -> DeepGemmRunnerInput:
"""Feed one-sided A2A output into DeepGEMM with fused expert remapping."""
from sglang.srt.layers.moe.token_dispatcher.standard import StandardDispatchOutput
from sglang.srt.runtime_context import get_parallel
if dispatch_output.hidden_states.dtype != torch.bfloat16:
raise TypeError(
"FlashInfer A2A + DeepGEMM requires a BF16 dispatch payload, got "
f"{dispatch_output.hidden_states.dtype}."
)
if dispatch_output.hidden_states_scale is not None:
raise ValueError(
"FlashInfer A2A + DeepGEMM expects unquantized BF16 dispatch; "
"hidden_states_scale must be None."
)
if dispatch_output.topk_output.topk_ids.dtype != torch.int32:
raise TypeError(
"FlashInfer A2A expert IDs must be int32 before DeepGEMM, got "
f"{dispatch_output.topk_output.topk_ids.dtype}."
)
standard_output = StandardDispatchOutput(
hidden_states=dispatch_output.hidden_states,
hidden_states_scale=None,
topk_output=dispatch_output.topk_output,
)
expert_start = get_parallel().moe_ep_rank * runner_config.num_local_experts
return pre_permute_standard_to_deep_gemm(
standard_output,
quant_info,
runner_config,
running_state,
expert_start=expert_start,
)
@register_post_permute("deep_gemm", "standard")
def post_permute_deep_gemm_to_standard(
runner_output: DeepGemmRunnerOutput,
@@ -1047,6 +1154,30 @@ def post_permute_deep_gemm_to_standard(
)
@register_post_permute("deep_gemm", "flashinfer")
def post_permute_deep_gemm_to_flashinfer(
runner_output: DeepGemmRunnerOutput,
quant_info: DeepGemmMoeQuantInfo,
runner_config: MoeRunnerConfig,
running_state: dict,
):
"""Reuse DeepGEMM's weighted post-permute and hand BF16 to A2A combine."""
from sglang.srt.layers.moe.token_dispatcher.flashinfer import (
FlashinferCombineInput,
)
standard_input = post_permute_deep_gemm_to_standard(
runner_output, quant_info, runner_config, running_state
)
if standard_input.hidden_states.dtype != torch.bfloat16:
raise TypeError(
"FlashInfer A2A + DeepGEMM combine payload must be BF16, got "
f"{standard_input.hidden_states.dtype}."
)
return FlashinferCombineInput(hidden_states=standard_input.hidden_states)
@register_pre_permute("deepep_ll", "deep_gemm")
def pre_permute_deepep_ll_to_deep_gemm(
dispatch_output: DeepEPLLDispatchOutput,
@@ -496,10 +496,10 @@ def fused_experts_none_to_flashinfer_cutedsl_fp4(
@register_fused_func("flashinfer", "flashinfer_cutedsl")
def fused_experts_flashinfer_to_flashinfer_cutedsl_fp4(
dispatch_output: FlashinferDispatchOutput,
dispatch_output: FlashinferDispatchOutput | StandardDispatchOutput,
quant_info: CuteDslFp4MoeQuantInfo,
runner_config: MoeRunnerConfig,
) -> FlashinferCombineInput:
) -> FlashinferCombineInput | StandardCombineInput:
"""CuteDSL fused func for flashinfer alltoall dispatcher.
Two cases depending on whether the dispatcher did FP4 quantization:
@@ -509,6 +509,10 @@ def fused_experts_flashinfer_to_flashinfer_cutedsl_fp4(
from sglang.srt.layers.moe.token_dispatcher.flashinfer import (
FlashinferCombineInput,
)
from sglang.srt.layers.moe.token_dispatcher.standard import (
StandardCombineInput,
StandardDispatchOutput,
)
from sglang.srt.layers.moe.topk import TopKOutputChecker
from sglang.srt.layers.quantization.fp4_utils import fp4_quantize
@@ -580,6 +584,9 @@ def fused_experts_flashinfer_to_flashinfer_cutedsl_fp4(
# Note: output contains routed expert results; shared_expert is handled separately
if isinstance(dispatch_output, StandardDispatchOutput):
return StandardCombineInput(hidden_states=output)
# Write into pre-allocated workspace buffer if available
if dispatch_output.moe_output is not None:
dispatch_output.moe_output.copy_(output)
@@ -92,6 +92,41 @@ def finalize_flashinfer_trtllm_deferred_output(
)
def _make_deferred_finalize_output(
result,
*,
top_k: int,
) -> FlashInferTrtllmDeferredFinalizeOutput:
"""Validate and adapt FlashInfer's ``do_finalize=False`` output ABI."""
gemm2_out, expert_weights, expanded_idx_to_permuted_idx = result[:3]
# Some FlashInfer versions size this buffer from routing_logits dtype while
# writing BF16 weights into it. Reinterpret only the live BF16 prefix.
if expert_weights.dtype == torch.float32:
n, k = expert_weights.shape
expert_weights = expert_weights.view(torch.bfloat16).view(-1, k)[:n]
if expert_weights.dtype != torch.bfloat16:
raise RuntimeError(
"FlashInfer deferred finalize must return BF16 expert weights, got "
f"{expert_weights.dtype}"
)
if gemm2_out.dtype != torch.bfloat16:
raise RuntimeError(
"FlashInfer deferred finalize must return BF16 GEMM2 output, got "
f"{gemm2_out.dtype}"
)
if expanded_idx_to_permuted_idx.dtype != torch.int32:
raise RuntimeError(
"FlashInfer deferred finalize must return Int32 permuted indices, got "
f"{expanded_idx_to_permuted_idx.dtype}"
)
return FlashInferTrtllmDeferredFinalizeOutput(
gemm2_out=gemm2_out,
expert_weights=expert_weights,
expanded_idx_to_permuted_idx=expanded_idx_to_permuted_idx,
top_k=top_k,
)
def round_up_to_multiple(x: int, m: int) -> int:
"""Round up *x* to the nearest multiple of *m*."""
return (x + m - 1) // m * m
@@ -714,6 +749,16 @@ def fused_experts_none_to_flashinfer_trtllm_fp8(
else Fp8QuantizationType.DeepSeekFp8
)
use_shuffled_weight = quant_info.use_mxfp8
defer_finalize = _deferred_finalize_enabled.get()
if defer_finalize and (
not quant_info.block_quant
or use_routed_topk
or not TopKOutputChecker.format_is_bypassed(topk_output)
):
raise RuntimeError(
"FP8 deferred finalize requires block quantization, the logits-based "
"FlashInfer TRTLLM backend, and bypassed TopK"
)
if quant_info.block_quant:
assert quant_info.weight_block_k is not None
@@ -736,16 +781,19 @@ def fused_experts_none_to_flashinfer_trtllm_fp8(
)
a_sf_t = a_sf.t()
# Allocate output inside symmetric memory context
with use_symmetric_memory(
get_tp_group(), disabled=not is_allocation_symmetric()
):
symm_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
symm_output = None
if not defer_finalize:
# The deferred path returns FlashInfer's permuted/padded GEMM2
# materialization and must not allocate the ordinary final output.
with use_symmetric_memory(
get_tp_group(), disabled=not is_allocation_symmetric()
):
symm_output = torch.empty(
hidden_states.shape[0],
hidden_states.shape[1],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
# Move kernel call outside context manager to avoid graph breaks
# during torch.compile for piecewise cuda graph.
@@ -791,10 +839,10 @@ def fused_experts_none_to_flashinfer_trtllm_fp8(
fp8_quantization_type=int(fp8_quantization_type),
activation_type=quant_info.activation_type,
)
output = cast(torch.Tensor, symm_output)
else:
assert TopKOutputChecker.format_is_bypassed(topk_output)
trtllm_fp8_block_scale_moe_out_wrapper(
common_kwargs = dict(
routing_logits=router_logits,
routing_bias=correction_bias,
hidden_states=a_q,
@@ -806,7 +854,6 @@ def fused_experts_none_to_flashinfer_trtllm_fp8(
gemm1_clamp_limit=quant_info.gemm1_clamp_limit,
gemm2_weights=quant_info.w2_weight,
gemm2_weights_scale=quant_info.w2_weight_scale_inv,
output=symm_output,
num_experts=quant_info.global_num_experts,
top_k=topk_config.top_k,
n_group=topk_config.num_expert_group,
@@ -822,10 +869,34 @@ def fused_experts_none_to_flashinfer_trtllm_fp8(
routing_method_type=routing_method_type,
use_shuffled_weight=use_shuffled_weight,
tune_max_num_tokens=next_power_of_2(a_q.shape[0]),
fp8_quantization_type=int(fp8_quantization_type),
activation_type=quant_info.activation_type,
)
output = symm_output
if defer_finalize:
from flashinfer.fused_moe import trtllm_fp8_block_scale_moe
deferred_kwargs = dict(
**common_kwargs,
do_finalize=False,
fp8_quantization_type=fp8_quantization_type,
enable_pdl=a_q.shape[0] <= _TRTLLM_MOE_PDL_MAX_TOKENS,
)
if quant_info.activation_type is not None:
from flashinfer.fused_moe.core import ActivationType
deferred_kwargs["activation_type"] = ActivationType(
quant_info.activation_type
)
output = _make_deferred_finalize_output(
trtllm_fp8_block_scale_moe(**deferred_kwargs),
top_k=topk_config.top_k,
)
else:
trtllm_fp8_block_scale_moe_out_wrapper(
**common_kwargs,
output=cast(torch.Tensor, symm_output),
fp8_quantization_type=int(fp8_quantization_type),
activation_type=quant_info.activation_type,
)
output = cast(torch.Tensor, symm_output)
else:
assert TopKOutputChecker.format_is_bypassed(topk_output)
assert quant_info.w13_input_scale is not None
@@ -1344,23 +1415,26 @@ def fused_experts_none_to_flashinfer_trtllm_routed(
)
@register_fused_func("flashinfer", "flashinfer_trtllm")
@register_fused_func("flashinfer", "flashinfer_trtllm_routed")
def fused_experts_flashinfer_to_flashinfer_trtllm_routed(
dispatch_output: FlashinferDispatchOutput,
def fused_experts_flashinfer_to_flashinfer_trtllm(
dispatch_output: FlashinferDispatchOutput | StandardDispatchOutput,
quant_info: MoeQuantInfo,
runner_config: MoeRunnerConfig,
) -> FlashinferCombineInput:
"""Fused function for flashinfer A2A + flashinfer_trtllm_routed runner.
) -> FlashinferCombineInput | StandardCombineInput:
"""Fused function for FlashInfer A2A + TRT-LLM Gen MoE.
FlashinferDispatchOutput and StandardDispatchOutput share the same field
layout (hidden_states, hidden_states_scale, topk_output), so the existing
FP8/FP4/BF16 implementations work unchanged. We wrap the returned
StandardCombineInput into a FlashinferCombineInput for the FlashinferDispatcher
combine path.
Both one-sided decode and AG+RS prefill materialize routing IDs and weights,
so the regular and explicitly-routed backend names enter TRT-LLM's routed
kernel. The dispatch formats share the fields consumed by the implementation;
only the combine wrapper differs.
"""
from sglang.srt.layers.moe.token_dispatcher.flashinfer import (
FlashinferCombineInput,
)
from sglang.srt.layers.moe.token_dispatcher.standard import (
StandardDispatchOutput,
)
if isinstance(quant_info, FlashInferTrtllmFp4MoeQuantInfo):
result = fused_experts_none_to_flashinfer_trtllm_fp4(
@@ -1370,6 +1444,16 @@ def fused_experts_flashinfer_to_flashinfer_trtllm_routed(
use_routed_topk=True,
)
elif isinstance(quant_info, FlashInferTrtllmFp8MoeQuantInfo):
if dispatch_output.hidden_states.dtype != torch.bfloat16:
raise TypeError(
"FlashInfer A2A + TRT-LLM Gen FP8 MoE requires a BF16 "
f"dispatch payload, got {dispatch_output.hidden_states.dtype}."
)
if dispatch_output.hidden_states_scale is not None:
raise ValueError(
"FlashInfer A2A + TRT-LLM Gen FP8 MoE quantizes locally; "
"the BF16 dispatch payload must not carry activation scales."
)
result = fused_experts_none_to_flashinfer_trtllm_fp8(
dispatch_output,
quant_info,
@@ -1385,8 +1469,18 @@ def fused_experts_flashinfer_to_flashinfer_trtllm_routed(
)
else:
raise TypeError(
f"Unexpected quant_info type for flashinfer a2a + flashinfer_trtllm_routed: {type(quant_info)}"
f"Unexpected quant_info type for flashinfer a2a + flashinfer_trtllm: {type(quant_info)}"
)
if (
isinstance(quant_info, FlashInferTrtllmFp8MoeQuantInfo)
and result.hidden_states.dtype != torch.bfloat16
):
raise TypeError(
"FlashInfer A2A + TRT-LLM Gen FP8 MoE must return a BF16 combine "
f"payload, got {result.hidden_states.dtype}."
)
if isinstance(dispatch_output, StandardDispatchOutput):
return result
return FlashinferCombineInput(hidden_states=result.hidden_states)
@@ -0,0 +1,347 @@
"""Qwen3.5 integration for FlashInfer MNNVL CuTe DSL AllReduce fusion."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Optional
import torch
from sglang.srt.arg_groups.overrides import resolving_view
from sglang.srt.layers.communicator import (
CommunicateWithAllReduceAndLayerNormFn,
LayerCommunicator,
ScatterMode,
get_attn_tp_context,
)
from sglang.srt.layers.dp_attention import is_dp_attention_enabled
from sglang.srt.layers.moe import get_moe_a2a_backend
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, ForwardMode
from sglang.srt.runtime_context import get_exec, get_parallel
logger = logging.getLogger(__name__)
def is_supported_forward_mode(forward_mode: ForwardMode) -> bool:
return forward_mode in (
ForwardMode.DECODE,
ForwardMode.EXTEND,
ForwardMode.TARGET_VERIFY,
)
def resolve_max_m(model_runner) -> int:
"""Use framework token bounds as the workspace-capacity source of truth."""
server_args = resolving_view(model_runner.server_args)
decode_config = server_args.cuda_graph_config.decode
prefill_config = server_args.cuda_graph_config.prefill
candidates = [
server_args.cutedsl_moe_max_num_tokens(),
model_runner.max_running_requests,
decode_config.max_bs,
prefill_config.max_bs,
*(decode_config.bs or []),
*(prefill_config.bs or []),
]
positive = [
int(value) for value in candidates if value is not None and int(value) > 0
]
if not positive:
raise RuntimeError("framework reported no positive fusion workspace M bound")
return max(positive)
@dataclass(frozen=True)
class Qwen35MoeFinalizeHandoff:
"""Unfinalized routed output plus the separately gated shared contribution."""
routed_output: torch.Tensor
expert_weights: torch.Tensor
permuted_indices: torch.Tensor
gated_shared_output: torch.Tensor
m: int
@classmethod
def from_flashinfer(
cls,
deferred_output,
*,
gated_shared_output: torch.Tensor,
m: int,
) -> Qwen35MoeFinalizeHandoff:
top_k = int(deferred_output.top_k)
return cls(
routed_output=deferred_output.gemm2_out.view(
-1, deferred_output.gemm2_out.shape[-1]
),
expert_weights=deferred_output.expert_weights.view(-1, top_k)[:m],
permuted_indices=deferred_output.expanded_idx_to_permuted_idx.view(
-1, top_k
)[:m],
gated_shared_output=gated_shared_output,
m=int(m),
)
class Qwen35FlashInferFusionService:
"""A lightweight model handle for the process-local FlashInfer workspace."""
def __init__(
self,
*,
hidden_size: int,
top_k: int,
rms_epsilon: float,
) -> None:
self.hidden_size = int(hidden_size)
self.top_k = int(top_k)
self.rms_epsilon = float(rms_epsilon)
self.max_m: int | None = None
self._workspace = None
@property
def is_prepared(self) -> bool:
return self._workspace is not None
def prepare(self, *, max_m: int) -> None:
if self._workspace is not None:
assert self.max_m is not None
if int(max_m) > self.max_m:
raise RuntimeError(
f"fusion workspace is already prepared for M_max={self.max_m}; "
f"refusing M_max={max_m}"
)
return
from sglang.srt.layers.flashinfer_mnnvl_cutedsl import (
get_flashinfer_mnnvl_cutedsl_ar_fusion,
)
workspace = get_flashinfer_mnnvl_cutedsl_ar_fusion(
hidden_size=self.hidden_size,
top_k=self.top_k,
max_m=int(max_m),
rms_epsilon=self.rms_epsilon,
# GemmaRMSNorm.gemma_weight is already checkpoint weight + 1.
weight_bias=0.0,
)
self._workspace = workspace
self.max_m = workspace.max_m
def supports(self, m: int) -> bool:
if self._workspace is None or self.max_m is None:
return False
return 1 <= int(m) <= self.max_m and self._workspace.supports(m)
def finalize(
self,
handoff: Qwen35MoeFinalizeHandoff,
residual: torch.Tensor,
gamma: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
self._validate_finalize(handoff, residual, gamma)
assert self._workspace is not None
return self._workspace.moe_finalize_all_reduce_rms_norm(
routed_output=handoff.routed_output,
expert_weights=handoff.expert_weights,
permuted_indices=handoff.permuted_indices,
gated_shared_output=handoff.gated_shared_output,
residual=residual,
gamma=gamma,
)
def all_reduce_residual_rms_norm(
self,
local_contribution: torch.Tensor,
residual: torch.Tensor,
gamma: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
self._validate_matrix(local_contribution, "local_contribution")
self._validate_matrix(residual, "residual", m=local_contribution.shape[0])
self._validate_gamma(gamma)
if not self.supports(local_contribution.shape[0]):
raise ValueError(f"unsupported M={local_contribution.shape[0]}")
assert self._workspace is not None
return self._workspace.all_reduce_residual_rms_norm(
local_contribution=local_contribution,
residual=residual,
gamma=gamma,
)
def _validate_finalize(
self,
handoff: Qwen35MoeFinalizeHandoff,
residual: torch.Tensor,
gamma: torch.Tensor,
) -> None:
if not self.supports(handoff.m):
raise ValueError(f"unsupported M={handoff.m}")
self._validate_matrix(handoff.routed_output, "routed_output", exact_m=False)
expected_metadata = (handoff.m, self.top_k)
if tuple(handoff.expert_weights.shape) != expected_metadata:
raise ValueError("expert_weights must have shape [M, top_k]")
if tuple(handoff.permuted_indices.shape) != expected_metadata:
raise ValueError("permuted_indices must have shape [M, top_k]")
if handoff.expert_weights.dtype != torch.bfloat16:
raise ValueError("expert_weights must be BF16")
if handoff.permuted_indices.dtype != torch.int32:
raise ValueError("permuted_indices must be Int32")
self._validate_matrix(
handoff.gated_shared_output, "gated_shared_output", m=handoff.m
)
self._validate_matrix(residual, "residual", m=handoff.m)
self._validate_gamma(gamma)
def _validate_matrix(
self,
tensor: torch.Tensor,
name: str,
*,
m: int | None = None,
exact_m: bool = True,
) -> None:
if tensor.ndim != 2 or tensor.shape[1] != self.hidden_size:
raise ValueError(f"{name} must have shape [M, hidden_size]")
if m is not None and exact_m and tensor.shape[0] != int(m):
raise ValueError(f"{name} has the wrong M dimension")
if tensor.dtype != torch.bfloat16 or not tensor.is_contiguous():
raise ValueError(f"{name} must be contiguous BF16")
def _validate_gamma(self, gamma: torch.Tensor) -> None:
if (
tuple(gamma.shape) != (self.hidden_size,)
or gamma.dtype != torch.bfloat16
or not gamma.is_contiguous()
):
raise ValueError("gamma must be contiguous BF16 [hidden_size]")
class Qwen35FlashInferLayerCommunicator(LayerCommunicator):
"""Qwen-only hooks; generic LayerCommunicator remains backend agnostic."""
fusion_service: Qwen35FlashInferFusionService | None = None
def prepare_attn(
self,
hidden_states,
residual,
forward_batch,
quant_format: str = "",
post_residual_addition=None,
):
if isinstance(hidden_states, Qwen35MoeFinalizeHandoff):
if not self.should_use_finalize(forward_batch, hidden_states.m):
raise RuntimeError("received deferred MoE output on an ineligible path")
if residual is None:
raise RuntimeError("deferred MoE finalize requires residual input")
if not hasattr(self.input_layernorm, "gemma_weight"):
raise RuntimeError("deferred Qwen finalize requires GemmaRMSNorm")
if post_residual_addition is not None:
residual = residual + post_residual_addition
assert self.fusion_service is not None
return self.fusion_service.finalize(
hidden_states, residual, self.input_layernorm.gemma_weight
)
return super().prepare_attn(
hidden_states,
residual,
forward_batch,
quant_format=quant_format,
post_residual_addition=post_residual_addition,
)
def prepare_mlp(
self,
hidden_states: torch.Tensor,
residual: torch.Tensor,
forward_batch: ForwardBatch,
cache=None,
):
if cache is not None:
self._context.cache = cache
if self.should_use_all_reduce_rms_norm(
forward_batch, int(hidden_states.shape[0]), residual
):
assert self.fusion_service is not None and residual is not None
return self.fusion_service.all_reduce_residual_rms_norm(
hidden_states,
residual,
self.post_attention_layernorm.gemma_weight,
)
return super().prepare_mlp(hidden_states, residual, forward_batch, cache=cache)
def should_use_all_reduce_rms_norm(
self,
forward_batch: ForwardBatch,
m: int,
residual: Optional[torch.Tensor],
) -> bool:
communicate_fn = self._communicate_with_all_reduce_and_layer_norm_fn
norm_fn = getattr(communicate_fn, "func", communicate_fn)
residual_input_mode = getattr(communicate_fn, "keywords", {}).get(
"residual_input_mode"
)
parallel = get_parallel()
return (
self._common_eligible(forward_batch, m)
and residual is not None
and hasattr(self.post_attention_layernorm, "gemma_weight")
and norm_fn
is CommunicateWithAllReduceAndLayerNormFn._gather_hidden_states_and_residual
and residual_input_mode is ScatterMode.TP_ATTN_FULL
and self._context.attn_dp_size == 1
and parallel.attn_tp_size == parallel.tp_size
and not get_exec().comm.enable_quant_communications
)
def should_use_finalize(self, forward_batch: ForwardBatch, m: int) -> bool:
parallel = get_parallel()
return (
self._common_eligible(forward_batch, m)
and self.layer_scatter_modes.mlp_mode is not ScatterMode.SCATTERED
and parallel.moe_ep_size == 1
)
def _common_eligible(self, forward_batch: ForwardBatch, m: int) -> bool:
parallel = get_parallel()
return bool(
self.fusion_service is not None
and self.fusion_service.is_prepared
and is_supported_forward_mode(forward_batch.forward_mode)
and self.fusion_service.supports(m)
and not is_dp_attention_enabled()
and parallel.attn_cp_size == 1
and not get_attn_tp_context().input_scattered
and get_moe_a2a_backend().is_none()
and self._context.tp_size > 1
)
def should_fuse_mlp_allreduce_with_next_layer(
self, forward_batch: ForwardBatch
) -> bool:
m = (
int(forward_batch.input_ids.shape[0])
if getattr(forward_batch, "input_ids", None) is not None
else 0
)
if self.should_use_finalize(forward_batch, m):
# The Qwen model consumes the final layer's handoff with its final
# GemmaRMSNorm, so this is intentionally also true for that layer.
return True
return super().should_fuse_mlp_allreduce_with_next_layer(forward_batch)
def prepare_qwen35_flashinfer_fusion(model, model_runner) -> None:
service = getattr(model, "flashinfer_mnnvl_cutedsl_fusion", None)
if service is None:
return
if model_runner.server_args.enable_pdmux:
raise RuntimeError(
"FlashInfer MNNVL CuTe DSL fusion does not support concurrent PDMux "
"streams sharing one mutable workspace"
)
service.prepare(max_m=resolve_max_m(model_runner))
logger.info(
"Prepared Qwen3.5 FlashInfer MNNVL CuTe DSL fusion workspace for M_max=%d",
service.max_m,
)
@@ -9,6 +9,7 @@ from sglang.kernels.kernel_api_logging import debug_kernel_api
from sglang.srt.environ import envs
from sglang.srt.layers.dp_attention import (
get_dp_global_num_tokens,
get_is_extend_in_batch,
is_dp_attention_enabled,
)
from sglang.srt.layers.moe.token_dispatcher import (
@@ -21,13 +22,18 @@ from sglang.srt.layers.moe.token_dispatcher import (
from sglang.srt.layers.moe.token_dispatcher.flashinfer_utils import (
TorchDistributedCommBackend,
)
from sglang.srt.layers.moe.token_dispatcher.standard import (
StandardCombineInput,
StandardDispatcher,
StandardDispatchOutput,
)
from sglang.srt.layers.moe.topk import (
StandardTopKOutput,
TopKOutput,
TopKOutputChecker,
)
from sglang.srt.layers.moe.utils import get_moe_runner_backend
from sglang.srt.runtime_context import get_schedule, get_spec
from sglang.srt.runtime_context import get_flags, get_parallel, get_schedule, get_spec
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
try:
@@ -46,6 +52,36 @@ logger = logging.getLogger(__name__)
MOE_NVFP4_DISPATCH = envs.SGLANG_MOE_NVFP4_DISPATCH.get()
# FlashInfer keys MNNVL allocations by workspace size; aligned tail padding gives
# concurrently live paths distinct persistent workspaces without extra token work.
_WORKSPACE_NAMESPACE_ALIGNMENT = 128
def _max_tokens_per_scattered_source(
dp_global_num_tokens: list[int], attn_tp_size: int
) -> int:
assert attn_tp_size > 0
max_dp_tokens = max(dp_global_num_tokens)
return (max_dp_tokens + attn_tp_size - 1) // attn_tp_size
def _scattered_source_token_counts(
dp_global_num_tokens: list[int], attn_tp_size: int
) -> list[int]:
assert attn_tp_size > 0
counts = []
for num_tokens in dp_global_num_tokens:
base, remainder = divmod(num_tokens, attn_tp_size)
counts.extend(
base + int(attn_tp_rank < remainder) for attn_tp_rank in range(attn_tp_size)
)
return counts
def _workspace_size_for_namespace(workspace_size: int, *, speculative: bool) -> int:
slot = int(speculative)
return workspace_size + slot * _WORKSPACE_NAMESPACE_ALIGNMENT
class FlashinferDispatchOutput(NamedTuple):
"""Flashinfer EP dispatch output."""
@@ -88,6 +124,7 @@ class FlashinferDispatcher(BaseDispatcher):
num_local_experts: int = None, # Unused
hidden_size: int = None,
params_dtype: torch.dtype = None, # Unused
moe_runner_config=None,
):
super().__init__()
if not use_flashinfer:
@@ -102,13 +139,28 @@ class FlashinferDispatcher(BaseDispatcher):
self.hidden_size = hidden_size
self.num_experts = num_experts
self.num_local_experts = num_local_experts
runner_backend = get_moe_runner_backend()
self.invalid_token_expert_id = (
-1
if get_moe_runner_backend().is_flashinfer_trtllm_routed()
if (
runner_backend.is_deep_gemm()
or runner_backend.is_flashinfer_trtllm()
or runner_backend.is_flashinfer_trtllm_routed()
)
else self.num_experts
)
# TODO: Can other moe runners use payload_in_workspace too?
self.payload_in_workspace = get_moe_runner_backend().is_flashinfer_cutlass()
if moe_runner_config is None:
from sglang.srt.layers.moe.moe_runner.base import MoeRunnerConfig
moe_runner_config = MoeRunnerConfig(
num_experts=num_experts,
num_local_experts=num_local_experts,
hidden_size=hidden_size,
top_k=router_topk,
)
self.prefill_dispatcher = StandardDispatcher(moe_runner_config)
# FlashInfer sizes the workspace from the maximum dispatched tokens per
# EP rank. See FlashInfer's moe_a2a_get_workspace_size_per_rank(),
@@ -167,19 +219,106 @@ class FlashinferDispatcher(BaseDispatcher):
pp_size=1,
cp_size=1,
)
self.moe_a2a = MoeAlltoAll(
mapping=self.mapping,
max_num_tokens=self.max_num_tokens,
top_k=self.router_topk,
num_experts=self.num_experts,
workspace_size_per_rank=self.workspace_size,
mnnvl_config=MnnvlConfig(comm_backend=TorchDistributedCommBackend(group)),
mnnvl_config = MnnvlConfig(comm_backend=TorchDistributedCommBackend(group))
is_speculative_model = get_flags().moe.speculative_context
def make_moe_a2a() -> MoeAlltoAll:
# Target and draft decode graphs can coexist; prefill/mixed extend use
# AG+RS and do not lease MNNVL A2A workspaces.
workspace_size = _workspace_size_for_namespace(
self.workspace_size,
speculative=is_speculative_model,
)
return MoeAlltoAll(
mapping=self.mapping,
max_num_tokens=self.max_num_tokens,
top_k=self.router_topk,
num_experts=self.num_experts,
workspace_size_per_rank=workspace_size,
mnnvl_config=mnnvl_config,
)
self.moe_a2a = make_moe_a2a()
def set_quant_config(self, quant_config: dict) -> None:
super().set_quant_config(quant_config)
self.prefill_dispatcher.set_quant_config(quant_config)
def _dispatch_prefill_allgather(
self, hidden_states: torch.Tensor, topk_output: TopKOutput
) -> StandardDispatchOutput:
# Eager extend can overlap another stream, so use BF16 all-gatherv instead
# of reusing pure-decode A2A signal state across streams.
if hidden_states.dtype != torch.bfloat16:
raise TypeError(
"FlashInfer WideEP prefill AG requires BF16 hidden states, got "
f"{hidden_states.dtype}."
)
if TopKOutputChecker.format_is_bypassed(topk_output):
topk_output = topk_output.to_standard()
if not TopKOutputChecker.format_is_standard(topk_output):
raise TypeError(
"FlashInfer WideEP prefill AG requires materialized top-k "
f"routing, got {type(topk_output).__name__}."
)
dp_global = get_dp_global_num_tokens()
if dp_global is None:
source_sizes = [hidden_states.shape[0]] * self.ep_size
else:
source_sizes = _scattered_source_token_counts(
dp_global, get_parallel().attn_tp_size
)
if len(source_sizes) != self.ep_size:
raise RuntimeError(
"FlashInfer WideEP prefill AG source geometry does not match "
f"EP: len(source_sizes)={len(source_sizes)}, ep_size={self.ep_size}."
)
if source_sizes[self.ep_rank] != hidden_states.shape[0]:
raise RuntimeError(
"FlashInfer WideEP prefill AG local source geometry mismatch: "
f"source_sizes[{self.ep_rank}]={source_sizes[self.ep_rank]} != "
f"hidden_states.shape[0]={hidden_states.shape[0]}."
)
topk_ids = topk_output.topk_ids.to(torch.int32)
hidden_states, topk_ids, topk_weights = get_parallel().tp_group.all_gatherv(
[hidden_states, topk_ids, topk_output.topk_weights],
sizes=source_sizes,
)
self.prefill_source_sizes = source_sizes
return self.prefill_dispatcher.dispatch(
hidden_states,
StandardTopKOutput(topk_weights, topk_ids, topk_output.router_logits),
)
@debug_kernel_api
def dispatch(
self, hidden_states: torch.Tensor, topk_output: TopKOutput
) -> FlashinferDispatchOutput:
) -> FlashinferDispatchOutput | StandardDispatchOutput:
if get_is_extend_in_batch():
return self._dispatch_prefill_allgather(hidden_states, topk_output)
self.active_moe_a2a = self.moe_a2a
# Block-wise FP8 runners quantize before GEMM, so keep dispatch/combine BF16;
# FP4 retains its packed wire path keyed by input_global_scale.
runner_backend = get_moe_runner_backend()
weight_dtype = self.quant_config.get("weight_dtype")
uses_bf16_fp8_payload = weight_dtype in (
torch.float8_e4m3fn,
torch.float8_e5m2,
) and (
runner_backend.is_deep_gemm()
or runner_backend.is_flashinfer_trtllm()
or runner_backend.is_flashinfer_trtllm_routed()
)
if uses_bf16_fp8_payload and hidden_states.dtype != torch.bfloat16:
raise TypeError(
"FlashInfer A2A with an FP8 DeepGEMM/TRT-LLM Gen MoE runner "
"requires BF16 dispatch and combine payloads, but received "
f"{hidden_states.dtype}."
)
output_dtype = hidden_states.dtype
x = hidden_states
x_sf = None
@@ -218,16 +357,8 @@ class FlashinferDispatcher(BaseDispatcher):
# CUDA-graph *capture*; on *replay* dispatch() is not re-executed and the
# value baked at capture is reused. Two cases, both rank-invariant:
#
# Case 1 — max(dp_global): DP attention feeding EP. The scheduler
# all-gathers per-DP-rank token counts into dp_global (length dp_size,
# identical on every rank), which differ across ranks, so we must take
# the max. FlashInfer A2A forces require_mlp_tp_gather=True (see
# require_mlp_tp_gather()), so: eager reads the live list; capture sees
# [num_tokens] * dp_size (uniform capture bs) and bakes max() == the
# bucket; replay reuses that baked value and every rank replays the same
# bucket because the decode graph runner sizes it from the cross-rank
# max. Without this, per-rank buckets could diverge -> geometry mismatch
# -> illegal memory access (issue #30242).
# Each EP source owns ceil(max(dp_global) / attn_tp_size) tokens; the
# shared maximum keeps graph geometry rank-uniform (issue #30242).
#
# Case 2 — x.shape[0]: no per-rank DP list (dp_global absent or scalar).
# This is SP attention feeding EP (tokens are sequence-parallel scattered
@@ -237,7 +368,10 @@ class FlashinferDispatcher(BaseDispatcher):
dp_global = get_dp_global_num_tokens()
if dp_global is not None and len(dp_global) > 1:
# Case 1
self.runtime_max_tokens_per_rank = max(dp_global)
attn_tp_size = get_parallel().attn_tp_size
self.runtime_max_tokens_per_rank = _max_tokens_per_scattered_source(
dp_global, attn_tp_size
)
else:
# Case 2. Guard against the #30242 failure mode: DP attention must
# never land here with ep_size > 1, because there x.shape[0] differs
@@ -252,9 +386,20 @@ class FlashinferDispatcher(BaseDispatcher):
)
self.runtime_max_tokens_per_rank = x.shape[0]
# MoeAlltoAll does not resize its max_num_tokens workspace; reject larger
# runtime geometry here before it becomes an illegal memory access.
assert self.runtime_max_tokens_per_rank <= self.max_num_tokens, (
"FlashInfer A2A runtime token geometry exceeds its fixed workspace: "
f"runtime_max_tokens_per_rank={self.runtime_max_tokens_per_rank} > "
f"max_num_tokens={self.max_num_tokens}. Increase "
"SGLANG_FLASHINFER_NUM_MAX_DISPATCH_TOKENS_PER_RANK to cover the "
"largest mixed prefill and speculative-verify batch."
)
# The recv buffer reserves runtime_max_tokens_per_rank slots for THIS
# rank, so it must cover this rank's own tokens. This holds in both cases
# (Case 1: max(dp_global) >= the local count; Case 2: exactly x.shape[0]),
# (Case 1: ceil(max(dp_global) / attn_tp_size) covers every token-scatter
# shard; Case 2: exactly x.shape[0]),
# so a violation signals a sizing/plumbing bug (e.g. an un-adjusted spec
# count) rather than a benign case.
assert self.runtime_max_tokens_per_rank >= x.shape[0], (
@@ -268,7 +413,7 @@ class FlashinferDispatcher(BaseDispatcher):
# padding slots whose expert_id would otherwise route to a real expert
# and waste downstream MoE compute. Sanitizing the padding to a
# sentinel id is structural, not optional.
recv_tensors = self.moe_a2a.dispatch(
recv_tensors = self.active_moe_a2a.dispatch(
topk_ids,
payloads,
self.runtime_max_tokens_per_rank,
@@ -290,7 +435,7 @@ class FlashinferDispatcher(BaseDispatcher):
# Provide an output tensor to fused_moe so it writes directly to our buffer
moe_output = None
if self.payload_in_workspace:
moe_output = self.moe_a2a.get_combine_payload_tensor_in_workspace(
moe_output = self.active_moe_a2a.get_combine_payload_tensor_in_workspace(
self.runtime_max_tokens_per_rank, self.hidden_size, output_dtype
).view(-1, self.hidden_size)
return FlashinferDispatchOutput(
@@ -301,10 +446,40 @@ class FlashinferDispatcher(BaseDispatcher):
)
@debug_kernel_api
def combine(self, combine_input: FlashinferCombineInput) -> torch.Tensor:
def combine(
self, combine_input: FlashinferCombineInput | StandardCombineInput
) -> torch.Tensor:
hidden_states = combine_input.hidden_states
if combine_input.format == CombineInputFormat.STANDARD:
if hidden_states.dtype != torch.bfloat16:
raise TypeError(
"FlashInfer WideEP prefill RS requires BF16 expert output, "
f"got {hidden_states.dtype}."
)
source_sizes = self.prefill_source_sizes
hidden_states = get_parallel().tp_group.reduce_scatterv(
hidden_states, sizes=source_sizes
)
del self.prefill_source_sizes
return hidden_states
weight_dtype = self.quant_config.get("weight_dtype")
runner_backend = get_moe_runner_backend()
if (
weight_dtype in (torch.float8_e4m3fn, torch.float8_e5m2)
and (
runner_backend.is_deep_gemm()
or runner_backend.is_flashinfer_trtllm()
or runner_backend.is_flashinfer_trtllm_routed()
)
and hidden_states.dtype != torch.bfloat16
):
raise TypeError(
"FlashInfer A2A FP8 MoE combine payload must be BF16, but "
f"received {hidden_states.dtype}."
)
output_hidden_size = hidden_states.shape[-1]
hidden_states = self.moe_a2a.combine(
hidden_states = self.active_moe_a2a.combine(
hidden_states.view(
self.ep_size, self.runtime_max_tokens_per_rank, output_hidden_size
),
@@ -313,4 +488,5 @@ class FlashinferDispatcher(BaseDispatcher):
)
del self.runtime_max_tokens_per_rank
del self.active_moe_a2a
return hidden_states
+3
View File
@@ -704,14 +704,17 @@ def speculative_moe_a2a_backend_context():
moe = get_flags().moe
original_backend = moe.a2a_backend
original_disable_fp4_allgather = moe.disable_fp4_allgather
original_speculative_context = moe.speculative_context
try:
moe.a2a_backend = get_speculative_moe_a2a_backend()
# Disable FP4 allgather for spec decode since MTP layers are unquantized
moe.disable_fp4_allgather = True
moe.speculative_context = True
yield
finally:
moe.a2a_backend = original_backend
moe.disable_fp4_allgather = original_disable_fp4_allgather
moe.speculative_context = original_speculative_context
# The type of method in top-K routing, for use in torch custom op
+128 -21
View File
@@ -75,6 +75,7 @@ if _use_aiter:
class Bf16GemmBackend(Enum):
AUTO = "auto"
CUTEDSL = "cutedsl"
FLASHINFER_PR4266 = "flashinfer_pr4266"
GEMV = "gemv"
TORCH = "torch"
@@ -87,16 +88,77 @@ class Bf16GemmBackend(Enum):
def is_gemv(self) -> bool:
return self == Bf16GemmBackend.GEMV
def is_flashinfer_pr4266(self) -> bool:
return self == Bf16GemmBackend.FLASHINFER_PR4266
def is_optimized(self) -> bool:
return self.is_cutedsl() or self.is_flashinfer_pr4266()
_BF16_GEMM_BACKEND: Optional[Bf16GemmBackend] = None
_cutedsl_bf16_gemm = None
_use_cutedsl_bf16_gemm = None
_hopper_bf16_gemv = None
_use_hopper_bf16_gemv = None
_flashinfer_pr4266_splitk_tactic = None
_flashinfer_pr4266_run_splitk_dense = None
_flashinfer_pr4266_direct_default_tactic = None
_flashinfer_pr4266_prefer_direct = None
_flashinfer_pr4266_run_direct_dense = None
_enable_bf16_splitk_gemm = False
# GB300 TP16 tactics measured under CUDA graph replay with PDL and cold weights.
# Unlisted shapes, including M=64, retain the existing TGV/cuBLAS path.
_FLASHINFER_PR4266_TUNED_TACTICS = {
(1, 256, 8192): (64, 8, 4, 11),
(2, 256, 8192): (64, 8, 4, 11),
(4, 256, 8192): (64, 8, 4, 11),
(8, 256, 8192): (64, 8, 4, 11),
(16, 256, 8192): (64, 8, 4, 10),
(24, 256, 8192): (64, 8, 4, 11),
(32, 256, 8192): (64, 8, 4, 12),
(1, 512, 8192): (64, 8, 4, 11),
(2, 512, 8192): (64, 8, 4, 12),
(4, 512, 8192): (64, 8, 4, 10),
(8, 512, 8192): (64, 8, 4, 12),
(16, 512, 8192): (64, 8, 4, 12),
(24, 512, 8192): (64, 8, 4, 12),
(32, 512, 8192): (64, 16, 4, 9),
(1, 2304, 8192): (128, 8, 4, 6),
(2, 2304, 8192): (64, 8, 2, 12),
(4, 2304, 8192): (128, 8, 4, 6),
(8, 2304, 8192): (64, 8, 4, 10),
(16, 2304, 8192): (64, 16, 4, 9),
(24, 2304, 8192): (64, 32, 2, 9),
(32, 2304, 8192): (64, 32, 2, 9),
(1, 2560, 8192): (64, 8, 2, 10),
(2, 2560, 8192): (64, 8, 2, 10),
(4, 2560, 8192): (64, 8, 2, 10),
(8, 2560, 8192): (64, 8, 2, 10),
(16, 2560, 8192): (64, 16, 2, 11),
(24, 2560, 8192): (64, 32, 2, 9),
(32, 2560, 8192): (64, 32, 2, 9),
}
def use_flashinfer_pr4266_bf16_gemm(m: int, n: int, k: int) -> bool:
return (m, n, k) in _FLASHINFER_PR4266_TUNED_TACTICS
def should_enable_bf16_splitk_gemm(backend: Bf16GemmBackend) -> bool:
"""Return whether the optional Split-K path should be initialized."""
return backend.is_optimized() and envs.SGLANG_ENABLE_BF16_SPLITK_GEMM.get()
def initialize_bf16_gemm_config(server_args: ServerArgs) -> None:
global _BF16_GEMM_BACKEND, _cutedsl_bf16_gemm, _use_cutedsl_bf16_gemm
global _BF16_GEMM_BACKEND
global _cutedsl_bf16_gemm, _use_cutedsl_bf16_gemm
global _flashinfer_pr4266_splitk_tactic
global _flashinfer_pr4266_run_splitk_dense
global _flashinfer_pr4266_direct_default_tactic
global _flashinfer_pr4266_prefer_direct
global _flashinfer_pr4266_run_direct_dense
global _enable_bf16_splitk_gemm
from sglang.srt.utils import is_sm100_supported
@@ -122,14 +184,17 @@ def initialize_bf16_gemm_config(server_args: ServerArgs) -> None:
_hopper_bf16_gemv = hopper_bf16_gemv
_use_hopper_bf16_gemv = use_hopper_bf16_gemv
elif backend.is_cutedsl():
elif backend.is_optimized():
if get_exec().deterministic.enable_deterministic_inference:
raise ValueError(
"--bf16-gemm-backend cutedsl is batch-size dependent and cannot "
"be combined with --enable-deterministic-inference"
)
if not is_sm100_supported():
raise ValueError("--bf16-gemm-backend cutedsl requires an SM10x GPU")
raise ValueError(
f"--bf16-gemm-backend {backend.value} requires "
"SM100/SM103 (Blackwell)"
)
from sglang.kernels.ops.gemm.cutedsl_bf16_gemm import (
cutedsl_bf16_gemm,
@@ -139,6 +204,25 @@ def initialize_bf16_gemm_config(server_args: ServerArgs) -> None:
_cutedsl_bf16_gemm = cutedsl_bf16_gemm
_use_cutedsl_bf16_gemm = use_cutedsl_bf16_gemm
_enable_bf16_splitk_gemm = False
if should_enable_bf16_splitk_gemm(backend):
from sglang.kernels.ops.gemm.flashinfer_pr4266_dense_bf16_gemm_sm100_direct import (
default_tactic,
prefer_direct_bf16_gemm_sm100,
run_direct_dense,
)
from sglang.kernels.ops.gemm.flashinfer_pr4266_dense_bf16_gemm_sm100_splitk import (
SplitKTactic,
run_splitk_dense,
)
_flashinfer_pr4266_splitk_tactic = SplitKTactic
_flashinfer_pr4266_run_splitk_dense = run_splitk_dense
_flashinfer_pr4266_direct_default_tactic = default_tactic
_flashinfer_pr4266_prefer_direct = prefer_direct_bf16_gemm_sm100
_flashinfer_pr4266_run_direct_dense = run_direct_dense
_enable_bf16_splitk_gemm = True
_BF16_GEMM_BACKEND = backend
@@ -148,22 +232,48 @@ def _bf16_gemm_dispatch_fake(
return x.new_empty((*x.shape[:-1], weight.shape[0]))
@register_custom_op(fake_impl=_bf16_gemm_dispatch_fake)
def bf16_gemm_dispatch(
def _flashinfer_pr4266_bf16_gemm(
x: torch.Tensor, weight: torch.Tensor, bias: Optional[torch.Tensor]
) -> torch.Tensor:
x_2d = x.view(-1, x.shape[-1])
out = torch.empty((x_2d.shape[0], weight.shape[0]), dtype=x.dtype, device=x.device)
m, n, k = x_2d.shape[0], weight.shape[0], weight.shape[1]
if bias is None and _flashinfer_pr4266_prefer_direct(m, n, k):
tactic = _flashinfer_pr4266_direct_default_tactic(m, n, k)
_flashinfer_pr4266_run_direct_dense(x_2d, weight.T, out, True, tactic)
else:
tactic = _flashinfer_pr4266_splitk_tactic(
*_FLASHINFER_PR4266_TUNED_TACTICS[(m, n, k)]
)
_flashinfer_pr4266_run_splitk_dense(
x_2d,
weight.T,
bias,
out,
True,
tactic,
)
return out.view(*x.shape[:-1], weight.shape[0])
def _bf16_gemm_dispatch_impl(
x: torch.Tensor, weight: torch.Tensor, bias: Optional[torch.Tensor]
) -> torch.Tensor:
m = x.numel() // x.shape[-1]
if _enable_bf16_splitk_gemm and use_flashinfer_pr4266_bf16_gemm(
m, weight.shape[0], weight.shape[1]
):
return _flashinfer_pr4266_bf16_gemm(x, weight, bias)
if (
_use_hopper_bf16_gemv is not None
and bias is None
and _use_hopper_bf16_gemv(
x.numel() // x.shape[-1], weight.shape[0], weight.shape[1]
)
and _use_hopper_bf16_gemv(m, weight.shape[0], weight.shape[1])
):
return _hopper_bf16_gemv(x.view(-1, x.shape[-1]), weight).view(
*x.shape[:-1], -1
)
if _use_cutedsl_bf16_gemm is not None and _use_cutedsl_bf16_gemm(
x.numel() // x.shape[-1], weight.shape[0], weight.shape[1]
m, weight.shape[0], weight.shape[1]
):
return _cutedsl_bf16_gemm(x.view(-1, x.shape[-1]), weight, bias).view(
*x.shape[:-1], -1
@@ -171,6 +281,13 @@ def bf16_gemm_dispatch(
return F.linear(x, weight, bias)
@register_custom_op(fake_impl=_bf16_gemm_dispatch_fake)
def bf16_gemm_dispatch(
x: torch.Tensor, weight: torch.Tensor, bias: Optional[torch.Tensor]
) -> torch.Tensor:
return _bf16_gemm_dispatch_impl(x, weight, bias)
def get_bf16_gemm_backend() -> Bf16GemmBackend:
global _BF16_GEMM_BACKEND
if _BF16_GEMM_BACKEND is None:
@@ -269,7 +386,7 @@ class UnquantizedLinearMethod(LinearMethodBase):
return tgemm.mm(x, layer.weight, bias, otype=x.dtype)
elif (
get_bf16_gemm_backend().is_cutedsl()
get_bf16_gemm_backend().is_optimized()
and x.is_cuda
and x.dtype == torch.bfloat16
and layer.weight.dtype == torch.bfloat16
@@ -283,17 +400,7 @@ class UnquantizedLinearMethod(LinearMethodBase):
# opaque op resolves it at runtime with concrete shapes,
# keeping the per-shape kernel choice.
return bf16_gemm_dispatch(x, layer.weight, bias)
if _use_cutedsl_bf16_gemm(
x.numel() // x.shape[-1],
layer.weight.shape[0],
layer.weight.shape[1],
):
x_shapes = x.shape
output = _cutedsl_bf16_gemm(
x.view(-1, x_shapes[-1]), layer.weight, bias
)
return output.view(*x_shapes[:-1], -1)
return F.linear(x, layer.weight, bias)
return _bf16_gemm_dispatch_impl(x, layer.weight, bias)
return F.linear(x, layer.weight, bias)
@@ -84,10 +84,8 @@ class RadixLinearAttention(nn.Module):
a: torch.Tensor,
b: torch.Tensor,
) -> torch.Tensor:
if (
forward_batch.forward_mode.is_extend()
and get_tc_piecewise_forward_context() is not None
):
is_extend = forward_batch.forward_mode.is_extend()
if is_extend and get_tc_piecewise_forward_context() is not None:
# Output shape from linear attention: (1, seq_len, num_v_heads, head_v_dim)
seq_len = mixed_qkv.shape[0]
output = torch.empty(
@@ -112,14 +110,102 @@ class RadixLinearAttention(nn.Module):
self.layer_id,
)
return output
else:
return get_attn_backend().forward(
layer=self,
forward_batch=forward_batch,
# Target verify rebuilds query_start_loc from the physical padded input,
# unlike ordinary extend where it retains the logical sequence ends.
should_trim_padded_extend = (
is_extend and not forward_batch.forward_mode.is_target_verify()
)
real_num_tokens = (
getattr(forward_batch, "num_token_non_padded_cpu", None)
if should_trim_padded_extend
else None
)
if real_num_tokens is not None and real_num_tokens < mixed_qkv.shape[0]:
# DP synchronization may append physical rows beyond the logical varlen
# layout; compute its real prefix, then restore the downstream shape.
output = torch.empty(
(1, mixed_qkv.shape[0], self.num_v_heads, self.head_v_dim),
dtype=mixed_qkv.dtype,
device=mixed_qkv.device,
)
_linear_attention_with_output_impl(
mixed_qkv=mixed_qkv,
a=a,
b=b,
output=output,
attention_layer=self,
forward_batch=forward_batch,
)
return output
return get_attn_backend().forward(
layer=self,
forward_batch=forward_batch,
mixed_qkv=mixed_qkv,
a=a,
b=b,
)
def _linear_attention_with_output_impl(
mixed_qkv: torch.Tensor,
a: torch.Tensor,
b: torch.Tensor,
output: torch.Tensor,
attention_layer: RadixLinearAttention,
forward_batch: ForwardBatch,
) -> None:
"""Run linear attention on the real prefix and initialize physical padding."""
real_num_tokens = min(forward_batch.num_token_non_padded_cpu, mixed_qkv.shape[0])
original_out_cache_loc = forward_batch.out_cache_loc
# Keep the original ForwardBatch object and only narrow cache locations for
# this backend call so model/backend state is still written to the same batch.
forward_batch.out_cache_loc = original_out_cache_loc[:real_num_tokens]
logical_output = output[:, :real_num_tokens]
try:
ret = get_attn_backend().forward(
layer=attention_layer,
forward_batch=forward_batch,
mixed_qkv=mixed_qkv[:real_num_tokens],
a=a[:real_num_tokens],
b=b[:real_num_tokens],
linear_attn_output=logical_output,
)
finally:
forward_batch.out_cache_loc = original_out_cache_loc
# FlashInfer GDN can write directly into the physical output's logical
# prefix. Other backends return their own tensor and keep the copy fallback.
if ret.data_ptr() != logical_output.data_ptr():
logical_output.copy_(ret)
# Physical padding participates in following residual, router, expert/MoE,
# and collective operations. Keep those inputs finite and deterministic.
output[:, real_num_tokens:].zero_()
def _unified_linear_attention_with_output_impl(
mixed_qkv: torch.Tensor,
a: torch.Tensor,
b: torch.Tensor,
output: torch.Tensor,
layer_id: int,
) -> None:
"""Eager implementation kept separate for backend-independent tests."""
context = get_tc_piecewise_forward_context()
forward_batch = context.forward_batch
attention_layers = context.attention_layers
attention_layer = attention_layers[layer_id]
_linear_attention_with_output_impl(
mixed_qkv=mixed_qkv,
a=a,
b=b,
output=output,
attention_layer=attention_layer,
forward_batch=forward_batch,
)
return
@register_custom_op(mutates_args=["output"])
@@ -131,31 +217,14 @@ def unified_linear_attention_with_output(
output: torch.Tensor,
layer_id: int,
) -> None:
"""
Custom op wrapper for linear attention computation only.
"""
context = get_tc_piecewise_forward_context()
forward_batch = context.forward_batch
attention_layers = context.attention_layers
attention_layer = attention_layers[layer_id]
real_num_tokens = forward_batch.num_token_non_padded_cpu
original_out_cache_loc = forward_batch.out_cache_loc
# Keep the original ForwardBatch object and only narrow cache locations for
# this backend call so model/backend state is still written to the same batch.
forward_batch.out_cache_loc = original_out_cache_loc[:real_num_tokens]
ret = get_attn_backend().forward(
layer=attention_layer,
forward_batch=forward_batch,
mixed_qkv=mixed_qkv[:real_num_tokens],
a=a[:real_num_tokens],
b=b[:real_num_tokens],
"""Custom op wrapper for linear attention computation only."""
_unified_linear_attention_with_output_impl(
mixed_qkv=mixed_qkv,
a=a,
b=b,
output=output,
layer_id=layer_id,
)
forward_batch.out_cache_loc = original_out_cache_loc
output[:, :real_num_tokens].copy_(ret)
return
bcg_unified_linear_attention_with_output = eager_on_graph(True)(
+27
View File
@@ -8,6 +8,7 @@ from torch import nn
from sglang.kernels.ops.sampling.murmur_hash import murmur_hash32
from sglang.srt.distributed import get_tp_group
from sglang.srt.environ import envs
from sglang.srt.layers.dp_attention import (
is_dp_attention_enabled,
)
@@ -68,6 +69,18 @@ _CUSTOM_SAMPLER_FACTORIES: Dict[str, Callable[[], "Sampler"]] = {}
_BUILT_IN_SAMPLING_BACKENDS = {"flashinfer", "pytorch", "ascend"}
def _trace_e2e_sampler(stage: str, **fields) -> None:
if not envs.SGLANG_TRACE_SAMPLER_E2E.get():
return
try:
parallel = get_parallel()
rank = f"dp={parallel.attn_dp_rank} tp={parallel.tp_rank}"
except Exception:
rank = "rank=unknown"
details = " ".join(f"{key}={value}" for key, value in fields.items())
print(f"SGLANG_TRACE_SAMPLER_E2E {rank} stage={stage} {details}", flush=True)
class Sampler(nn.Module):
def __init__(self):
super().__init__()
@@ -119,15 +132,23 @@ class Sampler(nn.Module):
to get the unique seed for each position.
"""
logits = logits_output.next_token_logits
_trace_e2e_sampler(
"forward_enter",
logits_shape=tuple(logits.shape),
all_greedy=sampling_info.is_all_greedy,
)
if _is_hip and logits.shape[0] == 0:
return torch.empty((0,), dtype=torch.int64, device=logits.device)
# Preprocess logits (custom processors and NaN handling)
_trace_e2e_sampler("preprocess_enter")
logits = self._preprocess_logits(logits, sampling_info)
_trace_e2e_sampler("preprocess_returned")
return_sampling_mask = any(sampling_info.return_sampling_masks or [])
if sampling_info.is_all_greedy:
_trace_e2e_sampler("greedy_enter")
if _use_aiter and not _disable_aiter_greedy_sample:
batch_next_token_ids = torch.empty(
logits.shape[0], device=logits.device, dtype=torch.int32
@@ -135,6 +156,9 @@ class Sampler(nn.Module):
_aiter_greedy_sample(batch_next_token_ids, logits)
else:
batch_next_token_ids = torch.argmax(logits, -1)
_trace_e2e_sampler(
"greedy_returned", output_shape=tuple(batch_next_token_ids.shape)
)
if return_sampling_mask:
self._attach_greedy_sampling_mask_to_output(
logits_output, sampling_info, batch_next_token_ids
@@ -243,8 +267,11 @@ class Sampler(nn.Module):
)
logprob_result.write_output_to(logits_output)
_trace_e2e_sampler("token_sync_enter")
self._sync_token_ids_across_tp(batch_next_token_ids, sampling_info)
_trace_e2e_sampler("token_sync_returned")
_trace_e2e_sampler("forward_returned")
return batch_next_token_ids
def _sample_from_probs(
+23 -19
View File
@@ -295,8 +295,12 @@ class FutureMap:
else:
self.new_seq_lens_cpu_pinned = None
self.fwd_prepare_d2h_stream = None
# Lazy-inited on the first non-empty stash (peeks tensor shapes); non-spec's is a no-op.
self._forward_buf_initialized = False
self.need_topk = False
self.need_hidden_states = False
self.topk_p_buf = None
self.topk_index_buf = None
self.hidden_states_buf = None
self.draft_probs_buf = None
self.dsa_topk_indices_buf = None
# ngram-only relay bufs
@@ -314,22 +318,18 @@ class FutureMap:
pool=req_to_token_pool,
)
def _lazy_init_forward_buf(self, payload: RelayPayload):
def _maybe_init_forward_bufs(self, payload: RelayPayload) -> None:
# Local import (see decide_needs_cpu_seq_lens): keep module-level deps leaf.
from sglang.srt.speculative.spec_utils import spec_need_hidden_states
self._forward_buf_initialized = True
# Spec extras are gated by spec_algo, not by the payload's shape, so a
# non-spec stash allocates no extra bufs (only output_tokens_buf).
self.need_topk = self.spec_algo.is_some() and self.spec_algo.need_topk()
self.need_hidden_states = (
# Prefill can omit spec extras; initialize each buffer when decode first
# carries it instead of fixing the layout from the first payload.
if not self.need_topk and (
self.spec_algo.is_some()
and spec_need_hidden_states()
and payload.hidden_states is not None
)
if self.need_topk:
and self.spec_algo.need_topk()
and payload.topk_p is not None
):
self.need_topk = True
topk_p0 = payload.topk_p[0]
topk_index0 = payload.topk_index[0]
self.topk_p_buf = torch.empty(
@@ -342,7 +342,13 @@ class FutureMap:
dtype=topk_index0.dtype,
device=self.device,
)
if self.need_hidden_states:
if not self.need_hidden_states and (
self.spec_algo.is_some()
and spec_need_hidden_states()
and payload.hidden_states is not None
):
self.need_hidden_states = True
hidden_states0 = payload.hidden_states[0]
self.hidden_states_buf = torch.empty(
(self.req_pool_size, *hidden_states0.shape),
@@ -350,8 +356,7 @@ class FutureMap:
device=self.device,
)
self.draft_probs_buf = None
if payload.draft_probs is not None:
if self.draft_probs_buf is None and payload.draft_probs is not None:
draft_probs0 = payload.draft_probs[0]
self.draft_probs_buf = torch.empty(
(self.req_pool_size, *draft_probs0.shape),
@@ -545,8 +550,7 @@ class FutureMap:
self.accept_tokens_buf[indices] = payload.accept_tokens
self.accept_lens_buf[indices] = payload.accept_lens
return
if not self._forward_buf_initialized:
self._lazy_init_forward_buf(payload)
self._maybe_init_forward_bufs(payload)
self._maybe_init_dsa_topk_indices_buf(payload)
self.output_tokens_buf[indices] = payload.bonus_tokens.to(
self.output_tokens_buf.dtype
+25 -12
View File
@@ -292,14 +292,16 @@ from sglang.srt.observability.trace import process_tracing_init, trace_set_threa
from sglang.srt.parser.reasoning_parser import ReasoningParser
from sglang.srt.platforms import current_platform
from sglang.srt.plugins import load_plugins
from sglang.srt.runtime_context import get_context, publish
from sglang.srt.runtime_context import get_context, get_spec, publish
from sglang.srt.sampling.sampling_batch_info import SamplingBatchInfo
from sglang.srt.sampling.sampling_params import TOP_K_ALL
from sglang.srt.server_args import PortArgs, ServerArgs, compute_world_size
from sglang.srt.session.session_controller import SessionController
from sglang.srt.speculative.base_spec_worker import BaseSpecWorker
from sglang.srt.speculative.dflash_utils import validate_dflash_request
from sglang.srt.speculative.eagle_utils import get_draft_recurrent_hidden_state_spec
from sglang.srt.speculative.eagle_utils import (
get_draft_recurrent_hidden_state_spec_from_config,
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.utils import (
DynamicGradMode,
@@ -1380,11 +1382,18 @@ class Scheduler(
)
if self.spec_algorithm.carries_draft_hidden_states():
# `draft_runner` aliases `draft_runner_list[0]` in the multi-layer
# worker, so a single accessor covers both shapes.
draft_runner = self.draft_worker.draft_worker.draft_runner
# Derive the rank-uniform PD wire schema from config because only the
# last prefill PP stage owns a draft runner.
draft_model_config = ModelConfig.from_server_args(
self.server_args,
model_path=get_spec().speculative_draft_model_path,
model_revision=get_spec().speculative_draft_model_revision,
is_draft_model=True,
)
disagg_hidden_size, disagg_hidden_states_dtype = (
get_draft_recurrent_hidden_state_spec(draft_runner)
get_draft_recurrent_hidden_state_spec_from_config(
draft_model_config, self.spec_algorithm
)
)
else:
disagg_hidden_size = 16 # minimal padding size for RDMA
@@ -3989,7 +3998,9 @@ class Scheduler(
# future_map relay / on_publish).
resolve_forward_inputs(batch, self.future_map)
with self._forward_isolation(batch, overlap=False):
batch_result = self.model_worker.forward_batch_generation(batch)
batch_result = self.model_worker.forward_batch_generation(
batch, pp_proxy_tensors=pp_proxy_tensors
)
# The isolation restore reverted the worker's in-forward SB edits;
# re-apply what must carry to the next iter.
batch.spec_info = batch_result.next_draft_input
@@ -4000,12 +4011,14 @@ class Scheduler(
batch.seq_lens_sum = int(batch.seq_lens_cpu.sum())
batch.input_ids = None # rebuilt next iter from draft_token
self.update_cache_from_scheduler(batch, batch_result)
# Sync D2H so the result processor can read CPU tensors.
# Only the last PP rank owns real results requiring D2H; other ranks
# consume device tensors rebuilt from the output ring.
batch_result.copy_done = self.device_module.Event()
batch_result.copy_to_cpu(
return_logprob=batch.return_logprob,
return_hidden_states=batch.return_hidden_states,
)
if batch_result.has_sampled_token_ids and self.ps.pp_size == 1:
batch_result.copy_to_cpu(
return_logprob=batch.return_logprob,
return_hidden_states=batch.return_hidden_states,
)
else:
kwargs = (
{"pp_proxy_tensors": pp_proxy_tensors}
@@ -1025,6 +1025,14 @@ class SchedulerPPMixin:
"next_token_ids": result.next_token_ids,
}
# Draft extend runs only on the last stage, but every rank needs its relayed
# output to fill PD auxiliary buffers.
draft_input = result.next_draft_input
if draft_input is not None and draft_input.topk_p is not None:
tensor_dict["draft_topk_p"] = draft_input.topk_p.contiguous()
tensor_dict["draft_topk_index"] = draft_input.topk_index.contiguous()
tensor_dict["draft_hidden_states"] = draft_input.hidden_states.contiguous()
if batch.return_logprob:
logprob_dict = get_logprob_dict_from_result(result)
tensor_dict = {
@@ -1172,17 +1180,45 @@ class SchedulerPPMixin:
logits_output = LogitsProcessorOutput(next_token_logits=None)
logits_output.auxiliary_device_output = auxiliary_output
next_token_ids = pp_outputs["next_token_ids"].to(torch.int64)
# Rebind the last stage's ring proposal as batch.spec_info so the PD result
# processor sees the same object on every rank.
next_draft_input = None
if "draft_topk_p" in pp_outputs.tensors:
from sglang.srt.speculative.eagle_info import EagleDraftInput
next_draft_input = EagleDraftInput(
topk_p=pp_outputs["draft_topk_p"],
topk_index=pp_outputs["draft_topk_index"],
hidden_states=pp_outputs["draft_hidden_states"],
bonus_tokens=next_token_ids,
num_tokens_per_req=1,
num_tokens_for_logprob_per_req=1,
)
batch.spec_info = next_draft_input
# PP rank 0 also relays into output_tokens_buf so the next iter's
# resolve_forward_inputs finds these tokens for the decode portion
# of mixed-chunk batches (which gather via mix_running_indices).
self.future_map.stash(
batch.req_pool_indices, RelayPayload(bonus_tokens=next_token_ids)
batch.req_pool_indices,
RelayPayload(
bonus_tokens=next_token_ids,
topk_p=None if next_draft_input is None else next_draft_input.topk_p,
topk_index=(
None if next_draft_input is None else next_draft_input.topk_index
),
hidden_states=(
None if next_draft_input is None else next_draft_input.hidden_states
),
),
)
batch.input_ids = None
output_result = GenerationBatchResult(
logits_output=logits_output,
pp_hidden_states_proxy_tensors=None,
next_token_ids=pp_outputs["next_token_ids"],
next_draft_input=next_draft_input,
extend_input_len_per_req=extend_input_len_per_req,
extend_logprob_start_len_per_req=extend_logprob_start_len_per_req,
can_run_cuda_graph=mb_metadata.can_run_cuda_graph,
+6 -2
View File
@@ -325,6 +325,7 @@ class TpModelWorker(BaseTpWorker):
is_multi_layer_eagle: bool = False,
context_length: Optional[int] = None,
draft_attention_backend: Optional[str] = None,
random_seed: Optional[int] = None,
):
# Parse args
self.server_args = server_args
@@ -384,8 +385,11 @@ class TpModelWorker(BaseTpWorker):
self.world_group = get_world_group()
# Sync random seed across TP workers.
# Elastic joiners cannot enter the launch-time WORLD broadcast.
if server_args.is_ep_joiner:
# Elastic joiners and last-stage-only draft workers cannot enter the WORLD
# broadcast, so they reuse the target's already-broadcast seed.
if random_seed is not None:
self.random_seed = random_seed
elif server_args.is_ep_joiner:
self.random_seed = get_device().random_seed
else:
self.random_seed = broadcast_pyobj(
@@ -23,6 +23,7 @@ class KVCacheBuildResult:
from typing import TYPE_CHECKING
from sglang.srt.arg_groups.overrides import resolving_view
from sglang.srt.configs.hybrid_arch import (
hybrid_gdn_config,
hybrid_lightning_config,
@@ -62,6 +63,29 @@ if TYPE_CHECKING:
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
def get_draft_kv_pool(
*,
draft_worker: BaseTpWorker,
spec_algorithm: SpeculativeAlgorithm,
server_args: ServerArgs,
):
"""Return the draft token-to-KV pool for the current draft worker,
or None when no draft KV pool is available."""
if draft_worker is None or spec_algorithm.is_ngram():
return None
# V2 draft workers exist only on their hosting PP stage; other ranks own no
# nested draft worker or draft KV pool.
if draft_worker.draft_worker is None:
return None
if resolving_view(server_args).enable_multi_layer_eagle:
draft_runner = draft_worker.draft_worker.draft_runner_list[0]
else:
draft_runner = draft_worker.draft_worker.draft_runner
return draft_runner.token_to_kv_pool
def maybe_register_hicache_draft(
*,
tree_cache,
@@ -161,6 +161,28 @@ MAMBA_CACHE_V2_ADDITIONAL_RATIO_OVERLAP_LAZY = 1
MAMBA_CACHE_V2_ADDITIONAL_RATIO_NO_OVERLAP = 1
MAMBA_CACHE_V2_ADDITIONAL_RATIO_NO_BUFFER = 1
def _pp_local_per_request_bytes(
total_bytes: int,
layer_ids: list[int],
start_layer: int,
end_layer: int,
) -> int:
# BaseLinearStateParams reports global bytes, but PP pools allocate only local
# layers; charge this stage its proportional per-request share.
if not layer_ids:
return 0
if total_bytes % len(layer_ids) != 0:
raise ValueError(
"Linear-state bytes must be uniform per layer: "
f"total_bytes={total_bytes}, num_layers={len(layer_ids)}"
)
local_layer_count = sum(
start_layer <= layer_id < end_layer for layer_id in layer_ids
)
return total_bytes // len(layer_ids) * local_layer_count
if TYPE_CHECKING:
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.mem_cache.unified_memory_pool import (
@@ -939,6 +939,8 @@ def build_prefill_registry(
)
reg.register_slot(slot, bind=bind)
# PP stage inputs live outside ForwardBatch; adopt runner-owned buffers for
# stable addresses and clear padding because prefill executes every bucket row.
if source is not None:
pp = getattr(source, "pp_proxy_tensors", None)
if pp is not None:
@@ -954,7 +956,10 @@ def build_prefill_registry(
reg.register_slot(
GraphSlot(
name=f"pp_proxy_tensors.{_key}",
shape_fn=lambda _bs, _mt, _s=tuple(_backing.shape): _s,
shape_fn=lambda _bs, mt, _tail=tuple(_backing.shape[1:]): (
mt,
*_tail,
),
dtype=_backing.dtype,
axis="tokens",
padding_policy=PaddingPolicy.ZERO,
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import time
from collections import defaultdict
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Any, Optional
import msgspec
@@ -80,6 +80,45 @@ def _align_pipeline_layers(layers: list, layer_model) -> list:
)
def has_standard_gqa_for_all_local_layers(
*, attention_layer_count: int, start_layer: int, end_layer: int
) -> bool:
"""Check the layers materialized on this pipeline rank, not the full model."""
return attention_layer_count >= end_layer - start_layer
def index_attention_layers_by_global_id(
attention_layers: list[Any],
mha_companion_layers: list[Any],
layer_model=None,
) -> tuple[list[Any], list[Any]]:
"""Pad PP-local attention metadata so global layer_id remains a valid index."""
if len(attention_layers) != len(mha_companion_layers):
raise ValueError("attention and MHA companion metadata must be parallel")
populated = [layer for layer in attention_layers if layer is not None]
if not populated or any(not hasattr(layer, "layer_id") for layer in populated):
if layer_model is not None:
return (
_align_pipeline_layers(attention_layers, layer_model),
_align_pipeline_layers(mha_companion_layers, layer_model),
)
return attention_layers, mha_companion_layers
max_layer_id = max(int(layer.layer_id) for layer in populated)
indexed_attention = [None] * (max_layer_id + 1)
indexed_companions = [None] * (max_layer_id + 1)
for attention, companion in zip(attention_layers, mha_companion_layers):
if attention is None:
if companion is not None:
raise ValueError("MHA companion has no primary attention layer")
continue
layer_id = int(attention.layer_id)
if layer_id < 0 or indexed_attention[layer_id] is not None:
raise ValueError(f"invalid or duplicate attention layer_id: {layer_id}")
indexed_attention[layer_id] = attention
indexed_companions[layer_id] = companion
return indexed_attention, indexed_companions
class GraphCapture(msgspec.Struct, frozen=True, kw_only=True):
runner: Optional[BaseRunner]
memory_phase: str
@@ -392,11 +431,22 @@ def capture_prefill_graph(
model_runner.dsa_indexers,
model_runner.mha_companion_layers,
) = compute_attention_and_moe_layers(layer_model)
model_runner.attention_layers = _align_pipeline_layers(
model_runner.attention_layers, layer_model
(
model_runner.attention_layers,
model_runner.mha_companion_layers,
) = index_attention_layers_by_global_id(
model_runner.attention_layers,
model_runner.mha_companion_layers,
layer_model,
)
if len(model_runner.attention_layers) < model_runner.model_config.num_hidden_layers:
if not has_standard_gqa_for_all_local_layers(
attention_layer_count=sum(
layer is not None for layer in model_runner.attention_layers
),
start_layer=model_runner.layer_info.start_layer,
end_layer=model_runner.layer_info.end_layer,
):
# TODO(yuwei): support Non-Standard GQA
log_info_on_rank0(
logger,
@@ -404,10 +454,6 @@ def capture_prefill_graph(
)
return result(None)
model_runner.mha_companion_layers = _align_pipeline_layers(
model_runner.mha_companion_layers, layer_model
)
tic = time.perf_counter()
before_mem = get_available_gpu_memory(model_runner.device, model_runner.gpu_id)
role = "draft" if model_runner.is_draft_worker else "target"
@@ -134,7 +134,7 @@ def _allocate_decode_buffers(
residual_shape = (
(max_num_token, pp_proxy_residual_num_blocks, hidden_size)
if pp_proxy_residual_num_blocks is not None
else (max_bs, hidden_size)
else (max_num_token, hidden_size)
)
pp_proxy_tensors["residual"] = torch.zeros(residual_shape, dtype=dtype)
if pp_proxy_topk_size is not None:
@@ -244,6 +244,14 @@ class BaseRunner(ABC):
self._pre_initialize_flashinfer_allreduce_workspace()
self._pre_initialize_fi_a2a_workspace()
# Model-owned communication resources may depend on the resolved
# request pool and must be compiled/allocated before graph capture.
prepare_model_resources = getattr(
mr.model, "prepare_before_cuda_graph_capture", None
)
if prepare_model_resources is not None:
prepare_model_resources(mr)
if should_run_flashinfer_autotune(self.model_runner):
buffers, batch_size = self._autotune_buffers()
assert (
@@ -38,6 +38,7 @@ Backend selection comes from cuda_graph_config.prefill:
from __future__ import annotations
import copy
import dataclasses
import inspect
import logging
from collections.abc import Sequence
@@ -328,10 +329,12 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
dtype=self.model_runner.dtype,
enable_mamba_track=self.mamba_track_enabled,
pp_size=self.pp_size,
hc_hidden_size=model_runner.model_config.hc_hidden_size,
pp_proxy_topk_size=model_runner.get_pp_proxy_topk_size(),
hc_hidden_size=getattr(
self.model_runner.model_config, "hc_hidden_size", None
),
pp_proxy_topk_size=self.model_runner.get_pp_proxy_topk_size(),
pp_proxy_residual_num_blocks=(
model_runner.get_pp_proxy_residual_num_blocks()
self.model_runner.get_pp_proxy_residual_num_blocks()
),
)
self.buffers.share_buffers()
@@ -385,11 +388,6 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
self._capture_lora = False
self.enable_cp_v2_bcg_capture = False
self.prefill_cp_bcg_input: Optional[PrefillCPBCGInput] = None
self._static_pp_proxy_tensors = (
PPProxyTensors(self.buffers.pp_proxy_tensors)
if self.buffers.pp_proxy_tensors is not None
else None
)
# TcPiecewise does its compile pass during backend construction.
# Wrap only that path with the prefill CUDA graph failure hint.
try:
@@ -646,6 +644,14 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
return forward_batch.positions
def _static_pp_proxy_tensors(self, num_tokens: int) -> Optional[PPProxyTensors]:
buffers = self.buffers.pp_proxy_tensors
if buffers is None:
return None
return PPProxyTensors(
{key: value[:num_tokens] for key, value in buffers.items()}
)
@contextmanager
def _prefill_forward_context(
self,
@@ -702,31 +708,25 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
set_is_extend_in_batch(False)
with self._prefill_forward_context(forward_batch):
pp_kwargs = self.model_runner._pp_kwargs(
self._static_pp_proxy_tensors(num_tokens)
)
if self._uses_eager_prefill_tail():
# BCG / Full: capture the transformer body only.
positions = self._get_layer_model_positions(forward_batch)
input_ids = forward_batch.input_ids
input_embeds = forward_batch.input_embeds
layer_kwargs = {}
if self._static_pp_proxy_tensors is not None:
layer_kwargs["pp_proxy_tensors"] = self._static_pp_proxy_tensors[
:num_tokens
]
if not self.model_runner.pp_group.is_first_rank:
input_ids = None
input_embeds = None
return self.layer_model.forward(
input_ids,
forward_batch.input_ids,
positions,
forward_batch,
input_embeds,
**layer_kwargs,
forward_batch.input_embeds,
**pp_kwargs,
)
# tc_piecewise: compile/capture the outer model.forward path.
return self.model_runner.model.forward(
forward_batch.input_ids,
forward_batch.positions,
forward_batch,
**pp_kwargs,
)
def _run_dummy_forward(self, num_tokens: int) -> None:
@@ -1568,6 +1568,19 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
else forward_batch.global_forward_mode
)
# The draft tail concatenates hidden states with padded input embeddings;
# expose the bucket-sized static view and refresh its live prefix below.
padded_spec_info = forward_batch.spec_info
if (
self.static_draft_hidden_states is not None
and padded_spec_info is not None
and getattr(padded_spec_info, "hidden_states", None) is not None
):
padded_spec_info = dataclasses.replace(
padded_spec_info,
hidden_states=self.static_draft_hidden_states[:static_num_tokens],
)
static_forward_batch = ForwardBatch(
forward_mode=pcg_forward_mode,
batch_size=bs,
@@ -1611,7 +1624,7 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
global_dp_buffer_len=forward_batch.global_dp_buffer_len,
mrope_positions=mrope_positions,
spec_algorithm=forward_batch.spec_algorithm,
spec_info=forward_batch.spec_info,
spec_info=padded_spec_info,
capture_hidden_mode=forward_batch.capture_hidden_mode,
num_token_non_padded=num_token_non_padded,
num_token_non_padded_cpu=forward_batch.num_token_non_padded_cpu,
@@ -1619,6 +1632,9 @@ class PrefillCudaGraphRunner(BaseCudaGraphRunner):
lora_ids=forward_batch.lora_ids,
sampling_info=forward_batch.sampling_info,
mm_inputs=forward_batch.mm_inputs,
# Multimodal preparation consumes mm_inputs but retains embeddings for
# later MTP draft extend, so copy that side channel into the replay view.
mm_input_embeds=forward_batch.mm_input_embeds,
temperature=forward_batch.temperature,
top_p=forward_batch.top_p,
dimensions=forward_batch.dimensions,
@@ -133,7 +133,7 @@ class DecodeInputBuffers(ForwardInputBuffers):
is_mhc = hc_hidden_size is not None
hs = hc_hidden_size if is_mhc else hidden_size
pp_proxy_tensors = {
"hidden_states": torch.zeros((max_bs, hs), dtype=dtype),
"hidden_states": torch.zeros((max_num_token, hs), dtype=dtype),
}
if not is_mhc:
# Only Kimi K3 supplies num_blocks: its PP bank is token-major
@@ -141,7 +141,7 @@ class DecodeInputBuffers(ForwardInputBuffers):
residual_shape = (
(max_num_token, pp_proxy_residual_num_blocks, hidden_size)
if pp_proxy_residual_num_blocks is not None
else (max_bs, hidden_size)
else (max_num_token, hidden_size)
)
pp_proxy_tensors["residual"] = torch.zeros(
residual_shape, dtype=dtype
@@ -356,7 +356,7 @@ class PrefillInputBuffers(ForwardInputBuffers):
hidden_size: int,
dtype: torch.dtype,
enable_mamba_track: bool,
pp_size: int,
pp_size: int = 1,
hc_hidden_size: Optional[int] = None,
pp_proxy_topk_size: Optional[int] = None,
pp_proxy_residual_num_blocks: Optional[int] = None,
+6 -1
View File
@@ -2740,7 +2740,12 @@ class DeepseekV2Model(nn.Module):
for i in range(len(self.layers)):
if isinstance(self.layers[i].mlp, DeepseekV2MoE):
# tp_size = get_parallel().tp_size
is_a2a_moe = is_deepep_class_backend()
# Keep the original deepep-class scope here and only add DeepEP v2,
# so unrelated backends' allocator sizing is unchanged.
is_a2a_moe = (
is_deepep_class_backend()
or get_moe_a2a_backend().is_deepep_v2()
)
tp_size = 1 if is_a2a_moe else get_parallel().tp_size
intermediate_size = (
config.moe_intermediate_size * config.n_shared_experts
+125 -7
View File
@@ -27,7 +27,10 @@ import torch.nn.functional as F
from torch import nn
from transformers import PretrainedConfig
from sglang.kernels.ops.elementwise.elementwise import fused_gate_sigmoid_mul_add
from sglang.kernels.ops.elementwise.elementwise import (
fused_gate_sigmoid_mul,
fused_gate_sigmoid_mul_add,
)
from sglang.srt.batch_overlap.two_batch_overlap import model_forward_maybe_tbo
from sglang.srt.distributed import (
get_pp_group,
@@ -154,6 +157,7 @@ def can_fuse_shared_expert(
or getattr(config, "shared_expert_intermediate_size", 0) <= 0
or config.shared_expert_intermediate_size != config.moe_intermediate_size
or get_moe_a2a_backend().is_deepep()
or get_moe_a2a_backend().is_deepep_v2()
or get_moe_a2a_backend().is_mori()
):
return False
@@ -341,6 +345,10 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
routing_method_type=RoutingMethodType.RenormalizeNaive,
num_fused_shared_experts=self.num_fused_shared_experts,
inplace=not _needs_hidden_after_experts,
enable_qwen35_fp8_deferred_finalize=(
config.model_type == "qwen3_5_moe_text"
and envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION.get()
),
)
self.gate = ReplicatedLinear(
@@ -369,6 +377,7 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
if (
get_moe_a2a_backend().is_deepep()
or get_moe_a2a_backend().is_mori()
or get_moe_a2a_backend().is_deepep_v2()
or get_moe_a2a_backend().is_flashinfer()
)
else {}
@@ -387,7 +396,11 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
else:
self.shared_expert_gate = torch.nn.Linear(config.hidden_size, 1, bias=False)
if get_moe_a2a_backend().is_deepep() or get_moe_a2a_backend().is_mori():
if (
get_moe_a2a_backend().is_deepep()
or get_moe_a2a_backend().is_deepep_v2()
or get_moe_a2a_backend().is_mori()
):
# TODO: we will support tp < ep in the future
self.ep_size = get_parallel().moe_ep_size
self.num_experts = (
@@ -532,6 +545,23 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
return shared_output
def _forward_deepep(self, hidden_states: torch.Tensor, forward_batch: ForwardBatch):
trace_e2e = envs.SGLANG_TRACE_QWEN_MOE_DEEPEP_E2E.get()
def trace_sync(stage: str):
if not trace_e2e:
return
print(
"SGLANG_TRACE_QWEN_MOE_DEEPEP_E2E "
f"stage={stage}_sync_enter tokens={hidden_states.shape[0]}",
flush=True,
)
torch.cuda.synchronize()
print(
"SGLANG_TRACE_QWEN_MOE_DEEPEP_E2E "
f"stage={stage}_sync_returned tokens={hidden_states.shape[0]}",
flush=True,
)
enable_dual_stream = (
is_npu()
and envs.SGLANG_NPU_USE_MULTI_STREAM.get()
@@ -564,6 +594,7 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
shared_event = self.alt_stream.record_event()
else:
shared_output = self._forward_shared_experts(hidden_states)
trace_sync("shared_expert")
topk_output = self.topk(
hidden_states,
router_logits,
@@ -578,36 +609,95 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
)
else:
topk_output = self.topk.empty_topk_output(hidden_states.device)
trace_sync("pre_experts")
final_hidden_states = self.experts(
hidden_states=hidden_states,
topk_output=topk_output,
)
trace_sync("post_experts")
if enable_dual_stream:
wait_share_stream()
elif enable_cuda_shared_overlap:
torch.cuda.current_stream().wait_event(shared_event)
if shared_output is not None:
trace_sync("pre_shared_add")
final_hidden_states.add_(shared_output)
trace_sync("post_shared_add")
return final_hidden_states
def _forward_router_experts(self, hidden_states: torch.Tensor):
@property
def supports_deferred_finalize(self) -> bool:
return bool(
self.experts.supports_deferred_finalize and self.shared_expert is not None
)
def _forward_router_experts(
self,
hidden_states: torch.Tensor,
*,
defer_finalize: bool = False,
):
# router_logits: (num_tokens, n_experts)
router_logits, _ = self.gate(hidden_states)
topk_output = self.topk(hidden_states, router_logits)
if defer_finalize:
if not self.supports_deferred_finalize:
raise RuntimeError(
"Qwen deferred finalize requires a compatible FlashInfer "
"TRTLLM MoE producer and a separate shared expert"
)
if not TopKOutputChecker.format_is_bypassed(topk_output):
raise RuntimeError(
"Qwen deferred finalize requires logits-based bypassed TopK"
)
return self.experts.forward_deferred_finalize(hidden_states, topk_output)
if self.enable_shared_expert_fusion and TopKOutputChecker.format_is_standard(
topk_output
):
topk_output = self._append_shared_to_topk_output(topk_output, hidden_states)
return self.experts(hidden_states, topk_output)
def _gate_shared_output_out_of_place(
self,
hidden_states: torch.Tensor,
shared_output: torch.Tensor,
) -> torch.Tensor:
if self.shared_expert_gate is None:
return shared_output
return fused_gate_sigmoid_mul(
hidden_states,
self.shared_expert_gate.weight.squeeze(0),
shared_output,
)
def forward_normal_dual_stream(
self,
hidden_states: torch.Tensor,
use_fused_gate: bool = False,
defer_finalize: bool = False,
) -> torch.Tensor:
current_stream = torch.cuda.current_stream()
if defer_finalize:
# Keep routed FC2 on the current stream as finalize's PDL dependency;
# the shared branch reads the same input and writes a separate output.
self.alt_stream.wait_stream(current_stream)
router_output = self._forward_router_experts(
hidden_states, defer_finalize=True
)
with torch.cuda.stream(self.alt_stream):
shared_output = self._forward_shared_experts(
hidden_states, apply_gate=False
)
if shared_output is not None:
shared_output = self._gate_shared_output_out_of_place(
hidden_states, shared_output
)
current_stream.wait_stream(self.alt_stream)
return router_output, shared_output
self.alt_stream.wait_stream(current_stream)
shared_output = (
self._forward_shared_experts(
@@ -648,11 +738,18 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
self,
hidden_states: torch.Tensor,
forward_batch: Optional[ForwardBatch] = None,
defer_finalize: bool = False,
) -> torch.Tensor:
num_tokens, hidden_dim = hidden_states.shape
hidden_states = hidden_states.view(-1, hidden_dim)
if defer_finalize and num_tokens == 0:
raise RuntimeError("Qwen deferred finalize does not support M=0")
if get_moe_a2a_backend().is_deepep() or get_moe_a2a_backend().is_mori():
if (
get_moe_a2a_backend().is_deepep()
or get_moe_a2a_backend().is_deepep_v2()
or get_moe_a2a_backend().is_mori()
):
return self._forward_deepep(hidden_states, forward_batch)
use_fused_gate = (
@@ -674,13 +771,34 @@ class Qwen2MoeSparseMoeBlock(nn.Module):
and not torch.compiler.is_compiling()
):
final_hidden_states, shared_output = self.forward_normal_dual_stream(
hidden_states, use_fused_gate=use_fused_gate
hidden_states,
use_fused_gate=use_fused_gate,
defer_finalize=defer_finalize,
)
else:
shared_output = self._forward_shared_experts(
hidden_states, apply_gate=not use_fused_gate
hidden_states, apply_gate=not use_fused_gate and not defer_finalize
)
if defer_finalize and shared_output is not None:
shared_output = self._gate_shared_output_out_of_place(
hidden_states, shared_output
)
final_hidden_states = self._forward_router_experts(
hidden_states, defer_finalize=defer_finalize
)
if defer_finalize:
if shared_output is None:
raise RuntimeError("Qwen deferred finalize requires shared output")
from sglang.srt.layers.moe.qwen35_flashinfer_fusion import (
Qwen35MoeFinalizeHandoff,
)
return Qwen35MoeFinalizeHandoff.from_flashinfer(
final_hidden_states,
gated_shared_output=shared_output,
m=num_tokens,
)
final_hidden_states = self._forward_router_experts(hidden_states)
if shared_output is not None:
if use_fused_gate:
+238 -13
View File
@@ -39,6 +39,7 @@ from sglang.srt.configs.qwen3_5 import (
# Distributed
from sglang.srt.distributed import get_pp_group
from sglang.srt.environ import envs
from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
from sglang.srt.eplb.expert_location import ModelConfigForExpertLocation
from sglang.srt.layers.attention.mamba.mamba import mamba_v2_sharded_weight_loader
@@ -135,6 +136,9 @@ _gdn_use_alt_stream = _is_cuda or (
_qknorm_use_alt_stream = _is_cuda or (
get_bool_env_var("SGLANG_QK_NORM_ALT_STREAM", "False") and _hip_use_alt_stream
)
_gdn_decode_fused_proj_conv = (
_is_cuda and envs.SGLANG_ENABLE_GDN_DECODE_FUSED_PROJ_CONV.get()
)
_is_amx_available = cpu_has_amx_support()
_is_xpu = is_xpu()
@@ -156,7 +160,12 @@ def _disable_shared_experts_fusion() -> bool:
# Resolved lazily: the flag is written by the owning model's gate before
# its layers build (per runner); models without a gate see the config
# intent through the accessor's fallback.
return is_shared_experts_fusion_disabled()
# The deferred-finalize ABI needs the shared expert as a separate, gated
# local contribution; it cannot consume a shared slot fused into routed MoE.
return bool(
envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION.get()
or is_shared_experts_fusion_disabled()
)
def _maybe_enable_silu_fp4_quant_fusion(mlp: nn.Module) -> None:
@@ -185,6 +194,24 @@ def _maybe_enable_silu_fp4_quant_fusion(mlp: nn.Module) -> None:
logger.info("Enabled fused SiLU+mul+FP4-quant for dense MLP down_proj input.")
def _use_mnnvl_cutedsl_fusion(config: Qwen3_5TextConfig, is_nextn: bool) -> bool:
return bool(
not is_nextn
and config.model_type == "qwen3_5_moe_text"
and envs.SGLANG_FLASHINFER_MNNVL_CUTEDSL_AR_FUSION.get()
)
def _layer_communicator_class(config: Qwen3_5TextConfig, is_nextn: bool):
if _use_mnnvl_cutedsl_fusion(config, is_nextn):
from sglang.srt.layers.moe.qwen35_flashinfer_fusion import (
Qwen35FlashInferLayerCommunicator,
)
return Qwen35FlashInferLayerCommunicator
return LayerCommunicator
if _is_cuda:
from sglang.kernels.ops.attention.fused_qk_rmsnorm_rope_gate import (
fused_qk_gemma_rmsnorm_rope_gate,
@@ -257,6 +284,27 @@ def _select_fused_ar_input_for_linear(hidden_states, linear: nn.Module):
)
def _finish_mlp_output(hidden_states, *, expect_deferred: bool):
if not expect_deferred:
if not isinstance(hidden_states, torch.Tensor):
from sglang.srt.layers.moe.qwen35_flashinfer_fusion import (
Qwen35MoeFinalizeHandoff,
)
if isinstance(hidden_states, Qwen35MoeFinalizeHandoff):
raise RuntimeError("unexpected deferred-finalize handoff")
hidden_states._sglang_needs_allreduce_fusion = True
return hidden_states
from sglang.srt.layers.moe.qwen35_flashinfer_fusion import (
Qwen35MoeFinalizeHandoff,
)
if not isinstance(hidden_states, Qwen35MoeFinalizeHandoff):
raise RuntimeError("Qwen3.5 expected a FlashInfer deferred-finalize handoff")
return hidden_states
if _is_npu:
from sgl_kernel_npu.norm.split_qkv_rmsnorm_rope import (
split_qkvgate_gemma_rmsnorm_rope,
@@ -730,10 +778,23 @@ class Qwen3_5GatedDeltaNet(nn.Module):
backend, projected_states_qkvz, projected_states_ba, forward_batch
)
if (
use_fused_decode_proj_conv = (
_gdn_decode_fused_proj_conv
and forward_batch.forward_mode.is_decode()
and isinstance(projected_states_qkvz, torch.Tensor)
and isinstance(projected_states_ba, torch.Tensor)
)
use_fused_contiguous_unpack = (
self.num_v_heads // self.num_k_heads in _GDN_FUSED_QKVZBA_RATIOS
and not _is_npu
):
)
if use_fused_decode_proj_conv:
# GDN owns indexed Conv1D state and the safe unpack/Conv boundary;
# it replaces these temporary B/A placeholders before recurrence.
mixed_qkv = (projected_states_qkvz, projected_states_ba)
z = None
b = projected_states_ba
a = projected_states_ba
elif use_fused_contiguous_unpack and not _is_npu:
if _is_cpu:
num_k_heads_tp = self.num_k_heads // self.attn_tp_size
num_v_heads_tp = self.num_v_heads // self.attn_tp_size
@@ -760,12 +821,22 @@ class Qwen3_5GatedDeltaNet(nn.Module):
)
mixed_qkv = torch.cat((query, key, value), dim=-1)
core_attn_out = self.attn(
attn_result = self.attn(
forward_batch,
mixed_qkv=mixed_qkv,
a=a,
b=b,
)
if use_fused_decode_proj_conv:
if not isinstance(attn_result, tuple) or len(attn_result) != 2:
raise RuntimeError(
"Fused GDN decode projection/Conv1D backend must return "
"(core_attn_out, z)"
)
core_attn_out, z = attn_result
else:
core_attn_out = attn_result
assert z is not None
z_shape_og = z.shape
# reshape input data into 2D tensor
@@ -862,7 +933,7 @@ class Qwen3_5LinearDecoderLayer(nn.Module):
_enable_qwen35_fused_ar_quant()
and _linear_accepts_fp8_tuple(self.linear_attn.in_proj_qkvz)
)
self.layer_communicator = LayerCommunicator(
self.layer_communicator = _layer_communicator_class(config, is_nextn)(
layer_scatter_modes=self.layer_scatter_modes,
input_layernorm=self.input_layernorm,
post_attention_layernorm=self.post_attention_layernorm,
@@ -913,6 +984,23 @@ class Qwen3_5LinearDecoderLayer(nn.Module):
forward_batch
)
)
defer_moe_finalize = (
fuse_mlp_allreduce
and isinstance(hidden_states, torch.Tensor)
and isinstance(self.mlp, Qwen2MoeSparseMoeBlock)
and hasattr(self.layer_communicator, "should_use_finalize")
and self.layer_communicator.should_use_finalize(
forward_batch, int(hidden_states.shape[0])
)
)
if (
fuse_mlp_allreduce
and self.layer_communicator.is_last_layer
and not defer_moe_finalize
):
# The last layer has no prepare_attn consumer for deferred AllReduce;
# fall back before MLP so postprocess_layer performs the collective.
fuse_mlp_allreduce = False
with get_forward().scoped(
fuse_mlp_allreduce=fuse_mlp_allreduce,
mlp_reduce_scatter=mlp_reduce_scatter,
@@ -921,11 +1009,14 @@ class Qwen3_5LinearDecoderLayer(nn.Module):
hidden_states = self.mlp(
hidden_states,
forward_batch,
defer_finalize=defer_moe_finalize,
)
else:
hidden_states = self.mlp(hidden_states)
if fuse_mlp_allreduce:
hidden_states._sglang_needs_allreduce_fusion = True
hidden_states = _finish_mlp_output(
hidden_states, expect_deferred=defer_moe_finalize
)
else:
hidden_states, residual = self.layer_communicator.postprocess_layer(
hidden_states, residual, forward_batch
@@ -1084,7 +1175,7 @@ class Qwen3_5AttentionDecoderLayer(nn.Module):
enable_fused_ar_quant = (
_enable_qwen35_fused_ar_quant() and _linear_accepts_fp8_tuple(self.qkv_proj)
)
self.layer_communicator = LayerCommunicator(
self.layer_communicator = _layer_communicator_class(config, is_nextn)(
layer_scatter_modes=self.layer_scatter_modes,
input_layernorm=self.input_layernorm,
post_attention_layernorm=self.post_attention_layernorm,
@@ -1319,6 +1410,21 @@ class Qwen3_5AttentionDecoderLayer(nn.Module):
forward_batch
)
)
defer_moe_finalize = (
fuse_mlp_allreduce
and isinstance(hidden_states, torch.Tensor)
and isinstance(self.mlp, Qwen2MoeSparseMoeBlock)
and hasattr(self.layer_communicator, "should_use_finalize")
and self.layer_communicator.should_use_finalize(
forward_batch, int(hidden_states.shape[0])
)
)
if (
fuse_mlp_allreduce
and self.layer_communicator.is_last_layer
and not defer_moe_finalize
):
fuse_mlp_allreduce = False
with get_forward().scoped(
fuse_mlp_allreduce=fuse_mlp_allreduce,
mlp_reduce_scatter=mlp_reduce_scatter,
@@ -1327,11 +1433,14 @@ class Qwen3_5AttentionDecoderLayer(nn.Module):
hidden_states = self.mlp(
hidden_states,
forward_batch,
defer_finalize=defer_moe_finalize,
)
else:
hidden_states = self.mlp(hidden_states)
if fuse_mlp_allreduce:
hidden_states._sglang_needs_allreduce_fusion = True
hidden_states = _finish_mlp_output(
hidden_states, expect_deferred=defer_moe_finalize
)
else:
hidden_states, residual = self.layer_communicator.postprocess_layer(
hidden_states, residual, forward_batch
@@ -1479,6 +1588,48 @@ class Qwen3_5ForCausalLM(nn.Module):
prefix=f"{prefix}.layers",
)
self.flashinfer_mnnvl_cutedsl_fusion = None
if _use_mnnvl_cutedsl_fusion(config, is_nextn):
if self.pp_group.world_size != 1:
raise RuntimeError(
"Qwen3.5 FlashInfer MNNVL CuTe DSL fusion currently requires PP=1"
)
unsupported_layers = [
layer.layer_id
for layer in self.layers
if not isinstance(layer.mlp, Qwen2MoeSparseMoeBlock)
or not layer.mlp.supports_deferred_finalize
]
if unsupported_layers:
raise RuntimeError(
"Qwen3.5 FlashInfer MNNVL CuTe DSL fusion currently "
"requires block-FP8 MoE weights with FlashInfer TRTLLM "
"deferred-finalize support on every layer; unsupported "
"layers: "
f"{unsupported_layers}"
)
from sglang.srt.layers.moe.qwen35_flashinfer_fusion import (
Qwen35FlashInferFusionService,
Qwen35FlashInferLayerCommunicator,
)
self.flashinfer_mnnvl_cutedsl_fusion = Qwen35FlashInferFusionService(
hidden_size=config.hidden_size,
top_k=config.num_experts_per_tok,
rms_epsilon=config.rms_norm_eps,
)
for layer in self.layers:
communicator = layer.layer_communicator
if not isinstance(communicator, Qwen35FlashInferLayerCommunicator):
raise RuntimeError(
"Qwen3.5 fusion-enabled layer has the wrong communicator"
)
communicator.fusion_service = self.flashinfer_mnnvl_cutedsl_fusion
logger.info(
"Installed one Qwen3.5 FlashInfer fusion handle for %d layers",
len(self.layers),
)
# Final normalization
if self.pp_group.is_last_rank:
self.norm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
@@ -1490,6 +1641,15 @@ class Qwen3_5ForCausalLM(nn.Module):
def get_input_embeddings(self):
return self.embed_tokens
def prepare_before_cuda_graph_capture(self, model_runner) -> None:
if self.flashinfer_mnnvl_cutedsl_fusion is None:
return
from sglang.srt.layers.moe.qwen35_flashinfer_fusion import (
prepare_qwen35_flashinfer_fusion,
)
prepare_qwen35_flashinfer_fusion(self, model_runner)
def set_dflash_layers_to_capture(self, layers_to_capture: list[int]):
self.layers_to_capture = layers_to_capture
for layer_id in self.layers_to_capture:
@@ -1513,6 +1673,16 @@ class Qwen3_5ForCausalLM(nn.Module):
pp_proxy_tensors: Optional[PPProxyTensors] = None,
input_deepstack_embeds: Optional[torch.Tensor] = None,
) -> Union[torch.Tensor, PPProxyTensors]:
if (
self.flashinfer_mnnvl_cutedsl_fusion is not None
and input_deepstack_embeds is not None
and input_deepstack_embeds.numel() > 0
):
raise RuntimeError(
"Qwen3.5 FlashInfer MNNVL CuTe DSL fusion currently supports "
"the text-only path, not deepstack visual inputs"
)
# Initialize hidden states
if self.pp_group.is_first_rank:
if input_embeds is None:
@@ -1564,12 +1734,62 @@ class Qwen3_5ForCausalLM(nn.Module):
}
)
# Apply final normalization
if hidden_states.shape[0] != 0:
# The final layer has no successor to consume its deferred MoE tail.
trace_final_norm = envs.SGLANG_TRACE_QWEN35_FINAL_NORM.get()
use_native_final_norm = envs.SGLANG_QWEN35_NATIVE_FINAL_NORM.get()
is_deferred_finalize = False
if self.flashinfer_mnnvl_cutedsl_fusion is not None:
from sglang.srt.layers.moe.qwen35_flashinfer_fusion import (
Qwen35MoeFinalizeHandoff,
)
is_deferred_finalize = isinstance(hidden_states, Qwen35MoeFinalizeHandoff)
if is_deferred_finalize:
if residual is None or self.flashinfer_mnnvl_cutedsl_fusion is None:
raise RuntimeError("invalid final deferred MoE handoff")
hidden_states, _ = self.flashinfer_mnnvl_cutedsl_fusion.finalize(
hidden_states, residual, self.norm.gemma_weight
)
elif hidden_states.shape[0] != 0:
if trace_final_norm:
print(
"SGLANG_TRACE_QWEN35_FINAL_NORM "
f"stage=pre_sync_enter hidden={tuple(hidden_states.shape)} "
f"hidden_stride={hidden_states.stride()} "
f"hidden_dtype={hidden_states.dtype} "
f"hidden_contiguous={hidden_states.is_contiguous()} "
f"residual={None if residual is None else tuple(residual.shape)} "
f"native={use_native_final_norm}",
flush=True,
)
torch.cuda.synchronize()
print(
"SGLANG_TRACE_QWEN35_FINAL_NORM stage=pre_sync_returned",
flush=True,
)
if residual is None:
hidden_states = self.norm(hidden_states)
hidden_states = (
self.norm.forward_native(hidden_states)
if use_native_final_norm
else self.norm(hidden_states)
)
else:
hidden_states, _ = self.norm(hidden_states, residual)
hidden_states, _ = (
self.norm.forward_native(hidden_states, residual)
if use_native_final_norm
else self.norm(hidden_states, residual)
)
if trace_final_norm:
print(
"SGLANG_TRACE_QWEN35_FINAL_NORM stage=post_sync_enter",
flush=True,
)
torch.cuda.synchronize()
print(
"SGLANG_TRACE_QWEN35_FINAL_NORM stage=post_sync_returned",
flush=True,
)
if len(aux_hidden_states) == 0:
return hidden_states
@@ -2064,6 +2284,11 @@ class Qwen3_5MoeForConditionalGeneration(Qwen3VLForConditionalGeneration):
def get_hidden_dim(self, module_name: str, layer_idx: int):
return self.model.get_hidden_dim(module_name, layer_idx)
def prepare_before_cuda_graph_capture(self, model_runner) -> None:
prepare = getattr(self.model, "prepare_before_cuda_graph_capture", None)
if prepare is not None:
prepare(model_runner)
def should_apply_lora(self, module_name: str) -> bool:
# Accept all language model layer modules (attention, linear_attn, mlp).
return module_name.startswith("model.layers.")
+17 -5
View File
@@ -154,12 +154,14 @@ class Qwen3_5ForCausalLMMTP(nn.Module):
return self.model.embed_tokens.weight, self.lm_head.weight
def set_embed_and_head(self, embed, head):
del self.model.embed_tokens.weight
if not self.config.tie_word_embeddings:
# A last-stage draft can share only the target lm_head under PP; retain its
# own embedding for the first-stage half it cannot receive.
if embed is not None:
del self.model.embed_tokens.weight
self.model.embed_tokens.weight = embed
if head is not None and not self.config.tie_word_embeddings:
del self.lm_head.weight
self.model.embed_tokens.weight = embed
self.lm_head.weight = head
self.lm_head.weight = head
torch.cuda.empty_cache()
torch.cuda.synchronize()
@@ -214,6 +216,16 @@ class Qwen3_5ForCausalLMMTP(nn.Module):
if not forward_batch.forward_mode.is_idle():
input_embeds = self.pre_fc_norm_embedding(input_embeds)
hidden_states = self.pre_fc_norm_hidden(hidden_states)
# Captured prefill gives padded embeddings but real-height target states;
# place the real rows in an equal-height slot whose padding stays unread.
if hidden_states.shape[0] != input_embeds.shape[0]:
rows = min(hidden_states.shape[0], input_embeds.shape[0])
slot = hidden_states.new_zeros(
(input_embeds.shape[0], hidden_states.shape[1])
)
slot[:rows] = hidden_states[:rows]
hidden_states = slot
hidden_states = torch.cat([input_embeds, hidden_states], dim=-1)
hidden_states = self.fc(hidden_states)
+47 -15
View File
@@ -107,8 +107,36 @@ class Qwen3_5ForCausalLM(nn.Module):
def get_input_embeddings(self) -> nn.Embedding:
return self.model.embed_tokens
def prepare_before_cuda_graph_capture(self, model_runner) -> None:
"""Forward model-owned warmup to the text backbone."""
self.model.prepare_before_cuda_graph_capture(model_runner)
def set_dflash_layers_to_capture(self, layer_ids: list[int]) -> None:
if self.pp_group.world_size > 1:
raise NotImplementedError("DFLASH/DSPARK aux hidden capture requires PP=1.")
num_layers = len(self.model.layers)
if sorted(set(layer_ids)) != list(layer_ids) or not all(
0 <= layer_id < num_layers - 1 for layer_id in layer_ids
):
raise ValueError(
"target_layer_ids must be unique, strictly increasing, and in "
f"[0, {num_layers - 1}); got {layer_ids}"
)
self.capture_aux_hidden_states = True
self.model.set_dflash_layers_to_capture(
[layer_id + 1 for layer_id in layer_ids]
)
def get_embed_and_head(self):
return self.model.embed_tokens.weight, self.lm_head.weight
# PP splits embedding and lm_head across first/last stages; the draft keeps
# its own copy of whichever half its stage cannot receive.
embed = (
None
if isinstance(self.model.embed_tokens, PPMissingLayer)
else self.model.embed_tokens.weight
)
head = None if isinstance(self.lm_head, PPMissingLayer) else self.lm_head.weight
return embed, head
def set_embed_and_head(self, embed, head):
del self.model.embed_tokens.weight
@@ -164,21 +192,25 @@ class Qwen3_5ForCausalLM(nn.Module):
params_dict = dict(self.named_parameters())
loaded_params: Set[str] = set()
body_weights = []
for name, loaded_weight in weights:
if name.startswith(_MODEL_PREFIX):
body_weights.append((name[len(_MODEL_PREFIX) :], loaded_weight))
elif name == "lm_head.weight":
if self.config.tie_word_embeddings:
continue
if "lm_head.weight" not in params_dict:
continue
param = params_dict["lm_head.weight"]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add("lm_head.weight")
# Keep prefix stripping lazy: materializing mmap-backed checkpoint tensors
# across model processes can exceed host memory before GPU copies finish.
def body_weights():
for name, loaded_weight in weights:
if name.startswith(_MODEL_PREFIX):
yield name[len(_MODEL_PREFIX) :], loaded_weight
elif name == "lm_head.weight":
if self.config.tie_word_embeddings:
continue
if "lm_head.weight" not in params_dict:
continue
param = params_dict["lm_head.weight"]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
loaded_params.add("lm_head.weight")
body_loaded = self.model.load_weights(body_weights)
body_loaded = self.model.load_weights(body_weights())
loaded_params.update(f"{_MODEL_PREFIX}{n}" for n in body_loaded)
if self.config.tie_word_embeddings and self.pp_group.is_last_rank:
+3
View File
@@ -497,6 +497,9 @@ class MoeFlags(_FlagGroupBase):
# speculative_moe_backend_context is active, so a draft gate's write also
# lands on the speculative leaf.
in_speculative_scope: bool = False
# Draft construction/execution uses a separate one-sided A2A workspace from
# the target model's concurrently live CUDA graphs.
speculative_context: bool = False
@dataclasses.dataclass
+2 -2
View File
@@ -1743,9 +1743,9 @@ class ServerArgs:
bf16_gemm_backend: A[
str,
Arg(
help="Choose the backend for unquantized BF16 GEMM operations. Options: 'auto' (default; selects 'cutedsl' on SM10x GPUs, except deterministic inference selects 'torch'; otherwise uses cuBLAS via torch.nn.functional.linear), 'cutedsl' (SGLang JIT CuTe DSL TGV BF16 GEMM on SM10x; dispatches between the CuTe DSL kernel and cuBLAS), 'torch' (always uses cuBLAS via torch.nn.functional.linear).",
help="Choose the backend for unquantized BF16 GEMM operations. Options: 'auto' (default; selects 'cutedsl' on SM10x GPUs, except deterministic inference selects 'torch'; otherwise uses cuBLAS via torch.nn.functional.linear), 'cutedsl' (SGLang JIT CuTe DSL TGV BF16 GEMM on SM10x; dispatches between the allowlisted low-M Split-K kernel, the CuTe DSL kernel, and cuBLAS; set SGLANG_ENABLE_BF16_SPLITK_GEMM=0 to disable Split-K), 'flashinfer_pr4266' (legacy compatibility alias for the optimized CuTe DSL path), 'gemv', 'torch' (always uses cuBLAS via torch.nn.functional.linear).",
cli_name="--bf16-gemm-backend",
choices=["auto", "cutedsl", "gemv", "torch"],
choices=["auto", "cutedsl", "flashinfer_pr4266", "gemv", "torch"],
),
NS("exec.kernel"),
] = "auto"
@@ -306,7 +306,7 @@ class DFlashWorkerV2(BaseSpecWorker):
bundle = build_draft_tp_worker(
server_args=server_args,
gpu_id=gpu_id,
ps=replace(ps, pp_rank=0),
ps=replace(ps, pp_rank=0, pp_size=1),
nccl_port=nccl_port,
target_model_config=target_worker.model_runner.model_config,
algo_label="DFLASH",
@@ -1662,13 +1662,16 @@ class DFlashWorkerV2(BaseSpecWorker):
batch: ScheduleBatch,
on_publish=None,
grammar_barrier=None,
pp_proxy_tensors=None,
) -> GenerationBatchResult:
self._validate_phase1_sampling_support(batch)
if batch.forward_mode.is_extend() or batch.is_extend_in_batch:
# Target prefill: capture DFlash aux hidden states for prompt tokens.
batch_output = self.target_worker.forward_batch_generation(
batch, capture_hidden_mode=CaptureHiddenMode.FULL
batch,
pp_proxy_tensors=pp_proxy_tensors,
capture_hidden_mode=CaptureHiddenMode.FULL,
)
logits_output, next_token_ids = (
@@ -131,7 +131,7 @@ class DSparkWorkerV2(BaseSpecWorker):
bundle = build_draft_tp_worker(
server_args=server_args,
gpu_id=gpu_id,
ps=replace(ps, pp_rank=0),
ps=replace(ps, pp_rank=0, pp_size=1),
nccl_port=nccl_port,
target_model_config=target_worker.model_runner.model_config,
algo_label="DSPARK",
+17 -3
View File
@@ -485,13 +485,27 @@ def get_draft_input_from_target_hidden_dim(model_runner: ModelRunner) -> int:
return target_hidden * num_aux
def get_draft_recurrent_hidden_state_spec_from_config(
model_config, spec_algorithm
) -> tuple[Optional[int], Optional[torch.dtype]]:
"""Return hidden_states width/dtype carried between draft decode steps.
Config-only so callers without a draft runner can reach it: prefill-side PP
builds the draft on the last stage alone, but the PD metadata wire schema it
feeds has to come out identical on every rank.
"""
if spec_algorithm.is_standalone():
return None, None
return model_config.spec_hidden_size, model_config.dtype
def get_draft_recurrent_hidden_state_spec(
model_runner: ModelRunner,
) -> tuple[Optional[int], Optional[torch.dtype]]:
"""Return hidden_states width/dtype carried between draft decode steps."""
if model_runner.spec_algorithm.is_standalone():
return None, None
return model_runner.model_config.spec_hidden_size, model_runner.model_config.dtype
return get_draft_recurrent_hidden_state_spec_from_config(
model_runner.model_config, model_runner.spec_algorithm
)
def eagle_prepare_for_verify(
@@ -7,6 +7,7 @@ from typing import List, Optional
import torch
from sglang.kernels.ops.speculative.topk1 import draft_topk1_postprocess
from sglang.srt.distributed import get_pp_group
from sglang.srt.distributed.parallel_state_wrapper import ParallelState
from sglang.srt.environ import envs
from sglang.srt.hardware_backend.npu.graph_runner.eagle_draft_extend_npu_graph_runner import (
@@ -87,6 +88,7 @@ from sglang.srt.speculative.eagle_worker_common import (
)
from sglang.srt.speculative.spec_info import SpeculativeAlgorithm
from sglang.srt.speculative.spec_utils import (
draft_pp_context,
draft_tp_context,
fast_sample,
get_plan_stream,
@@ -168,16 +170,17 @@ class EagleDraftWorker(EagleDraftWorkerBase):
ctx = empty_context()
with (
ctx
), speculative_moe_backend_context(), speculative_moe_a2a_backend_context(), draft_model_build_scope():
), draft_pp_context(), speculative_moe_backend_context(), speculative_moe_a2a_backend_context(), draft_model_build_scope():
self.draft_worker = TpModelWorker(
server_args=server_args,
gpu_id=gpu_id,
# spec workers don't support pipeline parallelism
ps=replace(ps, pp_rank=0),
ps=replace(ps, pp_rank=0, pp_size=1),
nccl_port=nccl_port,
is_draft_worker=True,
# The draft runs at absolute target positions.
context_length=target_worker.model_runner.model_config.context_len,
random_seed=target_worker.random_seed,
)
# Alias for better readability
@@ -314,7 +317,7 @@ class EagleDraftWorker(EagleDraftWorkerBase):
)
else:
if self.hot_token_id is not None:
if self.hot_token_id is not None and head is not None:
head = head.clone()
self.hot_token_id = self.hot_token_id.to(head.device)
head.data = head.data[self.hot_token_id]
@@ -1078,17 +1081,24 @@ class EAGLEWorkerV2(BaseSpecWorker):
get_spec().speculative_algorithm
)
self._draft_worker = EagleDraftWorker(
server_args,
gpu_id,
ps,
nccl_port,
target_worker,
# Only the last PP stage runs the draft; other EAGLEWorkerV2 instances
# return proxies so scheduler dispatch remains rank-uniform.
self._hosts_draft = get_pp_group().is_last_rank
self._draft_worker = (
EagleDraftWorker(
server_args,
gpu_id,
ps,
nccl_port,
target_worker,
)
if self._hosts_draft
else None
)
# Adaptive speculative
self.adaptive_controller: Optional[AdaptiveController] = None
if get_spec().speculative_adaptive:
if get_spec().speculative_adaptive and self._hosts_draft:
self.adaptive_controller = AdaptiveController(
self,
config_path=get_spec().speculative_adaptive_config,
@@ -1151,7 +1161,11 @@ class EAGLEWorkerV2(BaseSpecWorker):
)
def forward_batch_generation(
self, batch: ScheduleBatch, on_publish=None, grammar_barrier=None
self,
batch: ScheduleBatch,
on_publish=None,
grammar_barrier=None,
pp_proxy_tensors=None,
):
if batch.forward_mode.is_extend() or batch.is_extend_in_batch:
# Target prefill
@@ -1161,7 +1175,9 @@ class EAGLEWorkerV2(BaseSpecWorker):
else CaptureHiddenMode.FULL
)
batch_output = self.target_worker.forward_batch_generation(
batch, capture_hidden_mode=target_capture_mode
batch,
pp_proxy_tensors=pp_proxy_tensors,
capture_hidden_mode=target_capture_mode,
)
# Spec_v2 convention: batch.seq_lens = length BEFORE this iter's tokens.
@@ -1171,6 +1187,11 @@ class EAGLEWorkerV2(BaseSpecWorker):
if on_publish is not None:
on_publish(batch_output.new_seq_lens)
# A rank that does not host the draft (prefill-side PP builds it only on
# the last stage) forwards the target's proxy tensors and stops here.
if self._draft_worker is None:
return batch_output
# Draft prefill
with (
self.draft_worker.draft_tp_context(
@@ -143,7 +143,7 @@ class FrozenKVMTPDraftWorker(EagleDraftWorkerBase, TpModelWorker):
server_args=server_args,
gpu_id=gpu_id,
# spec workers don't support pipeline parallelism
ps=replace(ps, pp_rank=0),
ps=replace(ps, pp_rank=0, pp_size=1),
nccl_port=nccl_port,
is_draft_worker=True,
# The draft runs at absolute target positions.
@@ -164,7 +164,7 @@ class MultiLayerEagleDraftWorker(EagleDraftWorkerBase):
server_args=server_args,
gpu_id=gpu_id,
# spec workers don't support pipeline parallelism
ps=replace(ps, pp_rank=0),
ps=replace(ps, pp_rank=0, pp_size=1),
nccl_port=nccl_port,
is_draft_worker=True,
is_multi_layer_eagle=True,
@@ -427,7 +427,7 @@ class NGRAMWorker(BaseSpecWorker):
self.ngram_corpus.batch_put(batch_tokens)
def forward_batch_generation(
self, batch: ScheduleBatch, on_publish=None
self, batch: ScheduleBatch, on_publish=None, pp_proxy_tensors=None
) -> GenerationBatchResult:
fwd_stream = torch.get_device_module(self.device).current_stream()
record_stream_for_v2_verify(batch, None, fwd_stream)
@@ -442,7 +442,7 @@ class NGRAMWorker(BaseSpecWorker):
if batch.forward_mode.is_target_verify():
batch_result = self.target_worker.forward_batch_generation(
batch, is_verify=True
batch, pp_proxy_tensors=pp_proxy_tensors, is_verify=True
)
logits_output, can_run_cuda_graph = (
@@ -528,7 +528,9 @@ class NGRAMWorker(BaseSpecWorker):
batch.forward_mode = ForwardMode.DECODE
else:
batch_result = self.target_worker.forward_batch_generation(batch)
batch_result = self.target_worker.forward_batch_generation(
batch, pp_proxy_tensors=pp_proxy_tensors
)
logits_output, predict, can_run_cuda_graph = (
batch_result.logits_output,
batch_result.next_token_ids,
@@ -35,6 +35,8 @@ from sglang.srt.configs.hybrid_arch import mambaish_config
from sglang.srt.constrained.base_grammar_backend import GrammarMask
from sglang.srt.distributed.parallel_state import (
GroupCoordinator,
get_self_pp_group,
patch_pipeline_parallel_group,
patch_tensor_parallel_group,
)
from sglang.srt.environ import envs
@@ -676,6 +678,14 @@ def load_token_map(token_map_path: str) -> List[int]:
return torch.tensor(hot_token_id, dtype=torch.int64)
@contextmanager
def draft_pp_context():
# The draft model is one layer and never spans pipeline stages; give it a
# single-member pp group so it initializes as if pp were off.
with patch_pipeline_parallel_group(get_self_pp_group()):
yield
@contextmanager
def draft_tp_context(tp_group: GroupCoordinator):
# Draft model doesn't use dp and has its own tp group.
@@ -84,7 +84,7 @@ class StandaloneDraftWorker(EagleDraftWorker):
server_args=server_args,
gpu_id=gpu_id,
# spec workers don't support pipeline parallelism
ps=replace(ps, pp_rank=0),
ps=replace(ps, pp_rank=0, pp_size=1),
nccl_port=nccl_port,
is_draft_worker=True,
# The draft runs at absolute target positions.
@@ -0,0 +1,64 @@
import unittest
import torch
from sglang.srt.layers.attention.mamba.replay_state_indices_validator import (
validate_replay_state_indices_cpu,
)
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
class TestReplayStateIndicesValidator(unittest.TestCase):
def test_valid_unique_live_slots_and_padding(self):
validate_replay_state_indices_cpu(
torch.tensor([0, 2, 9, -1, -1], dtype=torch.int32),
valid_bs=3,
total_bs=5,
num_state_slots=10,
)
def test_rejects_duplicate_live_slot(self):
with self.assertRaisesRegex(AssertionError, r"duplicate_slots=\[7\]"):
validate_replay_state_indices_cpu(
torch.tensor([7, 2, 7, -1], dtype=torch.int32),
valid_bs=3,
total_bs=4,
num_state_slots=10,
)
def test_rejects_out_of_range_live_slots(self):
for bad_slot in (-1, -2, 10):
with self.subTest(bad_slot=bad_slot):
with self.assertRaisesRegex(AssertionError, "live rows"):
validate_replay_state_indices_cpu(
torch.tensor([3, bad_slot, -1], dtype=torch.int64),
valid_bs=2,
total_bs=3,
num_state_slots=10,
)
def test_rejects_non_sentinel_padding(self):
with self.assertRaisesRegex(AssertionError, "padded rows"):
validate_replay_state_indices_cpu(
torch.tensor([3, 5, 5], dtype=torch.int32),
valid_bs=2,
total_bs=3,
num_state_slots=10,
)
def test_requires_cpu_tensor(self):
if not torch.cuda.is_available():
self.skipTest("CUDA is unavailable")
with self.assertRaisesRegex(ValueError, "copied to CPU"):
validate_replay_state_indices_cpu(
torch.tensor([1], dtype=torch.int32, device="cuda"),
valid_bs=1,
total_bs=1,
num_state_slots=2,
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,392 @@
import unittest
import torch
from sglang.kernels.ops.attention.triton_gdn_fused_proj import (
can_use_fused_qkvzba_causal_conv1d_update_contiguous,
fused_qkvzba_causal_conv1d_update_contiguous,
fused_qkvzba_split_reshape_cat_contiguous,
)
# This is also the update implementation imported directly by GDNBackend on
# CUDA; the presence of the optional sgl_kernel AOT extension does not reroute
# GDN decode through srt.layers.attention.mamba.causal_conv1d.
from sglang.kernels.ops.mamba.causal_conv1d_triton import causal_conv1d_update
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=8, stage="base-b", runner_config="1-gpu-large")
def _reference(
qkvz,
ba,
state,
weight,
bias,
indices,
*,
qkv_dim,
v_dim,
num_v_heads,
head_v_dim,
activation,
):
qkv = qkvz[:, :qkv_dim]
out = torch.empty_like(qkv)
state_out = state.clone()
width = weight.shape[1]
for row, slot_tensor in enumerate(indices.cpu()):
slot = int(slot_tensor)
if slot < 0 or slot >= state.shape[0]:
out[row].copy_(qkv[row])
continue
# The deployed direct-Triton decode wrapper uses an effective
# state_len=width-1 even if the physical cache envelope is wider.
history = state[slot, :, : width - 1].float()
values = torch.cat((history, qkv[row, :, None].float()), dim=-1)
acc = (values * weight.float()).sum(dim=-1)
if bias is not None:
acc = acc + bias.float()
if activation in ("silu", "swish"):
acc = torch.nn.functional.silu(acc)
out[row].copy_(acc.to(qkv.dtype))
if width > 2:
state_out[slot, :, : width - 2].copy_(state[slot, :, 1 : width - 1])
state_out[slot, :, width - 2].copy_(qkv[row])
z = qkvz[:, qkv_dim:].reshape(-1, num_v_heads, head_v_dim).contiguous()
b, a = ba.split([num_v_heads, num_v_heads], dim=-1)
return out, z, b.contiguous(), a.contiguous(), state_out
@unittest.skipIf(not torch.cuda.is_available(), "CUDA is required")
class TestGDNDecodeFusedProjectionConv1D(unittest.TestCase):
def test_contiguous_unpack_ratio8_microbenchmark_baseline(self):
batch = 9
num_qk_heads = 1
num_v_heads = 8
head_dim = 128
qkv_dim = (2 * num_qk_heads + num_v_heads) * head_dim
v_dim = num_v_heads * head_dim
qkvz = torch.randn(
batch,
qkv_dim + v_dim,
device="cuda",
dtype=torch.bfloat16,
)
ba = torch.randn(
batch,
2 * num_v_heads,
device="cuda",
dtype=torch.bfloat16,
)
mixed_qkv, z, b, a = fused_qkvzba_split_reshape_cat_contiguous(
qkvz,
ba,
num_qk_heads,
num_v_heads,
head_dim,
head_dim,
)
torch.testing.assert_close(mixed_qkv, qkvz[:, :qkv_dim])
torch.testing.assert_close(
z, qkvz[:, qkv_dim:].reshape(batch, num_v_heads, head_dim)
)
torch.testing.assert_close(b, ba[:, :num_v_heads])
torch.testing.assert_close(a, ba[:, num_v_heads:])
def _run_case(
self,
*,
batch,
q_dim,
k_dim,
v_dim,
num_v_heads,
head_v_dim,
width,
state_len,
dtype,
with_bias,
activation,
strided_state=False,
with_padding=False,
):
torch.manual_seed(17)
device = "cuda"
qkv_dim = q_dim + k_dim + v_dim
qkvz = torch.randn(batch, qkv_dim + v_dim, device=device, dtype=dtype)
ba = torch.randn(batch, 2 * num_v_heads, device=device, dtype=dtype)
weight = torch.randn(qkv_dim, width, device=device, dtype=dtype) * 0.1
bias = (
torch.randn(qkv_dim, device=device, dtype=dtype) * 0.1
if with_bias
else None
)
slots = batch + 3
if strided_state:
backing = torch.randn(
slots,
state_len,
qkv_dim * 2,
device=device,
dtype=dtype,
)
state = backing[:, :, ::2].transpose(1, 2)
self.assertFalse(state.is_contiguous())
else:
state = torch.randn(slots, qkv_dim, state_len, device=device, dtype=dtype)
indices = torch.randperm(slots, device=device, dtype=torch.int64)[:batch]
if with_padding:
indices[-1] = -1
indices = indices.to(torch.int32)
ref = _reference(
qkvz,
ba,
state,
weight,
bias,
indices,
qkv_dim=qkv_dim,
v_dim=v_dim,
num_v_heads=num_v_heads,
head_v_dim=head_v_dim,
activation=activation,
)
state_test = state.clone(memory_format=torch.preserve_format)
out, z, b, a = fused_qkvzba_causal_conv1d_update_contiguous(
qkvz,
ba,
state_test,
weight,
bias,
indices,
qkv_dim=qkv_dim,
v_dim=v_dim,
num_v_heads=num_v_heads,
head_v_dim=head_v_dim,
activation=activation,
)
torch.cuda.synchronize()
atol = 2e-2 if dtype == torch.bfloat16 else 3e-3
output_max_diff = (out.float() - ref[0].float()).abs().max().item()
state_max_diff = (state_test.float() - ref[4].float()).abs().max().item()
print(
"case "
f"B={batch} QKV={qkv_dim} V={v_dim} W={width} "
f"dtype={dtype} output_max_diff={output_max_diff:.8g} "
f"state_max_diff={state_max_diff:.8g}",
flush=True,
)
torch.testing.assert_close(out, ref[0], rtol=0, atol=atol)
torch.testing.assert_close(z, ref[1], rtol=0, atol=0)
torch.testing.assert_close(b, ref[2], rtol=0, atol=0)
torch.testing.assert_close(a, ref[3], rtol=0, atol=0)
torch.testing.assert_close(state_test, ref[4], rtol=0, atol=0)
self.assertEqual(b.data_ptr() % 32, 0)
self.assertEqual(a.data_ptr() % 32, 0)
def test_random_shapes_widths_dtypes_and_state_updates(self):
cases = (
# Small boundary shapes.
dict(
batch=1,
q_dim=16,
k_dim=16,
v_dim=32,
num_v_heads=2,
head_v_dim=16,
width=2,
state_len=1,
dtype=torch.float16,
with_bias=False,
activation=None,
),
dict(
batch=17,
q_dim=32,
k_dim=32,
v_dim=64,
num_v_heads=4,
head_v_dim=16,
width=3,
state_len=5,
dtype=torch.bfloat16,
with_bias=True,
activation="silu",
strided_state=True,
with_padding=True,
),
# Qwen3.5-35B TP16 local GDN dimensions.
dict(
batch=32,
q_dim=128,
k_dim=128,
v_dim=256,
num_v_heads=2,
head_v_dim=128,
width=4,
state_len=3,
dtype=torch.bfloat16,
with_bias=False,
activation="silu",
),
# Large TP-local GDN dimensions with an 8:1 value/key head ratio.
dict(
batch=8,
q_dim=128,
k_dim=128,
v_dim=1024,
num_v_heads=8,
head_v_dim=128,
width=4,
state_len=3,
dtype=torch.bfloat16,
with_bias=False,
activation="silu",
),
)
for case in cases:
with self.subTest(case=case):
self._run_case(**case)
def test_cuda_graph_replay(self):
batch = 4
qkv_dim, v_dim, num_v_heads, head_v_dim = 128, 64, 2, 32
qkvz = torch.randn(batch, qkv_dim + v_dim, device="cuda", dtype=torch.bfloat16)
ba = torch.randn(batch, 2 * num_v_heads, device="cuda", dtype=torch.bfloat16)
weight = torch.randn(qkv_dim, 4, device="cuda", dtype=torch.bfloat16)
state = torch.randn(batch + 1, qkv_dim, 3, device="cuda", dtype=torch.bfloat16)
initial_state = state.clone()
indices = torch.arange(batch, device="cuda", dtype=torch.int32)
# Compile before capture; Triton compilation itself is not graph-safe.
fused_qkvzba_causal_conv1d_update_contiguous(
qkvz,
ba,
state,
weight,
None,
indices,
qkv_dim=qkv_dim,
v_dim=v_dim,
num_v_heads=num_v_heads,
head_v_dim=head_v_dim,
activation="silu",
)
state.copy_(initial_state)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
captured = fused_qkvzba_causal_conv1d_update_contiguous(
qkvz,
ba,
state,
weight,
None,
indices,
qkv_dim=qkv_dim,
v_dim=v_dim,
num_v_heads=num_v_heads,
head_v_dim=head_v_dim,
activation="silu",
)
state.copy_(initial_state)
graph.replay()
ref_state = initial_state.clone()
ref_qkv, ref_z, ref_b, ref_a = fused_qkvzba_split_reshape_cat_contiguous(
qkvz,
ba,
1,
num_v_heads,
32,
head_v_dim,
)
ref_qkv = causal_conv1d_update(
ref_qkv,
ref_state,
weight,
None,
"silu",
conv_state_indices=indices,
)
torch.testing.assert_close(captured[0], ref_qkv, rtol=0, atol=0)
torch.testing.assert_close(captured[1], ref_z, rtol=0, atol=0)
torch.testing.assert_close(captured[2], ref_b, rtol=0, atol=0)
torch.testing.assert_close(captured[3], ref_a, rtol=0, atol=0)
torch.testing.assert_close(state, ref_state, rtol=0, atol=0)
def test_out_of_range_state_slots_are_safely_masked(self):
torch.manual_seed(29)
batch = 4
qkv_dim, v_dim, num_v_heads, head_v_dim = 128, 64, 2, 32
qkvz = torch.randn(batch, qkv_dim + v_dim, device="cuda", dtype=torch.bfloat16)
ba = torch.randn(batch, 2 * num_v_heads, device="cuda", dtype=torch.bfloat16)
weight = torch.randn(qkv_dim, 4, device="cuda", dtype=torch.bfloat16)
state = torch.randn(3, qkv_dim, 3, device="cuda", dtype=torch.bfloat16)
# -1 is the expected padding sentinel; -2 and len(state) exercise the
# hard lower/upper bounds. Slot 1 remains a normal live update.
indices = torch.tensor([-2, -1, state.shape[0], 1], device="cuda")
indices = indices.to(torch.int32)
ref = _reference(
qkvz,
ba,
state,
weight,
None,
indices,
qkv_dim=qkv_dim,
v_dim=v_dim,
num_v_heads=num_v_heads,
head_v_dim=head_v_dim,
activation="silu",
)
state_test = state.clone()
out, z, b, a = fused_qkvzba_causal_conv1d_update_contiguous(
qkvz,
ba,
state_test,
weight,
None,
indices,
qkv_dim=qkv_dim,
v_dim=v_dim,
num_v_heads=num_v_heads,
head_v_dim=head_v_dim,
activation="silu",
)
torch.cuda.synchronize()
torch.testing.assert_close(out, ref[0], rtol=0, atol=2e-2)
torch.testing.assert_close(z, ref[1], rtol=0, atol=0)
torch.testing.assert_close(b, ref[2], rtol=0, atol=0)
torch.testing.assert_close(a, ref[3], rtol=0, atol=0)
torch.testing.assert_close(state_test, ref[4], rtol=0, atol=0)
def test_fp8_activation_is_an_explicit_fallback(self):
if not hasattr(torch, "float8_e4m3fn"):
self.skipTest("PyTorch has no FP8 dtype")
qkvz = torch.empty(1, 96, device="cuda", dtype=torch.float8_e4m3fn)
ba = torch.empty(1, 4, device="cuda", dtype=torch.bfloat16)
state = torch.empty(2, 64, 3, device="cuda", dtype=torch.bfloat16)
weight = torch.empty(64, 4, device="cuda", dtype=torch.bfloat16)
indices = torch.zeros(1, device="cuda", dtype=torch.int32)
eligible, reason = can_use_fused_qkvzba_causal_conv1d_update_contiguous(
qkvz,
ba,
state,
weight,
None,
indices,
qkv_dim=64,
v_dim=32,
num_v_heads=2,
activation="silu",
)
self.assertFalse(eligible)
self.assertIn("FP16, BF16, or FP32", reason)
if __name__ == "__main__":
unittest.main()
@@ -1,6 +1,7 @@
import random
import sys
from contextlib import nullcontext
from types import SimpleNamespace
import pytest
import torch
@@ -159,6 +160,42 @@ def test_compact_all_tokens_uses_tight_routing_independent_bound(
)
def test_compact_eager_keeps_masked_layout_for_cuda_graph(monkeypatch):
config = MoeRunnerConfig(
num_experts=128,
num_local_experts=16,
hidden_size=2048,
intermediate_size_per_partition=4096,
top_k=4,
activation="silu",
is_gated=True,
inplace=False,
)
monkeypatch.setattr(
deep_gemm_runner.envs.SGLANG_OPT_DG_COMPACT_EAGER, "get", lambda: True
)
capture = SimpleNamespace(disable_dispose_tensor=False)
monkeypatch.setattr(
deep_gemm_runner, "get_flags", lambda: SimpleNamespace(capture=capture)
)
hidden_states = torch.empty((128, 2048), device="meta")
quant_info = DeepGemmMoeQuantInfo(
w13_weight=torch.empty((1, 4096, 1), dtype=torch.float8_e4m3fn),
w2_weight=torch.empty((1, 2048, 1), dtype=torch.float8_e4m3fn),
use_fp8=True,
block_shape=[128, 128],
)
with envs.SGLANG_DEEPGEMM_STANDARD_LAYOUT.override("masked"):
assert not deep_gemm_runner._should_use_masked_standard_layout(
config, quant_info, hidden_states
)
capture.disable_dispose_tensor = True
assert deep_gemm_runner._should_use_masked_standard_layout(
config, quant_info, hidden_states
)
def test_standard_layout_auto_memory_policy(monkeypatch):
config = MoeRunnerConfig(
num_experts=512,
@@ -434,6 +434,41 @@ class TestEagleDsaSeedTransfer(unittest.TestCase):
self.assertEqual(future_map.dsa_topk_indices_buf.shape, (4, 3))
self.assertEqual(future_map.dsa_topk_indices_buf.dtype, torch.int32)
@patch(
"sglang.srt.speculative.spec_utils.spec_need_hidden_states",
return_value=False,
)
def test_future_map_initializes_topk_after_prefill_payload(self, _):
future_map = object.__new__(FutureMap)
future_map.spec_algo = SimpleNamespace(
is_some=Mock(return_value=True),
need_topk=Mock(return_value=True),
)
future_map.req_pool_size = 4
future_map.device = "cpu"
future_map.need_topk = False
future_map.need_hidden_states = False
future_map.topk_p_buf = None
future_map.topk_index_buf = None
future_map.hidden_states_buf = None
future_map.draft_probs_buf = None
future_map._maybe_init_forward_bufs(
RelayPayload(bonus_tokens=torch.zeros((2,), dtype=torch.int64))
)
self.assertFalse(future_map.need_topk)
future_map._maybe_init_forward_bufs(
RelayPayload(
bonus_tokens=torch.zeros((2,), dtype=torch.int64),
topk_p=torch.zeros((2, 3), dtype=torch.float32),
topk_index=torch.zeros((2, 3), dtype=torch.int64),
)
)
self.assertTrue(future_map.need_topk)
self.assertEqual(future_map.topk_p_buf.shape, (4, 3))
self.assertEqual(future_map.topk_index_buf.shape, (4, 3))
class TestDSV4C128StateIndices(unittest.TestCase):
def test_online_aligned_boundary_has_no_partial_state(self):
@@ -0,0 +1,284 @@
"""Unit tests for full-attention KV transfer with prefill pp_size > 1 on
hybrid-linear models (HybridLinearKVPool)."""
import unittest
from types import SimpleNamespace
import numpy as np
from sglang.srt.disaggregation.common.conn import CommonKVManager
from sglang.srt.disaggregation.mooncake.conn import MooncakeKVManager
from sglang.srt.disaggregation.prefill import _transfer_start_layer
from sglang.srt.disaggregation.utils import (
build_kv_layer_ids,
build_transfer_entry_pairs,
)
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=10, suite="base-a-test-cpu")
def _full_attention_ids(*, num_layers: int, interval: int) -> list:
return [i for i in range(num_layers) if i % interval == interval - 1]
def _hybrid_pool(*, start_layer: int) -> HybridLinearKVPool:
pool = HybridLinearKVPool.__new__(HybridLinearKVPool)
pool.start_layer = start_layer
return pool
class TestTransferStartLayer(CustomTestCase):
"""Bug regression: with prefill pp_size=2 on a 60-layer hybrid-linear model
(full_attention_interval=4), stage 1's pool.start_layer is 30 — a global
layer index counting linear layers. The decode peer's KV pointer list is
dense over the 15 full-attention layers only, so slicing dst[30:38] yielded
[] and an IndexError in mooncake send_kvcache_slice. The transfer offset
must be the count of full-attention layers before the stage boundary."""
def test_hybrid_stage1_translates_to_full_attention_offset(self):
cfg = SimpleNamespace(
full_attention_layer_ids=_full_attention_ids(num_layers=60, interval=4)
)
self.assertEqual(
_transfer_start_layer(
pool=_hybrid_pool(start_layer=30), hf_text_config=cfg
),
7,
)
def test_hybrid_stage0_is_zero(self):
cfg = SimpleNamespace(
full_attention_layer_ids=_full_attention_ids(num_layers=60, interval=4)
)
self.assertEqual(
_transfer_start_layer(pool=_hybrid_pool(start_layer=0), hf_text_config=cfg),
0,
)
def test_non_hybrid_pool_keeps_global_start_layer(self):
cfg = SimpleNamespace(full_attention_layer_ids=[])
self.assertEqual(
_transfer_start_layer(
pool=SimpleNamespace(start_layer=30), hf_text_config=cfg
),
30,
)
class _RecordingKVManager:
get_mha_kv_ptrs_with_pp = CommonKVManager.get_mha_kv_ptrs_with_pp
def __init__(self, *, prefill_start_layer: int, pp_size: int):
self.is_mla_backend = False
self.is_hybrid_mla_backend = False
self.enable_custom_mem_pool = False
self.pp_size = pp_size
self.kv_args = SimpleNamespace(prefill_start_layer=prefill_start_layer)
self.blocks = []
def _transfer_data(self, mooncake_session_id, transfer_blocks):
self.blocks.extend(transfer_blocks)
return 0
class TestHybridSendUsesLayerIdPairing(CustomTestCase):
"""Bug regression: a hybrid-linear (non-MLA-flagged) backend fell into the
positional MHA slicing path of _send_kvcache_generic even when both peers
published layer ids. For a stage with F full-attention layers against a
decode peer with N (F < N, F not dividing N), the draft-KV modulo heuristic
silently placed the V block at F * (N // F) instead of N wrong layers
transferred, no error. With layer ids published on both sides the pairing
must be exact."""
def _run_case(
self, *, model_full_ids: list, stage_full_ids: list, start_offset: int
):
num_stage = len(stage_full_ids)
num_model = len(model_full_ids)
src_ptrs = [1000 + i for i in range(2 * num_stage)]
dst_ptrs = [2000 + i for i in range(2 * num_model)]
item_lens = [10 + i for i in range(2 * num_stage)]
manager = _RecordingKVManager(prefill_start_layer=start_offset, pp_size=2)
rc = MooncakeKVManager._send_kvcache_generic(
manager,
mooncake_session_id="session",
src_data_ptrs=src_ptrs,
dst_data_ptrs=dst_ptrs,
item_lens=item_lens,
prefill_data_indices=np.array([0], dtype=np.int32),
dst_data_indices=np.array([0], dtype=np.int32),
executor=None,
src_layer_ids=stage_full_ids * 2,
dst_layer_ids=model_full_ids * 2,
)
self.assertEqual(rc, 0)
expected = [
(src_ptrs[i], dst_ptrs[start_offset + i], item_lens[i])
for i in range(num_stage)
] + [
(
src_ptrs[num_stage + i],
dst_ptrs[num_model + start_offset + i],
item_lens[num_stage + i],
)
for i in range(num_stage)
]
self.assertEqual(manager.blocks, expected)
def test_stage1_f8_of_n15(self):
ids = _full_attention_ids(num_layers=60, interval=4)
self._run_case(model_full_ids=ids, stage_full_ids=ids[7:], start_offset=7)
def test_stage0_f7_of_n15(self):
ids = _full_attention_ids(num_layers=60, interval=4)
self._run_case(model_full_ids=ids, stage_full_ids=ids[:7], start_offset=0)
def test_f5_of_n12(self):
ids = _full_attention_ids(num_layers=48, interval=4)
self._run_case(model_full_ids=ids, stage_full_ids=ids[:5], start_offset=0)
class TestGetMhaKvPtrsWithPp(CustomTestCase):
"""Derived property: the modulo heuristic in get_mha_kv_ptrs_with_pp exists
for the decode-has-draft-KV layout [K_main, V_main, draft_K, draft_V]. Pin
that geometry (15 main + 1 draft layer) so a future rewrite of the
heuristic (e.g. to fix the plain-MHA pp>1 F-not-dividing-N case) keeps the
draft case intact."""
def test_draft_kv_geometry_selects_main_v_block(self):
manager = SimpleNamespace(kv_args=SimpleNamespace(prefill_start_layer=0))
src_kv_ptrs = list(range(30))
dst_kv_ptrs = list(range(100, 132))
src_k, src_v, dst_k, dst_v, num_layers = (
CommonKVManager.get_mha_kv_ptrs_with_pp(manager, src_kv_ptrs, dst_kv_ptrs)
)
self.assertEqual(src_k, src_kv_ptrs[:15])
self.assertEqual(src_v, src_kv_ptrs[15:])
self.assertEqual(dst_k, dst_kv_ptrs[:15])
self.assertEqual(dst_v, dst_kv_ptrs[15:30])
self.assertEqual(num_layers, 15)
class TestBuildTransferEntryPairsDuplicateIds(CustomTestCase):
"""Derived property: layer ids repeat across the K and V tensor groups, so
pairing must consume dst occurrences in order (K with K, V with V) rather
than by plain id lookup."""
def test_k_then_v_occurrence_ordering(self):
pairs = build_transfer_entry_pairs(
src_layer_ids=[3, 7, 3, 7],
dst_layer_ids=[3, 7, 11, 3, 7, 11],
n_src=4,
n_dst=6,
allow_positional_fallback=False,
)
self.assertEqual(pairs, [(0, 0), (1, 1), (2, 3), (3, 4)])
def _hybrid_pool_with_ids(*, layer_ids: list) -> HybridLinearKVPool:
pool = HybridLinearKVPool.__new__(HybridLinearKVPool)
pool.full_attention_layer_id_mapping = layer_ids
pool.use_mla = False
return pool
class TestBuildKvLayerIds(CustomTestCase):
"""Bug regression: enabling EAGLE appended draft KV buffers to kv_data_ptrs
while kv_layer_ids described only the target's entries, so the ids were
suppressed entirely and the transfer fell back to positional slicing. Under
prefill pp_size > 1 that slices the wrong layers -- prefill pp=2 + EAGLE
produced garbled decode output while pp=1 + EAGLE did not."""
def _stage1_ids(self) -> list:
full = _full_attention_ids(num_layers=60, interval=4)
return [lid for lid in full if lid >= 30]
def test_draft_entries_get_a_reserved_band_above_the_target_range(self):
"""A draft pool that only reports a layer count, not ids."""
ids = build_kv_layer_ids(
token_to_kv_pool=_hybrid_pool_with_ids(layer_ids=self._stage1_ids()),
draft_token_to_kv_pool=SimpleNamespace(layer_num=1),
num_draft_entries=2,
num_hidden_layers=60,
)
stage1 = self._stage1_ids()
# k0..k(L-1) then v0..v(L-1) per pool, and the pools are concatenated --
# so the band repeats per group after the target's ids, not interleaved.
self.assertEqual(ids, stage1 + stage1 + [60, 60])
def test_hybrid_draft_pool_is_remapped_out_of_the_target_range(self):
"""The EAGLE draft pool for a hybrid-linear model is itself a
HybridLinearKVPool that numbers its single MTP layer from zero, so its
raw ids collide with target layer 0 and must be remapped into the band."""
ids = build_kv_layer_ids(
token_to_kv_pool=_hybrid_pool_with_ids(layer_ids=self._stage1_ids()),
draft_token_to_kv_pool=_hybrid_pool_with_ids(layer_ids=[0]),
num_draft_entries=2,
num_hidden_layers=60,
)
stage1 = self._stage1_ids()
self.assertEqual(ids, stage1 + stage1 + [60, 60])
def test_non_hybrid_pool_publishes_nothing(self):
self.assertEqual(
build_kv_layer_ids(
token_to_kv_pool=SimpleNamespace(),
draft_token_to_kv_pool=None,
num_draft_entries=0,
num_hidden_layers=60,
),
[],
)
def test_ragged_draft_registration_is_rejected(self):
with self.assertRaises(RuntimeError):
build_kv_layer_ids(
token_to_kv_pool=_hybrid_pool_with_ids(layer_ids=self._stage1_ids()),
draft_token_to_kv_pool=SimpleNamespace(layer_num=2),
num_draft_entries=3,
num_hidden_layers=60,
)
class TestDraftBandPairsAcrossPipelineStages(CustomTestCase):
"""Derived property: a pp=2 prefill stage and a pp=1 decode peer, both with
an EAGLE draft pool, must pair on layer id -- the stage's 8 full-attention
layers land on the decode peer's matching K and V entries, and the draft
band lands on the decode peer's draft entries rather than on layer 0."""
def test_stage1_pairs_onto_the_decode_layout(self):
full = _full_attention_ids(num_layers=60, interval=4)
stage1 = [lid for lid in full if lid >= 30]
src = build_kv_layer_ids(
token_to_kv_pool=_hybrid_pool_with_ids(layer_ids=stage1),
draft_token_to_kv_pool=_hybrid_pool_with_ids(layer_ids=[0]),
num_draft_entries=2,
num_hidden_layers=60,
)
dst = build_kv_layer_ids(
token_to_kv_pool=_hybrid_pool_with_ids(layer_ids=full),
draft_token_to_kv_pool=_hybrid_pool_with_ids(layer_ids=[0]),
num_draft_entries=2,
num_hidden_layers=60,
)
pairs = build_transfer_entry_pairs(
src, dst, len(src), len(dst), allow_positional_fallback=False
)
k_offset = len(full) - len(stage1)
self.assertEqual(
pairs,
# K block, then V block, then the two draft entries at the tail.
[(i, k_offset + i) for i in range(len(stage1))]
+ [(len(stage1) + i, len(full) + k_offset + i) for i in range(len(stage1))]
+ [
(2 * len(stage1), 2 * len(full)),
(2 * len(stage1) + 1, 2 * len(full) + 1),
],
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,133 @@
"""Staging slot ids stay aligned once a draft KV pool is registered.
The staging gather writes every k_buffer and then every v_buffer, while
kv_data_ptrs (and therefore kv_layer_ids) is ordered
[K target, V target, K draft, V draft]. Labelling slots with kv_layer_ids
silently pairs a layer's KV with another layer's staging slot as soon as a
draft pool exists.
"""
import unittest
from sglang.srt.disaggregation.utils import (
build_staging_slot_metadata,
build_transfer_entry_pairs,
)
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool, MHATokenToKVPool
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=10, suite="base-a-test-cpu")
class _Pool(MHATokenToKVPool):
def __init__(self, tag, layer_ids):
self.k_buffer = [f"{tag}K{i}" for i in layer_ids]
self.v_buffer = [f"{tag}V{i}" for i in layer_ids]
class _Wrapper(HybridLinearKVPool):
def __init__(self, inner):
self.full_kv_pool = inner
def _kv_layer_ids(target_ids, draft_ids):
"""kv_data_ptrs order: K target, V target, K draft, V draft."""
return list(target_ids) + list(target_ids) + list(draft_ids) + list(draft_ids)
class TestStagingDraftKvSlots(CustomTestCase):
def test_draft_slots_follow_gather_order(self):
target, draft = [87, 91], [92]
k_buffers, v_buffers, slot_ids = build_staging_slot_metadata(
kv_layer_ids=_kv_layer_ids(target, draft),
num_draft_entries=2,
kv_pool=_Pool("t", target),
draft_kv_pool=_Pool("d", draft),
)
self.assertEqual(k_buffers, ["tK87", "tK91", "dK92"])
self.assertEqual(v_buffers, ["tV87", "tV91", "dV92"])
self.assertEqual(slot_ids, [87, 91, 92, 87, 91, 92])
self.assertNotEqual(slot_ids, _kv_layer_ids(target, draft))
def test_without_draft_matches_kv_layer_ids(self):
# The two orders coincide with no draft pool, so every deployment that
# predates draft KV must keep its exact slot labelling.
target = [3, 7]
_, _, slot_ids = build_staging_slot_metadata(
kv_layer_ids=_kv_layer_ids(target, []),
num_draft_entries=0,
kv_pool=_Pool("t", target),
draft_kv_pool=None,
)
self.assertEqual(slot_ids, _kv_layer_ids(target, []))
def test_pp_stage_pairs_against_full_decode(self):
# A prefill stage holds a slice of the layers while decode holds them
# all, so the ids -- not the positions -- have to drive the pairing.
src = build_staging_slot_metadata(
kv_layer_ids=_kv_layer_ids([87, 91], [92]),
num_draft_entries=2,
kv_pool=_Pool("t", [87, 91]),
draft_kv_pool=_Pool("d", [92]),
)[2]
decode_target = [3, 7, 11, 87, 91]
dst = build_staging_slot_metadata(
kv_layer_ids=_kv_layer_ids(decode_target, [92]),
num_draft_entries=2,
kv_pool=_Pool("t", decode_target),
draft_kv_pool=_Pool("d", [92]),
)[2]
pairs = build_transfer_entry_pairs(src, dst, len(src), len(dst))
self.assertEqual(len(pairs), len(src))
for i, j in pairs:
self.assertEqual(src[i], dst[j])
self.assertEqual(len({j for _, j in pairs}), len(pairs))
def test_hybrid_wrapper_pools_are_unwrapped(self):
# A hybrid draft pool that is left wrapped looks exactly like a draft
# pool with no buffers, which drops draft KV out of staging.
target, draft = [87, 91], [92]
k_buffers, _, slot_ids = build_staging_slot_metadata(
kv_layer_ids=_kv_layer_ids(target, draft),
num_draft_entries=2,
kv_pool=_Wrapper(_Pool("t", target)),
draft_kv_pool=_Wrapper(_Pool("d", draft)),
)
self.assertEqual(k_buffers, ["tK87", "tK91", "dK92"])
self.assertEqual(slot_ids, [87, 91, 92, 87, 91, 92])
def test_undescribable_draft_still_yields_target_buffers(self):
# Returning nothing here left the caller skipping set_kv_buffer_tensors
# entirely, and staging then came up with no buffers at all.
class _NoBuffers:
pass
k_buffers, v_buffers, slot_ids = build_staging_slot_metadata(
kv_layer_ids=_kv_layer_ids([87], [92]),
num_draft_entries=2,
kv_pool=_Pool("t", [87]),
draft_kv_pool=_NoBuffers(),
)
self.assertEqual(k_buffers, ["tK87"])
self.assertEqual(v_buffers, ["tV87"])
self.assertEqual(slot_ids, [])
def test_pool_without_contiguous_tensors_is_declined(self):
# MLA pools have no k_buffer/v_buffer to stage; the caller relies on None
# to skip the registration rather than register empty lists.
class _NoBuffers:
pass
self.assertIsNone(
build_staging_slot_metadata(
kv_layer_ids=[],
num_draft_entries=0,
kv_pool=_NoBuffers(),
draft_kv_pool=None,
)
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,412 @@
import unittest
from unittest import mock
import torch
from sglang.srt.layers.attention.linear.kernels.gdn_flashinfer import (
FlashInferGDNKernel,
)
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
def _view_with_pointer_mod(
shape: tuple[int, ...], dtype: torch.dtype, pointer_mod: int
) -> torch.Tensor:
numel = 1
for dim in shape:
numel *= dim
element_size = dtype.itemsize
base = torch.empty(numel + 32 // element_size, dtype=dtype)
for offset in range(32 // element_size):
view = base[offset : offset + numel]
if view.data_ptr() % 32 == pointer_mod:
return view.view(shape)
raise AssertionError(f"Could not construct a pointer with mod32={pointer_mod}")
def _make_kernel_without_flashinfer() -> FlashInferGDNKernel:
kernel = object.__new__(FlashInferGDNKernel)
# Match the SM100 path used by the CPU-only fake prefill tests. Real
# instances initialize this from the detected SM architecture in __init__.
kernel._prefill_needs_fp32_state = False
kernel._aligned_input_buffers = {}
kernel._aligned_parameter_cache = {}
kernel._verify_intermediate_buffers = {}
kernel._alignment_fallback_warned = False
return kernel
class TestFlashInferGDNAlignment(unittest.TestCase):
def test_extend_writes_directly_to_preallocated_output(self):
kernel = _make_kernel_without_flashinfer()
kernel.use_state_pool = True
captured = {}
def fake_prefill(**kwargs):
captured.update(kwargs)
kwargs["output"].fill_(7.0)
kwargs["output_state"].copy_(kwargs["initial_state"])
return kwargs["output"], kwargs["output_state"]
kernel._prefill_fn = fake_prefill
q = torch.ones((1, 3, 1, 4), dtype=torch.bfloat16)
k = torch.ones_like(q)
v = torch.ones((1, 3, 2, 4), dtype=torch.bfloat16)
g = torch.zeros((1, 3, 2), dtype=torch.bfloat16)
beta = torch.ones_like(g)
ssm_states = torch.zeros((3, 2, 4, 4), dtype=torch.bfloat16)
physical_output = torch.empty((1, 5, 2, 4), dtype=v.dtype)
preallocated_output = physical_output[:, :3]
with mock.patch(
"sglang.kernels.ops.attention.fla.l2norm.l2norm_fwd",
side_effect=lambda tensor: tensor,
):
result, _, checkpoints = kernel.extend(
q,
k,
v,
g,
beta,
ssm_states=ssm_states,
cache_indices=torch.tensor([1], dtype=torch.int32),
query_start_loc=torch.tensor([0, 3], dtype=torch.int32),
output=preallocated_output,
)
self.assertEqual(captured["output"].data_ptr(), preallocated_output.data_ptr())
self.assertEqual(result.data_ptr(), preallocated_output.data_ptr())
torch.testing.assert_close(result, torch.full_like(result, 7.0))
self.assertIsNone(checkpoints)
def test_ratio8_bs1_split_view_reproduces_under_alignment(self):
# In a BF16 [b_local(8)|a_local(8)] projection, a begins 16 bytes in;
# contiguous() is a no-op at BS=1 and cannot meet FlashInfer's 32-byte ABI.
projected_ba = torch.empty((2, 16), dtype=torch.bfloat16)
_, a = projected_ba.split((8, 8), dim=-1)
a_bs1 = a[:1]
self.assertEqual(a_bs1.stride(), (16, 1))
self.assertTrue(a_bs1.is_contiguous())
self.assertEqual(a_bs1.data_ptr() % 32, 16)
self.assertEqual(a_bs1.contiguous().data_ptr(), a_bs1.data_ptr())
# BS>1 exposes the row gap, so contiguous() does allocate a rebased,
# allocator-aligned tensor. This explains why only BS=1 failed.
a_bs2 = a[:2]
self.assertFalse(a_bs2.is_contiguous())
repaired = a_bs2.contiguous()
self.assertNotEqual(repaired.data_ptr(), a_bs2.data_ptr())
self.assertEqual(repaired.data_ptr() % 32, 0)
def test_dynamic_repair_buffer_is_reused_without_allocator_churn(self):
kernel = _make_kernel_without_flashinfer()
source = _view_with_pointer_mod((1, 1, 8), torch.bfloat16, 16)
source.fill_(1)
first = kernel._prepare_dynamic_input("decode_a", source)
first_ptr = first.data_ptr()
self.assertEqual(first_ptr % 32, 0)
torch.testing.assert_close(first, source)
self.assertEqual(len(kernel._aligned_input_buffers), 1)
source.fill_(2)
second = kernel._prepare_dynamic_input("decode_a", source)
self.assertIs(second, first)
self.assertEqual(second.data_ptr(), first_ptr)
torch.testing.assert_close(second, source)
self.assertEqual(len(kernel._aligned_input_buffers), 1)
# Distinct kernel arguments cannot alias because both are live at the
# FlashInfer call boundary.
other = kernel._prepare_dynamic_input("decode_b", source)
self.assertNotEqual(other.data_ptr(), first_ptr)
self.assertEqual(len(kernel._aligned_input_buffers), 2)
def test_decode_repairs_read_only_arguments_before_flashinfer(self):
kernel = _make_kernel_without_flashinfer()
kernel.use_state_pool = True
captured = {}
def fake_decode(**kwargs):
captured.update(kwargs)
v = kwargs["v"]
return (
torch.zeros(
v.shape[0],
1,
v.shape[2],
v.shape[3],
dtype=v.dtype,
),
None,
)
kernel._decode_fn = fake_decode
q = torch.empty(1, 1, 1, 128, dtype=torch.bfloat16)
k = torch.empty_like(q)
v = torch.empty(1, 1, 8, 128, dtype=torch.bfloat16)
a = _view_with_pointer_mod((1, 1, 8), torch.bfloat16, 16)
b = _view_with_pointer_mod((1, 1, 8), torch.bfloat16, 16)
A_log = _view_with_pointer_mod((8,), torch.float32, 4)
dt_bias = _view_with_pointer_mod((8,), torch.bfloat16, 2)
state = torch.zeros(2, 8, 128, 128, dtype=torch.bfloat16)
cache_indices = _view_with_pointer_mod((1,), torch.int32, 4)
result = kernel.decode(
q,
k,
v,
a,
b,
A_log=A_log,
dt_bias=dt_bias,
ssm_states=state,
cache_indices=cache_indices,
query_start_loc=torch.tensor([0, 1], dtype=torch.int32),
)
self.assertEqual(result.shape, (1, 1, 8, 128))
for name in (
"q",
"k",
"v",
"A_log",
"a",
"dt_bias",
"b",
"initial_state",
"initial_state_indices",
):
with self.subTest(name=name):
self.assertEqual(captured[name].data_ptr() % 32, 0)
torch.testing.assert_close(captured["a"], a)
torch.testing.assert_close(captured["b"], b)
def test_gate_parameter_cache_preserves_backend_dtype_contract(self):
kernel = _make_kernel_without_flashinfer()
A_log = torch.empty(8, dtype=torch.bfloat16)
dt_bias = torch.empty(8, dtype=torch.bfloat16)
A_log_sm90, _ = kernel._prepare_gate_parameters(A_log, dt_bias)
A_log_sm100, _ = kernel._prepare_gate_parameters(
A_log, dt_bias, A_log_dtype=torch.float32
)
self.assertEqual(A_log_sm90.dtype, torch.bfloat16)
self.assertEqual(A_log_sm100.dtype, torch.float32)
self.assertEqual(A_log_sm90.data_ptr() % 32, 0)
self.assertEqual(A_log_sm100.data_ptr() % 32, 0)
self.assertIs(
kernel._prepare_gate_parameters(A_log, dt_bias)[0],
A_log_sm90,
)
def test_mutable_state_falls_back_without_losing_writeback(self):
kernel = _make_kernel_without_flashinfer()
captured = {}
expected = torch.empty(1)
class FakeFallback:
def decode(self, *args, **kwargs):
captured["args"] = args
captured["kwargs"] = kwargs
return expected
kernel._alignment_fallback_kernel = FakeFallback()
state = _view_with_pointer_mod((2, 8, 4, 4), torch.bfloat16, 16)
q = torch.empty(1, 1, 1, 4, dtype=torch.bfloat16)
k = torch.empty_like(q)
v = torch.empty(1, 1, 8, 4, dtype=torch.bfloat16)
a = torch.empty(1, 1, 8, dtype=torch.bfloat16)
b = torch.empty_like(a)
cache_indices = torch.zeros(1, dtype=torch.int32)
query_start_loc = torch.tensor([0, 1], dtype=torch.int32)
result = kernel.decode(
q,
k,
v,
a,
b,
A_log=torch.zeros(8),
dt_bias=torch.zeros(8, dtype=torch.bfloat16),
ssm_states=state,
cache_indices=cache_indices,
query_start_loc=query_start_loc,
)
self.assertIs(result, expected)
self.assertIs(captured["kwargs"]["ssm_states"], state)
self.assertEqual(len(kernel._aligned_input_buffers), 0)
def test_mutable_mtp_workspace_falls_back_without_copying(self):
kernel = _make_kernel_without_flashinfer()
kernel.use_state_pool = True
captured = {}
expected = torch.empty(1)
class FakeFallback:
def target_verify(self, **kwargs):
captured.update(kwargs)
return expected
kernel._alignment_fallback_kernel = FakeFallback()
q = torch.empty(1, 2, 1, 4, dtype=torch.bfloat16)
k = torch.empty_like(q)
v = torch.empty(1, 2, 8, 4, dtype=torch.bfloat16)
a = torch.empty(1, 2, 8, dtype=torch.bfloat16)
b = torch.empty_like(a)
state = torch.empty(2, 8, 4, 4, dtype=torch.bfloat16)
workspace = _view_with_pointer_mod((2, 2, 8, 4, 4), torch.bfloat16, 16)
result = kernel.target_verify(
torch.zeros(8),
torch.zeros(8, dtype=torch.bfloat16),
q,
k,
v,
a,
b,
ssm_states=state,
cache_indices=torch.zeros(1, dtype=torch.int32),
query_start_loc=torch.tensor([0, 2], dtype=torch.int32),
intermediate_states_buffer=workspace,
intermediate_state_indices=torch.zeros(1, 2, dtype=torch.int32),
cache_steps=2,
retrieve_parent_token=None,
)
self.assertIs(result, expected)
self.assertIs(captured["intermediate_states_buffer"], workspace)
self.assertEqual(len(kernel._aligned_input_buffers), 0)
def test_mtp_padded_capture_uses_stable_exact_batch_workspace_and_copies_back(self):
kernel = _make_kernel_without_flashinfer()
kernel.use_state_pool = True
captured_ptrs = []
def fake_mtp(**kwargs):
workspace = kwargs["intermediate_states_buffer"]
captured_ptrs.append(workspace.data_ptr())
self.assertEqual(workspace.shape[0], 8)
for row in range(workspace.shape[0]):
workspace[row].fill_(row + 1)
return torch.zeros_like(kwargs["v"]), None
kernel._mtp_fn = fake_mtp
workspace = torch.zeros((7, 2, 8, 4, 4), dtype=torch.bfloat16)
def run_once():
return kernel.target_verify(
torch.zeros(8),
torch.zeros(8, dtype=torch.bfloat16),
torch.empty(1, 16, 1, 4, dtype=torch.bfloat16),
torch.empty(1, 16, 1, 4, dtype=torch.bfloat16),
torch.empty(1, 16, 8, 4, dtype=torch.bfloat16),
torch.empty(1, 16, 8, dtype=torch.bfloat16),
torch.empty(1, 16, 8, dtype=torch.bfloat16),
ssm_states=torch.zeros(8, 8, 4, 4, dtype=torch.bfloat16),
cache_indices=torch.zeros(8, dtype=torch.int32),
query_start_loc=torch.arange(0, 18, 2, dtype=torch.int32),
intermediate_states_buffer=workspace,
intermediate_state_indices=torch.arange(8, dtype=torch.int32),
cache_steps=2,
retrieve_parent_token=None,
)
self.assertEqual(run_once().shape, (1, 16, 8, 4))
for row in range(workspace.shape[0]):
torch.testing.assert_close(
workspace[row], torch.full_like(workspace[row], row + 1)
)
self.assertEqual(len(kernel._verify_intermediate_buffers), 1)
workspace.zero_()
run_once()
self.assertEqual(captured_ptrs[0], captured_ptrs[1])
self.assertEqual(len(kernel._verify_intermediate_buffers), 1)
def test_mtp_pool_sized_batch_keeps_zero_copy_fast_path(self):
kernel = _make_kernel_without_flashinfer()
kernel.use_state_pool = True
workspace = torch.zeros((7, 2, 8, 4, 4), dtype=torch.bfloat16)
captured = {}
def fake_mtp(**kwargs):
captured.update(kwargs)
return torch.zeros_like(kwargs["v"]), None
kernel._mtp_fn = fake_mtp
result = kernel.target_verify(
torch.zeros(8),
torch.zeros(8, dtype=torch.bfloat16),
torch.empty(1, 4, 1, 4, dtype=torch.bfloat16),
torch.empty(1, 4, 1, 4, dtype=torch.bfloat16),
torch.empty(1, 4, 8, 4, dtype=torch.bfloat16),
torch.empty(1, 4, 8, dtype=torch.bfloat16),
torch.empty(1, 4, 8, dtype=torch.bfloat16),
ssm_states=torch.zeros(7, 8, 4, 4, dtype=torch.bfloat16),
cache_indices=torch.zeros(2, dtype=torch.int32),
query_start_loc=torch.arange(0, 6, 2, dtype=torch.int32),
intermediate_states_buffer=workspace,
intermediate_state_indices=torch.arange(7, dtype=torch.int32),
cache_steps=2,
retrieve_parent_token=None,
)
self.assertEqual(result.shape, (1, 4, 8, 4))
self.assertEqual(
captured["intermediate_states_buffer"].data_ptr(), workspace.data_ptr()
)
self.assertEqual(len(kernel._verify_intermediate_buffers), 0)
def test_mtp_padded_workspace_is_reused_across_sequential_layer_pools(self):
kernel = _make_kernel_without_flashinfer()
kernel.use_state_pool = True
captured_ptrs = []
call_value = 0
def fake_mtp(**kwargs):
nonlocal call_value
call_value += 1
scratch = kwargs["intermediate_states_buffer"]
captured_ptrs.append(scratch.data_ptr())
scratch.fill_(call_value)
return torch.zeros_like(kwargs["v"]), None
kernel._mtp_fn = fake_mtp
def run(pool):
kernel.target_verify(
torch.zeros(8),
torch.zeros(8, dtype=torch.bfloat16),
torch.empty(1, 16, 1, 4, dtype=torch.bfloat16),
torch.empty(1, 16, 1, 4, dtype=torch.bfloat16),
torch.empty(1, 16, 8, 4, dtype=torch.bfloat16),
torch.empty(1, 16, 8, dtype=torch.bfloat16),
torch.empty(1, 16, 8, dtype=torch.bfloat16),
ssm_states=torch.zeros(8, 8, 4, 4, dtype=torch.bfloat16),
cache_indices=torch.zeros(8, dtype=torch.int32),
query_start_loc=torch.arange(0, 18, 2, dtype=torch.int32),
intermediate_states_buffer=pool,
intermediate_state_indices=torch.arange(8, dtype=torch.int32),
cache_steps=2,
retrieve_parent_token=None,
)
first_pool = torch.zeros((7, 2, 8, 4, 4), dtype=torch.bfloat16)
second_pool = torch.zeros_like(first_pool)
run(first_pool)
run(second_pool)
self.assertEqual(captured_ptrs[0], captured_ptrs[1])
torch.testing.assert_close(first_pool, torch.ones_like(first_pool))
torch.testing.assert_close(second_pool, torch.full_like(second_pool, 2))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,87 @@
import unittest
import torch
from sglang.kernels.ops.moe.ep_moe_kernels import fused_moe_dispatch_index
from sglang.srt.layers.moe.moe_runner.base import (
FusedOpPool,
PermuteMethodPool,
)
from sglang.srt.layers.moe.token_dispatcher.flashinfer import (
_max_tokens_per_scattered_source,
_scattered_source_token_counts,
_workspace_size_for_namespace,
)
from sglang.srt.layers.quantization import fp8 # noqa: F401
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
class TestFlashinferA2AWideEPPlumbing(CustomTestCase):
def test_runner_paths_are_registered(self):
self.assertIn(("flashinfer", "flashinfer_trtllm"), FusedOpPool._fused_funcs)
self.assertIn(
("flashinfer", "flashinfer_trtllm_routed"), FusedOpPool._fused_funcs
)
self.assertIn(
("flashinfer", "deep_gemm"), PermuteMethodPool._pre_permute_methods
)
self.assertIn(
("deep_gemm", "flashinfer"), PermuteMethodPool._post_permute_methods
)
def test_dp4_tp4_uses_physical_source_rank_geometry(self):
self.assertEqual(_max_tokens_per_scattered_source([2048] * 4, 4), 512)
self.assertEqual(_max_tokens_per_scattered_source([1, 0, 0, 0], 4), 1)
self.assertEqual(_max_tokens_per_scattered_source([7, 3, 2, 1], 4), 2)
self.assertEqual(_max_tokens_per_scattered_source([512] * 16, 1), 512)
def test_target_and_draft_decode_use_distinct_workspaces(self):
sizes = {
_workspace_size_for_namespace(4096, speculative=speculative)
for speculative in (False, True)
}
self.assertEqual(sizes, {4096, 4224})
def test_prefill_ag_expands_dp_counts_to_physical_source_ranks(self):
self.assertEqual(
_scattered_source_token_counts([7, 3], 4),
[2, 2, 2, 1, 1, 1, 1, 0],
)
self.assertEqual(_scattered_source_token_counts([4] * 16, 1), [4] * 16)
def test_deepgemm_dispatch_marks_empty_expert_lanes_invalid(self):
topk_ids = torch.tensor([-1, 0, -1, 1], dtype=torch.int32, device="cuda")
masked_m, src2dst = fused_moe_dispatch_index(
topk_ids, num_local_experts=2, m_max=4
)
torch.testing.assert_close(
masked_m, torch.tensor([1, 1], dtype=torch.int32, device="cuda")
)
torch.testing.assert_close(
src2dst,
torch.tensor([-1, 0, -1, 4], dtype=torch.int32, device="cuda"),
)
def test_global_expert_mapping_is_fused_into_dispatch_index(self):
global_ids = torch.tensor(
[-1, 15, 16, 17, 31, 32], dtype=torch.int32, device="cuda"
)
masked_m, src2dst = fused_moe_dispatch_index(
global_ids, num_local_experts=2, m_max=4, expert_start=16
)
torch.testing.assert_close(
masked_m, torch.tensor([1, 1], dtype=torch.int32, device="cuda")
)
torch.testing.assert_close(
src2dst,
torch.tensor([-1, -1, 0, 4, -1, -1], dtype=torch.int32, device="cuda"),
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,63 @@
import sys
from types import SimpleNamespace
from unittest.mock import Mock, patch
import pytest
import torch
import sglang.srt.layers.moe.moe_runner.flashinfer_cutedsl as cutedsl_runner
from sglang.srt.layers.moe.token_dispatcher.standard import (
StandardCombineInput,
StandardDispatchOutput,
)
from sglang.srt.layers.moe.topk import StandardTopKOutput
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=2, suite="base-a-test-cpu")
def test_flashinfer_prefill_returns_standard_combine_input():
dispatch_output = StandardDispatchOutput(
hidden_states=torch.empty(2, 16, dtype=torch.bfloat16),
hidden_states_scale=None,
topk_output=StandardTopKOutput(
topk_weights=torch.empty(2, 1),
topk_ids=torch.zeros(2, 1, dtype=torch.int32),
router_logits=None,
),
)
expected_output = torch.empty(2, 16, dtype=torch.bfloat16)
wrapper = Mock()
wrapper.run.return_value = expected_output
quant_info = SimpleNamespace(
wrapper=wrapper,
use_per_token_activation=False,
a1_scale=torch.tensor(1.0),
a2_scale=torch.tensor(1.0),
w13_weight=object(),
w13_weight_sf=object(),
w1_alpha=object(),
w2_weight=object(),
w2_weight_sf=object(),
w2_alpha=object(),
)
runner_config = SimpleNamespace(activation="silu")
with patch(
"sglang.srt.layers.quantization.fp4_utils.fp4_quantize",
return_value=(
torch.empty(2, 8, dtype=torch.uint8),
torch.empty(2, 1, dtype=torch.float8_e4m3fn),
),
):
result = cutedsl_runner.fused_experts_flashinfer_to_flashinfer_cutedsl_fp4(
dispatch_output, quant_info, runner_config
)
assert isinstance(result, StandardCombineInput)
assert result.hidden_states is expected_output
wrapper.run.assert_called_once()
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,205 @@
from dataclasses import dataclass
from types import SimpleNamespace
import pytest
import torch
from sglang.srt.layers.flashinfer_mnnvl_cutedsl import (
FlashInferMNNVLCuteDSLARFusion,
_with_early_finalize_shared_load,
)
from sglang.srt.layers.moe.qwen35_flashinfer_fusion import (
Qwen35MoeFinalizeHandoff,
is_supported_forward_mode,
resolve_max_m,
)
from sglang.srt.model_executor.forward_batch_info import ForwardMode
from sglang.srt.models.qwen3_5_text import Qwen3_5ForCausalLM
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=20, suite="base-c-test-cpu")
@dataclass(frozen=True)
class _TestPreset:
load_shared_expert_before_pdl: bool = False
@dataclass(frozen=True)
class _TestTarget:
preset: object
@dataclass(frozen=True)
class _TestRoutes:
targets: tuple[_TestTarget, ...]
@dataclass(frozen=True)
class _TestProfile:
finalize_routes: _TestRoutes
@dataclass(frozen=True)
class _TestConfig:
profiles: tuple[_TestProfile, ...]
@pytest.mark.parametrize(
("forward_mode", "expected"),
[
(ForwardMode.DECODE, True),
(ForwardMode.EXTEND, True),
(ForwardMode.IDLE, False),
(ForwardMode.TARGET_VERIFY, True),
(ForwardMode.DRAFT_EXTEND_V2, False),
],
)
def test_supported_forward_modes(forward_mode, expected):
assert is_supported_forward_mode(forward_mode) is expected
def test_framework_capacity_is_maximum_of_all_sources():
graph = SimpleNamespace(
decode=SimpleNamespace(max_bs=512, bs=[1, 64, 256]),
prefill=SimpleNamespace(max_bs=4096, bs=[1024, 2048, 4096]),
)
server_args = SimpleNamespace(
cuda_graph_config=graph,
cutedsl_moe_max_num_tokens=lambda: 8192,
)
runner = SimpleNamespace(server_args=server_args, max_running_requests=2048)
assert resolve_max_m(runner) == 8192
def test_deferred_handoff_reuses_producer_storage():
m, top_k, hidden_size = 3, 10, 16
gemm2_out = torch.empty(m * top_k + 4, hidden_size, dtype=torch.bfloat16)
expert_weights = torch.empty(m, top_k, dtype=torch.bfloat16)
permuted_indices = torch.empty(m, top_k, dtype=torch.int32)
gated_shared_output = torch.empty(m, hidden_size, dtype=torch.bfloat16)
deferred = SimpleNamespace(
gemm2_out=gemm2_out,
expert_weights=expert_weights,
expanded_idx_to_permuted_idx=permuted_indices,
top_k=top_k,
)
handoff = Qwen35MoeFinalizeHandoff.from_flashinfer(
deferred,
gated_shared_output=gated_shared_output,
m=m,
)
assert handoff.routed_output.data_ptr() == gemm2_out.data_ptr()
assert handoff.expert_weights.data_ptr() == expert_weights.data_ptr()
assert handoff.permuted_indices.data_ptr() == permuted_indices.data_ptr()
assert handoff.gated_shared_output is gated_shared_output
def test_qwen_workspace_config_enables_only_supported_finalize_presets():
untouched_preset = object()
default_config = _TestConfig(
profiles=(
_TestProfile(
finalize_routes=_TestRoutes(
targets=(
_TestTarget(_TestPreset()),
_TestTarget(untouched_preset),
)
)
),
)
)
qwen_config = _with_early_finalize_shared_load(default_config)
assert qwen_config is not default_config
assert (
default_config.profiles[0]
.finalize_routes.targets[0]
.preset.load_shared_expert_before_pdl
is False
)
assert (
qwen_config.profiles[0]
.finalize_routes.targets[0]
.preset.load_shared_expert_before_pdl
is True
)
assert qwen_config.profiles[0].finalize_routes.targets[1].preset is untouched_preset
def test_wrapper_calls_only_the_stable_unified_api():
calls = []
wrapper = object.__new__(FlashInferMNNVLCuteDSLARFusion)
wrapper.hidden_size = 8
wrapper.top_k = 2
wrapper.max_m = 4
wrapper.rms_epsilon = 1e-5
wrapper.weight_bias = 0.0
wrapper.device = torch.device("cpu")
wrapper.workspace = object()
wrapper.supports = lambda m: True
wrapper._patterns = SimpleNamespace(
kARResidualRMSNorm=1,
kMoEFinalizeARResidualRMSNorm=7,
)
wrapper._allreduce_fusion = lambda **kwargs: calls.append(kwargs)
routed_output = torch.empty(8, 8, dtype=torch.bfloat16)
expert_weights = torch.empty(4, 2, dtype=torch.bfloat16)
permuted_indices = torch.empty(4, 2, dtype=torch.int32)
gated_shared_output = torch.empty(4, 8, dtype=torch.bfloat16)
residual = torch.empty(4, 8, dtype=torch.bfloat16)
gamma = torch.empty(8, dtype=torch.bfloat16)
norm_output = torch.empty_like(residual)
residual_output = torch.empty_like(residual)
wrapper.moe_finalize_all_reduce_rms_norm(
routed_output=routed_output,
expert_weights=expert_weights,
permuted_indices=permuted_indices,
gated_shared_output=gated_shared_output,
residual=residual,
gamma=gamma,
norm_output=norm_output,
residual_output=residual_output,
)
assert calls[0]["launch_with_pdl"] is True
assert "routed_scaling_factor" not in calls[0]
wrapper.all_reduce_residual_rms_norm(
local_contribution=residual,
residual=residual,
gamma=gamma,
norm_output=norm_output,
residual_output=residual_output,
)
assert calls[1]["pattern"] == 1
assert calls[1]["launch_with_pdl"] is True
assert "routed_scaling_factor" not in calls[1]
assert "expanded_idx_to_permuted_idx" not in calls[1]
def test_text_entry_wrapper_delegates_pre_capture_prepare():
calls = []
runner = object()
wrapper = SimpleNamespace(
model=SimpleNamespace(
prepare_before_cuda_graph_capture=lambda value: calls.append(value)
)
)
Qwen3_5ForCausalLM.prepare_before_cuda_graph_capture(wrapper, runner)
assert calls == [runner]
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,62 @@
import pytest
from sglang.srt.environ import envs
from sglang.srt.layers.quantization.unquant import (
_FLASHINFER_PR4266_TUNED_TACTICS,
Bf16GemmBackend,
should_enable_bf16_splitk_gemm,
use_flashinfer_pr4266_bf16_gemm,
)
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
@pytest.mark.parametrize("m,n,k", _FLASHINFER_PR4266_TUNED_TACTICS)
def test_flashinfer_pr4266_selects_tuned_oakhaven_shape(m: int, n: int, k: int):
assert use_flashinfer_pr4266_bf16_gemm(m, n, k)
@pytest.mark.parametrize("m", [0, 33, 64])
@pytest.mark.parametrize("n,k", [(256, 8192), (512, 8192), (2304, 8192), (2560, 8192)])
def test_flashinfer_pr4266_keeps_large_m_on_existing_path(m: int, n: int, k: int):
assert not use_flashinfer_pr4266_bf16_gemm(m, n, k)
@pytest.mark.parametrize(
"shape",
[
(1, 1024, 2048),
(3, 256, 8192),
(16, 8192, 4096),
(32, 4096, 8192),
],
)
def test_flashinfer_pr4266_rejects_unmeasured_shapes(shape: tuple[int, int, int]):
assert not use_flashinfer_pr4266_bf16_gemm(*shape)
def test_flashinfer_pr4266_backend_is_explicit():
assert Bf16GemmBackend.FLASHINFER_PR4266.value == "flashinfer_pr4266"
def test_bf16_splitk_is_enabled_by_default():
assert envs.SGLANG_ENABLE_BF16_SPLITK_GEMM.default is True
with envs.SGLANG_ENABLE_BF16_SPLITK_GEMM.override(True):
assert should_enable_bf16_splitk_gemm(Bf16GemmBackend.CUTEDSL)
def test_bf16_splitk_env_kill_switch():
with envs.SGLANG_ENABLE_BF16_SPLITK_GEMM.override(False):
assert not should_enable_bf16_splitk_gemm(Bf16GemmBackend.CUTEDSL)
def test_bf16_splitk_does_not_override_torch_backend():
with envs.SGLANG_ENABLE_BF16_SPLITK_GEMM.override(True):
assert not should_enable_bf16_splitk_gemm(Bf16GemmBackend.TORCH)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,232 @@
"""CPU regression coverage for padded linear-attention inputs and outputs."""
from types import SimpleNamespace
from unittest.mock import patch
import torch
import sglang.srt.layers.radix_linear_attention as radix_linear_attention
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=1, suite="base-a-test-cpu")
class _FakeAttentionBackend:
def forward(
self,
*,
layer,
forward_batch,
mixed_qkv,
a,
b,
linear_attn_output=None,
):
del layer
torch.testing.assert_close(forward_batch.out_cache_loc, torch.arange(3))
assert mixed_qkv.shape[0] == 3
assert a.shape[0] == 3
assert b.shape[0] == 3
if linear_attn_output is None:
return torch.full((1, 3, 2, 4), 5.0)
linear_attn_output.fill_(5.0)
return linear_attn_output
class _FailingAttentionBackend:
def forward(self, **kwargs):
del kwargs
raise RuntimeError("backend failure")
class _ExtendMode:
def is_extend(self):
return True
def is_target_verify(self):
return False
class _TargetVerifyMode:
def is_extend(self):
return True
def is_target_verify(self):
return True
class _PhysicalAttentionBackend:
def forward(self, *, layer, forward_batch, mixed_qkv, a, b):
del layer, forward_batch
assert mixed_qkv.shape[0] == 5
assert a.shape[0] == 5
assert b.shape[0] == 5
return torch.full((1, 5, 2, 4), 9.0)
class TestRadixLinearAttentionPadding(CustomTestCase):
def test_eager_padded_input_is_sliced_and_output_shape_is_restored(self):
layer = radix_linear_attention.RadixLinearAttention(
layer_id=0,
num_q_heads=1,
num_k_heads=1,
num_v_heads=2,
head_q_dim=4,
head_k_dim=4,
head_v_dim=4,
)
original_out_cache_loc = torch.arange(5)
forward_batch = SimpleNamespace(
forward_mode=_ExtendMode(),
num_token_non_padded_cpu=3,
out_cache_loc=original_out_cache_loc,
)
with (
patch.object(
radix_linear_attention,
"get_tc_piecewise_forward_context",
return_value=None,
),
patch.object(
radix_linear_attention,
"get_attn_backend",
return_value=_FakeAttentionBackend(),
),
):
output = layer.forward(
forward_batch=forward_batch,
mixed_qkv=torch.zeros((5, 8)),
a=torch.zeros((5, 2)),
b=torch.zeros((5, 2)),
)
torch.testing.assert_close(output[:, :3], torch.full((1, 3, 2, 4), 5.0))
torch.testing.assert_close(output[:, 3:], torch.zeros((1, 2, 2, 4)))
self.assertIs(forward_batch.out_cache_loc, original_out_cache_loc)
def test_target_verify_keeps_physical_rows_matching_its_metadata(self):
layer = radix_linear_attention.RadixLinearAttention(
layer_id=0,
num_q_heads=1,
num_k_heads=1,
num_v_heads=2,
head_q_dim=4,
head_k_dim=4,
head_v_dim=4,
)
original_out_cache_loc = torch.arange(5)
forward_batch = SimpleNamespace(
forward_mode=_TargetVerifyMode(),
num_token_non_padded_cpu=3,
out_cache_loc=original_out_cache_loc,
)
with (
patch.object(
radix_linear_attention,
"get_tc_piecewise_forward_context",
return_value=None,
),
patch.object(
radix_linear_attention,
"get_attn_backend",
return_value=_PhysicalAttentionBackend(),
),
):
output = layer.forward(
forward_batch=forward_batch,
mixed_qkv=torch.zeros((5, 8)),
a=torch.zeros((5, 2)),
b=torch.zeros((5, 2)),
)
torch.testing.assert_close(output, torch.full((1, 5, 2, 4), 9.0))
self.assertIs(forward_batch.out_cache_loc, original_out_cache_loc)
def test_eager_backend_failure_restores_out_cache_loc(self):
layer = radix_linear_attention.RadixLinearAttention(
layer_id=0,
num_q_heads=1,
num_k_heads=1,
num_v_heads=2,
head_q_dim=4,
head_k_dim=4,
head_v_dim=4,
)
original_out_cache_loc = torch.arange(5)
forward_batch = SimpleNamespace(
forward_mode=_ExtendMode(),
num_token_non_padded_cpu=3,
out_cache_loc=original_out_cache_loc,
)
with (
patch.object(
radix_linear_attention,
"get_tc_piecewise_forward_context",
return_value=None,
),
patch.object(
radix_linear_attention,
"get_attn_backend",
return_value=_FailingAttentionBackend(),
),
self.assertRaisesRegex(RuntimeError, "backend failure"),
):
layer.forward(
forward_batch=forward_batch,
mixed_qkv=torch.zeros((5, 8)),
a=torch.zeros((5, 2)),
b=torch.zeros((5, 2)),
)
self.assertIs(forward_batch.out_cache_loc, original_out_cache_loc)
def test_padded_output_tail_is_initialized(self):
for padded_num_tokens in (3, 5):
with self.subTest(padded_num_tokens=padded_num_tokens):
original_out_cache_loc = torch.arange(padded_num_tokens)
forward_batch = SimpleNamespace(
num_token_non_padded_cpu=3,
out_cache_loc=original_out_cache_loc,
)
context = SimpleNamespace(
forward_batch=forward_batch,
attention_layers=[object()],
)
output = torch.full((1, padded_num_tokens, 2, 4), float("nan"))
with (
patch.object(
radix_linear_attention,
"get_tc_piecewise_forward_context",
return_value=context,
),
patch.object(
radix_linear_attention,
"get_attn_backend",
return_value=_FakeAttentionBackend(),
),
):
radix_linear_attention._unified_linear_attention_with_output_impl(
mixed_qkv=torch.zeros((padded_num_tokens, 8)),
a=torch.zeros((padded_num_tokens, 2)),
b=torch.zeros((padded_num_tokens, 2)),
output=output,
layer_id=0,
)
torch.testing.assert_close(output[:, :3], torch.full((1, 3, 2, 4), 5.0))
torch.testing.assert_close(
output[:, 3:],
torch.zeros((1, padded_num_tokens - 3, 2, 4)),
)
self.assertIs(forward_batch.out_cache_loc, original_out_cache_loc)
if __name__ == "__main__":
import unittest
unittest.main()
@@ -1,5 +1,7 @@
import inspect
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import maybe_stub_sgl_kernel
@@ -48,5 +50,39 @@ class TestDecisionMethodsHaveNoHiddenBatchChannel(unittest.TestCase):
)
class TestMtpPhaseBoundaryOverlap(unittest.TestCase):
@staticmethod
def _batch(*, is_extend: bool, is_speculative: bool = True):
return SimpleNamespace(
is_extend_in_batch=is_extend,
forward_mode=SimpleNamespace(
is_extend=lambda: is_extend,
is_decode=lambda: not is_extend,
),
spec_algorithm=SimpleNamespace(is_none=lambda: not is_speculative),
grammar_needs_sync=lambda: False,
)
def _scheduler(self, *, require_mlp_sync: bool):
scheduler = object.__new__(Scheduler)
scheduler.require_mlp_sync = require_mlp_sync
scheduler.result_queue = [object()]
return scheduler
@patch(
"sglang.srt.managers.scheduler.envs."
"SGLANG_DISABLE_CONSECUTIVE_PREFILL_OVERLAP.get",
return_value=False,
)
def test_mtp_phase_crossing_keeps_overlap(self, _disable_consecutive_prefill):
extend = self._batch(is_extend=True)
decode = self._batch(is_extend=False)
for require_mlp_sync in (False, True):
scheduler = self._scheduler(require_mlp_sync=require_mlp_sync)
self.assertFalse(scheduler.is_disable_overlap_for_batch(decode, extend))
self.assertFalse(scheduler.is_disable_overlap_for_batch(extend, decode))
if __name__ == "__main__":
unittest.main()
@@ -19,6 +19,7 @@ from sglang.srt.configs.mamba_utils import (
Mamba2StateDType,
Mamba2StateShape,
)
from sglang.srt.mem_cache.kv_cache_configurator import _pp_local_per_request_bytes
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
@@ -81,6 +82,19 @@ class TestReplaySSMRingAccounting(CustomTestCase):
self.assertEqual(_gdn_params().replayssm_ring_bytes_per_req(record_len=0), 0)
self.assertEqual(_kda_params().replayssm_ring_bytes_per_req(record_len=0), 0)
def test_pp_local_state_budget(self):
# Four equal-cost linear layers globally, two owned by this PP stage.
self.assertEqual(
_pp_local_per_request_bytes(4096, [0, 1, 3, 4], 1, 4),
2048,
)
def test_pp_local_state_budget_empty_stage(self):
self.assertEqual(
_pp_local_per_request_bytes(4096, [0, 1, 3, 4], 5, 8),
0,
)
if __name__ == "__main__":
import sys
@@ -7,12 +7,47 @@ from sglang.srt.model_executor.model_runner_components import cuda_graph_setup
from sglang.srt.model_executor.model_runner_components.cuda_graph_setup import (
_align_pipeline_layers,
capture_decode_graph,
has_standard_gqa_for_all_local_layers,
index_attention_layers_by_global_id,
)
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
def test_standard_gqa_gate_uses_pipeline_local_layer_range():
# PP rank owns layers [23, 46), while the full model has 92 layers.
assert has_standard_gqa_for_all_local_layers(
attention_layer_count=23, start_layer=23, end_layer=46
)
assert not has_standard_gqa_for_all_local_layers(
attention_layer_count=22, start_layer=23, end_layer=46
)
def test_standard_gqa_gate_is_unchanged_without_pipeline_parallelism():
assert has_standard_gqa_for_all_local_layers(
attention_layer_count=92, start_layer=0, end_layer=92
)
def test_pipeline_attention_metadata_is_indexed_by_global_layer_id():
layer23 = SimpleNamespace(layer_id=23)
layer24 = SimpleNamespace(layer_id=24)
companion24 = object()
attention, companions = index_attention_layers_by_global_id(
[layer23, layer24], [None, companion24]
)
assert len(attention) == 25
assert all(layer is None for layer in attention[:23])
assert attention[23] is layer23
assert attention[24] is layer24
assert companions[23] is None
assert companions[24] is companion24
def test_model_runner_can_override_decode_graph_runner(monkeypatch):
from sglang.srt.runtime_context import get_context
@@ -1195,6 +1195,76 @@ class TestBuildPrefillRegistry(unittest.TestCase):
reg.fill_from(fb, raw_bs=2, padded_bs=2, raw_num_tokens=3, padded_num_tokens=8)
self.assertTrue(torch.equal(idx, torch.tensor([3, 4], dtype=torch.int64)))
def test_pp_proxy_token_slots_copy_head_and_zero_bucket_tail(self):
from sglang.srt.model_executor.cuda_graph_buffer_registry import (
build_prefill_registry,
)
from sglang.srt.model_executor.forward_batch_info import PPProxyTensors
hidden = torch.full((16, 4), 7.0)
residual = torch.full((16, 4), 7.0)
src = self._src(
pp_proxy_tensors={
"hidden_states": hidden,
"residual": residual,
}
)
reg = build_prefill_registry(
device=torch.device("cpu"),
max_bs=1,
max_num_token=16,
cache_loc_dtype=torch.int64,
source=src,
)
self.assertTrue(reg.has_slot("pp_proxy_tensors.hidden_states"))
fb = _MiniForwardBatch(
input_ids=torch.zeros(3, dtype=torch.int64),
positions=torch.zeros(3, dtype=torch.int64),
out_cache_loc=torch.zeros(3, dtype=torch.int64),
)
pp_proxy = PPProxyTensors(
{
"hidden_states": torch.ones((3, 4)),
"residual": torch.full((3, 4), 2.0),
}
)
reg.fill_from(
fb,
raw_bs=1,
padded_bs=1,
raw_num_tokens=3,
padded_num_tokens=8,
pp_proxy_tensors=pp_proxy,
)
self.assertTrue(torch.all(hidden[:3] == 1.0))
self.assertTrue(torch.all(residual[:3] == 2.0))
self.assertTrue(torch.all(hidden[3:8] == 0.0))
self.assertTrue(torch.all(residual[3:8] == 0.0))
self.assertTrue(torch.all(hidden[8:] == 7.0))
def test_prefill_input_buffers_allocate_pp_proxy_by_token(self):
from sglang.srt.model_executor.runner_utils.buffers import (
PrefillInputBuffers,
)
buffers = PrefillInputBuffers.create(
device=torch.device("cpu"),
max_bs=4,
max_num_tokens=16,
cache_loc_dtype=torch.int64,
is_multimodal=False,
hidden_size=8,
dtype=torch.bfloat16,
enable_mamba_track=False,
pp_size=2,
pp_proxy_topk_size=3,
)
self.assertEqual(
tuple(buffers.pp_proxy_tensors["hidden_states"].shape), (16, 8)
)
self.assertEqual(tuple(buffers.pp_proxy_tensors["residual"].shape), (16, 8))
self.assertEqual(tuple(buffers.pp_proxy_tensors["topk_indices"].shape), (16, 3))
def test_source_none_owns_allocated_buffers(self):
# source=None -> the registry allocates (owns) every slot.
from sglang.srt.model_executor.cuda_graph_buffer_registry import (
@@ -9,7 +9,12 @@ import torch
import sglang.srt.model_executor.model_runner_components.cuda_graph_setup as graph_setup
import sglang.srt.model_executor.runner.prefill_cuda_graph_runner as runner_module
from sglang.srt.model_executor.cuda_graph_config import Backend
from sglang.srt.model_executor.forward_batch_info import CaptureHiddenMode
from sglang.srt.model_executor.forward_batch_info import (
CaptureHiddenMode,
ForwardBatch,
ForwardMode,
PPProxyTensors,
)
from sglang.srt.model_executor.model_runner_components.cuda_graph_setup import (
capture_prefill_graph,
)
@@ -61,6 +66,32 @@ class _FakeKVIndexKernel:
return run
class _FakeGraphSlot:
def __init__(self, buffer):
self.buffer = buffer
def slice_for(self, _batch_size, num_tokens):
return self.buffer[:num_tokens]
class _FakeBatchRegistry:
def __init__(self):
self.slots = {
"input_ids": _FakeGraphSlot(torch.arange(4, dtype=torch.int64)),
"positions": _FakeGraphSlot(torch.arange(4, dtype=torch.int64)),
"out_cache_loc": _FakeGraphSlot(torch.arange(4, dtype=torch.int64)),
}
def fill_from(self, *_args, **_kwargs):
return None
def has_slot(self, name):
return name in self.slots
def get_slot(self, name):
return self.slots[name]
class TestPrefillCudaGraphRunnerChunkedPrefix(CustomTestCase):
def test_low_free_memory_still_captures_prefill_graph(self):
eager_runner = object()
@@ -86,6 +117,7 @@ class TestPrefillCudaGraphRunnerChunkedPrefix(CustomTestCase):
server_args=SimpleNamespace(),
model=SimpleNamespace(),
model_config=SimpleNamespace(context_len=8192, num_hidden_layers=1),
layer_info=SimpleNamespace(start_layer=0, end_layer=1),
req_to_token_pool=SimpleNamespace(size=1),
)
language_model = SimpleNamespace(layers=[object()])
@@ -98,7 +130,7 @@ class TestPrefillCudaGraphRunnerChunkedPrefix(CustomTestCase):
patch.object(
graph_setup,
"compute_attention_and_moe_layers",
return_value=([object()], [], [], [], []),
return_value=([object()], [], [], [], [None]),
),
patch.object(
graph_setup,
@@ -145,6 +177,63 @@ class TestPrefillCudaGraphRunnerChunkedPrefix(CustomTestCase):
self.assertIs(capture.runner, eager_runner)
def test_pp_proxy_output_is_trimmed_to_raw_prefill_tokens(self):
runner = PrefillCudaGraphRunner.__new__(PrefillCudaGraphRunner)
runner.raw_num_tokens = 3
output = PPProxyTensors(
{
"hidden_states": torch.arange(32).view(8, 4),
"residual": torch.arange(32, 64).view(8, 4),
}
)
trimmed = runner._finalize_execute_output(output)
self.assertIsInstance(trimmed, PPProxyTensors)
self.assertEqual(tuple(trimmed["hidden_states"].shape), (3, 4))
self.assertEqual(tuple(trimmed["residual"].shape), (3, 4))
def test_static_batch_preserves_consumed_multimodal_embeddings(self):
runner = PrefillCudaGraphRunner.__new__(PrefillCudaGraphRunner)
runner.capture_num_tokens = [4]
runner.buffer_registry = _FakeBatchRegistry()
runner.enable_cp_v2_bcg_capture = False
runner._is_full_backend = False
runner.backend = SimpleNamespace()
runner.has_mha_companion_layers = False
runner._prefill_static_buffers = None
runner.static_draft_hidden_states = None
runner.capture_return_pooled_hidden_states = False
runner._next_token_logits_buffer = lambda _rows: None
runner._prefill_logits_buffer_rows = lambda _batch: 1
runner._prepare_forward_metadata_for_replay = lambda *_args: None
mm_input_embeds = torch.randn(3, 8)
forward_batch = ForwardBatch(
forward_mode=ForwardMode.EXTEND,
batch_size=1,
input_ids=torch.arange(3, dtype=torch.int64),
req_pool_indices=torch.zeros(1, dtype=torch.int64),
seq_lens=torch.tensor([3], dtype=torch.int32),
out_cache_loc=torch.arange(3, dtype=torch.int64),
seq_lens_sum=3,
positions=torch.arange(3, dtype=torch.int64),
seq_lens_cpu=torch.tensor([3], dtype=torch.int32),
extend_seq_lens=torch.tensor([3], dtype=torch.int32),
extend_prefix_lens=torch.zeros(1, dtype=torch.int32),
extend_start_loc=torch.zeros(1, dtype=torch.int32),
extend_seq_lens_cpu=[3],
extend_prefix_lens_cpu=[0],
mm_inputs=None,
mm_input_embeds=mm_input_embeds,
capture_hidden_mode=CaptureHiddenMode.NULL,
global_forward_mode=ForwardMode.EXTEND,
)
static_batch = runner.load_batch(forward_batch)
self.assertIs(static_batch.mm_input_embeds, mm_input_embeds)
def test_prefix_chunk_capacity_is_aggregate_and_can_be_overridden(self):
graph_config = SimpleNamespace(
prefill=SimpleNamespace(full_prefill_prefix_chunk_tokens=None, max_bs=8)
@@ -581,9 +581,11 @@ class TestMoeFlagsGroup(_IsolatedServerArgs):
self.assertTrue(get_moe_a2a_backend().is_none())
# MTP layers are unquantized: fp4 allgather is forced off
self.assertTrue(get_flags().moe.disable_fp4_allgather)
self.assertTrue(get_flags().moe.speculative_context)
self.assertEqual(get_moe_runner_backend().name, "TRITON")
self.assertTrue(get_moe_a2a_backend().is_deepep())
self.assertFalse(get_flags().moe.disable_fp4_allgather)
self.assertFalse(get_flags().moe.speculative_context)
def test_swap_restores_on_exception(self):
from sglang.srt.layers.moe.utils import (