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,93 @@
"""fused_moe_preprocess must be bit-identical to the torch.sort-based path,
and the grouped GEMM must produce identical results under both block_size_m
configs (the block schedule and kernel config are chosen together).
"""
import pytest
import torch
from sglang.srt.layers.moe.moe_runner.triton_utils.inkling_moe import (
SMALL_M_BLOCK_SIZE_M,
compute_grouped_gemm_metadata,
fused_moe_preprocess,
get_src2dst,
grouped_gemm_triton,
)
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA only")
E = 256
TOPK = 6
def _reference(topk_ids_flat: torch.Tensor):
reorder_topk_ids, reorder_ids = torch.sort(
topk_ids_flat.to(torch.int16), stable=True
)
src2dst = get_src2dst(reorder_ids)
meta = compute_grouped_gemm_metadata(
reorder_topk_ids, E, block_size_m=SMALL_M_BLOCK_SIZE_M
)
return (src2dst, *meta, reorder_topk_ids)
def _ids(tokens: int, seed: int, skew: bool = False) -> torch.Tensor:
torch.manual_seed(seed)
if skew: # all tokens on few experts (stresses multi-block experts)
return torch.randint(0, 3, (tokens * TOPK,), dtype=torch.int32, device="cuda")
return (
torch.stack([torch.randperm(E, device="cuda")[:TOPK] for _ in range(tokens)])
.view(-1)
.to(torch.int32)
)
@requires_cuda
@pytest.mark.parametrize("tokens", [1, 2, 7, 32, 64, 170, 341]) # n = 6*T <= 2048
@pytest.mark.parametrize("skew", [False, True])
def test_matches_sort_path(tokens: int, skew: bool):
ids = _ids(tokens, seed=tokens, skew=skew)
ref = _reference(ids)
got = fused_moe_preprocess(ids, E)
names = [
"src2dst",
"num_tokens_per_expert",
"expert_token_offs",
"expert_block_offs",
"expert_block_schedule",
"reorder_topk_ids",
]
for tag, g, r in zip(names, got, ref):
assert g.shape == r.shape, (tag, g.shape, r.shape)
assert torch.equal(g.long(), r.long()), (
tag,
g[: min(16, g.numel())],
r[: min(16, r.numel())],
)
@requires_cuda
@pytest.mark.parametrize("tokens", [1, 16, 64])
def test_grouped_gemm_small_config_matches(tokens: int):
"""GEMM output must be identical whichever (block_size_m, config) runs."""
torch.manual_seed(tokens)
ids = _ids(tokens, seed=tokens)
m, k, n = tokens * TOPK, 768, 1024
a = (torch.randn(m, k, device="cuda") * 0.05).to(torch.bfloat16)
b = (torch.randn(E, n, k, device="cuda") * 0.02).to(torch.bfloat16)
sorted_ids, _ = torch.sort(ids.to(torch.int16), stable=True)
meta128 = compute_grouped_gemm_metadata(sorted_ids, E)
out128 = grouped_gemm_triton(a, b, E, *meta128)
pre = fused_moe_preprocess(ids, E)
out16 = grouped_gemm_triton(a, b, E, *pre[1:5], block_size_m=SMALL_M_BLOCK_SIZE_M)
# both are fp32-accumulated bf16 tensor-core dots; BLOCK_K differs so
# accumulation grouping may differ by a few ulp
torch.testing.assert_close(out16.float(), out128.float(), atol=1e-3, rtol=1e-3)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-x"]))
@@ -0,0 +1,70 @@
"""fused_decode_sconv_metadata must be bit-identical to the unfused prep.
The unfused reference is the exact op sequence `_prepare_decode_sconv_metadata`
used to launch: two arange calls + ones + precompute_helion_decode_metadata
(!= PAD, &, clamp, long, arange x2).
"""
import pytest
import torch
from sglang.srt.models.inkling_common.kernels.sconv import (
PAD_SLOT_ID,
fused_decode_sconv_metadata,
precompute_helion_decode_metadata,
)
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA only")
# cross the BLOCK=1024 grid boundary and hit odd sizes
BATCH_SIZES = [1, 2, 3, 17, 64, 160, 257, 1023, 1024, 1025]
def _reference(B: int, cache_indices: torch.Tensor):
device = cache_indices.device
query_start_loc = torch.arange(B + 1, dtype=torch.int32, device=device)
has_initial_state = torch.ones(B, dtype=torch.bool, device=device)
precomputed = precompute_helion_decode_metadata(
B=B, W=4, cache_indices=cache_indices, has_initial_state=has_initial_state
)
return query_start_loc, has_initial_state, precomputed
@requires_cuda
@pytest.mark.parametrize("b", BATCH_SIZES)
@pytest.mark.parametrize("idx_dtype", [torch.int32, torch.int64])
def test_matches_unfused(b: int, idx_dtype: torch.dtype):
torch.manual_seed(b)
cache_indices = torch.randint(0, 4096, (b,), dtype=idx_dtype, device="cuda")
# sprinkle PAD slots (cudagraph padding lanes)
pad = torch.rand(b, device="cuda") < 0.25
cache_indices[pad] = PAD_SLOT_ID
ref_qsl, ref_his, ref_meta = _reference(b, cache_indices)
qsl, his, meta = fused_decode_sconv_metadata(B=b, cache_indices=cache_indices)
for tag, got, ref in (
("query_start_loc", qsl, ref_qsl),
("has_initial_state", his, ref_his),
("cache_mask", meta["cache_mask"], ref_meta["cache_mask"]),
("safe_idx", meta["safe_idx"], ref_meta["safe_idx"]),
("cu", meta["cu"], ref_meta["cu"]),
("si", meta["si"], ref_meta["si"]),
):
assert got.dtype == ref.dtype, (tag, got.dtype, ref.dtype)
assert got.shape == ref.shape, (tag, got.shape, ref.shape)
assert torch.equal(got, ref), tag
@requires_cuda
def test_all_pad():
cache_indices = torch.full((8,), PAD_SLOT_ID, dtype=torch.int32, device="cuda")
_, _, meta = fused_decode_sconv_metadata(B=8, cache_indices=cache_indices)
assert not meta["cache_mask"].any()
assert (meta["safe_idx"] == 0).all()
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-x"]))
@@ -0,0 +1,174 @@
"""fused_extend_sconv_metadata must be bit-identical to the unfused prep.
The unfused reference is the exact op sequence _prepare_extend_common_metadata
+ precompute_helion_extend_metadata used to launch: zeros + cumsum + slice-copy
(or arange + ones for verify) + the has_initial_state compare, then != PAD, &,
clamp, long, to(int64), arange, searchsorted, clamp, to(int32).
"""
import pytest
import torch
from sglang.srt.models.inkling_common.kernels.sconv import (
HIS_ONES,
HIS_PREFIX,
HIS_SEQ_MINUS_EXT,
HIS_ZEROS,
PAD_SLOT_ID,
fused_extend_sconv_metadata,
precompute_helion_extend_metadata,
)
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA only")
# cross si tiles (BLOCK_T=256) and the single-tile B bound
BATCH_SIZES = [1, 2, 7, 64, 257, 1023]
def _ref_extend(B, extend_seq_lens, his_mode, his_src, cache_indices, T):
device = cache_indices.device
query_start_loc = torch.zeros(B + 1, dtype=torch.int32, device=device)
query_start_loc[1:] = extend_seq_lens.cumsum(dim=0)
if his_mode == HIS_ZEROS:
has_initial_state = torch.zeros(B, dtype=torch.bool, device=device)
elif his_mode == HIS_PREFIX:
has_initial_state = his_src > 0
else: # HIS_SEQ_MINUS_EXT
has_initial_state = (his_src[:B] - extend_seq_lens) > 0
meta = precompute_helion_extend_metadata(
B=B,
T=T,
W=4,
cache_indices=cache_indices,
has_initial_state=has_initial_state,
query_start_loc=query_start_loc,
)
return query_start_loc, has_initial_state, meta
def _ref_verify(B, draft_token_num, cache_indices):
device = cache_indices.device
query_start_loc = torch.arange(
0, (B + 1) * draft_token_num, draft_token_num, dtype=torch.int32, device=device
)
has_initial_state = torch.ones(B, dtype=torch.bool, device=device)
meta = precompute_helion_extend_metadata(
B=B,
T=B * draft_token_num,
W=4,
cache_indices=cache_indices,
has_initial_state=has_initial_state,
query_start_loc=query_start_loc,
)
return query_start_loc, has_initial_state, meta
def _assert_equal(got, ref):
for tag, g, r in (
("query_start_loc", got[0], ref[0]),
("has_initial_state", got[1], ref[1]),
("cache_mask", got[2]["cache_mask"], ref[2]["cache_mask"]),
("safe_idx", got[2]["safe_idx"], ref[2]["safe_idx"]),
("cu", got[2]["cu"], ref[2]["cu"]),
("si", got[2]["si"], ref[2]["si"]),
):
assert g.dtype == r.dtype, (tag, g.dtype, r.dtype)
assert g.shape == r.shape, (tag, g.shape, r.shape)
assert torch.equal(g, r), tag
def _cache_indices(b, idx_dtype):
ci = torch.randint(0, 4096, (b,), dtype=idx_dtype, device="cuda")
pad = torch.rand(b, device="cuda") < 0.25
ci[pad] = PAD_SLOT_ID
return ci
@requires_cuda
@pytest.mark.parametrize("b", BATCH_SIZES)
@pytest.mark.parametrize("his_mode", [HIS_ZEROS, HIS_PREFIX, HIS_SEQ_MINUS_EXT])
@pytest.mark.parametrize("lens_dtype", [torch.int32, torch.int64])
def test_extend_matches_unfused(b, his_mode, lens_dtype):
torch.manual_seed(b * 10 + his_mode)
lens = torch.randint(0, 33, (b,), dtype=lens_dtype, device="cuda")
lens[torch.rand(b, device="cuda") < 0.2] = 0 # zero-length sequences
T = int(lens.sum().item())
cache_indices = _cache_indices(b, torch.int32)
if his_mode == HIS_PREFIX:
his_src = torch.randint(0, 3, (b,), dtype=lens_dtype, device="cuda")
elif his_mode == HIS_SEQ_MINUS_EXT:
his_src = lens + torch.randint(0, 2, (b,), dtype=lens_dtype, device="cuda")
else:
his_src = None
ref = _ref_extend(b, lens, his_mode, his_src, cache_indices, T)
got = fused_extend_sconv_metadata(
B=b,
T=T,
cache_indices=cache_indices,
his_mode=his_mode,
extend_seq_lens=lens,
his_src=his_src,
)
assert got is not None
_assert_equal(got, ref)
@requires_cuda
@pytest.mark.parametrize("b", BATCH_SIZES)
@pytest.mark.parametrize("draft_token_num", [1, 9])
def test_verify_matches_unfused(b, draft_token_num):
torch.manual_seed(b)
cache_indices = _cache_indices(b, torch.int64)
ref = _ref_verify(b, draft_token_num, cache_indices)
got = fused_extend_sconv_metadata(
B=b,
T=b * draft_token_num,
cache_indices=cache_indices,
his_mode=HIS_ONES,
draft_token_num=draft_token_num,
)
assert got is not None
_assert_equal(got, ref)
@requires_cuda
def test_cu_not_spanning_T():
"""Dummy capture sequences: cu stops short of T; trailing si rows clamp to
B-1 exactly like the reference's searchsorted + clamp."""
b = 5
lens = torch.tensor([3, 0, 4, 0, 2], dtype=torch.int64, device="cuda")
T = int(lens.sum().item()) + 17
cache_indices = _cache_indices(b, torch.int32)
seq_lens = lens + 1
ref = _ref_extend(b, lens, HIS_SEQ_MINUS_EXT, seq_lens, cache_indices, T)
got = fused_extend_sconv_metadata(
B=b,
T=T,
cache_indices=cache_indices,
his_mode=HIS_SEQ_MINUS_EXT,
extend_seq_lens=lens,
his_src=seq_lens,
)
assert got is not None
_assert_equal(got, ref)
@requires_cuda
def test_fallback_past_batch_bound():
b = 1024 # > _FUSED_EXTEND_MAX_B
lens = torch.ones(b, dtype=torch.int64, device="cuda")
got = fused_extend_sconv_metadata(
B=b,
T=b,
cache_indices=_cache_indices(b, torch.int32),
his_mode=HIS_ZEROS,
extend_seq_lens=lens,
)
assert got is None
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-x"]))