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

349 lines
13 KiB
Python

from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Tuple
import torch
import triton
import triton.language as tl
from sglang.jit_kernel.utils import cache_once, is_arch_support_pdl, load_jit
from sglang.kernel_api_logging import debug_kernel_api
if TYPE_CHECKING:
from tvm_ffi.module import Module
_SCORING_FUNC_MAP = {
"sigmoid": 0,
"sqrtsoftplus": 1,
"softmax": 2,
}
@cache_once
def _jit_moe_fused_gate_module() -> Module:
return load_jit(
"moe_fused_gate",
cuda_files=["moe/moe_fused_gate.cuh"],
cuda_wrappers=[("moe_fused_gate", "MoEFusedGateKernel::run")],
)
@cache_once
def can_use_moe_fused_gate() -> bool:
logger = logging.getLogger(__name__)
try:
_jit_moe_fused_gate_module()
return True
except Exception as e:
logger.warning(f"Failed to load JIT MoE fused gate kernel: {e}")
return False
def moe_fused_gate_jit(
input: torch.Tensor,
bias: torch.Tensor,
topk: int,
scoring_func: str = "sigmoid",
num_fused_shared_experts: int = 0,
renormalize: bool = True,
routed_scaling_factor: float = 1.0,
apply_routed_scaling_factor_on_output: bool = False,
) -> Tuple[torch.Tensor, torch.Tensor]:
scoring_func_int = _SCORING_FUNC_MAP.get(scoring_func.lower())
assert (
scoring_func_int is not None
), f"Unknown scoring_func '{scoring_func}', must be one of {list(_SCORING_FUNC_MAP.keys())}"
assert input.dtype == torch.float32, "input must be float32"
assert bias.dtype == torch.float32, "bias must be float32"
assert input.ndim == 2, "input must be 2D"
assert bias.ndim == 1, "bias must be 1D"
assert input.size(1) == bias.size(0), "input and bias must have same num_experts"
assert topk > num_fused_shared_experts, "topk must be > num_fused_shared_experts"
num_rows, _ = input.shape
device = input.device
output = torch.empty(num_rows, topk, dtype=torch.float32, device=device)
indices = torch.empty(num_rows, topk, dtype=torch.int32, device=device)
module = _jit_moe_fused_gate_module()
module.moe_fused_gate(
input,
bias,
output,
indices,
topk,
scoring_func_int,
num_fused_shared_experts,
renormalize,
routed_scaling_factor,
apply_routed_scaling_factor_on_output,
)
return output, indices
@triton.jit
def _router_triton_kernel(
scores_ptr, # [M, N] fp32, GEMM output (raw logits)
bias_ptr, # [N] fp32
out_weights_ptr, # [M, K] fp32
out_indices_ptr, # [M, K] int32
M,
routed_scaling_factor,
moe_softcapping,
N: tl.constexpr,
K: tl.constexpr, # total topk (includes fused shared experts)
K_ROUTED: tl.constexpr, # K - num_fused_shared_experts
BLOCK_M: tl.constexpr, # rows processed per program (row tiling)
BLOCK_N: tl.constexpr, # >= N, power of 2
BLOCK_K: tl.constexpr, # >= K, power of 2
N_GROUP: tl.constexpr, # expert groups (1 = ungrouped)
TOPK_GROUP: tl.constexpr, # groups kept per token (grouped routing)
EXPERTS_PER_GROUP: tl.constexpr, # N // N_GROUP
BLOCK_G: tl.constexpr, # >= N_GROUP, power of 2
SCORING_FUNC: tl.constexpr, # 0 = sigmoid, 1 = sqrtsoftplus, 2 = softmax
HAS_SOFTCAP: tl.constexpr, # tanh softcapping (softmax only)
RENORMALIZE: tl.constexpr,
APPLY_SCALE: tl.constexpr, # apply_routed_scaling_factor_on_output
USE_PDL: tl.constexpr,
stride_sm,
stride_sn,
stride_wm,
stride_wk,
stride_im,
stride_ik,
) -> None:
# Row-tiled: each program handles BLOCK_M rows; all reductions run along the
# expert (N) axis. Tiling rows keeps CTAs large enough to stay occupancy-bound
# rather than launch-bound at small N (many tiny 1-warp CTAs otherwise).
pid = tl.program_id(0)
offs_m = pid * BLOCK_M + tl.arange(0, BLOCK_M) # [BLOCK_M]
offs_n = tl.arange(0, BLOCK_N) # [BLOCK_N]
mask_m = offs_m < M
mask_n = offs_n < N
# prefetch bias before PDL wait
bias = tl.load(bias_ptr + offs_n, mask=mask_n, other=0.0).to(
tl.float32
) # [BLOCK_N]
if USE_PDL:
tl.extra.cuda.gdc_wait()
row_ptr = scores_ptr + offs_m[:, None] * stride_sm + offs_n[None, :] * stride_sn
mask2d = mask_m[:, None] & mask_n[None, :]
scores = tl.load(row_ptr, mask=mask2d, other=0.0).to(
tl.float32
) # [BLOCK_M, BLOCK_N]
if SCORING_FUNC == 0:
# sigmoid(x) = 1 / (1 + exp(-x)); bias is for ranking only, weight is bias-free.
activated = tl.sigmoid(scores)
biased = activated + bias[None, :]
elif SCORING_FUNC == 1:
# sqrt(softplus(x)) = sqrt(log1p(exp(x))); guard against overflow when x is large.
sp = tl.where(scores > 20.0, scores, tl.log(1.0 + tl.exp(scores)))
activated = tl.sqrt(sp)
biased = activated + bias[None, :]
else:
# softmax over the row: weight is the softmax probability (bias kept), with
# optional tanh softcapping. Ranking by the (softcapped, biased) logit is
# monotonic with the softmax prob, so the topk loop below ranks on `biased`.
logit = scores
if HAS_SOFTCAP:
# tanh(z) = 2*sigmoid(2z) - 1 (avoids relying on tl.math.tanh availability).
z = logit / moe_softcapping
logit = moe_softcapping * (2.0 * tl.sigmoid(2.0 * z) - 1.0)
biased = logit + bias[None, :]
biased = tl.where(mask_n[None, :], biased, -float("inf"))
row_max = tl.max(biased, axis=1)[:, None] # [BLOCK_M, 1]
exp_row = tl.where(mask_n[None, :], tl.exp(biased - row_max), 0.0)
row_sum = tl.sum(exp_row, axis=1)[:, None] # [BLOCK_M, 1]
activated = exp_row / row_sum
biased = tl.where(mask_n[None, :], biased, -float("inf")) # [BLOCK_M, BLOCK_N]
# Map NaN -> a finite floor
biased = tl.where(biased == biased, biased, -1e30) # [BLOCK_M, BLOCK_N]
# Grouped routing (DeepSeek-V3 noaux_tc): per-group score = sum of the top-2
# biased values; keep TOPK_GROUP groups (lowest group id wins ties); mask the
# experts of dropped groups to -inf before the top-k below. Weight is still the
# bias-free `activated`. Constexpr N_GROUP <= 1 skips this entirely (ungrouped).
if N_GROUP > 1:
offs_g = tl.arange(0, BLOCK_G) # [BLOCK_G]
group_of_n = offs_n // EXPERTS_PER_GROUP # [BLOCK_N]
group_score = tl.full([BLOCK_M, BLOCK_G], -float("inf"), dtype=tl.float32)
for g in tl.static_range(N_GROUP):
in_g = (group_of_n[None, :] == g) & mask_n[None, :]
vals = tl.where(in_g, biased, -float("inf"))
top1 = tl.max(vals, axis=1)[:, None] # [BLOCK_M, 1]
vals2 = tl.where(vals >= top1, -float("inf"), vals)
top2 = tl.max(vals2, axis=1)[:, None] # [BLOCK_M, 1]
group_score = tl.where(offs_g[None, :] == g, top1 + top2, group_score)
gcur = group_score
keep = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32)
for _i in tl.static_range(TOPK_GROUP):
gmax = tl.max(gcur, axis=1)[:, None] # [BLOCK_M, 1]
glane = tl.where(gcur == gmax, offs_g[None, :], N_GROUP + 1)
win_g = tl.min(glane, axis=1)[:, None] # [BLOCK_M, 1] lowest-id on ties
keep = tl.where(group_of_n[None, :] == win_g, 1.0, keep)
gcur = tl.where(offs_g[None, :] == win_g, -float("inf"), gcur)
biased = tl.where(keep > 0.0, biased, -float("inf"))
offs_k = tl.arange(0, BLOCK_K) # [BLOCK_K]
mask_k_total = offs_k < K
mask_k_routed = offs_k < K_ROUTED
selected_vals = tl.zeros([BLOCK_M, BLOCK_K], dtype=tl.float32)
selected_idx = tl.zeros([BLOCK_M, BLOCK_K], dtype=tl.int32)
cur = biased # [BLOCK_M, BLOCK_N]
for k in tl.static_range(K_ROUTED):
max_val = tl.max(cur, axis=1)[:, None] # [BLOCK_M, 1]
is_max = cur == max_val
lane_id = tl.where(is_max, offs_n[None, :], N + 1) # lowest expert id wins ties
win_lane = tl.min(lane_id, axis=1)[:, None].to(tl.int32) # [BLOCK_M, 1]
win_activated = tl.sum(
tl.where(offs_n[None, :] == win_lane, activated, 0.0), axis=1
)[
:, None
] # [BLOCK_M, 1]
slot = offs_k[None, :] == k # [1, BLOCK_K]
selected_vals = tl.where(slot, win_activated, selected_vals)
selected_idx = tl.where(slot, win_lane, selected_idx)
cur = tl.where(offs_n[None, :] == win_lane, -float("inf"), cur)
routed_sum = tl.sum(tl.where(mask_k_routed[None, :], selected_vals, 0.0), axis=1)[
:, None
] # [BLOCK_M, 1]
# Fill fused-shared-expert slots: weight = routed_sum / routed_scaling_factor,
# id = num_experts + (slot - K_ROUTED).
if K_ROUTED < K:
is_shared = (offs_k[None, :] >= K_ROUTED) & mask_k_total[None, :]
shared_weight = routed_sum / routed_scaling_factor # [BLOCK_M, 1]
shared_idx = (N + (offs_k - K_ROUTED)).to(tl.int32)[None, :] # [1, BLOCK_K]
selected_vals = tl.where(is_shared, shared_weight, selected_vals)
selected_idx = tl.where(is_shared, shared_idx, selected_idx)
if USE_PDL:
tl.extra.cuda.gdc_launch_dependents()
if RENORMALIZE:
norm = tl.where(routed_sum > 0.0, routed_sum, 1.0) # [BLOCK_M, 1]
selected_vals = selected_vals / norm
if APPLY_SCALE:
selected_vals = selected_vals * routed_scaling_factor
out_w_ptr = (
out_weights_ptr + offs_m[:, None] * stride_wm + offs_k[None, :] * stride_wk
)
out_i_ptr = (
out_indices_ptr + offs_m[:, None] * stride_im + offs_k[None, :] * stride_ik
)
store_mask = mask_m[:, None] & mask_k_total[None, :]
tl.store(out_w_ptr, selected_vals, mask=store_mask)
tl.store(out_i_ptr, selected_idx, mask=store_mask)
@debug_kernel_api
def moe_fused_gate(
scores: torch.Tensor,
bias: torch.Tensor,
topk: int,
scoring_func: str = "sigmoid",
num_fused_shared_experts: int = 0,
renormalize: bool = True,
routed_scaling_factor: float = 1.0,
apply_routed_scaling_factor_on_output: bool = False,
moe_softcapping: float = 0.0,
num_expert_group: int = 1,
topk_group: int = 1,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Triton fused router: scoring + bias + topk + (optional) renorm/scale.
Mirrors the semantics of :func:`moe_fused_gate_jit` (the CUDA JIT kernel).
With ``num_expert_group > 1`` it performs DeepSeek-V3 grouped routing
(per-group top-2-sum group scores, keep ``topk_group`` groups, then top-k
within). The first argument is named ``scores`` (raw GEMM logits) to match
the existing call sites.
"""
scoring_func_int = _SCORING_FUNC_MAP.get(scoring_func.lower())
assert (
scoring_func_int is not None
), f"Unknown scoring_func '{scoring_func}', must be one of {list(_SCORING_FUNC_MAP.keys())}"
assert scores.dtype in (
torch.float32,
torch.float16,
torch.bfloat16,
), "scores must be float32/float16/bfloat16"
assert bias.dtype == torch.float32, "bias must be float32"
assert scores.ndim == 2, "scores must be 2D"
assert bias.ndim == 1, "bias must be 1D"
assert scores.size(1) == bias.size(0), "scores and bias must have same num_experts"
assert topk > num_fused_shared_experts, "topk must be > num_fused_shared_experts"
if routed_scaling_factor is None:
routed_scaling_factor = 1.0
M, N = scores.shape
K = topk
K_routed = topk - num_fused_shared_experts
if num_expert_group > 1:
assert N % num_expert_group == 0, "num_experts must be divisible by group count"
assert 1 <= topk_group <= num_expert_group, "invalid topk_group"
experts_per_group = N // num_expert_group
BLOCK_G = triton.next_power_of_2(num_expert_group)
weights = torch.empty((M, K), dtype=torch.float32, device=scores.device)
indices = torch.empty((M, K), dtype=torch.int32, device=scores.device)
BLOCK_N = triton.next_power_of_2(N) # 256 -> 256, 384 -> 512
BLOCK_K = triton.next_power_of_2(K) # 6 -> 8, 8 -> 8
# Single warp per program keeps the per-row top-k reductions on cheap warp
# shuffles; pack a few rows per program only when N is small so tiny launches
# stay occupancy-bound. Swept on H100/B200: this beats the AOT kernels across
# shapes, whereas larger tiles / more warps regress (register pressure).
BLOCK_M = max(1, min(4, 256 // BLOCK_N))
num_warps = 1
grid = (triton.cdiv(M, BLOCK_M),)
use_pdl = is_arch_support_pdl()
extra = {"launch_pdl": True} if use_pdl else {}
_router_triton_kernel[grid](
scores,
bias,
weights,
indices,
M,
float(routed_scaling_factor),
float(moe_softcapping),
N=N,
K=K,
K_ROUTED=K_routed,
BLOCK_M=BLOCK_M,
BLOCK_N=BLOCK_N,
BLOCK_K=BLOCK_K,
N_GROUP=num_expert_group,
TOPK_GROUP=topk_group,
EXPERTS_PER_GROUP=experts_per_group,
BLOCK_G=BLOCK_G,
SCORING_FUNC=scoring_func_int,
HAS_SOFTCAP=bool(moe_softcapping != 0.0),
RENORMALIZE=bool(renormalize),
APPLY_SCALE=bool(apply_routed_scaling_factor_on_output),
USE_PDL=use_pdl,
stride_sm=scores.stride(0),
stride_sn=scores.stride(1),
stride_wm=weights.stride(0),
stride_wk=weights.stride(1),
stride_im=indices.stride(0),
stride_ik=indices.stride(1),
num_warps=num_warps,
**extra,
)
return weights, indices