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

494 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Correctness tests for the moe_topk_sigmoid JIT kernel.
Validates against a pure-PyTorch reference and, when sgl_kernel is available,
cross-checks against the AOT implementation.
"""
import itertools
import os
import sys
from typing import Optional
import pytest
import torch
from sglang.jit_kernel.moe_topk_sigmoid import topk_sigmoid
try:
from sgl_kernel import topk_sigmoid as topk_sigmoid_aot
AOT_AVAILABLE = True
except ImportError:
AOT_AVAILABLE = False
# ---------------------------------------------------------------------------
# CI / full-range helpers
# ---------------------------------------------------------------------------
_is_ci = (
os.getenv("CI", "false").lower() == "true"
or os.getenv("GITHUB_ACTIONS", "false").lower() == "true"
)
# Power-of-2 configs covered by static dispatch (num_experts 1256)
# Plus 48 (non-power-of-2) to exercise the fallback path
NUM_TOKENS_FULL = [1, 16, 128, 512, 1024, 2048]
NUM_TOKENS_CI = [1, 128, 1024]
NUM_EXPERTS_FULL = [16, 32, 64, 128, 256, 48] # 48 = fallback path
NUM_EXPERTS_CI = [16, 64, 48]
TOPK_FULL = [1, 2, 4, 8]
TOPK_CI = [1, 4]
DTYPES_FULL = [torch.float32]
DTYPES_CI = [torch.float32, torch.bfloat16]
NUM_TOKENS = NUM_TOKENS_CI if _is_ci else NUM_TOKENS_FULL
NUM_EXPERTS = NUM_EXPERTS_CI if _is_ci else NUM_EXPERTS_FULL
TOPK_LIST = TOPK_CI if _is_ci else TOPK_FULL
DTYPES = DTYPES_CI if _is_ci else DTYPES_FULL
# ---------------------------------------------------------------------------
# Pure-PyTorch reference
# ---------------------------------------------------------------------------
def grouped_topk_gpu(
gating_output: torch.Tensor,
topk: int,
renormalize: bool,
num_expert_group: Optional[int] = None,
topk_group: Optional[int] = None,
num_fused_shared_experts: int = 0,
routed_scaling_factor: Optional[float] = None,
apply_routed_scaling_factor_on_output: Optional[bool] = False,
scoring_func: str = "softmax",
):
# Scoring function: softmax or sigmoid
if scoring_func == "softmax":
scores = torch.softmax(gating_output, dim=-1)
elif scoring_func == "sigmoid":
scores = gating_output.sigmoid()
else:
raise ValueError(f"Unsupported scoring function: {scoring_func}")
num_token = scores.shape[0]
num_experts = scores.shape[1]
group_scores = (
scores.view(num_token, num_expert_group, -1).max(dim=-1).values
) # [n, n_group]
group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
1
] # [n, top_k_group]
group_mask = torch.zeros_like(group_scores) # [n, n_group]
group_mask.scatter_(1, group_idx, 1) # [n, n_group]
score_mask = (
group_mask.unsqueeze(-1)
.expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
.reshape(num_token, -1)
) # [n, e]
tmp_scores = scores.masked_fill(
~score_mask.bool(), float("-inf")
) # [n, e] - use -inf like VLLM
topk_weights, topk_ids = torch.topk(
tmp_scores,
k=topk,
dim=-1,
sorted=(True if num_fused_shared_experts > 0 else True),
)
if num_fused_shared_experts:
topk_ids[:, -1] = torch.randint(
low=num_experts,
high=num_experts + num_fused_shared_experts,
size=(topk_ids.size(0),),
dtype=topk_ids.dtype,
device=topk_ids.device,
)
if routed_scaling_factor is not None:
topk_weights[:, -1] = (
topk_weights[:, :-1].sum(dim=-1) / routed_scaling_factor
)
if renormalize:
topk_weights_sum = (
topk_weights.sum(dim=-1, keepdim=True)
if num_fused_shared_experts == 0
else topk_weights[:, :-1].sum(dim=-1, keepdim=True)
)
topk_weights = topk_weights / topk_weights_sum
if apply_routed_scaling_factor_on_output:
topk_weights *= routed_scaling_factor
topk_weights, topk_ids = topk_weights.to(torch.float32), topk_ids.to(torch.int32)
return topk_weights, topk_ids
def biased_grouped_topk_impl(
gating_output: torch.Tensor,
correction_bias: torch.Tensor,
topk: int,
renormalize: bool,
num_expert_group: Optional[int] = None,
topk_group: Optional[int] = None,
num_fused_shared_experts: int = 0,
routed_scaling_factor: Optional[float] = None,
apply_routed_scaling_factor_on_output: Optional[bool] = False,
):
scores = gating_output.sigmoid()
num_token = scores.shape[0]
num_experts = scores.shape[1]
scores_for_choice = scores.view(num_token, -1) + correction_bias.unsqueeze(0)
group_scores = (
scores_for_choice.view(num_token, num_expert_group, -1)
.topk(2, dim=-1)[0]
.sum(dim=-1)
) # [n, n_group]
group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
1
] # [n, top_k_group]
group_mask = torch.zeros_like(group_scores) # [n, n_group]
group_mask.scatter_(1, group_idx, 1) # [n, n_group]
score_mask = (
group_mask.unsqueeze(-1)
.expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
.reshape(num_token, -1)
) # [n, e]
tmp_scores = scores_for_choice.masked_fill(
~score_mask.bool(), float("-inf")
) # [n, e]
_, topk_ids = torch.topk(
tmp_scores,
k=topk,
dim=-1,
sorted=(True if num_fused_shared_experts > 0 else True),
)
topk_weights = scores.gather(1, topk_ids)
if num_fused_shared_experts:
topk_ids[:, -1] = torch.randint(
low=num_experts,
high=num_experts + num_fused_shared_experts,
size=(topk_ids.size(0),),
dtype=topk_ids.dtype,
device=topk_ids.device,
)
if routed_scaling_factor is not None:
topk_weights[:, -1] = (
topk_weights[:, :-1].sum(dim=-1) / routed_scaling_factor
)
if renormalize:
topk_weights_sum = (
topk_weights.sum(dim=-1, keepdim=True)
if num_fused_shared_experts == 0
else topk_weights[:, :-1].sum(dim=-1, keepdim=True)
)
topk_weights = topk_weights / topk_weights_sum
if apply_routed_scaling_factor_on_output:
topk_weights *= routed_scaling_factor
topk_weights, topk_ids = topk_weights.to(torch.float32), topk_ids.to(torch.int32)
return topk_weights, topk_ids
def topk_sigmoid_torch_ref(
gating_output: torch.Tensor,
topk: int,
renormalize: bool,
correction_bias: torch.Tensor | None,
num_fused_shared_experts: int = 0,
routed_scaling_factor: float = 1.0,
apply_routed_scaling_factor_on_output: bool = True,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Reference: sigmoid → (add bias) → topk → (renormalize).
Indices are selected on biased scores; weights are the unbiased sigmoid values.
"""
num_experts = gating_output.shape[1]
scores = gating_output.float().sigmoid()
biased = scores if correction_bias is None else scores + correction_bias.float()
_, ref_ids = torch.topk(biased, k=topk, dim=-1)
ref_weights = scores.gather(1, ref_ids)
if num_fused_shared_experts > 0:
ref_ids[:, -1] = torch.randint(
low=num_experts,
high=num_experts + num_fused_shared_experts,
size=(ref_ids.size(0),),
dtype=ref_ids.dtype,
device=ref_ids.device,
)
ref_weights[:, -1] = ref_weights[:, :-1].sum(dim=-1) / routed_scaling_factor
if renormalize:
topk_weights_sum = (
ref_weights.sum(dim=-1, keepdim=True)
if num_fused_shared_experts == 0
else ref_weights[:, :-1].sum(dim=-1, keepdim=True)
)
ref_weights = ref_weights / topk_weights_sum
if apply_routed_scaling_factor_on_output:
ref_weights *= routed_scaling_factor
return ref_weights.float(), ref_ids.int()
def topk_sigmoid_grouped_ref(
gating_output: torch.Tensor,
topk: int,
renormalize: bool,
correction_bias: torch.Tensor | None,
num_fused_shared_experts: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]:
if correction_bias is not None:
return biased_grouped_topk_impl(
gating_output,
correction_bias,
topk,
renormalize,
num_expert_group=1,
topk_group=1,
num_fused_shared_experts=num_fused_shared_experts,
routed_scaling_factor=1.0,
apply_routed_scaling_factor_on_output=True,
)
else:
return grouped_topk_gpu(
gating_output,
topk,
renormalize,
num_expert_group=1,
topk_group=1,
num_fused_shared_experts=num_fused_shared_experts,
routed_scaling_factor=1.0,
apply_routed_scaling_factor_on_output=True,
scoring_func="sigmoid",
)
def topk_sigmoid_ref(
gating_output: torch.Tensor,
topk: int,
renormalize: bool,
correction_bias: torch.Tensor | None,
num_fused_shared_experts: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]:
return topk_sigmoid_torch_ref(
gating_output, topk, renormalize, correction_bias, num_fused_shared_experts
)
# ---------------------------------------------------------------------------
# Correctness: JIT vs PyTorch reference
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"num_tokens, num_experts, topk",
list(itertools.product(NUM_TOKENS, NUM_EXPERTS, TOPK_LIST)),
)
@pytest.mark.parametrize("dtype", DTYPES)
@pytest.mark.parametrize("renormalize", [False, True])
def test_topk_sigmoid_vs_ref(num_tokens, num_experts, topk, dtype, renormalize):
if topk > num_experts:
pytest.skip("topk > num_experts")
torch.manual_seed(num_tokens * num_experts)
gating = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
topk_sigmoid(topk_w, topk_i, gating, renormalize=renormalize)
ref_w, ref_i = topk_sigmoid_ref(gating, topk, renormalize, correction_bias=None)
# Compare sorted weights (indices may differ for ties when dtype != float32)
assert torch.allclose(
topk_w.sort(dim=-1)[0],
ref_w.sort(dim=-1)[0],
atol=1e-3,
rtol=1e-3,
), f"Weight mismatch (dtype={dtype}, n_exp={num_experts}, topk={topk}, renorm={renormalize})"
# Exact index match is only reliable for float32 (fp16/bf16 tie-breaking may differ)
if dtype == torch.float32:
assert torch.equal(
topk_i, ref_i
), f"Index mismatch (dtype={dtype}, n_exp={num_experts}, topk={topk})"
# ---------------------------------------------------------------------------
# Correctness: with correction_bias
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"num_tokens, num_experts, topk",
list(itertools.product(NUM_TOKENS, NUM_EXPERTS, TOPK_LIST)),
)
@pytest.mark.parametrize("renormalize", [False, True])
def test_topk_sigmoid_with_correction_bias(num_tokens, num_experts, topk, renormalize):
if topk > num_experts:
pytest.skip("topk > num_experts")
torch.manual_seed(num_tokens + num_experts + topk)
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
bias = torch.randn(num_experts, dtype=torch.float32, device="cuda")
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
topk_sigmoid(topk_w, topk_i, gating, renormalize=renormalize, correction_bias=bias)
ref_w, ref_i = topk_sigmoid_ref(gating, topk, renormalize, correction_bias=bias)
assert torch.allclose(
topk_w, ref_w, atol=1e-3, rtol=1e-3
), f"Weight mismatch with bias (n_exp={num_experts}, topk={topk}, renorm={renormalize})"
assert torch.equal(
topk_i, ref_i
), f"Index mismatch with bias (n_exp={num_experts}, topk={topk})"
# ---------------------------------------------------------------------------
# Correctness: with fused shared experts
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"num_tokens, num_experts, topk",
list(itertools.product(NUM_TOKENS, NUM_EXPERTS, TOPK_LIST)),
)
@pytest.mark.parametrize("renormalize", [False, True])
def test_topk_sigmoid_with_fused_shared_experts(
num_tokens, num_experts, topk, renormalize
):
if topk + 1 > num_experts:
pytest.skip("topk > num_experts")
torch.manual_seed(num_tokens + num_experts)
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
bias = torch.randn(num_experts, dtype=torch.float32, device="cuda")
topk_w = torch.empty((num_tokens, topk + 1), dtype=torch.float32, device="cuda")
topk_i = torch.empty((num_tokens, topk + 1), dtype=torch.int32, device="cuda")
topk_sigmoid(
topk_w,
topk_i,
gating,
renormalize=renormalize,
correction_bias=bias,
num_fused_shared_experts=1,
)
ref_w, ref_i = topk_sigmoid_ref(
gating, topk + 1, renormalize, correction_bias=bias, num_fused_shared_experts=1
)
assert torch.allclose(
topk_w, ref_w, atol=1e-3, rtol=1e-3
), f"Weight mismatch with bias (n_exp={num_experts}, topk={topk}, renorm={renormalize})"
assert torch.equal(
topk_i, ref_i
), f"Index mismatch with bias (n_exp={num_experts}, topk={topk})"
# ---------------------------------------------------------------------------
# Renormalization: weights should sum to 1 per row
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("num_tokens, num_experts, topk", [(128, 64, 4), (1, 8, 2)])
def test_renormalize_sums_to_one(num_tokens, num_experts, topk):
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
topk_sigmoid(topk_w, topk_i, gating, renormalize=True)
row_sums = topk_w.sum(dim=-1)
torch.testing.assert_close(
row_sums, torch.ones(num_tokens, device="cuda"), rtol=1e-4, atol=1e-4
)
# ---------------------------------------------------------------------------
# Output shape and dtype
# ---------------------------------------------------------------------------
def test_output_shapes_and_dtypes():
num_tokens, num_experts, topk = 64, 128, 4
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
topk_sigmoid(topk_w, topk_i, gating)
assert topk_w.shape == (num_tokens, topk)
assert topk_i.shape == (num_tokens, topk)
assert topk_w.dtype == torch.float32
assert topk_i.dtype == torch.int32
# ---------------------------------------------------------------------------
# Fallback path (non-power-of-2 experts)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("num_experts", [48, 96])
def test_fallback_non_power_of_two(num_experts):
num_tokens, topk = 64, 2
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
topk_sigmoid(topk_w, topk_i, gating, renormalize=True)
# Weights should be positive and sum to 1
assert torch.all(topk_w > 0)
torch.testing.assert_close(
topk_w.sum(dim=-1), torch.ones(num_tokens, device="cuda"), rtol=1e-4, atol=1e-4
)
# ---------------------------------------------------------------------------
# Cross-validation against AOT sgl_kernel
# ---------------------------------------------------------------------------
@pytest.mark.skipif(not AOT_AVAILABLE, reason="sgl_kernel not available")
@pytest.mark.parametrize(
"num_tokens, num_experts, topk",
list(itertools.product([1, 128, 1024], [8, 64, 128], [1, 4])),
)
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
@pytest.mark.parametrize("renormalize", [False, True])
def test_topk_sigmoid_vs_aot(num_tokens, num_experts, topk, dtype, renormalize):
if topk > num_experts:
pytest.skip("topk > num_experts")
torch.manual_seed(42)
gating = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
topk_w_jit = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
topk_i_jit = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
topk_sigmoid(topk_w_jit, topk_i_jit, gating, renormalize=renormalize)
topk_w_aot = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
topk_i_aot = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
topk_sigmoid_aot(topk_w_aot, topk_i_aot, gating, renormalize=renormalize)
assert torch.allclose(
topk_w_jit, topk_w_aot, atol=1e-3, rtol=1e-3
), f"JIT vs AOT weight mismatch (dtype={dtype}, n_exp={num_experts}, topk={topk})"
assert torch.equal(
topk_i_jit, topk_i_aot
), f"JIT vs AOT index mismatch (dtype={dtype}, n_exp={num_experts}, topk={topk})"
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))