Add Inkling model support (#31681)

Co-authored-by: Chunan Zeng <zcnrex@gmail.com>
Co-authored-by: Ke Bao <ispobaoke@gmail.com>
Co-authored-by: Yanbin Jiang <jybsuper@gmail.com>
Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com>
Co-authored-by: Qiaolin Yu <qiaolin.yu@radixark.ai>
Co-authored-by: Zhichen Zeng <zczeng@uw.edu>
Co-authored-by: Aurick Qiao <aurick@thinkingmachines.ai>
Co-authored-by: Joseph <jk@thinkingmachines.ai>
This commit is contained in:
Cheng Wan
2026-07-19 22:57:37 -07:00
committed by GitHub
co-authored by Chunan Zeng Ke Bao Yanbin Jiang Yuhao Yang Qiaolin Yu Zhichen Zeng Aurick Qiao Joseph
parent 829e9ce9d5
commit 02236fa38c
279 changed files with 74334 additions and 931 deletions
@@ -0,0 +1,203 @@
"""The fused attn prologue's conditional log-scaling-tau fold on the q path:
q_out must equal {per-head RMSNorm -> bf16 -> * tau -> bf16} (the unfused
{prologue -> apply_log_scaling_tau} rounding, exactly), and the k/v outputs
must be untouched by tau.
"""
import pytest
import torch
from sglang.jit_kernel.inkling_attn_prologue import inkling_attn_prologue_decode
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=40, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_cuda_ci(est_time=40, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
HEAD = 128
W = 4
EPS = 1e-6
def _run(t, dq, dkv, tau):
torch.manual_seed(1234)
dev = "cuda"
row = dq + 2 * dkv + 64 # packed qkvr row with an r tail
qkvr = torch.randn(t, row, device=dev, dtype=torch.bfloat16)
pool = 64
k_cache = torch.randn(pool, W - 1, dkv, device=dev, dtype=torch.bfloat16)
v_cache = torch.randn(pool, W - 1, dkv, device=dev, dtype=torch.bfloat16)
ci = torch.arange(t, device=dev, dtype=torch.int32) + 3
cm = torch.ones(t, device=dev, dtype=torch.bool)
kw = torch.randn(dkv, W, device=dev, dtype=torch.bfloat16) * 0.3
vw = torch.randn(dkv, W, device=dev, dtype=torch.bfloat16) * 0.3
qg = torch.randn(HEAD, device=dev, dtype=torch.bfloat16)
kg = torch.randn(HEAD, device=dev, dtype=torch.bfloat16)
slots = 256
loc = (torch.arange(t, device=dev, dtype=torch.int64) * 7 + 5) % slots
k_buf = torch.zeros(slots, dkv // HEAD, HEAD, device=dev, dtype=torch.bfloat16)
v_buf = torch.zeros_like(k_buf)
return inkling_attn_prologue_decode(
qkvr,
k_cache,
v_cache,
ci,
cm,
kw,
vw,
qg,
kg,
EPS,
loc,
k_buf,
v_buf,
0,
dq,
dq + dkv,
dq,
dkv,
activation=None,
use_residual=True,
do_store=True,
log_scaling_tau=tau,
)
def _q_ref(t, dq, dkv, tau):
torch.manual_seed(1234)
row = dq + 2 * dkv + 64
qkvr = torch.randn(t, row, device="cuda", dtype=torch.bfloat16)
# (regenerate the SAME rng stream as _run for the remaining tensors)
_ = torch.randn(64, W - 1, dkv, device="cuda", dtype=torch.bfloat16)
_ = torch.randn(64, W - 1, dkv, device="cuda", dtype=torch.bfloat16)
kw = torch.randn(dkv, W, device="cuda", dtype=torch.bfloat16) * 0.3
vw = torch.randn(dkv, W, device="cuda", dtype=torch.bfloat16) * 0.3
del kw, vw
qg = torch.randn(HEAD, device="cuda", dtype=torch.bfloat16)
q = qkvr[:, :dq].float().view(t, dq // HEAD, HEAD)
inv = torch.rsqrt(q.pow(2).mean(-1, keepdim=True) + EPS)
out = (q * inv * qg.float()).bfloat16()
if tau is not None:
out = (out.float() * tau.view(-1, 1, 1)).bfloat16()
return out.view(t, dq)
@pytest.mark.parametrize("t", [1, 3, 8, 32])
def test_prologue_decode_tau_fold(t):
dq, dkv = 2048, 256
tau = 1.0 + 0.1 * torch.rand(t, device="cuda", dtype=torch.float32)
q_tau, k_tau, v_tau, _ = _run(t, dq, dkv, tau)
q_ref = _q_ref(t, dq, dkv, tau)
torch.testing.assert_close(q_tau.float(), q_ref.float(), rtol=2e-2, atol=2e-2)
# tau must not touch the k/v legs.
q_off, k_off, v_off, _ = _run(t, dq, dkv, None)
assert torch.equal(k_tau, k_off)
assert torch.equal(v_tau, v_off)
# And with tau=None the q path matches the tau-free reference bit-wise
# modulo the norm's fp32 reduction (tolerance).
torch.testing.assert_close(
q_off.float(), _q_ref(t, dq, dkv, None).float(), rtol=2e-2, atol=2e-2
)
# The fold itself must be exactly {round -> fp32 mul -> round}: applying
# tau to the tau-free kernel output reproduces the fused output bit-wise.
refold = (q_off.float() * tau.view(-1, 1)).bfloat16()
assert torch.equal(q_tau, refold)
@pytest.mark.parametrize("t", [1, 7, 64, 2048])
def test_rel_logits_proj_prescale_tau(t):
"""RelLogitsProj's operand-side tau fold (r*tau before the einsum) must
match the legacy output-side scale within bf16 rounding."""
from sglang.kernels.ops.attention.log_scaling_tau import apply_log_scaling_tau
from sglang.srt.models.inkling_common.attn import RelLogitsProj
torch.manual_seed(t)
h, d_rel, e = 16, 16, 1024
m = RelLogitsProj(d_rel, e).cuda()
m.proj.data = torch.randn(d_rel, e, device="cuda", dtype=torch.bfloat16) * 0.1
r = torch.randn(t, h, d_rel, device="cuda", dtype=torch.bfloat16)
tau = 1.0 + 0.1 * torch.rand(t, device="cuda", dtype=torch.float32)
assert m._prescale_tau # default-on flag
out = m(r, tau)
ref = apply_log_scaling_tau(
torch.einsum("thd,de->the", r, m.proj), tau.view(-1, 1, 1)
)
torch.testing.assert_close(out.float(), ref.float(), rtol=2e-2, atol=2e-2)
# And without tau it is the plain einsum, bit-exact.
assert torch.equal(m(r), torch.einsum("thd,de->the", r, m.proj))
_QKVR_ROW = 2816 # dq 2048 + 2*dkv 512 + h*d_rel 256 (the TP4 packed row)
def _strided_r(t, h=16, d_rel=16, elem_offset=0):
"""r exactly as production builds it: the trailing slice of the packed
qkvr projection output, viewed [t, h, d_rel] (row stride = full row)."""
torch.manual_seed(t + elem_offset)
qkvr = torch.randn(t, _QKVR_ROW + elem_offset, device="cuda", dtype=torch.bfloat16)
off = _QKVR_ROW + elem_offset - h * d_rel
return qkvr[:, off:].view(t, h, d_rel)
@pytest.mark.parametrize("t", [1, 2, 48, 49, 64, 200, 1024])
def test_rel_logits_proj_strided_dispatch(t):
"""_project on the production strided layout must be BIT-identical to the
plain einsum in both dispatch bands -- the zero-copy batched matmul
(t <= _REL_PROJ_MATMUL_MAX_T) and {JIT row-compact -> einsum} above it.
Guards the band boundary, the as_strided compaction math, and the
batched-GEMM == flat-GEMM reduction-order claim the dispatch relies on."""
from sglang.srt.models.inkling_common.attn import RelLogitsProj
h, d_rel, e = 16, 16, 1024
m = RelLogitsProj(d_rel, e).cuda()
m.proj.data = torch.randn(d_rel, e, device="cuda", dtype=torch.bfloat16) * 0.1
assert m._proj_dispatch # default-on flag
r = _strided_r(t, h, d_rel)
ref = torch.einsum("thd,de->the", r.contiguous(), m.proj)
out = m(r)
assert out.is_contiguous()
assert torch.equal(out, ref)
# The tau path on the same strided layout (prescale compacts first).
from sglang.kernels.ops.attention.log_scaling_tau import apply_log_scaling_tau
tau = 1.0 + 0.1 * torch.rand(t, device="cuda", dtype=torch.float32)
ref_tau = apply_log_scaling_tau(ref, tau.view(-1, 1, 1))
torch.testing.assert_close(m(r, tau).float(), ref_tau.float(), rtol=2e-2, atol=2e-2)
def test_rel_logits_proj_dispatch_fallbacks():
"""The compact band must fall back to the plain einsum (and stay exact)
when the JIT copy is ineligible -- e.g. a 2-byte-aligned r slice -- and
flag-off must restore the undispatched einsum on every input."""
from sglang.srt.environ import envs
from sglang.srt.models.inkling_common.attn import (
_REL_PROJ_MATMUL_MAX_T,
RelLogitsProj,
)
h, d_rel, e = 16, 16, 1024
m = RelLogitsProj(d_rel, e).cuda()
m.proj.data = torch.randn(d_rel, e, device="cuda", dtype=torch.bfloat16) * 0.1
t = _REL_PROJ_MATMUL_MAX_T + 16 # inside the compact band
r_misaligned = _strided_r(t, h, d_rel, elem_offset=1)
assert r_misaligned.data_ptr() % 16 != 0
ref = torch.einsum("thd,de->the", r_misaligned.contiguous(), m.proj)
assert torch.equal(m(r_misaligned), ref)
with envs.SGLANG_OPT_USE_INKLING_REL_PROJ_DISPATCH.override(False):
m_off = RelLogitsProj(d_rel, e).cuda()
m_off.proj.data = m.proj.data
assert not m_off._proj_dispatch
r = _strided_r(t, h, d_rel)
assert torch.equal(m_off(r), torch.einsum("thd,de->the", r, m_off.proj))
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,69 @@
"""rel_proj_small_t: the single-launch small-t rel projection (optional tau
prescale folded in registers) must match the reference chains it replaces --
{r*tau -> bf16 round -> projection} -- within bf16 GEMM rounding, on both the
production strided-r layout and contiguous inputs."""
import pytest
import torch
from sglang.jit_kernel.inkling_rel_proj import rel_proj_small_t
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
H, K, E, ROW = 16, 16, 1024, 2816
def _make_r(t, strided):
torch.manual_seed(t + int(strided))
if strided:
qkvr = torch.randn(t, ROW, device="cuda", dtype=torch.bfloat16)
return qkvr[:, ROW - H * K :].view(t, H, K)
return torch.randn(t, H, K, device="cuda", dtype=torch.bfloat16)
def _ref(r, proj, tau):
rf = r.float()
if tau is not None:
# The prescale contract: r*tau rounds to bf16 BEFORE the dot.
rf = (rf * tau.view(-1, 1, 1)).bfloat16().float()
return torch.einsum("thd,de->the", rf, proj.float())
@pytest.mark.parametrize("t", [1, 2, 5, 16, 32])
@pytest.mark.parametrize("strided", [False, True])
@pytest.mark.parametrize("with_tau", [False, True])
def test_rel_proj_small_t(t, strided, with_tau):
r = _make_r(t, strided)
proj = torch.randn(K, E, device="cuda", dtype=torch.bfloat16) * 0.1
tau = (
1.0 + 0.1 * torch.rand(t, device="cuda", dtype=torch.float32)
if with_tau
else None
)
out = rel_proj_small_t(r, proj, tau)
assert out.is_contiguous() and out.shape == (t, H, E)
ref = _ref(r, proj, tau)
# fp32 accumulation, one bf16 round -- match the fp32 reference to
# 2 bf16 ulp (the GEMM reduction-order slack vs cuBLAS is within this).
torch.testing.assert_close(out.float(), ref, rtol=2e-2, atol=2e-2)
def test_rel_proj_tau_isolation():
"""tau must only scale: kernel(tau) == kernel(no tau) computed on the
pre-rounded r*tau operand -- guards the round-before-dot placement (a
fold AFTER the dot would diverge at large |logits|)."""
t = 8
r = _make_r(t, True)
proj = torch.randn(K, E, device="cuda", dtype=torch.bfloat16) * 0.1
tau = 1.0 + 0.5 * torch.rand(t, device="cuda", dtype=torch.float32)
out = rel_proj_small_t(r, proj, tau)
r_pre = (r.float() * tau.view(-1, 1, 1)).bfloat16()
assert torch.equal(out, rel_proj_small_t(r_pre.contiguous(), proj))
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,82 @@
"""The vectorized row-scale kernel must be BIT-identical to the triton
apply_log_scaling_tau kernel it replaces (same fp32 multiply + bf16 round),
including on the row-strided qkvr-slice layouts."""
import pytest
import torch
from sglang.jit_kernel.inkling_row_scale import row_scale_bf16
from sglang.kernels.ops.attention.log_scaling_tau import (
_apply_log_scaling_tau_kernel,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
def _triton_ref(x2d, tau):
import triton
rows, inner = x2d.shape
out = torch.empty(rows, inner, dtype=x2d.dtype, device=x2d.device)
total = rows * inner
_apply_log_scaling_tau_kernel[(triton.cdiv(total, 1024),)](
x2d, tau, out, x2d.stride(0), inner, total, BLOCK=1024
)
return out
@pytest.mark.parametrize("rows", [1, 3, 16, 200, 4096])
@pytest.mark.parametrize("inner", [8, 256, 2048, 16384])
@pytest.mark.parametrize("strided", [False, True])
def test_row_scale_bitexact(rows, inner, strided):
torch.manual_seed(rows + inner)
if strided:
packed = torch.randn(rows, inner + 40, device="cuda", dtype=torch.bfloat16)
x = packed[:, 8 : 8 + inner]
else:
x = torch.randn(rows, inner, device="cuda", dtype=torch.bfloat16)
tau = 1.0 + 0.1 * torch.rand(rows, device="cuda", dtype=torch.float32)
out = row_scale_bf16(x, tau)
ref = _triton_ref(x, tau)
assert torch.equal(out, ref)
@pytest.mark.parametrize("rows", [1, 3, 200, 4096])
@pytest.mark.parametrize("inner", [8, 256, 16384])
@pytest.mark.parametrize("strided", [False, True])
def test_row_compact_bitexact(rows, inner, strided):
"""The tau-less compaction flavor (kHasTau=false) must reproduce
.contiguous() exactly on the same strided layouts row_scale handles --
no other test exercises run_compact."""
from sglang.jit_kernel.inkling_row_scale import row_compact_bf16
torch.manual_seed(rows + inner)
if strided:
packed = torch.randn(rows, inner + 40, device="cuda", dtype=torch.bfloat16)
x = packed[:, 8 : 8 + inner]
else:
x = torch.randn(rows, inner, device="cuda", dtype=torch.bfloat16)
out = row_compact_bf16(x)
assert out.is_contiguous()
assert torch.equal(out, x.contiguous())
def test_dispatch_through_apply_log_scaling_tau():
from sglang.kernels.ops.attention.log_scaling_tau import apply_log_scaling_tau
torch.manual_seed(0)
for shape, view in (((7, 16, 16), (-1, 1, 1)), ((7, 2048), (-1, 1))):
x = torch.randn(*shape, device="cuda", dtype=torch.bfloat16)
tau = 1.0 + 0.1 * torch.rand(7, device="cuda", dtype=torch.float32)
out = apply_log_scaling_tau(x, tau.view(*view))
ref = _triton_ref(x.view(7, -1), tau).view(x.shape)
assert torch.equal(out, ref)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,90 @@
"""Precision test for the fused packed-topk unpack triton kernel used by the
Marlin MoE runner (fused-gate-topk support).
The FlashInfer / Inkling fused gate emits PackedTopKOutput -- int32
``(expert_id << 16) | bf16-weight-bits``. The Marlin runner reads topk_ids /
topk_weights separately, so it unpacks with a single Triton launch. This test
checks the kernel is bit-identical to the torch elementwise reference and that
pack -> unpack round-trips, across shapes / top_k / num_experts / weight
distributions.
"""
import sys
import pytest
import torch
from sglang.srt.layers.moe.moe_runner.marlin import _fused_unpack_packed_topk
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="1-gpu-large")
def _torch_unpack(packed: torch.Tensor):
ids = (packed >> 16).to(torch.int32)
w = (packed & 0xFFFF).to(torch.int16).view(torch.bfloat16).to(torch.float32)
return ids, w
def _torch_pack(ids: torch.Tensor, weights: torch.Tensor) -> torch.Tensor:
# inverse of the unpack, matching trtllm_lora_temp/topk_pack._pack_topk_kernel
wbits = weights.to(torch.bfloat16).view(torch.int16).to(torch.int32) & 0xFFFF
return (ids.to(torch.int32) << 16) | wbits
@pytest.mark.parametrize(
"num_tokens,top_k",
[
(1, 1),
(1, 2),
(3, 6),
(8, 2),
(127, 6),
(512, 6),
(631, 2),
(1024, 8),
(2048, 8),
],
)
@pytest.mark.parametrize("num_experts", [2, 8, 64, 256])
@pytest.mark.parametrize("wdist", ["uniform", "edge", "tiny"])
def test_unpack_matches_reference_and_roundtrips(num_tokens, top_k, num_experts, wdist):
torch.manual_seed(0)
ids = torch.randint(
0, num_experts, (num_tokens, top_k), dtype=torch.int32, device="cuda"
)
if wdist == "uniform":
w = torch.rand(num_tokens, top_k, device="cuda")
elif wdist == "edge":
choices = torch.tensor([0.0, 1.0, 0.5, 0.999, 1e-3], device="cuda")
w = choices[
torch.randint(0, choices.numel(), (num_tokens, top_k), device="cuda")
]
else:
w = torch.rand(num_tokens, top_k, device="cuda") * 1e-3
packed = _torch_pack(ids, w)
t_ids, t_w = _fused_unpack_packed_topk(packed)
r_ids, r_w = _torch_unpack(packed)
# bit-identical to the torch elementwise reference
assert torch.equal(t_ids, r_ids)
assert torch.equal(t_w, r_w)
# round-trip: ids exact, weights recover the bf16-rounded originals
assert torch.equal(t_ids, ids)
torch.testing.assert_close(
t_w, w.to(torch.bfloat16).to(torch.float32), rtol=0, atol=0
)
assert t_ids.dtype == torch.int32 and t_w.dtype == torch.float32
assert t_ids.shape == (num_tokens, top_k) and t_w.shape == (num_tokens, top_k)
def test_unpack_empty():
packed = torch.empty((0, 2), dtype=torch.int32, device="cuda")
ids, w = _fused_unpack_packed_topk(packed)
assert ids.shape == (0, 2) and w.shape == (0, 2)
assert ids.dtype == torch.int32 and w.dtype == torch.float32
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,229 @@
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=15, stage="base-b", runner_config="1-gpu-small")
"""Boundary-KV fix kernels (SGLANG_ENABLE_MTP_BOUNDARY_KV_FIX) vs a pure-torch reference.
Covers the three pieces behind the pool-free chain-MTP boundary-KV exactness
fix (widened draft-extend windows that rewrite draft KV rows keyed on rejected
chain proposals with their committed keys):
- compute_widened_draft_extend_locs_positions: batched out_cache_loc /
positions for the widened window (runs BEFORE the forward batch is built;
data-invalid rows zeroed, conv warm-up rows routed to the sacrificial
cache slot 0, sacrificial zone capped at the front);
- fill_widened_draft_extend_inputs: widened depth-0 window token/hidden
materialization (stash fronts | predict rows);
- stash_append_boundary_state: rolling per-request (token, base-hidden)
stash append, in both decode (accumulate valid_len) and prefill-seed
(SET valid_len, varlen sources) modes.
"""
import unittest
import torch
from sglang.srt.speculative.multi_layer_eagle_utils import (
compute_widened_draft_extend_locs_positions,
fill_widened_draft_extend_inputs_triton,
stash_append_boundary_state_triton,
)
from sglang.test.test_utils import CustomTestCase
DEV = "cuda"
# (bs, W, front, warmup, hidden, seq_lens, valid, accept)
CASES = [
# steady state: fully seeded stash, long sequences
(4, 4, 5, 3, 64, [100, 33, 200, 17], [5, 5, 5, 5], [1, 4, 2, 3]),
# fresh/short: partially seeded stash, seq_lens < front (degenerate rows)
(4, 4, 5, 3, 64, [3, 1, 6, 2], [2, 0, 5, 1], [1, 1, 4, 2]),
# d66-like 8-step chain shape without conv warm-up
(2, 9, 7, 0, 128, [50, 9], [7, 3], [9, 1]),
# minimal chain: 2 steps, W=3, front=1
(3, 3, 1, 0, 32, [10, 11, 12], [1, 1, 0], [1, 2, 3]),
]
def _ref_stash_append(
stash_t, stash_h, valid, src_t, src_h, ends, avail, rpis, set_valid
):
front = stash_t.shape[1]
for i, rpi in enumerate(rpis.tolist()):
m = min(int(avail[i]), front)
end = int(ends[i])
keep = front - m
stash_t[rpi, :keep] = stash_t[rpi, m:].clone()
stash_h[rpi, :keep] = stash_h[rpi, m:].clone()
stash_t[rpi, keep:] = src_t[end - m : end].to(stash_t.dtype)
stash_h[rpi, keep:] = src_h[end - m : end].to(stash_h.dtype)
if set_valid:
valid[rpi] = m
else:
valid[rpi] = min(int(valid[rpi]) + m, front)
def _ref_fill_and_locs(
predict, vhid, stash_t, stash_h, valid, seq_lens, rpis, req_to_token, W, warmup
):
bs = rpis.shape[0]
front = stash_t.shape[1]
width = W + front
h = stash_h.shape[2]
ids = torch.zeros(bs * width, dtype=torch.int64, device=DEV)
hid = torch.zeros(bs * width, h, dtype=stash_h.dtype, device=DEV)
pos = torch.zeros(bs * width, dtype=torch.int64, device=DEV)
loc = torch.zeros(bs * width, dtype=torch.int64, device=DEV)
for i, rpi in enumerate(rpis.tolist()):
seq_len = int(seq_lens[i])
first_valid = max(front - int(valid[rpi]), front - seq_len, 0)
# Sacrificial zone is capped at the front: original rows always write.
first_real = min(first_valid + warmup, front)
for j in range(width):
row = i * width + j
p = seq_len - front + j
if j >= front:
src = i * W + j - front
ids[row] = predict[src]
hid[row] = vhid[src]
pos[row] = p
loc[row] = req_to_token[rpi, p]
else:
if j >= first_valid:
ids[row] = stash_t[rpi, j]
hid[row] = stash_h[rpi, j]
pos[row] = p
if j >= first_real:
loc[row] = req_to_token[rpi, p]
return ids, hid, pos, loc
class TestBoundaryKvFixKernels(CustomTestCase):
def test_kernels_vs_reference(self):
for case_i, (
bs,
W,
front,
warmup,
hidden,
seq_lens,
valid,
accept,
) in enumerate(CASES):
with self.subTest(case=case_i):
self._run_case(bs, W, front, warmup, hidden, seq_lens, valid, accept)
def _run_case(self, bs, W, front, warmup, hidden, seq_lens, valid, accept):
torch.manual_seed(0)
pool, max_ctx = 32, 512
rpis = torch.randperm(pool, device=DEV)[:bs].to(torch.int64)
req_to_token = (
torch.arange(pool * max_ctx, device=DEV, dtype=torch.int32).reshape(
pool, max_ctx
)
+ 1000
)
seq_lens_t = torch.tensor(seq_lens, device=DEV, dtype=torch.int64)
stash_t = torch.randint(5, 900, (pool, front), device=DEV, dtype=torch.int64)
stash_h = torch.randn(pool, front, hidden, device=DEV, dtype=torch.bfloat16)
valid_t = torch.zeros(pool, dtype=torch.int32, device=DEV)
for i, rpi in enumerate(rpis.tolist()):
valid_t[rpi] = valid[i]
predict = torch.randint(5, 900, (bs * W,), device=DEV, dtype=torch.int64)
vhid = torch.randn(bs * W, hidden, device=DEV, dtype=torch.bfloat16)
r_ids, r_hid, r_pos, r_loc = _ref_fill_and_locs(
predict,
vhid,
stash_t,
stash_h,
valid_t,
seq_lens_t,
rpis,
req_to_token,
W,
warmup,
)
# --- locs / positions (pre-forward-batch torch path) ---
loc, pos = compute_widened_draft_extend_locs_positions(
seq_lens_t,
rpis,
req_to_token,
valid_t,
draft_token_num=W,
num_front_tokens=front,
num_warmup_tokens=warmup,
)
self.assertTrue(torch.equal(loc, r_loc), "locs mismatch")
self.assertTrue(torch.equal(pos, r_pos), "positions mismatch")
# --- widened window token/hidden fill ---
width = W + front
ids = torch.zeros(bs * width, dtype=torch.int64, device=DEV)
hid = torch.zeros(bs * width, hidden, dtype=torch.bfloat16, device=DEV)
fill_widened_draft_extend_inputs_triton(
ids,
hid,
predict,
vhid,
stash_t,
stash_h,
valid_t,
seq_lens_t,
rpis,
draft_token_num=W,
)
self.assertTrue(torch.equal(ids, r_ids), "fill mismatch on ids")
self.assertTrue(torch.equal(hid, r_hid), "fill mismatch on hid")
# --- decode-style stash roll-forward (accumulating valid_len) ---
accept_t = torch.tensor(accept, device=DEV, dtype=torch.int32)
ends = torch.arange(bs, device=DEV, dtype=torch.int64) * W + accept_t.to(
torch.int64
)
ref_t, ref_h, ref_v = stash_t.clone(), stash_h.clone(), valid_t.clone()
_ref_stash_append(
ref_t, ref_h, ref_v, predict, vhid, ends, accept_t, rpis, False
)
stash_append_boundary_state_triton(
predict,
vhid,
ends,
accept_t,
rpis,
stash_t,
stash_h,
valid_t,
set_valid=False,
)
self.assertTrue(torch.equal(stash_t, ref_t), "decode stash tokens mismatch")
self.assertTrue(torch.equal(stash_h, ref_h), "decode stash hiddens mismatch")
self.assertTrue(torch.equal(valid_t, ref_v), "decode stash valid_len mismatch")
# --- prefill-style seed (varlen segments, SET valid_len) ---
lens = torch.tensor(
[max(1, (i * 7) % (W + front)) for i in range(bs)],
device=DEV,
dtype=torch.int32,
)
starts = torch.cumsum(
torch.cat([torch.zeros(1, device=DEV, dtype=torch.int32), lens[:-1]]), 0
)
total = int(lens.sum())
src_t = torch.randint(5, 900, (total,), device=DEV, dtype=torch.int64)
src_h = torch.randn(total, hidden, device=DEV, dtype=torch.bfloat16)
ends2 = (starts + lens).to(torch.int64)
ref_t, ref_h, ref_v = stash_t.clone(), stash_h.clone(), valid_t.clone()
_ref_stash_append(ref_t, ref_h, ref_v, src_t, src_h, ends2, lens, rpis, True)
stash_append_boundary_state_triton(
src_t, src_h, ends2, lens, rpis, stash_t, stash_h, valid_t, set_valid=True
)
self.assertTrue(torch.equal(stash_t, ref_t), "seed stash tokens mismatch")
self.assertTrue(torch.equal(stash_h, ref_h), "seed stash hiddens mismatch")
self.assertTrue(torch.equal(valid_t, ref_v), "seed stash valid_len mismatch")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,197 @@
"""Unit tests for the fused conv-slot clear/copy kernels
(srt/mem_cache/mamba_slot_fused.py), checked bit-exact against the per-tensor
reference loop that MambaPool.clear_slots / copy_from fall back to.
Covers heterogeneous conv shapes, single- and multi-layer pools, single /
partial / full index sets, int32 indices, and the strided per-slot-envelope
layout used by page-major / unified pools.
"""
import unittest
import torch
from sglang.srt.mem_cache.mamba_slot_fused import (
build_conv_slot_descriptor,
fused_clear_conv_slots,
fused_copy_conv_slots,
)
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=30, stage="base-b", runner_config="1-gpu-small")
CONV_LEN = 3
# Representative hybrid conv-state trailing dims (a couple of KV-projection
# streams + wider residual streams); only the trailing dim differs.
HETERO_DIMS = [128, 128, 256, 256, 6144, 6144]
# (dims, num_layers, pool_size)
CONFIGS = [
(HETERO_DIMS, 1, 460), # single-layer draft pool, realistic size
(HETERO_DIMS, 3, 128), # multi-layer
([128], 1, 64), # single conv tensor
([256, 6144], 2, 32), # mixed shapes, 2 layers
]
def _make_convs(dims, num_layers, pool, device, seed):
g = torch.Generator(device=device).manual_seed(seed)
return [
torch.randn(
num_layers,
pool,
CONV_LEN,
d,
dtype=torch.bfloat16,
device=device,
generator=g,
)
for d in dims
]
def _envelope_views(buf, dims, num_layers, pool):
"""Strided per-slot-envelope views (page-major / unified layout): each conv
tensor is a slice inside a shared per-slot entry, so slot_stride is the whole
envelope, not the feature length."""
envelope = buf.shape[2]
views, off = [], 0
for d in dims:
views.append(
torch.as_strided(
buf,
size=(num_layers, pool, CONV_LEN, d),
stride=(pool * envelope, envelope, d, 1),
storage_offset=off,
)
)
off += CONV_LEN * d
return views
def _ref_clear(convs, idx):
for t in convs:
t[:, idx] = 0
def _ref_copy(convs, src, dst):
for t in convs:
t[:, dst] = t[:, src]
@unittest.skipUnless(torch.cuda.is_available(), "fused conv-slot kernels need CUDA")
class TestMambaSlotFused(CustomTestCase):
def test_clear_matches_reference(self):
dev = "cuda"
for dims, num_layers, pool in CONFIGS:
for n in sorted({1, pool // 3, pool}): # single / partial / all slots
with self.subTest(dims=dims, num_layers=num_layers, pool=pool, n=n):
base = _make_convs(dims, num_layers, pool, dev, seed=0)
idx = torch.randperm(pool, device=dev)[:n].to(torch.int64)
ref = [t.clone() for t in base]
got = [t.clone() for t in base]
_ref_clear(ref, idx)
fused_clear_conv_slots(build_conv_slot_descriptor(got), idx)
torch.cuda.synchronize()
for r, g in zip(ref, got):
self.assertTrue(torch.equal(r, g))
# Cleared slots are exactly zero; the rest is untouched.
keep = torch.ones(pool, dtype=torch.bool, device=dev)
keep[idx] = False
for g, b in zip(got, base):
self.assertTrue((g[:, idx] == 0).all().item())
self.assertTrue(torch.equal(g[:, keep], b[:, keep]))
def test_copy_matches_reference(self):
dev = "cuda"
for dims, num_layers, pool in CONFIGS:
with self.subTest(dims=dims, num_layers=num_layers, pool=pool):
base = _make_convs(dims, num_layers, pool, dev, seed=1)
perm = torch.randperm(pool, device=dev)
n = max(1, pool // 4)
src = perm[:n].to(torch.int64) # disjoint from dst (COW invariant)
dst = perm[n : 2 * n].to(torch.int64)
ref = [t.clone() for t in base]
got = [t.clone() for t in base]
_ref_copy(ref, src, dst)
fused_copy_conv_slots(build_conv_slot_descriptor(got), src, dst)
torch.cuda.synchronize()
for r, g in zip(ref, got):
self.assertTrue(torch.equal(r, g))
def test_strided_envelope_layout(self):
# Page-major / unified pools store conv tensors as strided views into a
# shared per-slot envelope (slot_stride = whole entry >> feat). The
# kernel reads real strides, so it must handle this; the whole envelope
# buffer (including the other streams' bytes in each slot) must be
# bit-exact vs the reference, proving no cross-stream clobber.
dev = "cuda"
num_layers, pool = 2, 48
dims = [128, 256, 6144]
envelope = sum(CONV_LEN * d for d in dims)
g = torch.Generator(device=dev).manual_seed(4)
buf = torch.randn(
num_layers, pool, envelope, dtype=torch.bfloat16, device=dev, generator=g
)
v0 = _envelope_views(buf, dims, num_layers, pool)[0]
self.assertFalse(v0.is_contiguous()) # strided view...
self.assertTrue(v0[0, 0].is_contiguous()) # ...but per-slot block is not
idx = torch.tensor([2, 7, 40], dtype=torch.int64, device=dev)
ref_buf = buf.clone()
got_buf = buf.clone()
_ref_clear(_envelope_views(ref_buf, dims, num_layers, pool), idx)
fused_clear_conv_slots(
build_conv_slot_descriptor(
_envelope_views(got_buf, dims, num_layers, pool)
),
idx,
)
torch.cuda.synchronize()
self.assertTrue(torch.equal(ref_buf, got_buf))
# copy on the same strided layout
src = torch.tensor([1, 20], dtype=torch.int64, device=dev)
dst = torch.tensor([30, 45], dtype=torch.int64, device=dev)
ref_buf = buf.clone()
got_buf = buf.clone()
_ref_copy(_envelope_views(ref_buf, dims, num_layers, pool), src, dst)
fused_copy_conv_slots(
build_conv_slot_descriptor(
_envelope_views(got_buf, dims, num_layers, pool)
),
src,
dst,
)
torch.cuda.synchronize()
self.assertTrue(torch.equal(ref_buf, got_buf))
def test_empty_indices_is_noop(self):
dev = "cuda"
base = _make_convs(HETERO_DIMS, 1, 16, dev, seed=2)
got = [t.clone() for t in base]
empty = torch.empty(0, dtype=torch.int64, device=dev)
desc = build_conv_slot_descriptor(got)
fused_clear_conv_slots(desc, empty)
fused_copy_conv_slots(desc, empty, empty)
torch.cuda.synchronize()
for b, g in zip(base, got):
self.assertTrue(torch.equal(b, g))
def test_int32_indices_accepted(self):
# deferred-clear/COW indices are staged as int32; the wrappers must upcast.
dev = "cuda"
base = _make_convs(HETERO_DIMS, 1, 32, dev, seed=3)
idx = torch.tensor([1, 5, 9], dtype=torch.int32, device=dev)
ref = [t.clone() for t in base]
got = [t.clone() for t in base]
_ref_clear(ref, idx.long())
fused_clear_conv_slots(build_conv_slot_descriptor(got), idx)
torch.cuda.synchronize()
for r, g in zip(ref, got):
self.assertTrue(torch.equal(r, g))
if __name__ == "__main__":
unittest.main()
@@ -17,6 +17,7 @@ import unittest
from typing import cast
from unittest.mock import MagicMock, patch
import torch
from torch.cuda import Event as CudaEvent
from torch.cuda import Stream as CudaStream
@@ -69,6 +70,7 @@ class TestLoRAOverlapLoaderUnitTests(CustomTestCase):
self.mock_lora_manager.memory_pool.uid_to_buffer_id = {}
self.mock_lora_manager.validate_lora_batch.return_value = True
self.mock_lora_manager.fetch_new_loras.side_effect = self._mark_loras_loaded
self.mock_lora_manager.pending_lora_load_events = {}
def tearDown(self):
self.torch_patcher.stop()
@@ -155,6 +157,46 @@ class TestLoRAOverlapLoaderUnitTests(CustomTestCase):
self.mock_lora_manager.fetch_new_loras.assert_not_called()
self.assertIn("lora_A", loader.lora_to_overlap_load_event)
def test_loader_uses_manager_pending_event_store(self):
loader = self._create_loader()
self.assertIs(
loader.lora_to_overlap_load_event,
self.mock_lora_manager.pending_lora_load_events,
)
def test_pending_load_is_synchronized_before_unload(self):
manager = LoRAManager.__new__(LoRAManager)
manager.device = torch.device("cuda:0")
manager.pending_lora_load_events = {}
manager.memory_pool = MagicMock()
manager.configs = {"lora_A": object()}
manager.loras = {"lora_A": object()}
lora_ref = MagicMock()
lora_ref.lora_id = "lora_A"
lora_ref.lora_name = "lora_A"
lora_ref.lora_path = "/tmp/lora_A"
lora_ref.pinned = False
manager.lora_refs = {"lora_A": lora_ref}
manager.num_pinned_loras = 0
manager.lora_modules = []
order = []
event = self._create_mock_event(False)
event.synchronize.side_effect = lambda: order.append("synchronize")
manager.memory_pool.remove_lora.side_effect = lambda _uid: (
order.append("remove") or 0
)
loader = LoRAOverlapLoader(manager)
loader.lora_to_overlap_load_event["lora_A"] = event
result = manager.unload_lora_adapter(lora_ref)
self.assertTrue(result.success)
self.assertEqual(order, ["synchronize", "remove"])
event.synchronize.assert_called_once_with()
self.assertNotIn("lora_A", manager.pending_lora_load_events)
def test_full_lifecycle_single_lora_load(self):
loader = self._create_loader()
@@ -396,5 +396,45 @@ class TestCreateGrammarBackend(unittest.TestCase):
self.assertIsNone(kwargs["model_eos_token_ids"])
class TestLlguidanceStructuralTagTriggerPairing(unittest.TestCase):
"""Bug regression: dispatch_structural_tag paired EVERY structure with
triggers[0]. Detectors with per-tool triggers (Inkling emits
<|message_model|>{name}<|content_invoke_tool_json|> per tool) produce
multiple distinct triggers, and llguidance's StructTag asserts
begin.startswith(trigger) — so any multi-tool constrained request
compiled to InvalidGrammarObject."""
def test_each_structure_pairs_with_its_own_trigger(self):
import json
from sglang.srt.constrained.llguidance_backend import GuidanceBackend
backend = object.__new__(GuidanceBackend)
backend._from_serialized = lambda serialized: serialized
begins = [
'<|message_model|>alpha<|content_invoke_tool_json|>{"name":"alpha","args":',
'<|message_model|>beta<|content_invoke_tool_json|>{"name":"beta","args":',
]
key = json.dumps(
{
"type": "structural_tag",
"structures": [
{
"begin": begin,
"schema": {"type": "object"},
"end": "<|end_message|>",
}
for begin in begins
],
"triggers": [
"<|message_model|>alpha<|content_invoke_tool_json|>",
"<|message_model|>beta<|content_invoke_tool_json|>",
],
}
)
result = backend.dispatch_structural_tag(key)
self.assertNotIsInstance(result, InvalidGrammarObject)
if __name__ == "__main__":
unittest.main()
@@ -259,19 +259,27 @@ class TestChatCompletionRequest(unittest.TestCase):
self.assertFalse(request.chat_template_kwargs.get("thinking"))
self.assertFalse(request.chat_template_kwargs.get("enable_thinking"))
def test_chat_completion_reasoning_effort_max(self):
"""`max` is an sglang extension on chat completion's top-level
`reasoning_effort` only; the Responses-API-style nested
`reasoning.effort` path stays aligned with OpenAI's three levels."""
def test_chat_completion_extended_reasoning_effort_levels(self):
"""Extended effort levels work in both supported request forms."""
from pydantic import ValidationError
messages = [{"role": "user", "content": "Hello"}]
request = ChatCompletionRequest(
model="test-model",
messages=messages,
reasoning_effort="max",
)
self.assertEqual(request.reasoning_effort, "max")
for effort in ("xhigh", "max"):
with self.subTest(effort=effort, request_form="top-level"):
request = ChatCompletionRequest(
model="test-model",
messages=messages,
reasoning_effort=effort,
)
self.assertEqual(request.reasoning_effort, effort)
with self.subTest(effort=effort, request_form="nested"):
request = ChatCompletionRequest(
model="test-model",
messages=messages,
reasoning={"effort": effort},
)
self.assertEqual(request.reasoning_effort, effort)
# Unknown values still rejected.
with self.assertRaises(ValidationError):
@@ -281,14 +289,79 @@ class TestChatCompletionRequest(unittest.TestCase):
reasoning_effort="ultra",
)
# Nested reasoning.effort=max is NOT promoted by normalize_reasoning_inputs:
# the Responses API path keeps the OpenAI low/medium/high contract.
def test_chat_completion_reasoning_effort_is_strictly_validated(self):
from pydantic import ValidationError
messages = [{"role": "user", "content": "Hello"}]
for request_kwargs, expected in (
({"reasoning_effort": 0.99}, 0.99),
({"reasoning": {"effort": 0.0}}, 0.0),
# numeric strings coerce identically on BOTH request surfaces
# (the top-level field's lax union already coerced them).
({"reasoning": {"effort": "0.5"}}, 0.5),
({"reasoning": {"effort": None, "reasoning_effort": 0.4}}, 0.4),
):
request = ChatCompletionRequest(
model="test-model", messages=messages, **request_kwargs
)
self.assertEqual(request.reasoning_effort, expected)
for request_kwargs in (
{"reasoning_effort": -0.1},
# 0.99 is the maximum valid effort; 1.0 is out of range.
{"reasoning_effort": 1.0},
{"reasoning_effort": 1.1},
{"reasoning_effort": float("nan")},
{"reasoning_effort": True},
{"reasoning": {"effort": "invalid"}},
{"reasoning": {"effort": 1.0}},
{"reasoning": {"effort": 1.1}},
{"reasoning": {"effort": "1.5"}},
):
with self.subTest(request_kwargs=request_kwargs), self.assertRaises(
ValidationError
):
ChatCompletionRequest(
model="test-model", messages=messages, **request_kwargs
)
def test_chat_completion_accepts_ordered_thinking_parts(self):
request = ChatCompletionRequest(
model="test-model",
messages=messages,
reasoning={"effort": "max"},
messages=[
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "first"},
{"type": "text", "text": "visible"},
{"type": "reasoning", "text": "second"},
],
}
],
)
self.assertNotEqual(request.reasoning_effort, "max")
parts = request.messages[0].content
self.assertEqual(
[part.type for part in parts], ["thinking", "text", "reasoning"]
)
def test_chat_completion_rejects_thinking_parts_outside_assistant(self):
"""Bug regression: adding the thinking part to the SHARED content-part
union silently widened acceptance to every role (user/system/tool) and
every model family, where downstream templates cannot render it —
replacing the previous clean 422 with template-dependent behavior."""
from pydantic import ValidationError
for role in ("user", "system", "tool"):
with self.subTest(role=role), self.assertRaises(ValidationError):
ChatCompletionRequest(
model="test-model",
messages=[
{
"role": role,
"content": [{"type": "thinking", "thinking": "x"}],
}
],
)
def test_chat_completion_json_format(self):
"""Test chat completion json format"""
@@ -2325,6 +2325,29 @@ class TestProcessToolCallsWithRequiredToolChoice(unittest.TestCase):
self.assertEqual(tool_calls[0].function.name, "get_weather")
self.assertEqual(fr["type"], "tool_calls")
def test_empty_parser_result_is_not_reported_as_tool_call(self):
with patch(
"sglang.srt.entrypoints.openai.serving_chat.FunctionCallParser"
) as ParserMock:
parser_instance = ParserMock.return_value
parser_instance.has_tool_call.return_value = True
parser_instance.detector.supports_structural_tag.return_value = True
parser_instance.parse_non_stream.return_value = ("Visible prefix.", [])
finish_reason = {"type": "stop", "matched": None}
tools = [{"type": "function", "function": {"name": "get_weather"}}]
tool_calls, text, fr = self.chat._process_tool_calls(
text="<|malformed_tool_call|>",
tools=tools,
finish_reason=finish_reason,
tool_choice="required",
)
self.assertIsNone(tool_calls)
self.assertEqual(text, "Visible prefix.")
self.assertEqual(fr, {"type": "stop", "matched": None})
def test_required_without_parser_falls_back_to_json(self):
"""tool_choice='required' without parser should parse as JSON array."""
self.chat.tool_call_parser = None
@@ -2393,5 +2416,156 @@ class TestNormalizeToolContent(unittest.TestCase):
self.assertEqual(result, "plain rich")
class InklingReasoningEffortTest(unittest.TestCase):
"""Inkling reasoning-effort mapping and validation."""
def test_named_levels(self):
parse = OpenAIServingChat._parse_inkling_reasoning_effort
self.assertEqual(parse("none"), 0.0)
self.assertEqual(parse("minimal"), 0.1)
self.assertEqual(parse("low"), 0.2)
self.assertEqual(parse("medium"), 0.7)
self.assertEqual(parse("high"), 0.9)
# "xhigh" and "max" are aliases for the same 0.99 ceiling
self.assertEqual(parse("xhigh"), 0.99)
self.assertEqual(parse("max"), 0.99)
self.assertEqual(parse("max"), parse("xhigh"))
def test_scalar_range_is_validated(self):
parse = OpenAIServingChat._parse_inkling_reasoning_effort
self.assertEqual(parse(0.5), 0.5)
self.assertEqual(parse(0.99), 0.99)
for value in (1.0, "1.0", 2.0, "1.5", -1.0, float("nan"), True):
with self.subTest(value=value), self.assertRaises(ValueError):
parse(value)
def test_invalid_and_none(self):
parse = OpenAIServingChat._parse_inkling_reasoning_effort
self.assertIsNone(parse(None))
with self.assertRaises(ValueError):
parse("garbage")
def test_env_default(self):
from sglang.srt.environ import envs
get = OpenAIServingChat._get_inkling_default_reasoning_effort
env = envs.SGLANG_INKLING_DEFAULT_REASONING_EFFORT
try:
env.clear() # unset -> EnvStr default "0.9"
self.assertEqual(get(), 0.9)
env.set("") # explicit empty still uses the protocol default
self.assertEqual(get(), 0.9)
env.set("0.7")
self.assertEqual(get(), 0.7)
for value in ("1.0", "1.1", "garbage"):
env.set(value)
with self.subTest(value=value), self.assertRaises(ValueError):
get()
finally:
env.clear()
def test_serving_does_not_prefill_model_message(self):
from sglang.srt.parser.inkling_tokenizer import INKLING_SPECIAL_TOKEN_IDS
class Tokenizer:
def encode(self, text, add_special_tokens=False):
return list(text.encode())
serving = object.__new__(OpenAIServingChat)
serving.chat_encoding_spec = "inkling"
serving.tokenizer_manager = Mock(tokenizer=Tokenizer())
request = ChatCompletionRequest(
model="test-model",
messages=[{"role": "user", "content": "hello"}],
reasoning_effort=0.5,
)
prompt_ids = serving._encode_messages(
[message.model_dump() for message in request.messages],
request,
thinking_mode=None,
)
self.assertEqual(prompt_ids[-1], INKLING_SPECIAL_TOKEN_IDS["<|end_message|>"])
def test_continue_final_message_resumes_open_model_text_block(self):
"""Bug regression: continue_final_message was silently ignored on the
inkling path — the trailing assistant message rendered as a CLOSED
historical turn (<|end_message|> + <|content_model_end_sampling|>), so
the model started a fresh turn instead of continuing. The prefix must
render as an OPEN model text block."""
from sglang.srt.parser.inkling_tokenizer import INKLING_SPECIAL_TOKEN_IDS
class Tokenizer:
def encode(self, text, add_special_tokens=False):
return list(text.encode())
serving = object.__new__(OpenAIServingChat)
serving.chat_encoding_spec = "inkling"
serving.tokenizer_manager = Mock(tokenizer=Tokenizer())
request = ChatCompletionRequest(
model="test-model",
messages=[
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "The answer"},
],
reasoning_effort=0.5,
continue_final_message=True,
)
prompt_ids = serving._encode_messages(
[message.model_dump() for message in request.messages],
request,
thinking_mode=None,
)
open_block = [
INKLING_SPECIAL_TOKEN_IDS["<|message_model|>"],
INKLING_SPECIAL_TOKEN_IDS["<|content_text|>"],
*list(b"The answer"),
]
self.assertEqual(prompt_ids[-len(open_block) :], open_block)
self.assertNotIn(
INKLING_SPECIAL_TOKEN_IDS["<|content_model_end_sampling|>"], prompt_ids
)
def test_continue_final_message_leaves_tool_call_turns_closed(self):
"""A trailing assistant message with tool_calls cannot be continued —
it must keep rendering as a closed historical turn."""
from sglang.srt.parser.inkling_tokenizer import INKLING_SPECIAL_TOKEN_IDS
class Tokenizer:
def encode(self, text, add_special_tokens=False):
return list(text.encode())
serving = object.__new__(OpenAIServingChat)
serving.chat_encoding_spec = "inkling"
serving.tokenizer_manager = Mock(tokenizer=Tokenizer())
request = ChatCompletionRequest(
model="test-model",
messages=[
{"role": "user", "content": "hello"},
{
"role": "assistant",
"content": "calling",
"tool_calls": [
{
"id": "call-1",
"type": "function",
"function": {"name": "weather", "arguments": "{}"},
}
],
},
],
reasoning_effort=0.5,
continue_final_message=True,
)
prompt_ids = serving._encode_messages(
[message.model_dump() for message in request.messages],
request,
thinking_mode=None,
)
self.assertEqual(
prompt_ids[-1],
INKLING_SPECIAL_TOKEN_IDS["<|content_model_end_sampling|>"],
)
if __name__ == "__main__":
unittest.main(verbosity=2)
@@ -22,6 +22,7 @@ from sglang.srt.function_call.gigachat3_detector import GigaChat3Detector
from sglang.srt.function_call.glm4_moe_detector import Glm4MoeDetector
from sglang.srt.function_call.glm47_moe_detector import Glm47MoeDetector
from sglang.srt.function_call.gpt_oss_detector import GptOssDetector
from sglang.srt.function_call.inkling_detector import InklingDetector
from sglang.srt.function_call.json_array_parser import JsonArrayParser
from sglang.srt.function_call.kimik2_detector import KimiK2Detector
from sglang.srt.function_call.lfm2_detector import Lfm2Detector
@@ -35,6 +36,249 @@ register_cpu_ci(est_time=15, suite="base-a-test-cpu")
register_cpu_ci(est_time=61, suite="base-c-test-cpu")
class TestInklingDetector(unittest.TestCase):
def setUp(self):
self.tools = [
Tool(
type="function",
function=Function(
name="weather",
description="Lookup weather",
parameters={"type": "object"},
),
)
]
def test_canonical_header_is_not_visible_content(self):
detector = InklingDetector()
source = (
"<|message_model|>weather<|content_invoke_tool_json|>"
'{"name":"weather","args":{"city":"SF"}}<|end_message|>'
)
result = detector.detect_and_parse(source, self.tools)
self.assertEqual(result.normal_text, "")
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.calls[0].name, "weather")
self.assertEqual(json.loads(result.calls[0].parameters), {"city": "SF"})
def test_streaming_header_is_buffered_until_the_tool_kind(self):
detector = InklingDetector()
chunks = [
"<|message_model|>",
"weat",
"her",
"<|content_invoke_tool_json|>",
'{"name":"weather",',
'"args":{"city":"SF"}}',
"<|end_message|>",
]
normal_text = ""
name = None
parameters = ""
for chunk in chunks:
result = detector.parse_streaming_increment(chunk, self.tools)
normal_text += result.normal_text
for call in result.calls:
name = call.name or name
parameters += call.parameters
self.assertEqual(normal_text, "")
self.assertEqual(name, "weather")
self.assertEqual(json.loads(parameters), {"city": "SF"})
def test_mismatched_header_is_rejected(self):
detector = InklingDetector()
source = (
"<|message_model|>other<|content_invoke_tool_json|>"
'{"name":"weather","args":{}}<|end_message|>'
)
result = detector.detect_and_parse(source, self.tools)
self.assertEqual(result.calls, [])
def test_rejected_call_does_not_leak_protocol_tokens(self):
"""Bug regression: the no-surviving-calls path returned the RAW text,
so a rejected call (e.g. header/payload mismatch) leaked <|...|>
protocol tokens into user-visible content."""
detector = InklingDetector()
source = (
"<|message_model|>other<|content_invoke_tool_json|>"
'{"name":"weather","args":{}}<|end_message|>'
)
result = detector.detect_and_parse(source, self.tools)
self.assertNotIn("<|", result.normal_text)
# Framework parity: the rejected tool-call REGION is dropped entirely
# (normal_text = content before the marker), like every other detector
# — the JSON payload must not surface as visible content either.
self.assertEqual(result.normal_text, "")
def test_headerless_legacy_tool_call_still_parses(self):
"""Spec tolerance: a bare <|content_invoke_tool_json|> block with no
<|message_model|>name header (the pre-canonical form) must keep
parsing in both modes."""
source = '<|content_invoke_tool_json|>{"name":"weather","args":{"city":"SF"}}<|end_message|>'
result = InklingDetector().detect_and_parse(source, self.tools)
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.calls[0].name, "weather")
streaming = InklingDetector()
name = None
parameters = ""
for char in source:
for call in streaming.parse_streaming_increment(char, self.tools).calls:
name = call.name or name
parameters += call.parameters
self.assertEqual(name, "weather")
self.assertEqual(json.loads(parameters), {"city": "SF"})
def test_streaming_two_sequential_tool_calls_get_distinct_indices(self):
"""Coverage for multi-call responses: two back-to-back canonical tool
calls must stream as tool_index 0 and 1 with per-call args."""
detector = InklingDetector()
source = (
"<|message_model|>weather<|content_invoke_tool_json|>"
'{"name":"weather","args":{"city":"SF"}}<|end_message|>'
"<|message_model|>weather<|content_invoke_tool_json|>"
'{"name":"weather","args":{"city":"NY"}}<|end_message|>'
)
args_by_index: dict = {}
for char in source:
for call in detector.parse_streaming_increment(char, self.tools).calls:
args_by_index[call.tool_index] = (
args_by_index.get(call.tool_index, "") + call.parameters
)
self.assertEqual(sorted(args_by_index), [0, 1])
self.assertEqual(json.loads(args_by_index[0]), {"city": "SF"})
self.assertEqual(json.loads(args_by_index[1]), {"city": "NY"})
def test_streaming_rejection_does_not_collide_tool_indices(self):
"""Bug regression: a rejected mid-stream call reset current_tool_id to
-1, so the NEXT valid call re-announced as tool_index 0 — colliding
with the first call's index and slicing its arguments against index
0's already-streamed args."""
detector = InklingDetector()
chunks = [
"<|message_model|>weather<|content_invoke_tool_json|>",
'{"name":"weather","args":{"city":"SF"}}<|end_message|>',
# header/payload mismatch -> rejected
"<|message_model|>other<|content_invoke_tool_json|>",
'{"name":"weather","args":{"city":"NY"}}<|end_message|>',
# valid again
"<|message_model|>weather<|content_invoke_tool_json|>",
'{"name":"weather","args":{"city":"LA"}}<|end_message|>',
]
args_by_index: dict = {}
for chunk in chunks:
for call in detector.parse_streaming_increment(chunk, self.tools).calls:
args_by_index[call.tool_index] = (
args_by_index.get(call.tool_index, "") + call.parameters
)
self.assertEqual(json.loads(args_by_index[0]), {"city": "SF"})
self.assertEqual(len(args_by_index), 2)
second_index = max(args_by_index)
self.assertGreater(second_index, 0)
self.assertEqual(json.loads(args_by_index[second_index]), {"city": "LA"})
def test_undeclared_tool_name_is_surfaced(self):
"""A call to a tool absent from the request's tool list surfaces as a
structured tool_call (OpenAI behavior for hallucinated tools) so agent
harnesses can return a tool error and let the model self-correct,
instead of the serialized invocation becoming terminal answer text."""
detector = InklingDetector()
source = (
"<|message_model|>document_search<|content_invoke_tool_json|>"
'{"name":"document_search","args":{"query":"q"}}<|end_message|>'
)
result = detector.detect_and_parse(source, self.tools)
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.calls[0].name, "document_search")
self.assertEqual(json.loads(result.calls[0].parameters), {"query": "q"})
self.assertNotIn("<|", result.normal_text)
def test_undeclared_tool_name_surfaces_in_streaming(self):
detector = InklingDetector()
source = (
"<|message_model|>document_search<|content_invoke_tool_json|>"
'{"name":"document_search","args":{"query":"q"}}<|end_message|>'
)
normal_text = ""
name = None
parameters = ""
for char in source:
result = detector.parse_streaming_increment(char, self.tools)
normal_text += result.normal_text
for call in result.calls:
name = call.name or name
parameters += call.parameters
self.assertEqual(name, "document_search")
self.assertEqual(json.loads(parameters), {"query": "q"})
self.assertNotIn("<|", normal_text)
def test_malformed_json_does_not_leak_protocol_tokens(self):
"""Malformed JSON must drop the protocol region and its tool header."""
detector = InklingDetector()
source = (
"<|message_model|>weather<|content_invoke_tool_json|>"
"{not json at all<|end_message|>"
)
result = detector.detect_and_parse(source, self.tools)
self.assertEqual(result.calls, [])
self.assertEqual(result.normal_text, "")
def test_parser_does_not_restore_malformed_tool_call_as_text(self):
"""The parser wrapper must preserve the detector's sanitized fallback."""
from sglang.srt.function_call.function_call_parser import FunctionCallParser
source = (
"Visible prefix."
"<|message_model|>weather<|content_invoke_tool_json|>"
"{not json at all<|end_message|>"
)
normal_text, calls = FunctionCallParser(self.tools, "inkling").parse_non_stream(
source
)
self.assertEqual(normal_text, "Visible prefix.")
self.assertEqual(calls, [])
def test_parser_preserves_text_without_tool_call_marker(self):
from sglang.srt.function_call.function_call_parser import FunctionCallParser
source = " Ordinary assistant text. "
normal_text, calls = FunctionCallParser(self.tools, "inkling").parse_non_stream(
source
)
self.assertEqual(normal_text, source)
self.assertEqual(calls, [])
def test_malformed_call_does_not_discard_an_earlier_valid_call(self):
source = (
"<|message_model|>weather<|content_invoke_tool_json|>"
'{"name":"weather","args":{"city":"SF"}}<|end_message|>'
"<|message_model|>weather<|content_invoke_tool_json|>"
"{not json at all<|end_message|>"
)
result = InklingDetector().detect_and_parse(source, self.tools)
self.assertEqual(result.normal_text, "")
self.assertEqual(len(result.calls), 1)
self.assertEqual(result.calls[0].name, "weather")
self.assertEqual(json.loads(result.calls[0].parameters), {"city": "SF"})
def test_clean_normal_text_strips_the_full_control_alphabet(self):
"""Fall-through text is cleaned against the whole shared control-token
alphabet, not a hand-picked subset."""
detector = InklingDetector()
source = (
"<|message_model|><|content_thinking|>leak<|end_message|>"
"<|message_user|>x<|content_audio_input|><|audio_end|>"
)
result = detector.detect_and_parse(source, self.tools)
self.assertNotIn("<|", result.normal_text)
def test_structural_tag_uses_the_canonical_header(self):
info = InklingDetector().structure_info()("weather")
header = "<|message_model|>weather<|content_invoke_tool_json|>"
self.assertEqual(info.trigger, header)
self.assertTrue(info.begin.startswith(header + '{"name":"weather"'))
class TestPythonicDetector(unittest.TestCase):
def setUp(self):
# Create sample tools for testing
@@ -3968,6 +4212,31 @@ class TestGetStructureConstraint(unittest.TestCase):
result = parser.get_structure_constraint("auto")
self.assertIsNone(result)
def test_inkling_auto_constrains_json_after_tool_trigger(self):
import xgrammar as xgr
from sglang.srt.parser.inkling_tokenizer import INKLING_SPECIAL_TOKEN_IDS
parser = self._make_parser("inkling", strict=False)
result = parser.get_structure_constraint("auto")
self.assertIsNotNone(result)
self.assertEqual(result[0], "structural_tag")
self.assertIsInstance(result[1], xgr.StructuralTag)
format_ = result[1].model_dump()["format"]
self.assertEqual(format_["type"], "token_triggered_tags")
self.assertEqual(
format_["trigger_tokens"],
[INKLING_SPECIAL_TOKEN_IDS["<|content_invoke_tool_json|>"]],
)
tag = format_["tags"][0]
self.assertEqual(
tag["end"]["token"], INKLING_SPECIAL_TOKEN_IDS["<|end_message|>"]
)
schema = tag["content"]["json_schema"]
self.assertEqual(schema["required"], ["name", "args"])
self.assertFalse(schema["additionalProperties"])
def test_kimi_named_tool_choice_returns_structural_tag(self):
from sglang.srt.entrypoints.openai.protocol import (
ToolChoice,
@@ -0,0 +1,249 @@
"""CUDA graph tests for multi-LoRA merged alignment."""
from __future__ import annotations
import ast
import sys
import types
from pathlib import Path
import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=15, stage="base-b", runner_config="1-gpu-small")
# Skipped on CI: newly-added inkling LoRA test, disabled pending stabilization.
pytestmark = pytest.mark.skip(reason="new inkling LoRA test; disabled on CI")
ALIGN_PATH = (
Path(__file__).resolve().parents[4]
/ "python/sglang/kernels/ops/moe/trtllm_lora_temp/virtual_experts.py"
)
def _load_align_function():
tree = ast.parse(ALIGN_PATH.read_text())
function = next(
node
for node in tree.body
if isinstance(node, ast.FunctionDef) and node.name == "_align_block_size_jit"
)
module = ast.fix_missing_locations(ast.Module(body=[function], type_ignores=[]))
namespace = {
"torch": torch,
"jit_moe_align_block_size": sys.modules[
"sglang.jit_kernel.moe_align"
].moe_align_block_size,
}
exec(compile(module, str(ALIGN_PATH), "exec"), namespace)
return namespace["_align_block_size_jit"]
def test_experimental_alignment_geometry_and_empty_input(monkeypatch):
calls = []
def fake_jit_align(*args):
(
topk_ids,
num_buckets,
block_size,
sorted_ids,
expert_ids,
total,
cumsum,
flag,
) = args
calls.append((num_buckets, cumsum.numel(), flag))
sorted_ids.fill_(topk_ids.numel())
expert_ids[:2] = torch.tensor([-1, num_buckets - 2])
total.fill_(2 * block_size)
monkeypatch.setitem(
sys.modules,
"sglang.jit_kernel.moe_align",
types.SimpleNamespace(moe_align_block_size=fake_jit_align),
)
align = _load_align_function()
topk_ids = torch.tensor([[-1, 383]], dtype=torch.int32)
sorted_ids, expert_ids, num_tokens_post_pad = align(topk_ids, 5, 384)
assert sorted_ids.numel() == 12 # int4-safe capacity above logical 10.
assert expert_ids.numel() == 3
assert calls == [(385, 386, True)]
assert expert_ids[:2].tolist() == [-1, 383]
assert num_tokens_post_pad.item() == 10
outputs = align(torch.empty((0, 6), dtype=torch.int32), 16, 384)
assert [tensor.numel() for tensor in outputs] == [0, 0, 1]
assert outputs[2].item() == 0
assert len(calls) == 1
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required")
def test_experimental_alignment_cuda_sentinel_and_max_expert():
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
_align_block_size_jit,
)
topk_ids = torch.tensor([[-1, 383], [0, 0]], device="cuda", dtype=torch.int32)
_, expert_ids, num_tokens_post_pad = _align_block_size_jit(topk_ids, 16, 384)
active_experts = expert_ids[: num_tokens_post_pad.item() // 16].cpu().tolist()
assert sorted(active_experts) == [-1, 0, 383]
def _assert_shared_outer_merged_align_semantics(
outputs: tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int],
token_lora_mapping: torch.Tensor,
*,
topk: int,
block_size: int,
num_slots: int,
) -> None:
"""Validate routing without depending on atomic scatter order."""
(
sorted_token_ids,
expert_ids,
num_tokens_post_padded,
token_lora_mask,
virtual_num_experts,
) = outputs
mapping = token_lora_mapping.cpu()
num_routes = mapping.numel() * topk
total_padded = int(num_tokens_post_padded.item())
sorted_cpu = sorted_token_ids[:total_padded].cpu()
experts_cpu = expert_ids[: total_padded // block_size].cpu()
assert virtual_num_experts == num_slots
assert torch.equal(token_lora_mask.cpu(), mapping >= 0)
assert (mapping == -1).any() # -1 is the runtime base/no-adapter sentinel.
assert (mapping == 0).any() # Slot 0 remains a valid adapter slot.
expected_routes = []
expected_total_padded = 0
for slot in range(num_slots):
slot_tokens = torch.nonzero(mapping == slot, as_tuple=False).flatten()
slot_routes = (
slot_tokens[:, None] * topk + torch.arange(topk)[None, :]
).flatten()
expected_routes.extend(slot_routes.tolist())
route_count = slot_routes.numel()
expected_total_padded += (
(route_count + block_size - 1) // block_size
) * block_size
assert total_padded == expected_total_padded
observed_routes = []
for block, slot in enumerate(experts_cpu.tolist()):
assert 0 <= slot < num_slots
block_routes = sorted_cpu[block * block_size : (block + 1) * block_size]
real_routes = block_routes[block_routes < num_routes].to(torch.long)
if real_routes.numel():
routed_tokens = torch.div(real_routes, topk, rounding_mode="floor")
assert torch.all(mapping[routed_tokens] == slot)
observed_routes.extend(real_routes.tolist())
assert torch.all((block_routes >= 0) & (block_routes <= num_routes))
assert sorted(observed_routes) == sorted(expected_routes)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required")
@pytest.mark.parametrize("num_slots", [2, 3, 4])
def test_multi_slot_shared_outer_merged_align_cuda_graph_parity(num_slots):
"""The fused hot path must replay with current multi-LoRA routing data."""
from sglang.jit_kernel.trtllm_lora_temp.moe_lora_merged_align import (
moe_lora_merged_align,
)
device = torch.device("cuda")
num_tokens = 37 # Exercise the multi-LoRA prefill-size routing contract.
topk = 6
block_size = 16
num_experts = 384
generator = torch.Generator(device=device).manual_seed(9000 + num_slots)
topk_ids = torch.randint(
0,
num_experts,
(num_tokens, topk),
device=device,
dtype=torch.int32,
generator=generator,
)
token_lora_mapping = torch.arange(
num_tokens, device=device, dtype=torch.int32
).remainder(num_slots)
token_lora_mapping[0] = -1
token_lora_mapping[-1] = -1
def invoke(fuse_scatter: bool):
return moe_lora_merged_align(
topk_ids,
token_lora_mapping,
num_experts,
shared_outer=True,
max_loras=num_slots,
block_size=block_size,
do_skip=True,
fuse_scatter=fuse_scatter,
)
# Compile both real kernel variants and initialize CUDA state off the
# capture stream. Production selects the fused variant for this geometry.
warmup_stream = torch.cuda.Stream()
warmup_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(warmup_stream):
for _ in range(2):
invoke(fuse_scatter=True)
invoke(fuse_scatter=False)
torch.cuda.current_stream().wait_stream(warmup_stream)
torch.cuda.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
fused_outputs = invoke(fuse_scatter=True)
split_outputs = invoke(fuse_scatter=False)
stable_outputs = (*fused_outputs[:4], *split_outputs[:4])
stable_addresses = tuple(tensor.data_ptr() for tensor in stable_outputs)
for replay in range(3):
if replay:
next_mapping = (
torch.arange(num_tokens, device=device, dtype=torch.int32)
.add_(replay)
.remainder_(num_slots)
)
# Move the base/no-adapter rows on every replay while retaining a
# valid adapter in slot 0.
next_mapping[replay] = -1
next_mapping[-replay - 1] = -1
token_lora_mapping.copy_(next_mapping)
topk_ids.copy_(torch.roll(topk_ids, shifts=1, dims=1))
for outputs in (fused_outputs, split_outputs):
outputs[0].fill_(-12345)
outputs[1].fill_(-12345)
outputs[2].fill_(-1)
outputs[3].fill_(False)
graph.replay()
torch.cuda.synchronize()
assert tuple(tensor.data_ptr() for tensor in stable_outputs) == stable_addresses
for outputs in (fused_outputs, split_outputs):
_assert_shared_outer_merged_align_semantics(
outputs,
token_lora_mapping,
topk=topk,
block_size=block_size,
num_slots=num_slots,
)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,270 @@
"""CUDA graph and numerical tests for direct Inkling decode LoRA kernels."""
from __future__ import annotations
import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=20, stage="base-c", runner_config="4-gpu-b200")
# Skipped on CI: newly-added inkling LoRA test, disabled pending stabilization.
pytestmark = pytest.mark.skip(reason="new inkling LoRA test; disabled on CI")
_B200_AVAILABLE = bool(
torch.cuda.is_available()
and torch.version.hip is None
and torch.cuda.get_device_capability()[0] == 10
)
E = 256
TOPK = 6
RANK = 32
DTYPE = torch.bfloat16
def _make_topk_ids(
num_tokens: int, *, device: torch.device, offset: int
) -> torch.Tensor:
tokens = torch.arange(num_tokens, device=device, dtype=torch.int32)[:, None]
routes = torch.arange(TOPK, device=device, dtype=torch.int32)[None, :]
topk_ids = (tokens * 11 + routes * 17 + offset).remainder(E - 1)
topk_ids[:, 0] = E - 1
return topk_ids.contiguous()
def _gate_reference(
shared: torch.Tensor,
gate_b: torch.Tensor,
topk_ids: torch.Tensor,
token_lora_mapping: torch.Tensor,
) -> torch.Tensor:
intermediate = gate_b.shape[2] // 2
gate_width = gate_b.shape[2]
flat_ids = topk_ids.reshape(-1).to(torch.long)
flat_slots = token_lora_mapping[:, None].expand(-1, TOPK).reshape(-1).to(torch.long)
active = flat_slots >= 0
routed_b = gate_b[flat_slots.clamp_min(0), flat_ids].to(torch.float32)
if shared.ndim == 3:
token = torch.arange(shared.shape[1], device=shared.device)
selected_shared = shared[token_lora_mapping.clamp_min(0).long(), token]
else:
selected_shared = shared
routed_shared = (
selected_shared[:, None, :].expand(-1, TOPK, -1).reshape(-1, 2 * RANK)
).float()
gate = torch.bmm(routed_b[:, :intermediate], routed_shared[:, :RANK, None]).squeeze(
-1
)
up = torch.bmm(routed_b[:, intermediate:], routed_shared[:, RANK:, None]).squeeze(
-1
)
result = torch.cat((gate, up), dim=1)
result[~active] = 0
return result.view(topk_ids.shape[0], TOPK, gate_width)
def _down_reference(
activation: torch.Tensor,
down_a: torch.Tensor,
topk_ids: torch.Tensor,
token_lora_mapping: torch.Tensor,
) -> torch.Tensor:
flat_ids = topk_ids.reshape(-1).to(torch.long)
flat_slots = token_lora_mapping[:, None].expand(-1, TOPK).reshape(-1).to(torch.long)
active = flat_slots >= 0
routed_a = down_a[flat_slots.clamp_min(0), flat_ids].to(torch.float32)
result = torch.bmm(routed_a, activation.to(torch.float32).unsqueeze(-1)).squeeze(-1)
result[~active] = 0
return result.view(topk_ids.shape[0], TOPK, RANK)
def _make_operands(
num_tokens: int, intermediate: int, num_slots: int, device: torch.device
):
gate_width = 2 * intermediate
generator = torch.Generator(device=device).manual_seed(9000 + num_tokens)
shared_shape = (
(num_slots, num_tokens, RANK) if num_slots > 1 else (num_tokens, RANK)
)
gate_half = torch.randn(
shared_shape, device=device, dtype=DTYPE, generator=generator
)
# Deliberately unrelated halves regression-protect the gated split.
up_half = (
torch.randn(shared_shape, device=device, dtype=DTYPE, generator=generator)
* -0.75
+ 0.25
)
shared = torch.cat((gate_half, up_half), dim=-1).contiguous()
gate_b = (
torch.randn(
(num_slots, E, gate_width, RANK),
device=device,
dtype=DTYPE,
generator=generator,
)
/ RANK**0.5
).contiguous()
activation = torch.randn(
(num_tokens * TOPK, intermediate),
device=device,
dtype=DTYPE,
generator=generator,
)
down_a = (
torch.randn(
(num_slots, E, RANK, intermediate),
device=device,
dtype=DTYPE,
generator=generator,
)
/ intermediate**0.5
).contiguous()
topk_ids = _make_topk_ids(num_tokens, device=device, offset=0)
token_lora_mapping = torch.arange(
num_tokens, device=device, dtype=torch.int32
).remainder(num_slots)
if num_tokens > 1:
token_lora_mapping[-1] = -1
gate_output = torch.empty(
(num_tokens, TOPK, gate_width), device=device, dtype=DTYPE
)
down_output = torch.empty((num_tokens, TOPK, RANK), device=device, dtype=DTYPE)
return (
shared,
gate_b,
activation,
down_a,
topk_ids,
token_lora_mapping,
gate_output,
down_output,
)
@pytest.mark.skipif(
not _B200_AVAILABLE,
reason="direct Inkling decode kernels are currently gated to B200",
)
@pytest.mark.parametrize(
("num_slots", "num_tokens", "intermediate"),
[(1, 1, 384), (2, 4, 768), (3, 4, 384), (4, 32, 768)],
)
def test_direct_decode_cuda_graph_replay_and_base_weights(
num_tokens: int, intermediate: int, num_slots: int
):
from sglang.srt.lora.marlin_lora_temp.direct_decode import (
direct_decode_down_shrink,
direct_decode_gate_expand,
)
device = torch.device("cuda")
(
shared,
gate_b,
activation,
down_a,
topk_ids,
token_lora_mapping,
gate_output,
down_output,
) = _make_operands(num_tokens, intermediate, num_slots, device)
def invoke() -> None:
direct_decode_gate_expand(
shared, gate_b, topk_ids, token_lora_mapping, gate_output
)
direct_decode_down_shrink(
activation, down_a, topk_ids, token_lora_mapping, down_output
)
# Compile and initialize CUDA state outside capture.
warmup_stream = torch.cuda.Stream()
warmup_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(warmup_stream):
for _ in range(3):
invoke()
torch.cuda.current_stream().wait_stream(warmup_stream)
torch.cuda.synchronize()
assert int(topk_ids[0, 0]) == 255
torch.testing.assert_close(
gate_output.float(),
_gate_reference(shared, gate_b, topk_ids, token_lora_mapping),
rtol=0.03,
atol=0.01,
)
torch.testing.assert_close(
down_output.float(),
_down_reference(activation, down_a, topk_ids, token_lora_mapping),
rtol=0.03,
atol=0.01,
)
stable_tensors = (
shared,
gate_b,
activation,
down_a,
topk_ids,
token_lora_mapping,
gate_output,
down_output,
)
stable_addresses = tuple(tensor.data_ptr() for tensor in stable_tensors)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
invoke()
# Mutate every captured input in place. Replay must follow stable addresses
# and read the new expert ids and values, including expert 255.
shared.mul_(-0.5).add_(0.125)
gate_b.mul_(0.75).add_(0.001)
activation.mul_(0.625).sub_(0.03125)
down_a.mul_(-0.875).add_(0.0005)
topk_ids.copy_(_make_topk_ids(num_tokens, device=device, offset=29))
token_lora_mapping.copy_((token_lora_mapping + 1).remainder(num_slots))
if num_tokens > 1:
token_lora_mapping[0] = -1
gate_output.fill_(float("nan"))
down_output.fill_(float("nan"))
graph.replay()
torch.cuda.synchronize()
assert tuple(tensor.data_ptr() for tensor in stable_tensors) == stable_addresses
assert int(topk_ids[0, 0]) == 255
torch.testing.assert_close(
gate_output.float(),
_gate_reference(shared, gate_b, topk_ids, token_lora_mapping),
rtol=0.03,
atol=0.01,
)
torch.testing.assert_close(
down_output.float(),
_down_reference(activation, down_a, topk_ids, token_lora_mapping),
rtol=0.03,
atol=0.01,
)
# Base/None replay retains the same captured pointers and zeroes the loaded
# adapter weights in place. Both kernels must fully overwrite their output.
gate_b.zero_()
down_a.zero_()
gate_output.fill_(float("nan"))
down_output.fill_(float("nan"))
graph.replay()
torch.cuda.synchronize()
assert tuple(tensor.data_ptr() for tensor in stable_tensors) == stable_addresses
assert torch.count_nonzero(gate_output).item() == 0
assert torch.count_nonzero(down_output).item() == 0
assert torch.isfinite(gate_output).all().item()
assert torch.isfinite(down_output).all().item()
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,654 @@
"""CUDA parity for the multi-slot shared-outer Marlin prefill factorization."""
from __future__ import annotations
import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=45, stage="base-b", runner_config="1-gpu-small")
# The fused MoE LoRA-add kernel's shared-memory footprint exceeds the opt-in
# ceiling of the small-GPU CI runner (~99 KiB on L4) at rank=128, so the
# generic-fallback parity case OOMs there. Skip this file on CI rather than
# shrink the production kernel to a small-GPU block config.
pytestmark = pytest.mark.skip(
reason="fused MoE LoRA-add kernel needs more opt-in shared memory than the "
"small-GPU CI runner provides"
)
_CUDA_BF16_AVAILABLE = bool(
torch.cuda.is_available()
and torch.version.hip is None
and torch.cuda.get_device_capability()[0] >= 8
)
def _set_mapping(mapping: torch.Tensor, num_slots: int, offset: int) -> None:
"""Select active slots while exercising both no-adapter encodings."""
rows = torch.arange(mapping.numel(), dtype=torch.int32)
values = (rows + offset).remainder(num_slots - 1).add_(1)
values[(rows + 2 * offset).remainder(7) == 0] = 0
values[(rows + 3 * offset + 1).remainder(11) == 0] = -1
mapping.copy_(values.to(mapping.device))
def _reference_gate(
hidden_states: torch.Tensor,
gate_a: torch.Tensor,
gate_b: torch.Tensor,
topk_ids: torch.Tensor,
mapping: torch.Tensor,
) -> torch.Tensor:
"""Materialize shrink/expand with the same BF16 stage boundary."""
active = mapping >= 0
slots = mapping.clamp_min(0).long()
experts = topk_ids.long()
rank = gate_b.shape[-1]
intermediate_size = gate_b.shape[2] // 2
selected_a = gate_a[slots, 0]
shared_rank = torch.einsum(
"mh,mrh->mr", hidden_states.float(), selected_a.float()
).to(hidden_states.dtype)
selected_b = gate_b[slots[:, None], experts]
gate = torch.einsum(
"mkr,mkir->mki",
shared_rank[:, None, :rank].expand(-1, experts.shape[1], -1).float(),
selected_b[:, :, :intermediate_size].float(),
)
up = torch.einsum(
"mkr,mkir->mki",
shared_rank[:, None, rank:].expand(-1, experts.shape[1], -1).float(),
selected_b[:, :, intermediate_size:].float(),
)
output = torch.cat((gate, up), dim=-1).to(hidden_states.dtype)
output[~active] = 0
return output
def _reference_down(
activation: torch.Tensor,
down_a: torch.Tensor,
down_b: torch.Tensor,
topk_ids: torch.Tensor,
topk_weights: torch.Tensor,
mapping: torch.Tensor,
base_output: torch.Tensor,
routed_scaling_factor: float,
) -> torch.Tensor:
"""Materialize routed shrink, weighted rank sum, and selected shared B."""
slots = mapping.clamp_min(0).long()
experts = topk_ids.long()
selected_a = down_a[slots[:, None], experts]
routed_rank = torch.einsum(
"mki,mkri->mkr", activation.float(), selected_a.float()
).to(activation.dtype)
rank_sum = (
(routed_rank.float() * topk_weights.float().unsqueeze(-1))
.sum(dim=1)
.mul(routed_scaling_factor)
.to(activation.dtype)
)
output = base_output.clone()
for slot in range(down_b.shape[0]):
rows = mapping == slot
if rows.any():
output[rows] = torch.addmm(
base_output[rows], rank_sum[rows], down_b[slot, 0].T
)
return output
def _reference_generic_delta(
hidden_states: torch.Tensor,
lora_a: torch.Tensor,
lora_b: torch.Tensor,
topk_ids: torch.Tensor,
topk_weights: torch.Tensor,
mapping: torch.Tensor,
*,
shared_a: bool,
shared_b: bool,
mul_routed_weight: bool,
) -> torch.Tensor:
num_tokens, topk = topk_ids.shape
output = hidden_states.new_zeros(num_tokens, topk, lora_b.shape[2])
routed_inputs = hidden_states.shape[0] == topk_ids.numel()
for token in range(num_tokens):
slot = int(mapping[token])
if slot < 0:
continue
for route in range(topk):
expert = int(topk_ids[token, route])
if expert < 0:
continue
row = token * topk + route if routed_inputs else token
a = lora_a[slot, 0 if shared_a else expert]
b = lora_b[slot, 0 if shared_b else expert]
shrink = torch.mv(a.float(), hidden_states[row].float()).to(
hidden_states.dtype
)
rank = b.shape[1]
if shrink.numel() == 2 * rank:
output_half = b.shape[0] // 2
delta = torch.cat(
(
torch.mv(b[:output_half].float(), shrink[:rank].float()),
torch.mv(b[output_half:].float(), shrink[rank:].float()),
)
).to(hidden_states.dtype)
else:
delta = torch.mv(b.float(), shrink.float()).to(hidden_states.dtype)
if mul_routed_weight:
delta.mul_(topk_weights[token, route])
output[token, route] = delta
return output
def _run_factored_pipeline(
*,
hidden_states: torch.Tensor,
activation: torch.Tensor,
gate_a: torch.Tensor,
gate_b: torch.Tensor,
down_a: torch.Tensor,
down_b: torch.Tensor,
topk_ids: torch.Tensor,
topk_weights: torch.Tensor,
mapping: torch.Tensor,
gate_rank: torch.Tensor,
gate_output: torch.Tensor,
down_routed_rank: torch.Tensor,
down_rank_sum: torch.Tensor,
down_output: torch.Tensor,
full_routing_cache: dict,
collapsed_routing_cache: dict,
routed_scaling_factor: float,
) -> None:
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
merged_experts_fused_moe_lora_add,
)
from sglang.srt.lora.marlin_lora_temp.shared_outer import weighted_topk_rank_sum
num_tokens = topk_ids.shape[0]
num_experts = gate_b.shape[1]
collapsed_ids = mapping.view(num_tokens, 1)
collapsed_weights = topk_weights[:, :1]
# Capture the same four routing domains as the production schedule. The
# dictionaries must remain distinct because full and collapsed top-k have
# different flattened token domains.
merged_experts_fused_moe_lora_add(
output=gate_output,
hidden_states=hidden_states,
lora_a=gate_a,
lora_b=gate_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=True,
experts_shared_outer_loras_b=False,
routing_cache=full_routing_cache,
stage="routing",
prewarm_a_routing=False,
prewarm_b_routing=True,
local_expert_offset=0,
local_num_experts=num_experts,
)
merged_experts_fused_moe_lora_add(
output=gate_output,
hidden_states=hidden_states,
lora_a=gate_a,
lora_b=gate_b,
topk_ids=collapsed_ids,
topk_weights=collapsed_weights,
token_lora_mapping=mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=True,
experts_shared_outer_loras_b=False,
routing_cache=collapsed_routing_cache,
stage="routing",
prewarm_a_routing=True,
prewarm_b_routing=False,
local_expert_offset=0,
local_num_experts=num_experts,
)
merged_experts_fused_moe_lora_add(
output=activation,
hidden_states=activation,
lora_a=down_a,
lora_b=down_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=True,
routing_cache=full_routing_cache,
stage="routing",
prewarm_a_routing=True,
prewarm_b_routing=False,
local_expert_offset=0,
local_num_experts=num_experts,
)
merged_experts_fused_moe_lora_add(
output=down_output,
hidden_states=down_rank_sum,
lora_a=down_a,
lora_b=down_b,
topk_ids=collapsed_ids,
topk_weights=collapsed_weights,
token_lora_mapping=mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=True,
routing_cache=collapsed_routing_cache,
stage="routing",
prewarm_a_routing=False,
prewarm_b_routing=True,
local_expert_offset=0,
local_num_experts=num_experts,
)
merged_experts_fused_moe_lora_add(
output=gate_output,
hidden_states=hidden_states,
lora_a=gate_a,
lora_b=gate_b,
topk_ids=collapsed_ids,
topk_weights=collapsed_weights,
token_lora_mapping=mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=True,
experts_shared_outer_loras_b=False,
routing_cache=collapsed_routing_cache,
stage="shrink",
prewarm_b_routing=False,
intermediate_buffer=gate_rank,
local_expert_offset=0,
local_num_experts=num_experts,
)
merged_experts_fused_moe_lora_add(
output=gate_output,
hidden_states=hidden_states,
lora_a=gate_a,
lora_b=gate_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=True,
experts_shared_outer_loras_b=False,
routing_cache=full_routing_cache,
fuse_add_to_output=False,
use_direct_expand_add=True,
stage="expand",
intermediate_buffer=gate_rank,
broadcast_intermediate=True,
local_expert_offset=0,
local_num_experts=num_experts,
)
merged_experts_fused_moe_lora_add(
output=activation,
hidden_states=activation,
lora_a=down_a,
lora_b=down_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=True,
routing_cache=full_routing_cache,
stage="shrink",
prewarm_b_routing=False,
intermediate_buffer=down_routed_rank,
local_expert_offset=0,
local_num_experts=num_experts,
)
weighted_topk_rank_sum(
down_routed_rank,
topk_weights,
down_rank_sum,
routed_scaling_factor,
block_m=1,
)
merged_experts_fused_moe_lora_add(
output=down_output,
hidden_states=down_rank_sum,
lora_a=down_a,
lora_b=down_b,
topk_ids=collapsed_ids,
topk_weights=collapsed_weights,
token_lora_mapping=mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=True,
routing_cache=collapsed_routing_cache,
fuse_add_to_output=True,
use_direct_expand_add=False,
stage="expand",
intermediate_buffer=down_rank_sum,
local_expert_offset=0,
local_num_experts=num_experts,
)
@pytest.mark.skipif(
not _CUDA_BF16_AVAILABLE,
reason="multi-prefill parity requires a CUDA GPU with BF16 tensor cores",
)
@pytest.mark.parametrize(
("num_slots", "num_tokens"), [(2, 33), (3, 64), (4, 65), (5, 33), (8, 64), (16, 65)]
)
def test_multi_shared_outer_prefill_cuda_graph_parity(
num_slots: int, num_tokens: int
) -> None:
"""Replay full+collapsed routing while adapter selections change in place."""
device = torch.device("cuda")
dtype = torch.bfloat16
num_experts, router_topk = 4, 2
hidden_size = intermediate_size = 128
rank = 16
scale = 1.25
generator = torch.Generator(device=device).manual_seed(2027 + num_slots)
def randn(*shape: int) -> torch.Tensor:
return (
torch.randn(*shape, device=device, dtype=dtype, generator=generator) * 0.05
)
hidden_states = randn(num_tokens, hidden_size)
activation = randn(num_tokens, router_topk, intermediate_size)
gate_a = randn(num_slots, 1, 2 * rank, hidden_size)
gate_b = randn(num_slots, num_experts, 2 * intermediate_size, rank)
down_a = randn(num_slots, num_experts, rank, intermediate_size)
down_b = randn(num_slots, 1, hidden_size, rank)
# Slot 0 is the production base/None representation: it remains a valid
# mapping value, while every attached operand is zero in address-stable pool
# storage. This catches accidental stale reads that a -1-only test misses.
gate_a[0].zero_()
gate_b[0].zero_()
down_a[0].zero_()
down_b[0].zero_()
token = torch.arange(num_tokens, device=device, dtype=torch.int32)[:, None]
route = torch.arange(router_topk, device=device, dtype=torch.int32)[None, :]
topk_ids = (token + 2 * route).remainder(num_experts).contiguous()
topk_weights = (
torch.tensor([0.25, 0.75], device=device, dtype=torch.float32)
.expand(num_tokens, -1)
.contiguous()
)
mapping = torch.empty(num_tokens, device=device, dtype=torch.int32)
_set_mapping(mapping, num_slots, offset=0)
gate_rank = torch.empty(num_tokens, 2 * rank, device=device, dtype=dtype)
gate_output = torch.empty(
num_tokens,
router_topk,
2 * intermediate_size,
device=device,
dtype=dtype,
)
down_routed_rank = torch.empty(
num_tokens, router_topk, rank, device=device, dtype=dtype
)
down_rank_sum = torch.empty(num_tokens, rank, device=device, dtype=dtype)
base_output = randn(num_tokens, hidden_size)
down_output = base_output.clone()
common = dict(
hidden_states=hidden_states,
activation=activation.view(num_tokens * router_topk, intermediate_size),
gate_a=gate_a,
gate_b=gate_b,
down_a=down_a,
down_b=down_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
mapping=mapping,
gate_rank=gate_rank,
gate_output=gate_output,
down_routed_rank=down_routed_rank,
down_rank_sum=down_rank_sum,
down_output=down_output,
routed_scaling_factor=scale,
)
# Compile JIT and initialize CUDA-library state outside capture. These
# throwaway caches deliberately do not enter the captured graph.
warmup_stream = torch.cuda.Stream()
warmup_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(warmup_stream):
_run_factored_pipeline(
**common, full_routing_cache={}, collapsed_routing_cache={}
)
torch.cuda.current_stream().wait_stream(warmup_stream)
torch.cuda.synchronize()
full_routing_cache: dict = {}
collapsed_routing_cache: dict = {}
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
_run_factored_pipeline(
**common,
full_routing_cache=full_routing_cache,
collapsed_routing_cache=collapsed_routing_cache,
)
assert full_routing_cache and collapsed_routing_cache
routing_tensors = tuple(
tensor
for cache in (full_routing_cache, collapsed_routing_cache)
for value in cache.values()
for tensor in value
)
stable_tensors = (
mapping,
gate_rank,
gate_output,
down_routed_rank,
down_rank_sum,
down_output,
*routing_tensors,
)
stable_addresses = tuple(tensor.data_ptr() for tensor in stable_tensors)
for offset in (1, 3):
_set_mapping(mapping, num_slots, offset)
assert torch.any(mapping == -1).item()
assert torch.any(mapping == 0).item()
gate_rank.fill_(float("nan"))
gate_output.fill_(float("nan"))
down_routed_rank.fill_(float("nan"))
down_rank_sum.fill_(float("nan"))
down_output.copy_(base_output)
expected_gate = _reference_gate(
hidden_states, gate_a, gate_b, topk_ids, mapping
)
expected_down = _reference_down(
activation,
down_a,
down_b,
topk_ids,
topk_weights,
mapping,
base_output,
scale,
)
graph.replay()
torch.cuda.synchronize()
assert tuple(tensor.data_ptr() for tensor in stable_tensors) == stable_addresses
torch.testing.assert_close(gate_output, expected_gate, rtol=0.025, atol=0.025)
torch.testing.assert_close(down_output, expected_down, rtol=0.025, atol=0.025)
base_rows = mapping <= 0
assert torch.count_nonzero(gate_output[base_rows]).item() == 0
torch.testing.assert_close(
down_output[base_rows], base_output[base_rows], rtol=0, atol=0
)
assert torch.isfinite(gate_output).all().item()
assert torch.isfinite(down_output).all().item()
@pytest.mark.skipif(
not _CUDA_BF16_AVAILABLE,
reason="generic fallback parity requires a CUDA GPU with BF16 tensor cores",
)
@pytest.mark.parametrize(
("num_slots", "rank", "shared_outer", "ep"),
[(5, 32, True, False), (8, 128, True, True), (16, 128, False, True)],
)
def test_generic_fallback_cuda_graph_parity(
num_slots: int, rank: int, shared_outer: bool, ep: bool
) -> None:
from sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts import (
merged_experts_fused_moe_lora_add,
)
device = torch.device("cuda")
dtype = torch.bfloat16
num_tokens, topk = 8, 2
num_experts = 2 if ep else 4
hidden_size = intermediate_size = 64
generator = torch.Generator(device=device).manual_seed(3100 + num_slots + rank)
def randn(*shape: int) -> torch.Tensor:
return (
torch.randn(*shape, device=device, dtype=dtype, generator=generator) * 0.03
)
gate_a = randn(num_slots, 1 if shared_outer else num_experts, 2 * rank, hidden_size)
gate_b = randn(num_slots, num_experts, 2 * intermediate_size, rank)
down_a = randn(num_slots, num_experts, rank, intermediate_size)
down_b = randn(num_slots, 1 if shared_outer else num_experts, hidden_size, rank)
hidden_states = randn(num_tokens, hidden_size)
activation = randn(num_tokens * topk, intermediate_size)
topk_ids = (
torch.arange(num_tokens * topk, device=device, dtype=torch.int32)
.remainder(num_experts)
.view(num_tokens, topk)
)
if ep:
topk_ids[::2, 1] = -1
topk_weights = torch.tensor([0.4, 0.6], device=device, dtype=torch.float32).expand(
num_tokens, -1
)
mapping = torch.arange(num_tokens, device=device, dtype=torch.int32).remainder(
num_slots
)
mapping[::5] = -1
gate_output = torch.empty(
num_tokens, topk, 2 * intermediate_size, device=device, dtype=dtype
)
down_output = torch.empty(num_tokens, topk, hidden_size, device=device, dtype=dtype)
def run(gate_cache: dict, down_cache: dict) -> None:
gate_output.zero_()
merged_experts_fused_moe_lora_add(
output=gate_output,
hidden_states=hidden_states,
lora_a=gate_a,
lora_b=gate_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=mapping,
mul_routed_weight=False,
experts_shared_outer_loras_a=shared_outer,
experts_shared_outer_loras_b=False,
routing_cache=gate_cache,
fuse_add_to_output=False,
use_direct_expand_add=True,
local_num_experts=num_experts,
)
down_output.zero_()
merged_experts_fused_moe_lora_add(
output=down_output,
hidden_states=activation,
lora_a=down_a,
lora_b=down_b,
topk_ids=topk_ids,
topk_weights=topk_weights,
token_lora_mapping=mapping,
mul_routed_weight=True,
experts_shared_outer_loras_a=False,
experts_shared_outer_loras_b=shared_outer,
routing_cache=down_cache,
use_direct_expand_add=False,
local_num_experts=num_experts,
zero_intermediate=ep and shared_outer,
)
def assert_expected() -> None:
torch.testing.assert_close(
gate_output,
_reference_generic_delta(
hidden_states,
gate_a,
gate_b,
topk_ids,
topk_weights,
mapping,
shared_a=shared_outer,
shared_b=False,
mul_routed_weight=False,
),
rtol=2e-2,
atol=2e-2,
)
torch.testing.assert_close(
down_output,
_reference_generic_delta(
activation,
down_a,
down_b,
topk_ids,
topk_weights,
mapping,
shared_a=False,
shared_b=shared_outer,
mul_routed_weight=True,
),
rtol=2e-2,
atol=2e-2,
)
run({}, {})
torch.cuda.synchronize()
assert_expected()
gate_cache: dict = {}
down_cache: dict = {}
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
run(gate_cache, down_cache)
for offset in (0, 1):
mapping.copy_(
torch.arange(num_tokens, device=device, dtype=torch.int32)
.add(offset)
.remainder(num_slots)
)
mapping[(torch.arange(num_tokens, device=device) + offset) % 5 == 0] = -1
graph.replay()
assert_expected()
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,228 @@
"""CPU-only tests for experimental_sgl_marlin's correctness contract."""
from __future__ import annotations
import types
import pytest
from sglang.srt.lora.marlin_lora_temp.policy import (
use_post_reduce_down_delta,
validate_experimental_sgl_marlin_contract,
validate_experimental_sgl_marlin_server_args,
)
from sglang.srt.lora.trtllm_lora_temp.specialized_expand import _get_gated_a_half
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=5, stage="base-b", runner_config="1-gpu-small")
# Skipped on CI: newly-added inkling LoRA test, disabled pending stabilization.
pytestmark = pytest.mark.skip(reason="new inkling LoRA test; disabled on CI")
def _config(**overrides):
values = dict(
activation="silu",
is_gated=True,
gemm1_alpha=None,
gemm1_clamp_limit=None,
swiglu_limit=None,
apply_router_weight_on_input=False,
no_combine=False,
num_experts=256,
num_local_experts=256,
)
values.update(overrides)
return types.SimpleNamespace(**values)
def _validate(config=None, **overrides):
values = dict(
runner_config=config or _config(),
moe_ep_size=1,
device_capability=(9, 0),
)
values.update(overrides)
return validate_experimental_sgl_marlin_contract(**values)
def _validate_server(**overrides):
ep_size = overrides.pop("ep_size", 4)
moe_a2a_backend = overrides.pop("moe_a2a_backend", "none")
values = dict(
enable_lora=True,
lora_paths=[],
lora_use_virtual_experts=True,
init_expert_location="trivial",
ep_num_redundant_experts=0,
enable_eplb=False,
elastic_ep_backend=None,
enable_elastic_expert_backup=False,
elastic_ep_rejoin=False,
experts_shared_outer_loras=False,
max_lora_rank=64,
lora_backend="triton",
)
values.update(overrides)
return validate_experimental_sgl_marlin_server_args(
types.SimpleNamespace(**values),
types.SimpleNamespace(
ep_size=ep_size,
moe_a2a_backend=moe_a2a_backend,
),
)
def test_supported_contract_passes():
_validate()
def test_supported_ep_contract_passes():
_validate(config=_config(num_local_experts=64), moe_ep_size=4)
@pytest.mark.parametrize(
("enable_lora", "lora_paths"),
[(None, []), (False, []), (False, ["ignored=/tmp/adapter"])],
)
def test_base_only_ep_preserves_stock_marlin_placement_support(enable_lora, lora_paths):
_validate_server(
enable_lora=enable_lora,
lora_paths=lora_paths,
init_expert_location="random",
ep_num_redundant_experts=1,
enable_eplb=True,
elastic_ep_backend="mooncake",
enable_elastic_expert_backup=True,
elastic_ep_rejoin=True,
)
def test_base_only_ep_rejects_unsupported_a2a():
with pytest.raises(ValueError, match="moe-a2a-backend none"):
_validate_server(enable_lora=False, moe_a2a_backend="deepep")
def test_lora_rejects_non_triton_backend():
with pytest.raises(ValueError, match="requires --lora-backend triton"):
_validate_server(lora_backend="csgmv")
# base-only servers are free to pick any dense backend
_validate_server(enable_lora=False, lora_backend="csgmv")
@pytest.mark.parametrize("ep_size", [1, 4])
def test_lora_requires_virtual_experts(ep_size):
with pytest.raises(ValueError, match="lora-use-virtual-experts"):
_validate_server(ep_size=ep_size, lora_use_virtual_experts=False)
@pytest.mark.parametrize(
"setting",
[
{"init_expert_location": "random"},
{"ep_num_redundant_experts": 1},
{"enable_eplb": True},
{"elastic_ep_backend": "mooncake"},
{"enable_elastic_expert_backup": True},
{"elastic_ep_rejoin": True},
],
)
def test_lora_ep_rejects_nontrivial_placement_features(setting):
with pytest.raises(ValueError, match="trivial expert placement"):
_validate_server(**setting)
def test_adapter_paths_implicitly_enable_lora_ep_validation():
with pytest.raises(ValueError, match="trivial expert placement"):
_validate_server(
enable_lora=None,
lora_paths=["adapter=/tmp/adapter"],
enable_eplb=True,
)
def test_supported_lora_ep_passes():
_validate_server(experts_shared_outer_loras=True, max_lora_rank=64)
@pytest.mark.parametrize("rank", [65, 128, 256])
def test_shared_outer_lora_ep_allows_generic_rank_fallback(rank):
_validate_server(experts_shared_outer_loras=True, max_lora_rank=rank)
@pytest.mark.parametrize(
("config", "message"),
[
(_config(activation="relu2"), "activation must be 'silu'"),
(_config(is_gated=False), "only gated SwiGLU"),
(_config(gemm1_alpha=1.0), "gemm1_alpha"),
(_config(gemm1_clamp_limit=7.0), "gemm1_clamp_limit"),
(_config(swiglu_limit=7.0), "swiglu_limit"),
(
_config(apply_router_weight_on_input=True),
"apply_router_weight_on_input",
),
(_config(no_combine=True), "no_combine"),
],
)
def test_rejects_unimplemented_activation_semantics(config, message):
with pytest.raises(ValueError, match=message):
_validate(config)
@pytest.mark.parametrize(
"overrides",
[
{"config": _config(num_local_experts=128)},
{"config": _config(num_local_experts=63), "moe_ep_size": 4},
{"config": _config(num_experts=255, num_local_experts=64), "moe_ep_size": 4},
{"moe_ep_size": 0},
],
)
def test_rejects_incoherent_expert_parallelism(overrides):
config = overrides.get("config")
validation_overrides = {k: v for k, v in overrides.items() if k != "config"}
with pytest.raises(ValueError, match="moe_ep_size|num_local_experts"):
_validate(config, **validation_overrides)
def test_rejects_pre_hopper_gpu():
with pytest.raises(ValueError, match="compute capability 9.0 or newer"):
_validate(device_capability=(8, 0))
def test_direct_expand_always_splits_gated_gate_up_a():
assert _get_gated_a_half(intermediate_width=64, rank=32, output_width=768) == 384
assert _get_gated_a_half(intermediate_width=32, rank=32, output_width=6144) == 0
def test_direct_expand_rejects_invalid_intermediate_width():
with pytest.raises(ValueError, match="intermediate width"):
_get_gated_a_half(intermediate_width=48, rank=32, output_width=768)
@pytest.mark.parametrize(
("run_lora", "scale", "num_tokens", "expected"),
[
(True, 1.0, 1, True),
(True, 1.0, 2048, True),
(True, 1.0, 2049, False),
(True, 0.5, 32, False),
(False, 1.0, 32, False),
],
)
def test_post_reduce_down_policy(run_lora, scale, num_tokens, expected):
assert (
use_post_reduce_down_delta(
run_lora=run_lora,
routed_scaling_factor=scale,
num_tokens=num_tokens,
)
is expected
)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,945 @@
"""CPU-only runtime-flow tests for experimental_sgl_marlin."""
from __future__ import annotations
import importlib.util
import sys
import types
from pathlib import Path
from types import SimpleNamespace
import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=5, stage="base-b", runner_config="1-gpu-small")
# Skipped on CI: newly-added inkling LoRA test, disabled pending stabilization.
pytestmark = pytest.mark.skip(reason="new inkling LoRA test; disabled on CI")
REPO_ROOT = Path(__file__).resolve().parents[4]
LORA_TEMP_ROOT = REPO_ROOT / "python/sglang/srt/lora"
MARLIN_RUNNER_PATH = LORA_TEMP_ROOT / "marlin_lora_temp/moe_runner.py"
MARLIN_POLICY_PATH = LORA_TEMP_ROOT / "marlin_lora_temp/policy.py"
TWO_STREAM_PATH = LORA_TEMP_ROOT / "trtllm_lora_temp/__init__.py"
def _stub_module(monkeypatch, name: str, **attributes):
parts = name.split(".")
for end in range(1, len(parts)):
package_name = ".".join(parts[:end])
if package_name not in sys.modules:
package = types.ModuleType(package_name)
package.__path__ = []
monkeypatch.setitem(sys.modules, package_name, package)
module = types.ModuleType(name)
for key, value in attributes.items():
setattr(module, key, value)
monkeypatch.setitem(sys.modules, name, module)
if len(parts) > 1:
parent = sys.modules[".".join(parts[:-1])]
monkeypatch.setattr(parent, parts[-1], module, raising=False)
return module
def _load_file(monkeypatch, name: str, path: Path):
parts = name.split(".")
for end in range(1, len(parts)):
package_name = ".".join(parts[:end])
if package_name not in sys.modules:
package = types.ModuleType(package_name)
package.__path__ = []
monkeypatch.setitem(sys.modules, package_name, package)
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, name, module)
spec.loader.exec_module(module)
if len(parts) > 1:
parent = sys.modules[".".join(parts[:-1])]
monkeypatch.setattr(parent, parts[-1], module, raising=False)
return module
def _load_marlin_runner(monkeypatch, name: str):
_stub_module(monkeypatch, "sglang.srt.utils", is_cuda=lambda: False)
_load_file(
monkeypatch,
"sglang.srt.lora.marlin_lora_temp.policy",
MARLIN_POLICY_PATH,
)
return _load_file(monkeypatch, name, MARLIN_RUNNER_PATH)
@pytest.mark.parametrize(("tokens", "expected"), [(256, True), (257, False)])
def test_two_stream_token_threshold_is_inclusive(monkeypatch, tokens, expected):
lora_envs = SimpleNamespace(
SGLANG_TWO_STREAM_MAX_TOKENS=SimpleNamespace(get=lambda: 256)
)
_stub_module(monkeypatch, "sglang.srt.environ", envs=SimpleNamespace())
_stub_module(
monkeypatch,
"sglang.srt.lora.trtllm_lora_temp.environ",
lora_envs=lora_envs,
)
module = _load_file(monkeypatch, "_two_stream_under_test", TWO_STREAM_PATH)
assert module.is_two_stream_active(torch.empty(tokens, 1)) is expected
@pytest.mark.parametrize(
("combined_rank", "rank", "expected"),
[(128, 64, True), (128, 128, False), (256, 64, False)],
)
def test_two_stream_dense_lora_rank_falls_back(
monkeypatch, combined_rank, rank, expected
):
lora_envs = SimpleNamespace(
SGLANG_TWO_STREAM_MAX_TOKENS=SimpleNamespace(get=lambda: 256)
)
_stub_module(monkeypatch, "sglang.srt.environ", envs=SimpleNamespace())
_stub_module(
monkeypatch,
"sglang.srt.lora.trtllm_lora_temp.environ",
lora_envs=lora_envs,
)
module = _load_file(monkeypatch, "_two_stream_rank_under_test", TWO_STREAM_PATH)
assert (
module.supports_two_stream_dense_lora(
torch.empty(1, combined_rank, 1), torch.empty(1, 1, rank)
)
is expected
)
class _CombineInput:
def __init__(self, hidden_states):
self.hidden_states = hidden_states
class _DispatchOutput:
def __init__(self, hidden_states, topk_output):
self.hidden_states = hidden_states
self.topk_output = topk_output
def _run_marlin_policy(
monkeypatch,
*,
tokens: int,
master: bool = True,
two_stream: bool = False,
capture: bool = False,
active_lora: bool = True,
base_mapping: bool = False,
direct_decode: bool = False,
ep: bool = False,
slots: int = 1,
rank: int = 1,
shared_outer: bool = True,
base_value: float = 0.0,
):
module = _load_marlin_runner(monkeypatch, "_marlin_runner_under_test")
# The hermetic runner uses tiny CPU tensors. Explicitly emulate the exact
# B200/Inkling eligibility gate so these tests exercise the fused schedule.
module._use_fused_shared_outer_tail = (
lambda _info, _hidden, num_tokens, _hidden_size, _topk: num_tokens <= 512
)
module._use_direct_decode_kernels = lambda *_args, **_kwargs: direct_decode
calls = SimpleNamespace(
merged=[],
split_gate_checks=0,
weighted_rank_sums=0,
fused_tails=0,
direct_gate=0,
direct_down=0,
marlin_is_ep=[],
align_num_experts=[],
cache3_was_zero=None,
schedule=[],
zeroed=[],
event_records=[],
event_waits=[],
)
class _FakeStream:
def __init__(self, name):
self.name = name
def wait_stream(self, other):
calls.schedule.append(("wait_stream", self.name, other.name))
def wait_event(self, event):
item = ("wait_event", self.name, event.name)
calls.schedule.append(item)
calls.event_waits.append(item)
main_stream = _FakeStream("main")
side_stream = _FakeStream("side")
stream_state = {"current": main_stream}
event_count = 0
class _FakeEvent:
def __init__(self):
nonlocal event_count
self.name = f"event{event_count}"
event_count += 1
def record(self):
item = ("record", self.name, stream_state["current"].name)
calls.schedule.append(item)
calls.event_records.append(item)
class _StreamContext:
def __init__(self, stream):
self.stream = stream
self.previous = None
def __enter__(self):
self.previous = stream_state["current"]
stream_state["current"] = self.stream
def __exit__(self, *_args):
stream_state["current"] = self.previous
monkeypatch.setattr(torch.cuda, "Event", _FakeEvent)
monkeypatch.setattr(torch.cuda, "current_stream", lambda: stream_state["current"])
monkeypatch.setattr(torch.cuda, "stream", _StreamContext)
monkeypatch.setattr(torch.cuda, "is_current_stream_capturing", lambda: capture)
original_zero = torch.Tensor.zero_
def tracked_zero(intermediate, *args, **kwargs):
item = ("zero", stream_state["current"].name, id(intermediate))
calls.schedule.append(item)
calls.zeroed.append(item)
return original_zero(intermediate, *args, **kwargs)
monkeypatch.setattr(torch.Tensor, "zero_", tracked_zero)
def merged_experts_fused_moe_lora_add(**kwargs):
intermediate = kwargs.get("intermediate_buffer")
stage = kwargs.get("stage")
calls.schedule.append(
(
"merged",
stage,
stream_state["current"].name,
id(intermediate) if intermediate is not None else None,
)
)
calls.merged.append(
{
"stage": stage,
"stream": stream_state["current"].name,
"intermediate_shape": (
tuple(kwargs["intermediate_buffer"].shape)
if kwargs.get("intermediate_buffer") is not None
else None
),
"broadcast": kwargs.get("broadcast_intermediate", False),
"prewarm_a": kwargs.get("prewarm_a_routing", True),
"prewarm_b": kwargs.get("prewarm_b_routing", True),
"topk_shape": tuple(kwargs["topk_ids"].shape),
"cache_id": id(kwargs.get("routing_cache")),
"shared_a": kwargs["experts_shared_outer_loras_a"],
"shared_b": kwargs["experts_shared_outer_loras_b"],
"fuse_add": kwargs.get("fuse_add_to_output", True),
"direct_expand": kwargs.get("use_direct_expand_add", False),
"mul_routed_weight": kwargs["mul_routed_weight"],
"zero_intermediate": kwargs.get("zero_intermediate", False),
"mapping": kwargs["token_lora_mapping"].clone(),
"intermediate_id": (
id(intermediate) if intermediate is not None else None
),
}
)
if stage == "expand":
if kwargs.get("fuse_add_to_output", True):
active = kwargs["token_lora_mapping"] >= 0
kwargs["output"][active].add_(1)
else:
kwargs["output"].fill_(0)
if stage == "shrink":
return intermediate
def is_two_stream_active(_hidden_states):
calls.split_gate_checks += 1
return two_stream
_stub_module(
monkeypatch,
"sglang.srt.layers.moe.token_dispatcher.standard",
StandardCombineInput=_CombineInput,
StandardDispatchOutput=_DispatchOutput,
)
_stub_module(
monkeypatch,
"sglang.srt.lora.trtllm_lora_temp",
get_lora_side_stream=lambda: side_stream,
is_two_stream_active=is_two_stream_active,
)
_stub_module(
monkeypatch,
"sglang.srt.lora.trtllm_lora_temp.environ",
experimental_lora_enabled=lambda: master,
)
_stub_module(
monkeypatch,
"sglang.kernels.ops.moe.trtllm_lora_temp.virtual_experts",
merged_experts_fused_moe_lora_add=merged_experts_fused_moe_lora_add,
)
_stub_module(
monkeypatch,
"sglang.srt.model_executor.runner",
get_is_capture_mode=lambda: capture,
)
def fake_align(_topk_ids, _block_size, num_experts, **_kwargs):
calls.align_num_experts.append(num_experts)
return (
torch.zeros(1, dtype=torch.int32),
torch.zeros(1, dtype=torch.int32),
torch.ones(1, dtype=torch.int32),
)
module.moe_align_block_size = fake_align
module.marlin_make_workspace = lambda *_args, **_kwargs: None
module.get_scalar_type = lambda *_args, **_kwargs: None
def fake_marlin_gemm(_x, output, *_args, **_kwargs):
calls.marlin_is_ep.append(_kwargs["is_ep"])
if len(calls.marlin_is_ep) == 2:
calls.cache3_was_zero = bool(torch.count_nonzero(output) == 0)
calls.schedule.append(
("marlin", stream_state["current"].name, len(calls.schedule))
)
output.fill_(base_value if len(calls.marlin_is_ep) == 2 else 0)
return output
module.moe_wna16_marlin_gemm = fake_marlin_gemm
def fake_silu_and_mul_add_delta(_x, _delta, output):
calls.schedule.append(("activation", stream_state["current"].name))
output.fill_(0)
module.silu_and_mul_add_delta = fake_silu_and_mul_add_delta
module.silu_and_mul = lambda _x, output: output.fill_(0)
def fake_triton_reduce(_input, output, _scale):
calls.schedule.append(("reduce", stream_state["current"].name))
output.copy_(_input.sum(dim=1) * _scale)
module.moe_sum_reduce_triton = fake_triton_reduce
def fake_weighted_rank_sum(routed_rank, weights, output, scale, *, block_m):
calls.weighted_rank_sums += 1
calls.schedule.append(
("weighted", stream_state["current"].name, id(routed_rank))
)
output.copy_(
(routed_rank * weights.to(routed_rank.dtype).unsqueeze(-1)).sum(dim=1)
* scale
)
module.weighted_topk_rank_sum = fake_weighted_rank_sum
def fake_fused_tail(
routed_base,
routed_rank,
weights,
_shared_b,
output,
scale,
*,
block_m,
block_k,
):
calls.fused_tails += 1
calls.schedule.append(
("fused_tail", stream_state["current"].name, block_m, block_k)
)
output.copy_(routed_base.sum(dim=1) * scale)
module.fused_base_shared_lora_reduce = fake_fused_tail
module.fused_base_shared_lora_reduce_config = lambda _tokens: (1, 32)
def fake_direct_gate(_shared, _weight, _topk_ids, _mapping, output):
calls.direct_gate += 1
calls.schedule.append(("direct_gate", stream_state["current"].name))
output.fill_(0)
def fake_direct_down(_activation, _weight, _topk_ids, _mapping, output):
calls.direct_down += 1
calls.schedule.append(("direct_down", stream_state["current"].name))
output.fill_(0)
module.direct_decode_gate_expand = fake_direct_gate
module.direct_decode_down_shrink = fake_direct_down
hidden_size, num_experts, expert_size, topk, max_rank = 2, 1, 16, 2, rank
hidden_states = torch.zeros(tokens, hidden_size)
topk_ids = torch.zeros(tokens, topk, dtype=torch.int32)
if ep:
topk_ids[:, 1] = -1
topk_weights = torch.ones(tokens, topk)
dispatch_output = _DispatchOutput(
hidden_states,
SimpleNamespace(topk_ids=topk_ids, topk_weights=topk_weights),
)
quant_info = SimpleNamespace(
w13_qweight=torch.empty(num_experts, 2),
w13_bias=None,
w13_scales=torch.ones(1),
w13_global_scale=None,
w13_qzeros=None,
w13_g_idx=None,
w13_g_idx_sort_indices=None,
w2_qweight=torch.empty(num_experts, 1),
w2_bias=None,
w2_scales=torch.ones(1),
w2_global_scale=None,
w2_qzeros=None,
w2_g_idx=None,
w2_g_idx_sort_indices=None,
expert_map=None,
global_num_experts=num_experts,
weight_bits=4,
is_k_full=True,
)
lora_info = SimpleNamespace(
lora_use_virtual_experts=True,
max_lora_rank=max_rank,
has_active_lora=active_lora,
gate_up_lora_a_weights=torch.zeros(
slots,
1 if shared_outer else num_experts,
2 * max_rank,
hidden_size,
),
gate_up_lora_b_weights=torch.zeros(
slots, num_experts, 2 * expert_size, max_rank
),
down_lora_a_weights=torch.zeros(slots, num_experts, max_rank, expert_size),
down_lora_b_weights=torch.zeros(
slots,
1 if shared_outer else num_experts,
hidden_size,
max_rank,
),
token_lora_mapping=(
torch.full((tokens,), -1, dtype=torch.int32)
if base_mapping
else torch.arange(tokens, dtype=torch.int32).remainder(slots)
),
experts_shared_outer_loras=shared_outer,
)
runner_config = SimpleNamespace(
activation="silu",
routed_scaling_factor=1.0,
num_experts=num_experts,
num_local_experts=num_experts,
)
if ep:
runner_config.num_experts = 2 * num_experts
result = module.fused_experts_experimental_sgl_marlin_lora(
dispatch_output, quant_info, runner_config, lora_info
)
assert result.hidden_states.shape == hidden_states.shape
calls.input_ptr = hidden_states.data_ptr()
calls.result_ptr = result.hidden_states.data_ptr()
calls.result = result.hidden_states
calls.capture_event_count = len(module._MARLIN_LORA_OVERLAP_EVENTS)
return calls
@pytest.mark.parametrize(("master", "split_gate_checks"), [(False, 0), (True, 1)])
def test_two_stream_batch_gate_is_master_gated(monkeypatch, master, split_gate_checks):
calls = _run_marlin_policy(monkeypatch, tokens=1, master=master, two_stream=True)
assert calls.split_gate_checks == split_gate_checks
shrinks = [call for call in calls.merged if call["stage"] == "shrink"]
assert shrinks[0]["stream"] == ("side" if master else "main")
def test_shared_outer_factorization_runtime_flow(monkeypatch):
tokens = 16
calls = _run_marlin_policy(monkeypatch, tokens=tokens)
gate_expand = [
call for call in calls.merged if call["stage"] == "expand" and call["broadcast"]
]
down_shrink = [call for call in calls.merged if call["stage"] == "shrink"]
routing = [call for call in calls.merged if call["stage"] == "routing"]
assert len(gate_expand) == 1
assert len(down_shrink) == 1
assert calls.weighted_rank_sums == 0
assert calls.fused_tails == 1
assert gate_expand[0]["intermediate_shape"] == (tokens, 2)
assert down_shrink[0]["intermediate_shape"] == (tokens, 2, 1)
assert [(call["prewarm_a"], call["prewarm_b"]) for call in routing] == [
(False, True),
(True, False),
]
assert down_shrink[0]["prewarm_b"] is False
@pytest.mark.parametrize(
("slots", "rank"),
[(8, 128), (16, 128)],
)
def test_ep_shared_outer_uses_safe_generic_fallback(monkeypatch, slots, rank):
calls = _run_marlin_policy(
monkeypatch,
tokens=1,
ep=True,
slots=slots,
rank=rank,
)
generic_down = [
call for call in calls.merged if call["stage"] == "all" and call["shared_b"]
]
assert len(generic_down) == 1
assert generic_down[0]["zero_intermediate"] is True
assert generic_down[0]["direct_expand"] is (rank <= 64)
def test_ep_shared_outer_low_rank_multi_slot_takes_factored_path(monkeypatch):
# Slot-count gates are lifted: EP shared-outer rank<=64 pools of any size
# collapse through the factored prefill path instead of the zeroed generic
# fallback (routing is by adapter slot, so no unowned regions are read).
calls = _run_marlin_policy(
monkeypatch,
tokens=1,
ep=True,
slots=5,
rank=32,
)
generic_down = [
call for call in calls.merged if call["stage"] == "all" and call["shared_b"]
]
assert not generic_down
def test_ep_per_expert_layout_uses_generic_fallback(monkeypatch):
calls = _run_marlin_policy(
monkeypatch,
tokens=1,
ep=True,
slots=8,
rank=128,
shared_outer=False,
)
generic_down = [
call
for call in calls.merged
if call["stage"] == "all" and call["mul_routed_weight"]
]
assert len(generic_down) == 1
assert generic_down[0]["shared_b"] is False
assert generic_down[0]["zero_intermediate"] is False
assert generic_down[0]["direct_expand"] is False
@pytest.mark.parametrize(
("case", "expected"),
[
("supported", True),
("multi_slot", False),
("non_shared", False),
("rank_too_large", False),
("single_route", False),
("empty_batch", False),
("mismatched_experts", False),
],
)
def test_shared_outer_factorization_eligibility_is_narrow(monkeypatch, case, expected):
module = _load_marlin_runner(monkeypatch, "_marlin_runner_eligibility")
rank = 65 if case == "rank_too_large" else 32
slots = 2 if case == "multi_slot" else 1
experts = 4
hidden = 8
intermediate = 3
info = SimpleNamespace(
max_lora_rank=rank,
experts_shared_outer_loras=case != "non_shared",
gate_up_lora_a_weights=torch.empty(slots, 1, 2 * rank, hidden),
gate_up_lora_b_weights=torch.empty(slots, experts, 2 * intermediate, rank),
down_lora_a_weights=torch.empty(
slots,
experts + (1 if case == "mismatched_experts" else 0),
rank,
intermediate,
),
down_lora_b_weights=torch.empty(slots, 1, hidden, rank),
)
tokens = 0 if case == "empty_batch" else 1
topk = 1 if case == "single_route" else 2
assert module._use_shared_outer_factorization(info, tokens, topk) is expected
@pytest.mark.parametrize(
("case", "expected"),
[
("supported", True),
("three_slots", True),
("one_slot", False),
("five_slots", True),
("sixteen_slots", True),
("large_batch", False),
("ep", False),
("hopper", False),
],
)
def test_multi_shared_outer_decode_factorization_is_narrow(monkeypatch, case, expected):
module = _load_marlin_runner(monkeypatch, "_marlin_runner_multi_policy")
monkeypatch.setattr(
torch.cuda,
"get_device_capability",
lambda _device: (9, 0) if case == "hopper" else (10, 0),
)
slots = (
1
if case == "one_slot"
else (
3
if case == "three_slots"
else 5 if case == "five_slots" else 16 if case == "sixteen_slots" else 4
)
)
info = SimpleNamespace(
max_lora_rank=32,
experts_shared_outer_loras=True,
gate_up_lora_a_weights=SimpleNamespace(shape=(slots, 1, 64, 6144)),
gate_up_lora_b_weights=SimpleNamespace(shape=(slots, 256, 768, 32)),
down_lora_a_weights=SimpleNamespace(shape=(slots, 256, 32, 384)),
down_lora_b_weights=SimpleNamespace(shape=(slots, 1, 6144, 32)),
)
hidden_states = SimpleNamespace(
is_cuda=True, dtype=torch.bfloat16, device=torch.device("cuda")
)
assert (
module._use_multi_shared_outer_decode_factorization(
info,
hidden_states,
num_tokens=33 if case == "large_batch" else 32,
hidden_size=6144,
router_topk=6,
num_experts=256,
intermediate_size=384,
ep_active=case == "ep",
)
is expected
)
@pytest.mark.parametrize(
("case", "expected"),
[
("supported", True),
("four_slots", True),
("decode_boundary", False),
("one_slot", False),
("five_slots", True),
("ep", True),
("ep_decode", True),
("non_shared", False),
("rank_too_large", False),
("single_route", False),
("mismatched_shape", False),
],
)
def test_multi_shared_outer_prefill_factorization_is_narrow(
monkeypatch, case, expected
):
module = _load_marlin_runner(monkeypatch, "_marlin_runner_prefill_policy")
slots = (
1
if case == "one_slot"
else 5 if case == "five_slots" else 4 if case == "four_slots" else 2
)
rank = 65 if case == "rank_too_large" else 32
experts = 256
intermediate = 384
hidden = 6144
info = SimpleNamespace(
max_lora_rank=rank,
experts_shared_outer_loras=case != "non_shared",
gate_up_lora_a_weights=SimpleNamespace(shape=(slots, 1, 2 * rank, hidden)),
gate_up_lora_b_weights=SimpleNamespace(
shape=(slots, experts, 2 * intermediate, rank)
),
down_lora_a_weights=SimpleNamespace(
shape=(
slots,
experts + (1 if case == "mismatched_shape" else 0),
rank,
intermediate,
)
),
down_lora_b_weights=SimpleNamespace(shape=(slots, 1, hidden, rank)),
)
assert (
module._use_multi_shared_outer_prefill_factorization(
info,
num_tokens=32 if case in ("decode_boundary", "ep_decode") else 33,
hidden_size=hidden,
router_topk=1 if case == "single_route" else 6,
num_experts=experts,
intermediate_size=intermediate,
ep_active=case in ("ep", "ep_decode"),
)
is expected
)
@pytest.mark.parametrize(
("case", "expected"),
[
("supported", True),
("unfactored", False),
("unfused", False),
("ep", False),
("large_batch", False),
],
)
def test_direct_decode_selection_is_narrow(monkeypatch, case, expected):
module = _load_marlin_runner(monkeypatch, "_marlin_runner_direct_policy")
info = SimpleNamespace(
gate_up_lora_b_weights=SimpleNamespace(shape=(3, 256, 768, 32)),
down_lora_a_weights=SimpleNamespace(shape=(3, 256, 32, 384)),
)
assert (
module._use_direct_decode_kernels(
info,
factored_shared_outer=case != "unfactored",
fused_shared_outer_tail=case != "unfused",
ep_active=case == "ep",
num_tokens=33 if case == "large_batch" else 32,
num_experts=256,
intermediate_size=384,
)
is expected
)
@pytest.mark.parametrize(
("case", "expected"),
[
("supported", True),
("boundary_m", True),
("hopper", False),
("fp16", False),
("rank64", False),
("hidden", False),
("topk", False),
("large_m", False),
],
)
def test_fused_shared_outer_tail_is_b200_inkling_specific(monkeypatch, case, expected):
module = _load_marlin_runner(monkeypatch, "_marlin_runner_tail_policy")
monkeypatch.setattr(
torch.cuda,
"get_device_capability",
lambda _device: (9, 0) if case == "hopper" else (10, 0),
)
info = SimpleNamespace(max_lora_rank=64 if case == "rank64" else 32)
hidden_states = SimpleNamespace(
is_cuda=True,
dtype=torch.float16 if case == "fp16" else torch.bfloat16,
device=torch.device("cuda"),
)
assert (
module._use_fused_shared_outer_tail(
info,
hidden_states,
513 if case == "large_m" else 512 if case == "boundary_m" else 32,
4096 if case == "hidden" else 6144,
8 if case == "topk" else 6,
)
is expected
)
def test_multi_prefill_collapses_only_shared_factors_and_separates_caches(
monkeypatch,
):
calls = _run_marlin_policy(monkeypatch, tokens=64, slots=3)
routing = [call for call in calls.merged if call["stage"] == "routing"]
full_routing = [call for call in routing if call["topk_shape"] == (64, 2)]
collapsed_routing = [call for call in routing if call["topk_shape"] == (64, 1)]
assert [(call["prewarm_a"], call["prewarm_b"]) for call in full_routing] == [
(False, True), # real-route per-expert gate B
(True, False), # real-route per-expert down A
]
assert [(call["prewarm_a"], call["prewarm_b"]) for call in collapsed_routing] == [
(True, False), # collapsed selected shared gate A
(False, True), # collapsed selected shared down B
]
assert len({call["cache_id"] for call in full_routing}) == 1
assert len({call["cache_id"] for call in collapsed_routing}) == 1
assert full_routing[0]["cache_id"] != collapsed_routing[0]["cache_id"]
gate_shrink = next(
call for call in calls.merged if call["stage"] == "shrink" and call["shared_a"]
)
gate_expand = next(
call for call in calls.merged if call["stage"] == "expand" and call["broadcast"]
)
down_shrink = next(
call
for call in calls.merged
if call["stage"] == "shrink" and not call["shared_a"]
)
down_expand = next(
call for call in calls.merged if call["stage"] == "expand" and call["shared_b"]
)
assert gate_shrink["topk_shape"] == (64, 1)
assert gate_shrink["intermediate_shape"] == (64, 2)
assert gate_expand["topk_shape"] == (64, 2)
assert gate_expand["intermediate_shape"] == (64, 2)
assert down_shrink["topk_shape"] == (64, 2)
assert down_shrink["intermediate_shape"] == (64, 2, 1)
assert down_expand["topk_shape"] == (64, 1)
assert down_expand["intermediate_shape"] == (64, 1)
assert down_expand["fuse_add"] is True
assert down_expand["direct_expand"] is False
assert down_expand["mul_routed_weight"] is False
assert calls.weighted_rank_sums == 1
# The mapped one-token-per-CTA tail remains decode-only.
assert calls.fused_tails == 0
def test_multi_prefill_none_rows_preserve_base_reduction(monkeypatch):
calls = _run_marlin_policy(
monkeypatch,
tokens=64,
slots=2,
capture=True,
active_lora=False,
base_mapping=True,
base_value=3.0,
)
down_expand = next(
call for call in calls.merged if call["stage"] == "expand" and call["shared_b"]
)
assert down_expand["topk_shape"] == (64, 1)
assert torch.equal(down_expand["mapping"], torch.full((64,), -1, dtype=torch.int32))
# The fake Marlin down output is 3 for each of two routes. The collapsed
# shared-B expand masks every None row, so it must leave the base sum at 6.
torch.testing.assert_close(calls.result, torch.full_like(calls.result, 6.0))
assert calls.fused_tails == 0
def test_direct_decode_skips_virtual_routing_and_zero_fill(monkeypatch):
calls = _run_marlin_policy(
monkeypatch, tokens=16, two_stream=True, direct_decode=True
)
assert [call for call in calls.merged if call["stage"] == "routing"] == []
assert [call for call in calls.merged if call["stage"] == "shrink"] == []
assert calls.direct_gate == 1
assert calls.direct_down == 1
assert calls.zeroed == []
assert calls.fused_tails == 1
def test_ep_uses_local_alignment_and_skips_nonlocal_marlin_blocks(monkeypatch):
calls = _run_marlin_policy(monkeypatch, tokens=16, ep=True)
assert calls.align_num_experts == [1]
assert calls.marlin_is_ep == [True, True]
assert calls.cache3_was_zero is True
def test_factored_decode_two_stream_schedule_and_ownership(monkeypatch):
calls = _run_marlin_policy(monkeypatch, tokens=16, two_stream=True)
shrinks = [call for call in calls.merged if call["stage"] == "shrink"]
assert len(shrinks) == 1
assert shrinks[0]["stream"] == "side"
assert shrinks[0]["prewarm_b"] is False
assert len(calls.zeroed) == 1
assert calls.zeroed[0][1] == "side"
buffer_id = shrinks[0]["intermediate_id"]
assert calls.zeroed[0][2] == buffer_id
zero_index = calls.schedule.index(calls.zeroed[0])
shrink_index = next(
index
for index, item in enumerate(calls.schedule)
if item[:3] == ("merged", "shrink", "side")
)
down_record = calls.event_records[-1]
down_wait = calls.event_waits[-1]
record_index = calls.schedule.index(down_record)
wait_index = calls.schedule.index(down_wait)
fused_index = next(
index for index, item in enumerate(calls.schedule) if item[0] == "fused_tail"
)
assert down_record[2] == "side"
assert down_wait == ("wait_event", "main", down_record[1])
assert zero_index < shrink_index < record_index < wait_index < fused_index
assert fused_index == len(calls.schedule) - 1
def test_factored_decode_single_stream_fallback_has_one_main_shrink(monkeypatch):
calls = _run_marlin_policy(monkeypatch, tokens=16, two_stream=False)
shrinks = [call for call in calls.merged if call["stage"] == "shrink"]
assert len(shrinks) == 1
assert shrinks[0]["stream"] == "main"
assert shrinks[0]["prewarm_b"] is False
assert len(calls.zeroed) == 1
assert calls.zeroed[0][1] == "main"
assert calls.event_records == []
assert calls.event_waits == []
buffer_id = shrinks[0]["intermediate_id"]
assert calls.zeroed[0][2] == buffer_id
second_marlin_index = max(
index for index, item in enumerate(calls.schedule) if item[0] == "marlin"
)
zero_index = calls.schedule.index(calls.zeroed[0])
shrink_index = next(
index
for index, item in enumerate(calls.schedule)
if item[:3] == ("merged", "shrink", "main")
)
fused_index = next(
index for index, item in enumerate(calls.schedule) if item[0] == "fused_tail"
)
assert second_marlin_index < zero_index < shrink_index < fused_index
def test_factored_decode_capture_base_rows_keep_main_owned_buffers(monkeypatch):
calls = _run_marlin_policy(
monkeypatch,
tokens=16,
two_stream=True,
capture=True,
active_lora=False,
base_mapping=True,
)
shrinks = [call for call in calls.merged if call["stage"] == "shrink"]
assert len(shrinks) == 1
assert len(calls.zeroed) == 1
assert calls.zeroed[0][1] == "side"
assert calls.zeroed[0][2] == shrinks[0]["intermediate_id"]
assert calls.weighted_rank_sums == 0
assert calls.fused_tails == 1
assert calls.capture_event_count == 3
assert calls.result_ptr != calls.input_ptr
torch.testing.assert_close(calls.result, torch.zeros_like(calls.result))
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,306 @@
"""CUDA parity tests for the fused shared-outer Marlin decode reduction."""
from __future__ import annotations
import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=20, stage="base-b", runner_config="1-gpu-small")
# Skipped on CI: newly-added inkling LoRA test, disabled pending stabilization.
pytestmark = pytest.mark.skip(reason="new inkling LoRA test; disabled on CI")
_CUDA_BF16_AVAILABLE = bool(
torch.cuda.is_available()
and torch.version.hip is None
and torch.cuda.get_device_capability()[0] >= 8
)
def _reference_reduce(
routed_base: torch.Tensor,
routed_rank: torch.Tensor,
topk_weights: torch.Tensor,
shared_b: torch.Tensor,
routed_scaling_factor: float,
) -> torch.Tensor:
"""Materialize the three production operations with their BF16 boundaries."""
operand_dtype = routed_base.dtype
base_sum = (
routed_base.to(torch.float32)
.sum(dim=1)
.mul(routed_scaling_factor)
.to(operand_dtype)
)
rank_sum = (
(routed_rank.to(torch.float32) * topk_weights.to(torch.float32).unsqueeze(-1))
.sum(dim=1)
.mul(routed_scaling_factor)
.to(operand_dtype)
)
base_sum.addmm_(rank_sum, shared_b.T)
return base_sum
def _mapped_reference_reduce(
routed_base: torch.Tensor,
routed_rank: torch.Tensor,
topk_weights: torch.Tensor,
shared_b: torch.Tensor,
token_lora_mapping: torch.Tensor,
routed_scaling_factor: float,
) -> torch.Tensor:
dtype = routed_base.dtype
base_sum = routed_base.float().sum(dim=1).mul(routed_scaling_factor).to(dtype)
rank_sum = (
(routed_rank.float() * topk_weights.unsqueeze(-1))
.sum(dim=1)
.mul(routed_scaling_factor)
.to(dtype)
)
output = base_sum.clone()
for slot in range(shared_b.shape[0]):
rows = token_lora_mapping == slot
if rows.any():
output[rows] = torch.addmm(
base_sum[rows], rank_sum[rows], shared_b[slot, 0].T
)
return output
@pytest.mark.skipif(
not _CUDA_BF16_AVAILABLE,
reason="fused shared-outer reduction requires a CUDA GPU with BF16 tensor cores",
)
@pytest.mark.parametrize(
("num_tokens", "rank", "routed_scaling_factor", "hidden_width"),
[
(1, 16, 1.0, 128),
(2, 32, 1.75, 137),
(4, 64, 1.0, 128),
(32, 16, 1.75, 137),
(512, 64, 1.0, 137),
],
)
def test_fused_base_shared_lora_reduce_cuda_graph_parity(
num_tokens: int,
rank: int,
routed_scaling_factor: float,
hidden_width: int,
):
"""Graph replay matches sum + rounded rank reduction + shared-B addmm."""
from sglang.srt.lora.marlin_lora_temp.shared_outer import (
fused_base_shared_lora_reduce,
fused_base_shared_lora_reduce_config,
)
device = torch.device("cuda")
topk = 6
dtype = torch.bfloat16
generator = torch.Generator(device=device).manual_seed(
1000 + num_tokens * 100 + rank + hidden_width
)
routed_base = (
torch.randn(
(num_tokens, topk, hidden_width),
device=device,
dtype=dtype,
generator=generator,
)
* 0.05
)
routed_rank = (
torch.randn(
(num_tokens, topk, rank),
device=device,
dtype=dtype,
generator=generator,
)
* 0.05
)
topk_weights = torch.softmax(
torch.randn(
(num_tokens, topk),
device=device,
dtype=torch.float32,
generator=generator,
),
dim=1,
).contiguous()
shared_b = (
torch.randn(
(hidden_width, rank),
device=device,
dtype=dtype,
generator=generator,
)
* 0.05
)
output = torch.empty((num_tokens, hidden_width), device=device, dtype=dtype)
block_m, block_k = fused_base_shared_lora_reduce_config(num_tokens)
def invoke() -> None:
fused_base_shared_lora_reduce(
routed_base,
routed_rank,
topk_weights,
shared_b,
output,
routed_scaling_factor,
block_m=block_m,
block_k=block_k,
)
# Compile the rank/block specialization and initialize CUDA state away from
# the capture stream.
warmup_stream = torch.cuda.Stream()
warmup_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(warmup_stream):
for _ in range(3):
invoke()
torch.cuda.current_stream().wait_stream(warmup_stream)
torch.cuda.synchronize()
stable_tensors = (routed_base, routed_rank, topk_weights, shared_b, output)
stable_addresses = tuple(tensor.data_ptr() for tensor in stable_tensors)
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
invoke()
for replay in range(2):
# Mutate every captured operand in place so replay proves that the graph
# follows stable addresses rather than values observed during capture.
routed_base.mul_(0.75).add_(0.002 * (replay + 1))
routed_rank.mul_(-0.5).add_(0.001 * (replay + 1))
topk_weights.copy_(torch.roll(topk_weights, shifts=1, dims=1))
shared_b.mul_(0.875).add_(0.0005 * (replay + 1))
output.fill_(float("nan"))
graph.replay()
torch.cuda.synchronize()
assert tuple(tensor.data_ptr() for tensor in stable_tensors) == stable_addresses
expected = _reference_reduce(
routed_base,
routed_rank,
topk_weights,
shared_b,
routed_scaling_factor,
)
torch.testing.assert_close(output, expected, rtol=0.03, atol=0.004)
assert torch.isfinite(output).all().item()
@pytest.mark.skipif(
not _CUDA_BF16_AVAILABLE,
reason="mapped shared-outer reduction requires CUDA BF16 tensor cores",
)
@pytest.mark.parametrize(
("num_slots", "num_tokens", "hidden_width"),
[(2, 1, 128), (3, 32, 137)],
)
def test_fused_base_mapped_shared_lora_reduce_cuda_graph_parity(
num_tokens: int, num_slots: int, hidden_width: int
):
from sglang.srt.lora.marlin_lora_temp.shared_outer import (
fused_base_mapped_shared_lora_reduce,
)
device = torch.device("cuda")
generator = torch.Generator(device=device).manual_seed(
7000 + num_tokens * 100 + num_slots * 10 + hidden_width
)
routed_base = 0.05 * torch.randn(
(num_tokens, 6, hidden_width),
device=device,
dtype=torch.bfloat16,
generator=generator,
)
routed_rank = 0.05 * torch.randn(
(num_tokens, 6, 32),
device=device,
dtype=torch.bfloat16,
generator=generator,
)
topk_weights = torch.softmax(
torch.randn(
(num_tokens, 6),
device=device,
dtype=torch.float32,
generator=generator,
),
dim=1,
).contiguous()
shared_b = 0.05 * torch.randn(
(num_slots, 1, hidden_width, 32),
device=device,
dtype=torch.bfloat16,
generator=generator,
)
token_lora_mapping = torch.arange(
num_tokens, device=device, dtype=torch.int32
).remainder(num_slots)
if num_tokens > 1:
token_lora_mapping[-1] = -1
output = torch.empty(
(num_tokens, hidden_width), device=device, dtype=torch.bfloat16
)
def invoke() -> None:
fused_base_mapped_shared_lora_reduce(
routed_base,
routed_rank,
topk_weights,
shared_b,
token_lora_mapping,
output,
1.75,
block_k=64,
)
warmup_stream = torch.cuda.Stream()
warmup_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(warmup_stream):
for _ in range(3):
invoke()
torch.cuda.current_stream().wait_stream(warmup_stream)
torch.cuda.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
invoke()
token_lora_mapping.copy_((token_lora_mapping + 1).remainder(num_slots))
if num_tokens > 1:
token_lora_mapping[0] = -1
routed_base.mul_(0.75)
routed_rank.mul_(-0.5)
shared_b.mul_(0.875)
output.fill_(float("nan"))
graph.replay()
torch.cuda.synchronize()
expected = _mapped_reference_reduce(
routed_base,
routed_rank,
topk_weights,
shared_b,
token_lora_mapping,
1.75,
)
torch.testing.assert_close(output, expected, rtol=0.03, atol=0.004)
assert torch.isfinite(output).all().item()
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,119 @@
"""Hermetic regression tests for Inkling shared-sink LoRA normalization."""
from __future__ import annotations
import ast
from pathlib import Path
from types import SimpleNamespace
import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=1, stage="base-b", runner_config="1-gpu-small")
# Skipped on CI: newly-added inkling LoRA test, disabled pending stabilization.
pytestmark = pytest.mark.skip(reason="new inkling LoRA test; disabled on CI")
REPO_ROOT = Path(__file__).resolve().parents[4]
LORA_PATH = REPO_ROOT / "python/sglang/srt/lora/lora.py"
def _load_normalizer_class():
tree = ast.parse(LORA_PATH.read_text())
source_class = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "LoRAAdapter"
)
method_names = {"_normalize_shared_expert_moe", "normalize_gate_up_proj"}
methods = [
node
for node in source_class.body
if isinstance(node, ast.FunctionDef) and node.name in method_names
]
assert {method.name for method in methods} == method_names
test_class = ast.ClassDef(
name="_NormalizerUnderTest",
bases=[],
keywords=[],
body=methods,
decorator_list=[],
)
namespace = {"Dict": dict, "re": __import__("re"), "torch": torch}
exec(
compile(
ast.fix_missing_locations(ast.Module(body=[test_class], type_ignores=[])),
str(LORA_PATH),
"exec",
),
namespace,
)
return namespace[test_class.name]
def _normalizer(num_shared: int = 2, *, text_config: bool = False):
normalizer = _load_normalizer_class()()
config = SimpleNamespace(
architectures=None if text_config else ["InklingForConditionalGeneration"],
model_type="inkling_text" if text_config else "inkling",
n_shared_experts=num_shared,
)
normalizer.base_hf_config = config
return normalizer
@pytest.mark.parametrize("text_config", [False, True])
def test_proj_named_shared_sink_factors_gain_the_expert_axis(text_config):
n, rank, hidden, intermediate = 2, 3, 5, 7
prefix = "model.layers.0.mlp.shared_experts"
gate_a = torch.arange(rank * hidden).reshape(rank, hidden)
gate_b = torch.arange(n * 2 * intermediate * rank).reshape(
n * 2 * intermediate, rank
)
down_a = torch.arange(rank * n * intermediate).reshape(rank, n * intermediate)
down_b = torch.arange(hidden * rank).reshape(hidden, rank)
weights = {
f"{prefix}.gate_up_proj.lora_A.weight": gate_a,
f"{prefix}.gate_up_proj.lora_B.weight": gate_b,
f"{prefix}.down_proj.lora_A.weight": down_a,
f"{prefix}.down_proj.lora_B.weight": down_b,
}
normalizer = _normalizer(n, text_config=text_config)
normalizer._normalize_shared_expert_moe(weights)
normalizer.normalize_gate_up_proj(list(weights), weights)
torch.testing.assert_close(
weights[f"{prefix}.gate_up_proj.lora_A.weight"],
gate_a.unsqueeze(0).repeat(1, 2, 1),
)
torch.testing.assert_close(
weights[f"{prefix}.gate_up_proj.lora_B.weight"],
gate_b.reshape(n, 2 * intermediate, rank),
)
torch.testing.assert_close(
weights[f"{prefix}.down_proj.lora_A.weight"],
down_a.reshape(rank, n, intermediate).transpose(0, 1).contiguous(),
)
torch.testing.assert_close(
weights[f"{prefix}.down_proj.lora_B.weight"], down_b.unsqueeze(0)
)
def test_named_per_expert_outer_factor_is_not_collapsed_to_shared_outer():
name = "model.layers.0.mlp.shared_experts.1.gate_up_proj.lora_A.weight"
weight = torch.arange(15).reshape(3, 5)
weights = {name: weight}
_normalizer()._normalize_shared_expert_moe(weights)
torch.testing.assert_close(weights[name], weight)
assert weights[name].dim() == 2
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,312 @@
"""Hermetic stream-order checks for Inkling shared/routed overlap."""
from __future__ import annotations
import ast
import sys
import types
from pathlib import Path
from types import SimpleNamespace
import pytest
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=5, stage="base-b", runner_config="1-gpu-small")
# Skipped on CI: this hermetic check re-parses the InklingMoE forward source and
# pins its exact stream-order, so it breaks on unrelated refactors of that
# method. Skip until it is rebuilt against a stable seam.
pytestmark = pytest.mark.skip(
reason="refactor-fragile source-parsing unit test; skipped on CI"
)
REPO_ROOT = Path(__file__).resolve().parents[4]
MOE_PATH = REPO_ROOT / "python/sglang/srt/models/inkling_common/moe.py"
INKLING_DENSE_PATH = (
REPO_ROOT / "python/sglang/srt/lora/trtllm_lora_temp/inkling_dense.py"
)
class _Flag:
def __init__(self, value: bool):
self.value = value
def get(self) -> bool:
return self.value
class _Stream:
def __init__(self, name: str, events: list[str]):
self.name = name
self.events = events
def wait_stream(self, other: _Stream) -> None:
self.events.append(f"{self.name}.wait({other.name})")
class _StreamContext:
def __init__(self, cuda, stream: _Stream):
self.cuda = cuda
self.stream = stream
self.previous = None
def __enter__(self):
self.previous = self.cuda.current
self.cuda.current = self.stream
self.cuda.events.append(f"enter({self.stream.name})")
def __exit__(self, *_):
self.cuda.events.append(f"exit({self.stream.name})")
self.cuda.current = self.previous
class _Cuda:
def __init__(self, events: list[str]):
self.events = events
self.current = _Stream("main", events)
def current_stream(self) -> _Stream:
return self.current
def stream(self, stream: _Stream) -> _StreamContext:
return _StreamContext(self, stream)
class _Tensor:
def __init__(self, name: str, events: list[str], *, tokens: int = 1):
self.name = name
self.events = events
self.shape = (tokens, 8)
self.dtype = "bf16"
self.is_cuda = True
def record_stream(self, stream: _Stream) -> None:
self.events.append(f"{self.name}.record({stream.name})")
def __add__(self, other: _Tensor) -> _Tensor:
self.events.append(f"add({self.name},{other.name})")
return _Tensor("sum", self.events)
def _load_forward(fake_torch, capture: bool = False):
tree = ast.parse(MOE_PATH.read_text())
source_class = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "InklingMoE"
)
forward = next(
node
for node in source_class.body
if isinstance(node, ast.FunctionDef) and node.name == "forward"
)
test_class = ast.ClassDef(
name="_InklingMoEForwardUnderTest",
bases=[],
keywords=[],
body=[forward],
decorator_list=[],
)
namespace = {
"ForwardBatch": object,
"envs": SimpleNamespace(
SGLANG_OPT_USE_INKLING_MULTI_STREAM_OVERLAP=_Flag(True)
),
# capture gating: overlap only inside cuda-graph capture
"get_is_capture_mode": lambda: capture,
"get_ar_buffer": lambda *_: None,
"get_tensor_model_parallel_group": lambda: SimpleNamespace(world_size=1),
"lora_compatible_layout_enabled": lambda: True,
"torch": fake_torch,
}
exec(
compile(
ast.fix_missing_locations(ast.Module(body=[test_class], type_ignores=[])),
str(MOE_PATH),
"exec",
),
namespace,
)
return namespace[test_class.name]
def _load_lora_overlap_policy():
tree = ast.parse(INKLING_DENSE_PATH.read_text())
policy = next(
node
for node in tree.body
if isinstance(node, ast.FunctionDef)
and node.name == "allow_inkling_moe_two_stream"
)
namespace = {}
exec(
compile(
ast.fix_missing_locations(ast.Module(body=[policy], type_ignores=[])),
str(INKLING_DENSE_PATH),
"exec",
),
namespace,
)
return namespace[policy.name]
def _make_moe(events: list[str], cuda: _Cuda, capture: bool = False):
fake_torch = SimpleNamespace(Tensor=_Tensor, cuda=cuda)
moe = _load_forward(fake_torch, capture)()
moe.alt_stream = _Stream("alt", events)
moe.shared_experts = SimpleNamespace(
lora_backend=SimpleNamespace(batch_info=SimpleNamespace(has_active_lora=True))
)
moe.experts = SimpleNamespace()
moe._clone_fused_sink_input = False
moe._fused_ar_shared = False
moe.gate = lambda x: (
_Tensor("topk_weights", events),
_Tensor("topk_ids", events),
_Tensor("gammas", events),
None,
)
def forward_shared(x, gammas):
events.append(f"shared({cuda.current.name})")
return _Tensor("shared_out", events)
def forward_routed(*_):
assert cuda.current.name == "main"
events.append("routed(main)")
return _Tensor("routed_out", events)
moe._forward_shared = forward_shared
moe._forward_routed = forward_routed
return moe
def _install_lora_policy(monkeypatch, *, main_alloc: bool, capture: bool = False):
lora_envs = SimpleNamespace(SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC=_Flag(main_alloc))
monkeypatch.setitem(
sys.modules,
"sglang.srt.lora.trtllm_lora_temp.environ",
types.SimpleNamespace(lora_envs=lora_envs),
)
monkeypatch.setitem(
sys.modules,
"sglang.srt.model_executor.runner_utils.capture_mode",
types.SimpleNamespace(get_is_capture_mode=lambda: capture),
)
monkeypatch.setitem(
sys.modules,
"sglang.srt.lora.trtllm_lora_temp.inkling_dense",
types.SimpleNamespace(allow_inkling_moe_two_stream=_load_lora_overlap_policy()),
)
@pytest.mark.parametrize("tokens", [1, 32])
def test_direct_sink_keeps_decode_overlap(monkeypatch, tokens):
events: list[str] = []
cuda = _Cuda(events)
_install_lora_policy(monkeypatch, main_alloc=True, capture=True)
moe = _make_moe(events, cuda, capture=True)
moe.forward(_Tensor("x", events, tokens=tokens), reduce=False)
assert events == [
"x.record(alt)",
"gammas.record(alt)",
"alt.wait(main)",
"enter(alt)",
"shared(alt)",
"exit(alt)",
"routed(main)",
"main.wait(alt)",
"shared_out.record(main)",
"add(routed_out,shared_out)",
]
def test_lora_prefill_stays_serial(monkeypatch):
# Even when capture would allow overlap, the M>32 LoRA policy forces serial.
events: list[str] = []
cuda = _Cuda(events)
_install_lora_policy(monkeypatch, main_alloc=True, capture=True)
moe = _make_moe(events, cuda, capture=True)
moe.forward(_Tensor("x", events, tokens=33), reduce=False)
assert events == [
"routed(main)",
"shared(main)",
"add(routed_out,shared_out)",
]
def test_captured_prefill_stays_serial_even_base_only(monkeypatch):
# Capture forces has_lora_work (one schedule for every replay), so
# prefill-sized batches (>32 tokens) are serial even with no live adapter.
events: list[str] = []
cuda = _Cuda(events)
_install_lora_policy(monkeypatch, main_alloc=True, capture=True)
moe = _make_moe(events, cuda, capture=True)
moe.shared_experts.lora_backend.batch_info.has_active_lora = False
moe.forward(_Tensor("x", events, tokens=33), reduce=False)
assert events == [
"routed(main)",
"shared(main)",
"add(routed_out,shared_out)",
]
def test_eager_forward_stays_serial_even_base_only(monkeypatch):
# overlap is gated on cuda-graph capture; eager forwards are serial.
events: list[str] = []
cuda = _Cuda(events)
_install_lora_policy(monkeypatch, main_alloc=False, capture=False)
moe = _make_moe(events, cuda, capture=False)
moe.shared_experts.lora_backend.batch_info.has_active_lora = False
moe.forward(_Tensor("x", events, tokens=33), reduce=False)
assert events == [
"routed(main)",
"shared(main)",
"add(routed_out,shared_out)",
]
def test_lora_overlap_stays_serial_without_main_alloc(monkeypatch):
events: list[str] = []
cuda = _Cuda(events)
_install_lora_policy(monkeypatch, main_alloc=False, capture=True)
moe = _make_moe(events, cuda, capture=True)
moe.forward(_Tensor("x", events), reduce=False)
assert events == [
"routed(main)",
"shared(main)",
"add(routed_out,shared_out)",
]
def test_capture_keeps_lora_schedule_without_active_adapter(monkeypatch):
events: list[str] = []
cuda = _Cuda(events)
_install_lora_policy(monkeypatch, main_alloc=False, capture=True)
moe = _make_moe(events, cuda, capture=True)
moe.shared_experts.lora_backend.batch_info.has_active_lora = False
moe.forward(_Tensor("x", events), reduce=False)
assert events == [
"routed(main)",
"shared(main)",
"add(routed_out,shared_out)",
]
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))
@@ -19,19 +19,164 @@ from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_cuda_ci(est_time=9, stage="base-b", runner_config="1-gpu-small")
register_amd_ci(est_time=9, suite="stage-b-test-1-gpu-small-amd")
import ast
import types
import unittest
import unittest.mock as mock
from enum import Enum
from pathlib import Path
import torch
from sglang.srt.lora.mem_pool import (
LoRAMemoryPool,
_get_moe_ep_context,
_get_moe_tp_context,
from sglang.srt.lora.eviction_policy import get_eviction_policy
REPO_ROOT = Path(__file__).resolve().parents[4]
MOE_UTILS_PATH = REPO_ROOT / "python/sglang/srt/layers/moe/utils.py"
STANDARD_DISPATCHER_PATH = (
REPO_ROOT / "python/sglang/srt/layers/moe/token_dispatcher/standard.py"
)
class _FakeBaseLayerWithLoRA:
pass
class _FakeFusedMoEWithLoRA(_FakeBaseLayerWithLoRA):
pass
_LORA_LAYERS_STUB = types.ModuleType("sglang.srt.lora.layers")
_LORA_LAYERS_STUB.BaseLayerWithLoRA = _FakeBaseLayerWithLoRA
_LORA_LAYERS_STUB.FusedMoEWithLoRA = _FakeFusedMoEWithLoRA
_LORA_ADAPTER_STUB = types.ModuleType("sglang.srt.lora.lora")
_LORA_ADAPTER_STUB.LoRAAdapter = object
with mock.patch.dict(
"sys.modules",
{
"sglang.srt.lora.layers": _LORA_LAYERS_STUB,
"sglang.srt.lora.lora": _LORA_ADAPTER_STUB,
},
):
import sglang.srt.lora.mem_pool as mem_pool_module
from sglang.srt.lora.mem_pool import (
EMPTY_SLOT,
LoRAMemoryPool,
_get_moe_ep_context,
_get_moe_tp_context,
_moe_runner_keeps_global_expert_ids,
)
class _IdentityMoeSlices:
def slice_moe_lora_a_weights(self, weights, _rank, _target):
return weights
def slice_moe_lora_b_weights(self, weights, _rank, _target):
return weights
class _FakeSharedMoeLayer(_IdentityMoeSlices):
is_shared_fused_moe = True
def __init__(self, tp_rank: int = 0):
self.moe_tp_rank = tp_rank
class _FakeRoutedMoeLayer(_FakeFusedMoEWithLoRA, _IdentityMoeSlices):
def __init__(self, tp_rank: int = 0):
object.__setattr__(
self,
"base_layer",
types.SimpleNamespace(
moe_tp_rank=tp_rank,
is_shared_fused_moe=False,
),
)
def slice_moe_lora_a_weights(self, weights, _rank, _target):
return weights
def slice_moe_lora_b_weights(self, weights, _rank, _target):
return weights
class _FakeDenseLayer:
def slice_lora_a_weights(self, weights, _rank):
return weights
def slice_lora_b_weights(self, weights, _rank):
return weights
def _load_lora_weight_to_buffer(pool, **kwargs):
with mock.patch.dict("sys.modules", {"sglang.srt.lora.layers": _LORA_LAYERS_STUB}):
return pool.load_lora_weight_to_buffer(**kwargs)
def _load_moe_backend_enum():
tree = ast.parse(MOE_UTILS_PATH.read_text())
backend = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "MoeRunnerBackend"
)
namespace = {"Enum": Enum}
exec(
compile(
ast.fix_missing_locations(ast.Module(body=[backend], type_ignores=[])),
str(MOE_UTILS_PATH),
"exec",
),
namespace,
)
return namespace[backend.name]
def _load_standard_dispatcher(get_parallel, get_backend):
tree = ast.parse(STANDARD_DISPATCHER_PATH.read_text())
source = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "StandardDispatcher"
)
init = next(
node
for node in source.body
if isinstance(node, ast.FunctionDef) and node.name == "__init__"
)
class _DispatcherBase:
def __init__(self):
pass
test_class = ast.ClassDef(
name="_StandardDispatcherUnderTest",
bases=[ast.Name(id="_DispatcherBase", ctx=ast.Load())],
keywords=[],
body=[init],
decorator_list=[],
)
namespace = {
"_DispatcherBase": _DispatcherBase,
"MoeRunnerConfig": object,
"get_parallel": get_parallel,
"get_moe_runner_backend": get_backend,
"_use_aiter": False,
"get_moe_a2a_backend": lambda: types.SimpleNamespace(
supports_aiter=lambda: False
),
}
exec(
compile(
ast.fix_missing_locations(ast.Module(body=[test_class], type_ignores=[])),
str(STANDARD_DISPATCHER_PATH),
"exec",
),
namespace,
)
return namespace[test_class.name]
def _make_pool(
*,
num_experts_global: int,
@@ -60,6 +205,159 @@ def _make_pool(
return pool
class _IterationOrderedSet(set):
def __init__(self, values, iteration_order):
super().__init__(values)
self.iteration_order = iteration_order
def __iter__(self):
return iter(self.iteration_order)
class TestDeterministicPoolSlots(unittest.TestCase):
@staticmethod
def _prepare(iteration_order):
pool = LoRAMemoryPool.__new__(LoRAMemoryPool)
pool.max_loras_per_batch = 4
pool.uid_to_buffer_id = {}
pool.buffer_id_to_uid = [EMPTY_SLOT] * 4
pool.eviction_policy = get_eviction_policy("lru")
loaded = []
pool.load_lora_weight_to_buffer = lambda uid, slot, *_args: loaded.append(
(uid, slot)
)
uids = _IterationOrderedSet(
{None, "adapter-a", "adapter-b", "adapter-c"}, iteration_order
)
pool.prepare_lora_batch(
cur_uids=uids,
lora_adapters={},
lora_modules=[],
lora_refs={},
lora_embed_tokens_module=None,
lora_lm_head_module=None,
)
return loaded, list(pool.eviction_policy.access_order)
def test_uid_iteration_order_does_not_change_slots_or_lru(self):
forward = [None, "adapter-a", "adapter-b", "adapter-c"]
reverse = list(reversed(forward))
expected = (
[
(None, 0),
("adapter-a", 1),
("adapter-b", 2),
("adapter-c", 3),
],
["adapter-a", "adapter-b", "adapter-c"],
)
self.assertEqual(self._prepare(forward), expected)
self.assertEqual(self._prepare(reverse), expected)
@staticmethod
def _evict_after_touch(iteration_order):
pool = LoRAMemoryPool.__new__(LoRAMemoryPool)
pool.max_loras_per_batch = 4
pool.uid_to_buffer_id = {
None: 0,
"adapter-a": 1,
"adapter-b": 2,
"adapter-c": 3,
}
pool.buffer_id_to_uid = [None, "adapter-a", "adapter-b", "adapter-c"]
pool.eviction_policy = get_eviction_policy("lru")
for uid in ("adapter-a", "adapter-b", "adapter-c"):
pool.eviction_policy.mark_used(uid)
pool.load_lora_weight_to_buffer = lambda *_args: None
common = dict(
lora_adapters={},
lora_modules=[],
lora_refs={},
lora_embed_tokens_module=None,
lora_lm_head_module=None,
)
pool.prepare_lora_batch(
cur_uids=_IterationOrderedSet(
{"adapter-a", "adapter-b", "adapter-c"}, iteration_order
),
**common,
)
pool.prepare_lora_batch(cur_uids={"adapter-d"}, **common)
return pool.uid_to_buffer_id, list(pool.eviction_policy.access_order)
def test_uid_iteration_order_does_not_change_next_lru_victim(self):
forward = ["adapter-a", "adapter-b", "adapter-c"]
reverse = list(reversed(forward))
expected = (
{None: 0, "adapter-d": 1, "adapter-b": 2, "adapter-c": 3},
["adapter-b", "adapter-c", "adapter-d"],
)
self.assertEqual(self._evict_after_touch(forward), expected)
self.assertEqual(self._evict_after_touch(reverse), expected)
class TestBufferSlotClearing(unittest.TestCase):
@staticmethod
def _buffers(pool):
layered = [
tensor
for buffers in (*pool.A_buffer.values(), *pool.B_buffer.values())
for tensor in buffers
]
direct = [
tensor
for buffers in (
pool.embedding_A_buffer,
pool.embedding_B_buffer,
pool.lm_head_A_buffer,
pool.lm_head_B_buffer,
pool.new_embeddings_buffer,
)
for tensor in buffers.values()
]
return layered + direct
@classmethod
def _make_pool(cls):
pool = LoRAMemoryPool.__new__(LoRAMemoryPool)
pool.num_layer = 2
pool.A_buffer = {"a": [torch.ones(2, 1, 2) for _ in range(2)]}
pool.B_buffer = {"b": [torch.full((2, 1, 2), float("nan")) for _ in range(2)]}
pool.embedding_A_buffer = {"a": torch.ones(2, 1, 2)}
pool.embedding_B_buffer = {"b": torch.full((2, 1, 2), float("nan"))}
pool.lm_head_A_buffer = {"a": torch.ones(2, 1, 2)}
pool.lm_head_B_buffer = {"b": torch.full((2, 1, 2), float("inf"))}
pool.new_embeddings_buffer = {"embeddings": torch.full((2, 1, 2), float("nan"))}
for tensor in cls._buffers(pool):
tensor[1].fill_(7)
pool.uid_to_buffer_id = {"adapter": 0}
pool.buffer_id_to_uid = ["adapter", "other"]
pool.eviction_policy = get_eviction_policy("lru")
pool.eviction_policy.mark_used("adapter")
return pool
def test_remove_lora_clears_slot_in_place_and_residency(self):
pool = self._make_pool()
tensors = self._buffers(pool)
pointers = [tensor.data_ptr() for tensor in tensors]
self.assertEqual(pool.remove_lora("adapter"), 0)
for tensor, pointer in zip(tensors, pointers):
torch.testing.assert_close(tensor[0], torch.zeros_like(tensor[0]))
torch.testing.assert_close(tensor[1], torch.full_like(tensor[1], 7))
self.assertTrue(torch.isfinite(tensor).all())
self.assertEqual(tensor.data_ptr(), pointer)
self.assertNotIn("adapter", pool.uid_to_buffer_id)
self.assertIs(pool.buffer_id_to_uid[0], EMPTY_SLOT)
self.assertNotIn("adapter", pool.eviction_policy.access_order)
def _make_fake_base_model(num_experts: int) -> torch.nn.Module:
"""Return a `torch.nn.Module` whose `.config` exposes `num_experts`.
@@ -338,6 +636,251 @@ class TestIterLocalExpertWeightsTensor(unittest.TestCase):
list(pool._iter_local_expert_weights(weights, "weights"))
class TestSharedMoeProductionLoad(unittest.TestCase):
def test_unmarked_2d_shared_expert_uses_dense_buffers(self):
pool = _make_pool(
num_experts_global=8,
moe_ep_size=1,
moe_ep_rank=0,
moe_use_local_expert_ids=False,
)
pool.num_layer = 1
pool.tp_rank = 0
pool.max_lora_rank = 2
pool.target_modules = {"down_proj"}
pool.experts_shared_outer_loras = False
pool.strict_loading = True
pool.lora_added_tokens_size = 0
pool.pin_memory_available = False
pool.enable_lora_overlap_loading = False
pool.base_model = object()
pool.A_buffer = {
"down_proj": [torch.full((1, 2, 3), -1.0)],
"down_proj_shared_moe": [torch.full((1, 2, 2, 3), -7.0)],
}
pool.B_buffer = {
"down_proj": [torch.full((1, 5, 2), -1.0)],
"down_proj_shared_moe": [torch.full((1, 1, 5, 2), -7.0)],
}
pool.embedding_A_buffer = {}
pool.embedding_B_buffer = {}
pool.lm_head_A_buffer = {}
pool.lm_head_B_buffer = {}
pool.new_embeddings_buffer = {}
down_a = torch.arange(6, dtype=torch.float32).reshape(2, 3)
down_b = torch.arange(10, dtype=torch.float32).reshape(5, 2)
adapter = types.SimpleNamespace(
config=types.SimpleNamespace(r=2),
scaling=2.5,
layers=[
types.SimpleNamespace(
weights={
"model.layers.0.mlp.shared_experts.down_proj.lora_A.weight": down_a,
"model.layers.0.mlp.shared_experts.down_proj.lora_B.weight": down_b,
},
pinned_weights={},
)
],
embedding_layers={},
added_tokens_embeddings={},
)
_load_lora_weight_to_buffer(
pool,
uid="dense-shared",
buffer_id=0,
lora_adapter=adapter,
lora_modules=[
{"model.layers.0.mlp.shared_experts.down_proj": _FakeDenseLayer()}
],
lora_embed_tokens_module=None,
lora_lm_head_module=None,
)
torch.testing.assert_close(pool.A_buffer["down_proj"][0][0], down_a)
torch.testing.assert_close(pool.B_buffer["down_proj"][0][0], down_b)
self.assertTrue(torch.all(pool.A_buffer["down_proj_shared_moe"][0] == -7))
self.assertTrue(torch.all(pool.B_buffer["down_proj_shared_moe"][0] == -7))
def test_rank3_loads_shared_gate_b_and_down_a(self):
"""Shared-sink weights stay replicated even on a nonzero EP rank."""
pool = _make_pool(
num_experts_global=256,
moe_ep_size=4,
moe_ep_rank=3,
moe_use_local_expert_ids=True,
)
pool.num_layer = 1
pool.max_lora_rank = 2
pool.target_modules = {"gate_up_proj", "down_proj"}
pool.experts_shared_outer_loras = True
pool.strict_loading = True
pool.lora_added_tokens_size = 0
pool.pin_memory_available = False
pool.enable_lora_overlap_loading = False
pool.base_model = object()
pool.A_buffer = {
"gate_up_proj": [torch.full((1, 4, 5), -7.0)],
"down_proj": [torch.full((1, 2, 3), -7.0)],
"gate_up_proj_shared_moe": [torch.full((1, 1, 4, 5), -1.0)],
"down_proj_shared_moe": [torch.full((1, 2, 2, 3), -1.0)],
}
pool.B_buffer = {
"gate_up_proj": [torch.full((1, 6, 2), -7.0)],
"down_proj": [torch.full((1, 5, 2), -7.0)],
"gate_up_proj_shared_moe": [torch.full((1, 2, 6, 2), -1.0)],
"down_proj_shared_moe": [torch.full((1, 1, 5, 2), -1.0)],
}
pool.embedding_A_buffer = {}
pool.embedding_B_buffer = {}
pool.lm_head_A_buffer = {}
pool.lm_head_B_buffer = {}
pool.new_embeddings_buffer = {}
gate_b = torch.arange(2 * 6 * 2, dtype=torch.float32).reshape(2, 6, 2)
down_a = torch.arange(2 * 2 * 3, dtype=torch.float32).reshape(2, 2, 3)
adapter = types.SimpleNamespace(
config=types.SimpleNamespace(r=2),
scaling=2.5,
layers=[
types.SimpleNamespace(
weights={
"model.layers.0.mlp.shared_experts.gate_up_proj.lora_B.weight": gate_b,
"model.layers.0.mlp.shared_experts.down_proj.lora_A.weight": down_a,
},
pinned_weights={},
)
],
embedding_layers={},
added_tokens_embeddings={},
)
shared_sink = _FakeSharedMoeLayer(tp_rank=1)
_load_lora_weight_to_buffer(
pool,
uid="shared",
buffer_id=0,
lora_adapter=adapter,
lora_modules=[{"shared_experts": shared_sink}],
lora_embed_tokens_module=None,
lora_lm_head_module=None,
)
torch.testing.assert_close(pool.A_buffer["down_proj_shared_moe"][0][0], down_a)
torch.testing.assert_close(
pool.B_buffer["gate_up_proj_shared_moe"][0][0], gate_b * adapter.scaling
)
self.assertTrue(torch.all(pool.A_buffer["down_proj"][0] == -7))
self.assertTrue(torch.all(pool.B_buffer["gate_up_proj"][0] == -7))
pool.A_buffer["down_proj_shared_moe"][0].fill_(-1)
pool.B_buffer["gate_up_proj_shared_moe"][0].fill_(-1)
adapter.layers[0].weights = {
**{
f"model.layers.0.mlp.shared_experts.{i}.gate_up_proj.lora_B.weight": gate_b[
i
]
for i in range(2)
},
**{
f"model.layers.0.mlp.shared_experts.{i}.down_proj.lora_A.weight": down_a[
i
]
for i in range(2)
},
}
_load_lora_weight_to_buffer(
pool,
uid="shared-named",
buffer_id=0,
lora_adapter=adapter,
lora_modules=[{"shared_experts": shared_sink}],
lora_embed_tokens_module=None,
lora_lm_head_module=None,
)
torch.testing.assert_close(pool.A_buffer["down_proj_shared_moe"][0][0], down_a)
torch.testing.assert_close(
pool.B_buffer["gate_up_proj_shared_moe"][0][0], gate_b * adapter.scaling
)
def test_missing_inner_factors_clear_reused_shared_and_routed_slots(self):
for suffix, expert_path in (
("_shared_moe", "shared_experts"),
("_moe", "experts"),
):
with self.subTest(suffix=suffix):
pool = _make_pool(
num_experts_global=2,
moe_ep_size=1,
moe_ep_rank=0,
moe_use_local_expert_ids=False,
)
pool.num_layer = 1
pool.max_lora_rank = 2
pool.target_modules = {"gate_up_proj", "down_proj"}
pool.experts_shared_outer_loras = True
pool.strict_loading = True
pool.lora_added_tokens_size = 0
pool.pin_memory_available = False
pool.enable_lora_overlap_loading = False
pool.base_model = object()
pool.A_buffer = {
f"gate_up_proj{suffix}": [torch.full((1, 1, 4, 5), -1.0)],
f"down_proj{suffix}": [torch.full((1, 2, 2, 3), -1.0)],
}
pool.B_buffer = {
f"gate_up_proj{suffix}": [torch.full((1, 2, 6, 2), -1.0)],
f"down_proj{suffix}": [torch.full((1, 1, 5, 2), -1.0)],
}
pool.embedding_A_buffer = {}
pool.embedding_B_buffer = {}
pool.lm_head_A_buffer = {}
pool.lm_head_B_buffer = {}
pool.new_embeddings_buffer = {}
adapter = types.SimpleNamespace(
config=types.SimpleNamespace(r=2),
scaling=2.5,
layers=[
types.SimpleNamespace(
weights={
f"model.layers.0.mlp.{expert_path}.gate_up_proj."
"lora_A.weight": torch.ones(1, 4, 5),
f"model.layers.0.mlp.{expert_path}.down_proj."
"lora_B.weight": torch.ones(1, 5, 2),
},
pinned_weights={},
)
],
embedding_layers={},
added_tokens_embeddings={},
)
module = (
_FakeSharedMoeLayer()
if suffix == "_shared_moe"
else _FakeRoutedMoeLayer()
)
_load_lora_weight_to_buffer(
pool,
uid="adapter",
buffer_id=0,
lora_adapter=adapter,
lora_modules=[{"experts": module}],
lora_embed_tokens_module=None,
lora_lm_head_module=None,
)
self.assertEqual(
torch.count_nonzero(pool.A_buffer[f"down_proj{suffix}"][0]), 0
)
self.assertEqual(
torch.count_nonzero(pool.B_buffer[f"gate_up_proj{suffix}"][0]), 0
)
class TestModuleLevelHelpers(unittest.TestCase):
"""`_get_moe_ep_context` / `_moe_runner_keeps_global_expert_ids`
must degrade gracefully when the MoE EP group or runner backend is
@@ -357,6 +900,48 @@ class TestModuleLevelHelpers(unittest.TestCase):
self.assertEqual(tp_size, 1)
self.assertEqual(tp_rank, 0)
def test_keeps_global_expert_ids_defaults_to_false(self):
# Without a specific flashinfer backend selected, default is False.
self.assertFalse(_moe_runner_keeps_global_expert_ids())
def test_real_backend_predicate_matches_dispatcher_and_pool(self):
backends = _load_moe_backend_enum()
expected_global = {
backends.FLASHINFER_TRTLLM,
backends.EXPERIMENTAL_SGL_TRTLLM,
backends.FLASHINFER_TRTLLM_ROUTED,
backends.FLASHINFER_CUTLASS,
backends.FLASHINFER_MXFP4,
backends.FLASHINFER_CUTEDSL,
}
config = types.SimpleNamespace(
num_experts=8,
num_local_experts=2,
num_fused_shared_experts=0,
)
parallel = types.SimpleNamespace(moe_ep_size=4, moe_ep_rank=1)
state = types.SimpleNamespace(backend=None)
standard_dispatcher = _load_standard_dispatcher(
get_parallel=lambda: parallel,
get_backend=lambda: state.backend,
)
moe_utils = types.ModuleType("sglang.srt.layers.moe.utils")
moe_utils.get_moe_runner_backend = lambda: state.backend
for backend in backends:
state.backend = backend
with mock.patch.dict(
"sys.modules", {"sglang.srt.layers.moe.utils": moe_utils}
):
dispatcher = standard_dispatcher(config)
self.assertEqual(
dispatcher.skip_local_expert_mapping,
backend in expected_global,
)
self.assertEqual(
_moe_runner_keeps_global_expert_ids(),
backend in expected_global,
)
class TestPoolInitPicksUpEpContext(unittest.TestCase):
"""`LoRAMemoryPool.__init__` should read EP context from the module-
@@ -377,16 +962,19 @@ class TestPoolInitPicksUpEpContext(unittest.TestCase):
`init_buffers` — we only care about the EP-context state.
"""
with (
mock.patch(
"sglang.srt.lora.mem_pool._get_moe_ep_context",
mock.patch.object(
mem_pool_module,
"_get_moe_ep_context",
return_value=(ep_size, ep_rank),
),
mock.patch(
"sglang.srt.lora.mem_pool._get_moe_tp_context",
mock.patch.object(
mem_pool_module,
"_get_moe_tp_context",
return_value=(moe_tp_size, moe_tp_rank),
),
mock.patch(
"sglang.srt.lora.mem_pool._moe_runner_keeps_global_expert_ids",
mock.patch.object(
mem_pool_module,
"_moe_runner_keeps_global_expert_ids",
return_value=keeps_global,
),
mock.patch.object(LoRAMemoryPool, "init_buffers", lambda self, _m: None),
@@ -655,8 +1243,6 @@ class TestLoadBufferPassesMoeTpRankToSlice(unittest.TestCase):
shapes the test does not provide)."""
def test_moe_tp_rank_used_for_slicing_when_ep_lt_tp(self):
from sglang.srt.lora.layers import FusedMoEWithLoRA
# tp=4 ep=2 → moe_tp_size=2. Pick OUTER rank 3 so moe_tp_rank=1.
# The two values differ; the bug would surface on this exact rank.
pool = LoRAMemoryPool.__new__(LoRAMemoryPool)
@@ -689,64 +1275,65 @@ class TestLoadBufferPassesMoeTpRankToSlice(unittest.TestCase):
pool.lm_head_B_buffer = {}
pool.new_embeddings_buffer = {}
captured_ranks = []
moe_mod = _FakeRoutedMoeLayer(tp_rank=1)
moe_mod = mock.MagicMock(spec=FusedMoEWithLoRA)
for ab in ("A", "B"):
with self.subTest(ab=ab):
captured_ranks = []
def capture_a(weights, tp_rank, target_module):
captured_ranks.append(("A", target_module, tp_rank))
raise TestLoadBufferPassesMoeTpRankToSlice._StopAfterCapture()
def capture(weights, tp_rank, target_module):
captured_ranks.append((ab, target_module, tp_rank))
raise TestLoadBufferPassesMoeTpRankToSlice._StopAfterCapture()
def capture_b(weights, tp_rank, target_module):
captured_ranks.append(("B", target_module, tp_rank))
raise TestLoadBufferPassesMoeTpRankToSlice._StopAfterCapture()
moe_mod.slice_moe_lora_a_weights = (
capture if ab == "A" else lambda weights, *_args: weights
)
moe_mod.slice_moe_lora_b_weights = (
capture if ab == "B" else lambda weights, *_args: weights
)
moe_mod.slice_moe_lora_a_weights.side_effect = capture_a
moe_mod.slice_moe_lora_b_weights.side_effect = capture_b
# The per-expert key creates the A or B dictionary whose
# production call site invokes the matching MoE slicer.
adapter = mock.MagicMock()
adapter.config.r = 4
adapter.scaling = 1.0
adapter.embedding_layers = {}
adapter.added_tokens_embeddings = {}
adapter.layers = [
types.SimpleNamespace(
weights={
f"model.layers.0.mlp.experts.0.gate_up_proj.lora_{ab}.weight": torch.zeros(
8, 4
),
},
pinned_weights={},
)
]
# Adapter with one per-expert MoE LoRA-A weight. The expert regex
# `experts\.(\d+)\.` must match the key, which routes the weight
# into `temp_A_buffer["gate_up_proj_moe"]` — the dict shape that
# makes `temp_A_buffer.get("gate_up_proj_moe") is not None` true,
# which in turn triggers `slice_moe_lora_a_weights` (and the
# capture).
adapter = mock.MagicMock()
adapter.config.r = 4
adapter.scaling = 1.0
adapter.embedding_layers = {}
adapter.added_tokens_embeddings = {}
adapter.layers = [
types.SimpleNamespace(
weights={
"model.layers.0.mlp.experts.0.gate_up_proj.lora_A.weight": (
torch.zeros(8, 4)
),
},
pinned_weights={},
)
]
with self.assertRaises(
TestLoadBufferPassesMoeTpRankToSlice._StopAfterCapture
):
_load_lora_weight_to_buffer(
pool,
uid="test",
buffer_id=0,
lora_adapter=adapter,
lora_modules=[{"mlp.experts": moe_mod}],
lora_embed_tokens_module=None,
lora_lm_head_module=None,
)
with self.assertRaises(TestLoadBufferPassesMoeTpRankToSlice._StopAfterCapture):
pool.load_lora_weight_to_buffer(
uid="test",
buffer_id=0,
lora_adapter=adapter,
lora_modules=[{"mlp.experts": moe_mod}],
lora_embed_tokens_module=None,
lora_lm_head_module=None,
)
self.assertGreater(len(captured_ranks), 0, "slicing was never invoked")
for ab, target_module, rank in captured_ranks:
self.assertEqual(
rank,
pool.moe_tp_rank,
f"slice_moe_lora_{ab.lower()}_weights for {target_module} "
f"received rank={rank}; expected moe_tp_rank="
f"{pool.moe_tp_rank} (outer tp_rank is {pool.tp_rank}). "
"Passing the outer tp_rank slices past "
"intermediate_size_per_partition when ep_size < tp_size.",
)
self.assertEqual(len(captured_ranks), 1, "slicing was never invoked")
_, target_module, rank = captured_ranks[0]
self.assertEqual(
rank,
pool.moe_tp_rank,
f"slice_moe_lora_{ab.lower()}_weights for {target_module} "
f"received rank={rank}; expected moe_tp_rank="
f"{pool.moe_tp_rank} (outer tp_rank is {pool.tp_rank}). "
"Passing the outer tp_rank slices past "
"intermediate_size_per_partition when ep_size < tp_size.",
)
if __name__ == "__main__":
@@ -118,6 +118,7 @@ def _make_model_runner(
sa.max_running_requests = max_running_requests
sa.disaggregation_decode_extra_slots = disaggregation_decode_extra_slots
sa.enable_dsa_cache_layer_split = False
sa.kv_cache_dtype = "auto"
mr.server_args = sa
spec = MagicMock()
@@ -0,0 +1,242 @@
import unittest
from sglang.srt.entrypoints.openai.chat_encoding import encode_simple_chat
from sglang.srt.parser.inkling_renderer import render_inkling_messages
from sglang.srt.parser.inkling_tokenizer import (
CONTENT_INVOKE_TOOL_JSON,
CONTENT_MODEL_END_SAMPLING,
CONTENT_TEXT,
CONTENT_THINKING,
CONTENT_XML,
END_MESSAGE,
INKLING_SPECIAL_TOKEN_IDS,
MESSAGE_MODEL,
MESSAGE_SYSTEM,
MESSAGE_USER,
)
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=1, suite="base-a-test-cpu")
def _text(value: str) -> list[int]:
return list(value.encode())
class _InklingTokenizer:
def encode_special(self, token: str) -> int:
return INKLING_SPECIAL_TOKEN_IDS[token]
def encode_text(self, text: str) -> list[int]:
return _text(text)
class _BaseTokenizer:
chat_template = None
def encode(self, text: str, add_special_tokens: bool = False) -> list[int]:
return _text(text)
def _block(role: str, kind: str, payload: str, author: str = "") -> list[int]:
return (
[INKLING_SPECIAL_TOKEN_IDS[role]]
+ _text(author)
+ [
INKLING_SPECIAL_TOKEN_IDS[kind],
*_text(payload),
INKLING_SPECIAL_TOKEN_IDS[END_MESSAGE],
]
)
class TestInklingRenderer(unittest.TestCase):
def setUp(self):
self.tokenizer = _InklingTokenizer()
def test_generation_prompt_is_not_prefilled(self):
actual = render_inkling_messages(
[{"role": "user", "content": "hello"}], self.tokenizer
)
self.assertEqual(
actual,
_block(MESSAGE_SYSTEM, CONTENT_TEXT, "Thinking effort level: 0.9")
+ _block(MESSAGE_USER, CONTENT_TEXT, "hello"),
)
self.assertNotEqual(actual[-1], INKLING_SPECIAL_TOKEN_IDS[MESSAGE_MODEL])
def test_tool_system_and_effort_have_canonical_prefix_order(self):
tools = [
{
"type": "function",
"function": {
"name": "weather",
"description": "Lookup weather",
"parameters": {"type": "object"},
},
}
]
actual = render_inkling_messages(
[
{"role": "system", "content": "original"},
{"role": "user", "content": "question"},
],
self.tokenizer,
tools=tools,
reasoning_effort=0.8764,
)
tool_json = (
'[{"description":"Lookup weather","name":"weather",'
'"parameters":{"type":"object"},"type":"function"}]'
)
expected = (
_block(
MESSAGE_SYSTEM,
CONTENT_XML,
tool_json,
author="tool_declare",
)
+ _block(MESSAGE_SYSTEM, CONTENT_TEXT, "original")
+ _block(MESSAGE_SYSTEM, CONTENT_TEXT, "Thinking effort level: 0.88")
+ _block(MESSAGE_USER, CONTENT_TEXT, "question")
)
self.assertEqual(actual, expected)
def test_multiturn_conversation_has_one_fixed_effort_directive(self):
system = {"role": "system", "content": "system"}
user1 = {"role": "user", "content": "user1"}
assistant1 = {"role": "assistant", "content": "assistant1"}
user2 = {"role": "user", "content": "user2"}
prefix = (
_block(MESSAGE_SYSTEM, CONTENT_TEXT, "system")
+ _block(MESSAGE_SYSTEM, CONTENT_TEXT, "Thinking effort level: 0.2")
+ _block(MESSAGE_USER, CONTENT_TEXT, "user1")
)
turn1 = render_inkling_messages(
[system, user1], self.tokenizer, reasoning_effort=0.2
)
turn2 = render_inkling_messages(
[system, user1, assistant1, user2],
self.tokenizer,
reasoning_effort=0.2,
)
self.assertEqual(turn1, prefix)
self.assertEqual(
turn2,
prefix
+ _block(MESSAGE_MODEL, CONTENT_TEXT, "assistant1")
+ [INKLING_SPECIAL_TOKEN_IDS[CONTENT_MODEL_END_SAMPLING]]
+ _block(MESSAGE_USER, CONTENT_TEXT, "user2"),
)
def test_historical_assistant_preserves_parts_and_ends_sampling(self):
actual = render_inkling_messages(
[
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "first"},
{"type": "text", "text": "visible"},
{"type": "reasoning", "text": "second"},
],
"tool_calls": [
{
"id": "call-1",
"function": {
"name": "weather",
"arguments": '{"city":"SF"}',
},
}
],
}
],
self.tokenizer,
)
expected = (
_block(MESSAGE_SYSTEM, CONTENT_TEXT, "Thinking effort level: 0.9")
+ _block(MESSAGE_MODEL, CONTENT_THINKING, "first")
+ _block(MESSAGE_MODEL, CONTENT_TEXT, "visible")
+ _block(MESSAGE_MODEL, CONTENT_THINKING, "second")
+ _block(
MESSAGE_MODEL,
CONTENT_INVOKE_TOOL_JSON,
'{"name":"weather","args":{"city":"SF"}}',
author="weather",
)
+ [INKLING_SPECIAL_TOKEN_IDS[CONTENT_MODEL_END_SAMPLING]]
)
self.assertEqual(actual, expected)
def test_empty_assistant_message_does_not_emit_bare_terminator(self):
"""Bug regression: an assistant message that renders zero blocks
(content None, no reasoning, no tool calls) appended a bare
<|content_model_end_sampling|> with no preceding model block —
injecting a malformed turn terminator into the prompt."""
actual = render_inkling_messages(
[
{"role": "user", "content": "hi"},
{"role": "assistant", "content": None},
{"role": "user", "content": "again"},
],
self.tokenizer,
)
self.assertNotIn(INKLING_SPECIAL_TOKEN_IDS[CONTENT_MODEL_END_SAMPLING], actual)
def test_reasoning_content_cannot_reorder_thinking_parts(self):
with self.assertRaisesRegex(ValueError, "cannot mix"):
render_inkling_messages(
[
{
"role": "assistant",
"reasoning_content": "legacy",
"content": [{"type": "thinking", "thinking": "ordered"}],
}
],
self.tokenizer,
)
def test_reasoning_effort_is_two_decimal_quantized_and_validated(self):
for value, expected in (
(0.8766, "0.88"),
(0.0, "0"),
(0.99, "0.99"),
(0.125, "0.12"),
(0.875, "0.88"),
):
with self.subTest(value=value):
actual = render_inkling_messages(
[{"role": "user", "content": "q"}],
self.tokenizer,
reasoning_effort=value,
)
directive = _block(
MESSAGE_SYSTEM,
CONTENT_TEXT,
f"Thinking effort level: {expected}",
)
self.assertEqual(actual[: len(directive)], directive)
for value in (-0.1, 1.0, 1.1, float("nan")):
with self.subTest(value=value), self.assertRaises(ValueError):
render_inkling_messages(
[{"role": "user", "content": "q"}],
self.tokenizer,
reasoning_effort=value,
)
def test_offline_encoder_uses_the_same_inkling_format(self):
actual = encode_simple_chat(
tokenizer=_BaseTokenizer(),
spec="inkling",
messages=[{"role": "user", "content": "hello"}],
)
self.assertEqual(
actual,
_block(MESSAGE_SYSTEM, CONTENT_TEXT, "Thinking effort level: 0.9")
+ _block(MESSAGE_USER, CONTENT_TEXT, "hello"),
)
if __name__ == "__main__":
unittest.main()
@@ -9,6 +9,7 @@ from sglang.srt.parser.reasoning_parser import (
Gemma4Detector,
Glm45Detector,
HunyuanDetector,
InklingDetector,
KimiDetector,
KimiK2Detector,
Nemotron3Detector,
@@ -166,6 +167,89 @@ class TestQwen3Detector(CustomTestCase):
self.assertEqual(result.reasoning_text, "")
class TestInklingDetector(CustomTestCase):
def test_streaming_routes_blocks_across_all_string_boundaries(self):
detector = InklingDetector()
source = (
"<|message_model|><|content_thinking|>think<|end_message|>"
"<|message_model|><|content_text|>answer<|end_message|>"
"<|content_model_end_sampling|>"
)
reasoning = ""
content = ""
for char in source:
result = detector.parse_streaming_increment(char)
reasoning += result.reasoning_text
content += result.normal_text
self.assertEqual(reasoning, "think")
self.assertEqual(content, "answer")
def test_tool_header_is_preserved_for_the_tool_parser(self):
detector = InklingDetector()
source = (
"<|message_model|>weather<|content_invoke_tool_json|>"
'{"name":"weather","args":{"city":"SF"}}<|end_message|>'
)
content = ""
for char in source:
content += detector.parse_streaming_increment(char).normal_text
self.assertEqual(content, source)
def test_quoted_message_model_token_inside_content_is_preserved(self):
"""Bug regression: the header branch flipped to header state on ANY
<|message_model|> occurrence, so a literal token the model wrote
inside a content block (e.g. quoting the protocol) silently swallowed
all payload text up to the next control token."""
detector = InklingDetector()
source = (
"<|message_model|><|content_text|>Header token: <|message_model|>"
" then more text<|end_message|>"
)
result = detector.detect_and_parse(source)
self.assertEqual(
result.normal_text, "Header token: <|message_model|> then more text"
)
def test_control_token_inside_tool_header_shares_the_full_alphabet(self):
"""Bug regression: the tool-call detector validated headers against
INKLING_SPECIAL_TOKENS while the reasoning parser keyed on the larger
control alphabet (+ <|model_trigger_generation|>), so a control token
smuggled inside a header passed one machine and not the other."""
from sglang.srt.function_call.inkling_detector import (
InklingDetector as ToolDetector,
)
detector = ToolDetector()
prefix, name = detector._split_trailing_tool_header(
"<|message_model|>weather<|model_trigger_generation|>"
)
self.assertIsNone(name)
def test_continuation_stream_text_survives_chunk_boundaries(self):
"""Bug regression: text arriving with no open block (a
continue_final_message stream resumes MID text block) was routed to
content only when a chunk held no control token; a chunk like
'ld<|end_message|>' silently dropped the 'ld'. All out-of-block text
must reach content regardless of chunking."""
source = (
" world<|end_message|><|message_model|><|content_text|>next<|end_message|>"
)
for chunks in (
[source],
[
" wor",
"ld<|end_message|>",
"<|message_model|><|content_text|>next<|end_message|>",
],
list(source),
):
detector = InklingDetector()
content = ""
for chunk in chunks:
content += detector.parse_streaming_increment(chunk).normal_text
self.assertEqual(content, " worldnext", msg=f"chunks={chunks!r}")
class TestKimiDetector(CustomTestCase):
def setUp(self):
self.detector = KimiDetector()
@@ -74,6 +74,7 @@ class TestModelOverridableWhitelist(CustomTestCase):
"disable_overlap_schedule",
"uses_mamba_radix_cache",
"mamba_radix_cache_strategy",
"mamba_full_memory_ratio",
"speculative_moe_runner_backend",
"speculative_moe_a2a_backend",
"disable_shared_experts_fusion",
@@ -20,6 +20,7 @@ from sglang.srt.utils.hf_transformers.common import (
_is_deepseek_ocr_model,
_override_v_head_dim_if_zero,
_patch_text_config,
attach_additional_stop_token_ids,
check_gguf_file,
get_context_length,
get_hf_text_config,
@@ -451,6 +452,37 @@ class TestGetHfTextConfig(unittest.TestCase):
self.assertEqual(cfg.rope_scaling["type"], "llama3")
# ---------------------------------------------------------------------------
# attach_additional_stop_token_ids
# ---------------------------------------------------------------------------
class TestAttachAdditionalStopTokenIds(unittest.TestCase):
"""Bug regression: the Inkling bundle ships eos metadata unset while its
turn-final marker <|content_model_end_sampling|> sits in added_tokens; the
old detector only recognized <|eom_id|>, so generation ran to max length
(documented by the Inkling GSM8K test)."""
@staticmethod
def _tokenizer(added):
return SimpleNamespace(get_added_vocab=lambda: added)
def test_inkling_end_sampling_registers_as_stop(self):
tok = self._tokenizer({"<|content_model_end_sampling|>": 200006})
attach_additional_stop_token_ids(tok)
self.assertEqual(tok.additional_stop_token_ids, {200006})
def test_eom_id_still_registers_as_stop(self):
tok = self._tokenizer({"<|eom_id|>": 128008})
attach_additional_stop_token_ids(tok)
self.assertEqual(tok.additional_stop_token_ids, {128008})
def test_no_known_marker_yields_none(self):
tok = self._tokenizer({"<|other|>": 7})
attach_additional_stop_token_ids(tok)
self.assertIsNone(tok.additional_stop_token_ids)
# ---------------------------------------------------------------------------
# _fix_special_tokens_pattern
# ---------------------------------------------------------------------------
@@ -0,0 +1,173 @@
"""CPU unit test for Inkling per-expert RL weight-sync loading.
Exercises ``_load_per_expert_param`` on a simulated EP x MoE-TP grid (parallel
helpers monkeypatched, no process groups) and checks every (ep_rank, tp_rank)
against a reference fused stack built directly from the full per-expert weights:
- EP: global expert id remapped to the rank's contiguous local block,
non-owned experts consumed without touching the stack
- MoE-TP: w13 slices the intermediate dim (dim 0 of gate/up), w2 dim 1
- w13 row layout: Inkling-interleaved vs contiguous [gate || up]
(lora_compatible_layout_enabled() or inference_moe_w13_interleaved=False)
- trtllm MoE layouts rejected loudly
Run: python3 test/srt/models/test_inkling_per_expert_sync.py
"""
import types
import unittest
import torch
import sglang.srt.models.inkling as inkling_mod
N_EXPERTS, I_FULL, H = 8, 6, 4
class _FakeModel:
"""Just enough of InklingForConditionalGeneration for _load_per_expert_param."""
def __init__(self, interleaved: bool, moe=None):
self.text_config = types.SimpleNamespace(
n_routed_experts=N_EXPERTS,
inference_moe_w13_interleaved=interleaved,
)
self._moe = moe if moe is not None else types.SimpleNamespace()
def get_submodule(self, path):
return self._moe
_load_per_expert_param = (
inkling_mod.InklingForConditionalGeneration._load_per_expert_param
)
def _full_weights(seed=0):
g = torch.Generator().manual_seed(seed)
return {
(e, proj): torch.randn(
(H, I_FULL) if proj == "down_proj" else (I_FULL, H), generator=g
)
for e in range(N_EXPERTS)
for proj in ("gate_proj", "up_proj", "down_proj")
}
def _expected_stacks(full, ep_size, ep_rank, tp_size, tp_rank, contiguous):
"""Reference: what the fused w13/w2 stacks must contain on this rank."""
local = N_EXPERTS // ep_size
i_tp = I_FULL // tp_size
w13 = torch.empty(local, 2 * i_tp, H)
w2 = torch.empty(local, H, i_tp)
for e_local in range(local):
e = ep_rank * local + e_local
gate = full[(e, "gate_proj")][tp_rank * i_tp : (tp_rank + 1) * i_tp]
up = full[(e, "up_proj")][tp_rank * i_tp : (tp_rank + 1) * i_tp]
if contiguous:
w13[e_local] = torch.cat([gate, up], dim=0)
else: # Inkling-interleaved rows [g0, u0, g1, u1, ...]
w13[e_local, 0::2] = gate
w13[e_local, 1::2] = up
w2[e_local] = full[(e, "down_proj")][:, tp_rank * i_tp : (tp_rank + 1) * i_tp]
return w13, w2
class TestPerExpertSync(unittest.TestCase):
def setUp(self):
self._saved = {
n: getattr(inkling_mod, n)
for n in (
"get_moe_expert_parallel_world_size",
"get_moe_expert_parallel_rank",
"get_moe_tensor_parallel_rank",
"lora_compatible_layout_enabled",
)
}
def tearDown(self):
for n, f in self._saved.items():
setattr(inkling_mod, n, f)
def _patch(self, ep_size, ep_rank, tp_rank, lora_layout=False):
inkling_mod.get_moe_expert_parallel_world_size = lambda: ep_size
inkling_mod.get_moe_expert_parallel_rank = lambda: ep_rank
inkling_mod.get_moe_tensor_parallel_rank = lambda: tp_rank
inkling_mod.lora_compatible_layout_enabled = lambda: lora_layout
def _run_rank(
self, full, ep_size, ep_rank, tp_size, tp_rank, *, interleaved, lora_layout
):
self._patch(ep_size, ep_rank, tp_rank, lora_layout)
model = _FakeModel(interleaved)
local, i_tp = N_EXPERTS // ep_size, I_FULL // tp_size
params_dict = {
"model.layers.0.mlp.experts.w13_weight": torch.nn.Parameter(
torch.full((local, 2 * i_tp, H), float("nan")), requires_grad=False
),
"model.layers.0.mlp.experts.w2_weight": torch.nn.Parameter(
torch.full((local, H, i_tp), float("nan")), requires_grad=False
),
}
loaded = set()
for (e, proj), w in full.items():
name = f"model.layers.0.mlp.experts.{e}.{proj}.weight"
self.assertTrue(model._load_per_expert_param(params_dict, loaded, name, w))
contiguous = lora_layout or not interleaved
exp_w13, exp_w2 = _expected_stacks(
full, ep_size, ep_rank, tp_size, tp_rank, contiguous
)
got_w13 = params_dict["model.layers.0.mlp.experts.w13_weight"].data
got_w2 = params_dict["model.layers.0.mlp.experts.w2_weight"].data
self.assertFalse(torch.isnan(got_w13).any(), "unwritten w13 slots")
self.assertFalse(torch.isnan(got_w2).any(), "unwritten w2 slots")
torch.testing.assert_close(got_w13, exp_w13, rtol=0, atol=0)
torch.testing.assert_close(got_w2, exp_w2, rtol=0, atol=0)
self.assertEqual(loaded, set(params_dict))
def test_ep1_tp1_interleaved(self):
# the validated RL rollout config (weight-checker <=1e-6 on 4layer + 951B)
self._run_rank(_full_weights(), 1, 0, 1, 0, interleaved=True, lora_layout=False)
def test_ep_tp_grid_interleaved(self):
full = _full_weights(1)
for ep_rank in range(4):
for tp_rank in range(2):
self._run_rank(
full, 4, ep_rank, 2, tp_rank, interleaved=True, lora_layout=False
)
def test_ep_tp_grid_contiguous_layouts(self):
full = _full_weights(2)
# contiguous via the LoRA-serving layout and via a non-interleaved config
for interleaved, lora_layout in ((True, True), (False, False)):
for ep_rank in range(2):
self._run_rank(
full,
2,
ep_rank,
2,
1,
interleaved=interleaved,
lora_layout=lora_layout,
)
def test_trtllm_layout_rejected(self):
self._patch(1, 0, 0)
moe = types.SimpleNamespace(use_flashinfer_trtllm_moe=True)
model = _FakeModel(True, moe=moe)
params_dict = {
"model.layers.0.mlp.experts.w13_weight": torch.nn.Parameter(
torch.zeros(N_EXPERTS, 2 * I_FULL, H), requires_grad=False
)
}
with self.assertRaises(NotImplementedError):
model._load_per_expert_param(
params_dict,
set(),
"model.layers.0.mlp.experts.0.gate_proj.weight",
torch.zeros(I_FULL, H),
)
if __name__ == "__main__":
unittest.main()