support qwen 3.8 flash next (#37500)

Co-authored-by: ch-wan <54331508+ch-wan@users.noreply.github.com>
Co-authored-by: ispobock <26454835+ispobock@users.noreply.github.com>
Co-authored-by: JustinTong0323 <115166877+JustinTong0323@users.noreply.github.com>
Co-authored-by: samuellees <26428561+samuellees@users.noreply.github.com>
Co-authored-by: YAMY1234 <74099316+YAMY1234@users.noreply.github.com>
Co-authored-by: yhyang201 <47235274+yhyang201@users.noreply.github.com>
Co-authored-by: yizhang2077 <25844240+yizhang2077@users.noreply.github.com>
Co-authored-by: zijiexia <37504505+zijiexia@users.noreply.github.com>
Co-authored-by: Shinto C V <cshintov@gmail.com>
Co-authored-by: Julian Huang <huangzhilin.hzl@antgroup.com>
Co-authored-by: Xiaoyu Zhang <1182563586@qq.com>
Co-authored-by: yhyang201 <yhyang201@gmail.com>
This commit is contained in:
Qiaolin Yu
2026-09-08 13:56:21 -07:00
committed by GitHub
co-authored by ch-wan ispobock JustinTong0323 samuellees YAMY1234 yhyang201 yizhang2077 zijiexia Shinto C V Julian Huang Xiaoyu Zhang yhyang201
parent afe90a8bc9
commit 52fecfdf09
91 changed files with 16418 additions and 79 deletions
@@ -0,0 +1,90 @@
"""Qwen3.8-Flash-Next (Qwen4-Exp) E2E on B200; the plain-serving case is kept:
MTP's verify widths never exercise the QSA sparse-decode path."""
import unittest
from sglang.srt.utils import kill_process_tree
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.kits.eval_accuracy_kit import GSM8KMixin
from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST,
CustomTestCase,
popen_launch_server,
try_cached_model,
)
register_cuda_ci(est_time=1500, stage="base-c", runner_config="4-gpu-b200")
MODEL = "RadixArk/Qwen3.8-Flash-Next-NVFP4"
SERVER_LAUNCH_TIMEOUT = 3600
GSM8K_SCORE_THRESHOLD = 0.94
BASE_ARGS = [
"--tp-size",
"4",
"--mem-fraction-static",
"0.85",
"--chunked-prefill-size",
"8192",
"--linear-attn-prefill-backend",
"flashinfer",
"--linear-attn-decode-backend",
"flashinfer",
"--mamba-ssm-dtype",
"bfloat16",
"--reasoning-parser",
"qwen3-thinking",
]
class _Qwen4ExpServer:
speculative_args: list[str] = []
model = try_cached_model(MODEL)
base_url = DEFAULT_URL_FOR_TEST
gsm8k_backend = "sgl_eval"
gsm8k_thinking = True
gsm8k_num_examples = 200
gsm8k_num_threads = 32
gsm8k_max_tokens = 16384
gsm8k_score_threshold = GSM8K_SCORE_THRESHOLD
@classmethod
def setUpClass(cls):
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=SERVER_LAUNCH_TIMEOUT,
other_args=BASE_ARGS + cls.speculative_args,
)
@classmethod
def tearDownClass(cls):
if hasattr(cls, "process") and cls.process:
kill_process_tree(cls.process.pid)
class TestQwen4ExpBase(_Qwen4ExpServer, GSM8KMixin, CustomTestCase):
"""Normal autoregressive serving."""
class TestQwen4ExpMTP(_Qwen4ExpServer, GSM8KMixin, CustomTestCase):
"""NEXTN MTP serving (3 steps, topk 1, 4 draft tokens)."""
# GSM8K accept length measured at 3.02-3.03 (max 4.0 with 3 steps);
# 2.9 leaves noise margin while still failing on a real drop.
gsm8k_accept_length_thres = 2.9
speculative_args = [
"--speculative-algorithm",
"NEXTN",
"--speculative-num-steps",
"3",
"--speculative-eagle-topk",
"1",
"--speculative-num-draft-tokens",
"4",
]
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,195 @@
from types import SimpleNamespace
import pytest
import torch
from torch import nn
from sglang.srt.layers.quantization.unquant import UnquantizedEmbeddingMethod
from sglang.srt.layers.vocab_parallel_embedding import (
VocabParallelEmbeddingShardIndices,
)
from sglang.srt.models import qwen4_exp as qwen4_exp_module
from sglang.srt.models.qwen4_exp import (
Qwen4ExpPinnedHostEmbedding,
Qwen4ExpPLELayer,
)
from sglang.srt.utils import set_weight_attrs
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="1-gpu-large")
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available(), reason="CUDA is required for this test."
)
def _make_source_embedding(
*,
dtype=torch.bfloat16,
embedding_dim=7,
vocab_start=0,
vocab_end=8,
org_vocab_size=8,
tp_size=1,
num_added_embeddings=0,
):
local_rows = vocab_end - vocab_start
weight = nn.Parameter(
torch.empty((local_rows, embedding_dim), dtype=dtype, device="cuda"),
requires_grad=False,
)
set_weight_attrs(
weight,
{
"input_dim": 1,
"output_dim": 0,
"weight_loader": lambda *_args, **_kwargs: None,
},
)
shard_indices = VocabParallelEmbeddingShardIndices(
padded_org_vocab_start_index=vocab_start,
padded_org_vocab_end_index=vocab_end,
padded_added_vocab_start_index=org_vocab_size,
padded_added_vocab_end_index=org_vocab_size,
org_vocab_start_index=vocab_start,
org_vocab_end_index=vocab_end,
added_vocab_start_index=org_vocab_size,
added_vocab_end_index=org_vocab_size,
)
return SimpleNamespace(
weight=weight,
quant_config=None,
enable_tp=True,
use_attn_tp_group=False,
tp_size=tp_size,
num_embeddings=org_vocab_size + num_added_embeddings,
org_vocab_size=org_vocab_size,
padding_size=1,
num_added_embeddings=num_added_embeddings,
use_presharded_weights=False,
org_vocab_size_padded=org_vocab_size,
num_embeddings_padded=org_vocab_size + num_added_embeddings,
shard_indices=shard_indices,
embedding_dim=embedding_dim,
weight_scale=None,
quant_method=UnquantizedEmbeddingMethod(),
num_embeddings_per_partition=local_rows,
num_org_embeddings_per_partition=local_rows,
num_added_embeddings_per_partition=0,
)
def _load_rows(offloaded, rows):
pointer = offloaded.weight.data_ptr()
offloaded.weight_loader(offloaded.weight, rows)
assert offloaded.weight.data_ptr() == pointer
assert offloaded.weight.is_pinned()
assert offloaded.weight.weight_loader.__self__ is offloaded
assert offloaded.quant_method is None
@pytest.mark.parametrize("input_dtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("embedding_dim", [7, 64, 257])
def test_qwen4_ple_pinned_gather_tp1(input_dtype, embedding_dim):
source = _make_source_embedding(embedding_dim=embedding_dim)
offloaded = Qwen4ExpPinnedHostEmbedding(source)
rows = torch.arange(8 * embedding_dim, dtype=torch.bfloat16, device="cuda").reshape(
8, embedding_dim
)
_load_rows(offloaded, rows)
ids = torch.tensor([[0, 7, 3], [4, 1, 6]], dtype=input_dtype, device="cuda")
expected = rows.index_select(0, ids.long().flatten()).reshape(
*ids.shape, embedding_dim
)
actual = offloaded(ids)
assert actual.shape == expected.shape
assert actual.is_contiguous()
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
def test_qwen4_ple_pinned_gather_shard_boundaries_and_out_buffer():
embedding_dim = 13
source = _make_source_embedding(
embedding_dim=embedding_dim,
vocab_start=4,
vocab_end=8,
org_vocab_size=8,
tp_size=2,
)
offloaded = Qwen4ExpPinnedHostEmbedding(source)
rows = torch.arange(8 * embedding_dim, dtype=torch.bfloat16, device="cuda").reshape(
8, embedding_dim
)
_load_rows(offloaded, rows)
ids = torch.tensor([[-1, 3, 4], [7, 8, 100]], device="cuda")
output = torch.full(
(*ids.shape, embedding_dim),
torch.nan,
dtype=torch.bfloat16,
device="cuda",
)
actual = offloaded.gather(ids, out=output)
expected = torch.zeros_like(output)
expected[0, 2] = rows[4]
expected[1, 0] = rows[7]
assert actual.data_ptr() == output.data_ptr()
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
def test_qwen4_ple_pinned_gather_empty_input():
offloaded = Qwen4ExpPinnedHostEmbedding(_make_source_embedding())
_load_rows(offloaded, torch.zeros((8, 7), dtype=torch.bfloat16, device="cuda"))
ids = torch.empty((0, 3), dtype=torch.int64, device="cuda")
actual = offloaded.gather(ids)
assert actual.shape == (0, 3, 7)
assert actual.numel() == 0
def test_qwen4_ple_pinned_embedding_rejects_unsupported_weights():
with pytest.raises(TypeError, match="requires bfloat16"):
Qwen4ExpPinnedHostEmbedding(_make_source_embedding(dtype=torch.float16))
with pytest.raises(NotImplementedError, match="added vocabulary"):
Qwen4ExpPinnedHostEmbedding(_make_source_embedding(num_added_embeddings=1))
def test_qwen4_ple_prefetch_buffer_lifecycle(monkeypatch):
layer = Qwen4ExpPLELayer.__new__(Qwen4ExpPLELayer)
nn.Module.__init__(layer)
layer.ple_embed_dim = 7
layer.ple_embedding = SimpleNamespace(
ngram_embedding=Qwen4ExpPinnedHostEmbedding(
_make_source_embedding(embedding_dim=layer.ple_embed_dim)
)
)
layer._graph_prefetch_buffers = {}
layer._eager_prefetch_buffer = None
lookup_ids = torch.empty((0,), dtype=torch.int64, device="cuda")
monkeypatch.setattr(qwen4_exp_module, "get_is_capture_mode", lambda: False)
eager_large = layer._get_prefetch_buffer(8, lookup_ids)
eager_small = layer._get_prefetch_buffer(3, lookup_ids)
assert eager_small.data_ptr() == eager_large.data_ptr()
assert layer._eager_prefetch_buffer.shape == (8, layer.ple_embed_dim)
eager_grown = layer._get_prefetch_buffer(12, lookup_ids)
eager_grown_small = layer._get_prefetch_buffer(4, lookup_ids)
assert eager_grown_small.data_ptr() == eager_grown.data_ptr()
assert layer._eager_prefetch_buffer.shape == (12, layer.ple_embed_dim)
monkeypatch.setattr(qwen4_exp_module, "get_is_capture_mode", lambda: True)
graph_three = layer._get_prefetch_buffer(3, lookup_ids)
graph_five = layer._get_prefetch_buffer(5, lookup_ids)
graph_three_reused = layer._get_prefetch_buffer(3, lookup_ids)
assert graph_three_reused.data_ptr() == graph_three.data_ptr()
assert graph_five.data_ptr() != graph_three.data_ptr()
assert set(layer._graph_prefetch_buffers) == {3, 5}
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,84 @@
import sys
import pytest
import torch
import torch.nn.functional as F
from sglang.srt.layers.hc_mix_triton import (
_FUSED_MIX_MAX_ROWS,
fused_hc_mix,
fused_hc_mix_supported,
)
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=30, stage="base-b-kernel-unit", runner_config="4-gpu-b200")
HC_COUNT = 4
HIDDEN_SIZE = 2560
LOWRANK = 320
def _reference_mix(
hyper_input_normed: torch.Tensor,
w_down: torch.Tensor,
w_up: torch.Tensor,
hc: int,
hs: int,
compute_dtype: torch.dtype = torch.float64,
) -> torch.Tensor:
"""Mirrors GatedResidual._mix_compute in hyperconnection.py."""
x = hyper_input_normed.to(compute_dtype)
t = F.silu(F.linear(x, w_down.to(compute_dtype)) / hc)
u = torch.sigmoid(F.linear(t, w_up.to(compute_dtype)))
return (u.unflatten(-1, (hc, hs)) * x.unflatten(-1, (hc, hs))).mean(dim=-2)
def _make_inputs(num_tokens: int, dtype: torch.dtype):
torch.manual_seed(0)
x = torch.randn(num_tokens, HC_COUNT * HIDDEN_SIZE, dtype=dtype, device="cuda")
w_down = (
torch.randn(LOWRANK, HC_COUNT * HIDDEN_SIZE, dtype=dtype, device="cuda") * 0.02
)
w_up = (
torch.randn(HC_COUNT * HIDDEN_SIZE, LOWRANK, dtype=dtype, device="cuda") * 0.02
)
return x, w_down, w_up
_TOLERANCES = {
torch.bfloat16: dict(rtol=1e-2, atol=5e-3),
torch.float16: dict(rtol=2e-3, atol=1e-3),
}
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize("num_tokens", [1, 4, 7, _FUSED_MIX_MAX_ROWS])
def test_fused_hc_mix_matches_reference(dtype, num_tokens):
x, w_down, w_up = _make_inputs(num_tokens, dtype)
assert fused_hc_mix_supported(x, w_down, w_up)
out = fused_hc_mix(x, w_down, w_up, HC_COUNT, HIDDEN_SIZE)
ref = _reference_mix(x, w_down, w_up, HC_COUNT, HIDDEN_SIZE)
torch.testing.assert_close(out.to(torch.float64), ref, **_TOLERANCES[dtype])
def test_fused_hc_mix_no_less_accurate_than_eager():
"""The fused kernel (fp32 accumulation throughout) must not be farther
from the fp64 reference than the eager bf16 chain it replaces."""
x, w_down, w_up = _make_inputs(8, torch.bfloat16)
ref = _reference_mix(x, w_down, w_up, HC_COUNT, HIDDEN_SIZE)
fused = fused_hc_mix(x, w_down, w_up, HC_COUNT, HIDDEN_SIZE)
eager = _reference_mix(
x, w_down, w_up, HC_COUNT, HIDDEN_SIZE, compute_dtype=torch.bfloat16
)
fused_err = (fused.to(torch.float64) - ref).abs().max()
eager_err = (eager.to(torch.float64) - ref).abs().max()
assert fused_err <= eager_err * 1.5 + 1e-6
def test_fused_hc_mix_gate_rejects_prefill_rows():
x, w_down, w_up = _make_inputs(_FUSED_MIX_MAX_ROWS + 1, torch.bfloat16)
assert not fused_hc_mix_supported(x, w_down, w_up)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -0,0 +1,141 @@
import pytest
import torch
from sglang.kernels.ops.elementwise.fast_topk import fast_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 _check_topk_values(score, lengths, indices, topk, row_starts):
"""fast_topk leaves order and tie-breaking unspecified,
so compare the sorted top-k values rather than index sets."""
for b in range(score.shape[0]):
start = int(row_starts[b]) if row_starts is not None else 0
length = int(lengths[b])
section = score[b, start : start + length]
row = indices[b]
if length <= topk:
# naive path: identity indices, then -1 fill
assert torch.equal(
row[:length].cpu(), torch.arange(length, dtype=torch.int32)
)
assert (row[length:] == -1).all()
continue
assert (row >= 0).all(), "long rows must fill every slot"
picked = section[row.long()]
expected = torch.topk(section, topk).values
assert torch.equal(
picked.sort(descending=True).values, expected.sort(descending=True).values
), f"row {b}: top-{topk} value multiset mismatch"
@pytest.mark.parametrize("topk", [512, 2048])
@pytest.mark.parametrize(
"batch,length",
[
(1, 4096),
(7, 3000),
(33, 32768),
(128, 2050),
],
)
def test_fast_topk_long_rows(topk, batch, length):
torch.manual_seed(0)
score = torch.randn(batch, length, dtype=torch.float32, device="cuda")
lengths = torch.full((batch,), length, dtype=torch.int32, device="cuda")
indices = fast_topk(score, lengths, topk)
_check_topk_values(score, lengths, indices, topk, None)
@pytest.mark.parametrize("topk", [512, 2048])
def test_fast_topk_short_and_mixed_rows(topk):
torch.manual_seed(0)
max_len = topk + 128
batch = 8
score = torch.randn(batch, max_len, dtype=torch.float32, device="cuda")
# rows shorter than k (naive path), exactly k, and longer than k
lens = [1, topk // 3, topk - 1, topk, topk + 1, topk + 7, 17, max_len]
lengths = torch.tensor(lens[:batch], dtype=torch.int32, device="cuda")
indices = fast_topk(score, lengths, topk)
_check_topk_values(score, lengths, indices, topk, None)
@pytest.mark.parametrize("topk", [512, 2048])
def test_fast_topk_ragged_with_row_starts(topk):
torch.manual_seed(0)
batch, width = 16, 8192
score = torch.randn(batch, width, dtype=torch.float32, device="cuda")
row_starts = torch.randint(0, 2048, (batch,), dtype=torch.int32, device="cuda")
lengths = torch.randint(1, 2048, (batch,), dtype=torch.int32, device="cuda")
lengths = torch.minimum(lengths, width - row_starts).to(torch.int32)
# ensure some rows are longer than k
lengths[0] = min(width - int(row_starts[0]), topk + 100)
indices = fast_topk(score, lengths, topk, row_starts=row_starts)
_check_topk_values(score, lengths, indices, topk, row_starts)
@pytest.mark.parametrize("topk", [512, 2048])
def test_fast_topk_row_stride(topk):
torch.manual_seed(0)
batch, length = 8, 4096
base = torch.randn(batch, 2 * length, dtype=torch.float32, device="cuda")
score = base[:, :length] # stride(0) == 2*length, stride(1) == 1
lengths = torch.full((batch,), length, dtype=torch.int32, device="cuda")
indices = fast_topk(score, lengths, topk)
_check_topk_values(score, lengths, indices, topk, None)
@pytest.mark.parametrize("topk", [512, 2048])
@pytest.mark.parametrize(
"fill",
[
"binary", # only 0s and 1s: extreme duplication at the threshold bin
"few_levels", # a handful of distinct levels incl. negatives
"constant", # whole rows of one value
],
)
def test_fast_topk_duplicate_heavy(topk, fill):
torch.manual_seed(0)
batch, length = 16, 8192
if fill == "binary":
score = torch.randint(0, 2, (batch, length), dtype=torch.float32, device="cuda")
elif fill == "few_levels":
levels = torch.tensor([-5.0, -1.0, 0.0, 0.5, 2.0], device="cuda")
score = levels[torch.randint(0, 5, (batch, length), device="cuda")]
else:
score = torch.full((batch, length), 3.25, dtype=torch.float32, device="cuda")
lengths = torch.full((batch,), length, dtype=torch.int32, device="cuda")
indices = fast_topk(score, lengths, topk)
_check_topk_values(score, lengths, indices, topk, None)
@pytest.mark.parametrize("topk", [512, 2048])
def test_fast_topk_negative_and_zero(topk):
torch.manual_seed(0)
batch, length = 8, 16384
score = torch.randn(batch, length, dtype=torch.float32, device="cuda") * 100
score[:, : length // 3] = 0.0 # long zero prefix
score[:, length // 3 : length // 2] = -1e30 # very negative block
lengths = torch.full((batch,), length, dtype=torch.int32, device="cuda")
indices = fast_topk(score, lengths, topk)
_check_topk_values(score, lengths, indices, topk, None)
def test_fast_topk_unsupported_k():
score = torch.randn(2, 4096, dtype=torch.float32, device="cuda")
lengths = torch.full((2,), 4096, dtype=torch.int32, device="cuda")
with pytest.raises(RuntimeError, match="topk"):
fast_topk(score, lengths, 1024)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,87 @@
import pytest
import torch
from sglang.kernels.ops.layernorm.grouped_gemma_rmsnorm import grouped_gemma_rmsnorm
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 _reference_grouped_gemma_rmsnorm(
x: torch.Tensor,
weight: torch.Tensor,
group_size: int,
eps: float,
compute_dtype: torch.dtype = torch.float32,
) -> torch.Tensor:
"""Mirrors GroupedGemmaRMSNorm.forward (hyperconnection.py); keep them in sync."""
x_float = x.to(compute_dtype)
hidden = x_float.shape[-1]
x_grouped = x_float.reshape(*x_float.shape[:-1], hidden // group_size, group_size)
variance = x_grouped.pow(2).mean(dim=-1, keepdim=True)
x_norm = (x_grouped * torch.rsqrt(variance + eps)).flatten(-2)
return x_norm * (1.0 + weight.to(compute_dtype))
# Tolerances are at the output-dtype quantization floor, measured against the
# fp64 reference on 4xB300 (sm103): bf16 max rel err 3.9e-3 (1 ulp), fp16
# 4.9e-4 (0.5 ulp). The kernel computes in fp32 like the eager reference.
_TOLERANCES = {
torch.bfloat16: dict(rtol=5e-3, atol=5e-3),
torch.float16: dict(rtol=1e-3, atol=1e-3),
}
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize(
"num_tokens,hidden_size,group_size",
[
(1, 10240, 2560), # production shape (HC 4 x 2560)
(7, 10240, 2560),
(128, 10240, 2560),
(33, 1024, 512),
(5, 512, 512), # single group == plain gemma rmsnorm
(1024, 2048, 1024),
],
)
@pytest.mark.parametrize("eps", [1e-6, 1e-5])
def test_grouped_gemma_rmsnorm_correctness(
dtype, num_tokens, hidden_size, group_size, eps
):
torch.manual_seed(0)
x = torch.randn(num_tokens, hidden_size, dtype=dtype, device="cuda")
weight = torch.randn(hidden_size, dtype=dtype, device="cuda") * 0.2
out = grouped_gemma_rmsnorm(x, weight, group_size, eps)
expected = _reference_grouped_gemma_rmsnorm(
x, weight, group_size, eps, compute_dtype=torch.float64
).to(dtype)
torch.testing.assert_close(out, expected, **_TOLERANCES[dtype])
def test_grouped_gemma_rmsnorm_out_param():
x = torch.randn(64, 10240, dtype=torch.bfloat16, device="cuda")
weight = torch.randn(10240, dtype=torch.bfloat16, device="cuda") * 0.2
out = torch.empty_like(x)
result = grouped_gemma_rmsnorm(x, weight, 2560, 1e-6, out=out)
expected = _reference_grouped_gemma_rmsnorm(
x, weight, 2560, 1e-6, compute_dtype=torch.float64
).to(x.dtype)
assert result.data_ptr() == out.data_ptr()
torch.testing.assert_close(result, expected, **_TOLERANCES[x.dtype])
def test_grouped_gemma_rmsnorm_bad_group_size():
x = torch.randn(4, 10240, dtype=torch.bfloat16, device="cuda")
weight = torch.zeros(10240, dtype=torch.bfloat16, device="cuda")
with pytest.raises(RuntimeError, match="group_size"):
grouped_gemma_rmsnorm(x, weight, 1000, 1e-6)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,161 @@
import pytest
import torch
import torch.nn.functional as F
from sglang.kernels.ops.elementwise.hc_combine import hc_combine
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")
HC_COUNT = 4
HIDDEN_SIZE = 2560
def _reference_hc_combine(
block_output: torch.Tensor,
residual: torch.Tensor,
normed_residual: torch.Tensor,
inject_weight: torch.Tensor,
hc: int,
hs: int,
compute_dtype: torch.dtype = torch.float32,
) -> torch.Tensor:
"""Eager reference mirroring ``GatedResidual._combine_compute``;
``compute_dtype=torch.float64`` is the near-exact reference."""
R = residual.to(compute_dtype).unflatten(-1, (hc, hs))
gates = 2 * torch.sigmoid(
F.linear(normed_residual.to(compute_dtype), inject_weight.to(compute_dtype))
/ hc
)
injection = block_output.to(compute_dtype).unsqueeze(-2) * gates.unsqueeze(-1)
return (R + injection).flatten(-2)
def _make_inputs(
num_tokens: int, dtype: torch.dtype, hc: int = HC_COUNT, hs: int = HIDDEN_SIZE
):
torch.manual_seed(0)
block_output = torch.randn(num_tokens, hs, dtype=dtype, device="cuda")
residual = torch.randn(num_tokens, hc * hs, dtype=dtype, device="cuda")
normed_residual = torch.randn(num_tokens, hc * hs, dtype=dtype, device="cuda")
inject_weight = torch.randn(hc, hc * hs, dtype=dtype, device="cuda") * 0.02
return block_output, residual, normed_residual, inject_weight
# Worst case over M in {1, 7, 128, 8192} against the fp64 reference on B300 (sm103):
# bf16 max rel err 7.8e-3 (1 ulp at a binade edge), fp16 below 1e-3 (1 ulp = 9.8e-4);
# the residual is fp32 reordering flipping the final rounding.
_TOLERANCES = {
torch.bfloat16: dict(rtol=1e-2, atol=5e-3),
torch.float16: dict(rtol=1e-3, atol=1e-3),
}
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize("num_tokens", [1, 7, 128, 8192])
def test_hc_combine_correctness(dtype, num_tokens):
block_output, residual, normed_residual, inject_weight = _make_inputs(
num_tokens, dtype
)
out = hc_combine(
block_output,
residual,
normed_residual,
inject_weight,
HC_COUNT,
HIDDEN_SIZE,
)
expected = _reference_hc_combine(
block_output,
residual,
normed_residual,
inject_weight,
HC_COUNT,
HIDDEN_SIZE,
compute_dtype=torch.float64,
).to(dtype)
torch.testing.assert_close(out, expected, **_TOLERANCES[dtype])
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
def test_hc_combine_out_param(dtype):
block_output, residual, normed_residual, inject_weight = _make_inputs(64, dtype)
out = torch.empty_like(residual)
result = hc_combine(
block_output,
residual,
normed_residual,
inject_weight,
HC_COUNT,
HIDDEN_SIZE,
out=out,
)
expected = _reference_hc_combine(
block_output,
residual,
normed_residual,
inject_weight,
HC_COUNT,
HIDDEN_SIZE,
compute_dtype=torch.float64,
).to(dtype)
assert result.data_ptr() == out.data_ptr()
torch.testing.assert_close(result, expected, **_TOLERANCES[dtype])
def test_hc_combine_3d_input():
dtype = torch.bfloat16
block_output, residual, normed_residual, inject_weight = _make_inputs(32, dtype)
block_output = block_output.reshape(4, 8, HIDDEN_SIZE)
residual = residual.reshape(4, 8, HC_COUNT * HIDDEN_SIZE)
normed_residual = normed_residual.reshape(4, 8, HC_COUNT * HIDDEN_SIZE)
out = hc_combine(
block_output,
residual,
normed_residual,
inject_weight,
HC_COUNT,
HIDDEN_SIZE,
)
expected = _reference_hc_combine(
block_output,
residual,
normed_residual,
inject_weight,
HC_COUNT,
HIDDEN_SIZE,
compute_dtype=torch.float64,
).to(dtype)
assert out.shape == residual.shape
torch.testing.assert_close(out, expected, **_TOLERANCES[dtype])
def test_hc_combine_bad_hidden_size():
dtype = torch.bfloat16
block_output, residual, normed_residual, inject_weight = _make_inputs(
4,
dtype,
hc=4,
hs=1000, # 4 * 1000 = 4000, not a multiple of 2048
)
with pytest.raises(RuntimeError, match="2048"):
hc_combine(
block_output,
residual,
normed_residual,
inject_weight,
4,
1000,
)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,273 @@
"""Fused QSA indexer-prep kernels must match the eager indexer path bit-for-bit,
up to rare last-ulp RMSNorm flips (see assert_bit_comparable)."""
from types import SimpleNamespace
import pytest
import torch
from sglang.test.ci.ci_register import register_cuda_ci
register_cuda_ci(est_time=120, stage="base-b-kernel-unit", runner_config="1-gpu-large")
from sglang.srt.layers.attention.qsa.kernel import (
average_pool_qsa_keys,
expand_qsa_block_indices,
torch_expand_qsa_block_indices,
)
from sglang.srt.layers.attention.qsa.qsa_indexer import QSAIndexer
from sglang.srt.layers.rotary_embedding.mrope import MRotaryEmbedding
# MRotaryEmbedding reads the exec config bag at init; publish a minimal
# process context for the bare pytest process.
from sglang.srt.runtime_context import publish
from sglang.srt.server_args import ServerArgs
publish(ServerArgs(model_path="dummy"), role="test")
HEAD_DIM = 128
NUM_Q_HEADS = 4
RATIO = 4
HIDDEN = 2560
EPS = 1e-6
def _make_config():
return SimpleNamespace(
indexer_n_heads=NUM_Q_HEADS,
indexer_kv_heads=1,
indexer_head_dim=HEAD_DIM,
indexer_budget=2048,
indexer_compress_ratio=RATIO,
hidden_size=HIDDEN,
rms_norm_eps=EPS,
)
def _make_rotary(mrope_section, mrope_interleaved, device, dtype=torch.bfloat16):
return MRotaryEmbedding(
head_size=HEAD_DIM,
rotary_dim=HEAD_DIM,
max_position_embeddings=32768,
base=1000000,
is_neox_style=True,
dtype=dtype,
mrope_section=mrope_section,
mrope_interleaved=mrope_interleaved,
)
def _make_indexer(rotary, device, dtype=torch.bfloat16):
# Build under the model dtype like ModelRunner does; device-only .to()
# afterwards so the fp32 cos_sin_cache buffer keeps its dtype.
prev_dtype = torch.get_default_dtype()
torch.set_default_dtype(dtype)
try:
indexer = QSAIndexer(
_make_config(), layer_id=0, quant_config=None, rotary_emb=rotary
)
indexer.to(device=device)
finally:
torch.set_default_dtype(prev_dtype)
with torch.no_grad():
out_features = (NUM_Q_HEADS + 1) * HEAD_DIM
indexer.index_qk_proj.weight.data.copy_(
torch.randn(out_features, HIDDEN, device=device, dtype=dtype) * 0.02
)
for norm in (indexer.q_layernorm, indexer.k_layernorm):
w = torch.randn(HEAD_DIM, device=device, dtype=dtype) * 0.1
norm._weight_loader(norm.weight, w)
return indexer
class FakePool:
"""Minimal stand-in for the QSA KV pool buffers used by the indexer."""
index_state_dtype = torch.bfloat16
def __init__(self, num_slots, num_compressed, device, dtype=torch.bfloat16):
self.key_state = torch.zeros(num_slots, 1, HEAD_DIM, dtype=dtype, device=device)
self.qsa_rope_position_buffer = torch.zeros(
num_slots, 3, dtype=torch.int64, device=device
)
self.compressed = torch.zeros(
num_compressed, 1, HEAD_DIM, dtype=dtype, device=device
)
def get_qsa_key_state_buffer(self, layer_id):
return self.key_state
def set_qsa_key_state_buffer(self, layer_id, loc, token_k):
self.key_state[loc.long()] = token_k.to(self.key_state.dtype)
def set_qsa_rope_position_buffer(self, loc, positions):
positions = positions.long()
if positions.ndim == 1:
positions = positions.unsqueeze(0).expand(3, -1)
self.qsa_rope_position_buffer[loc.long()] = positions.transpose(0, 1)
def get_qsa_rope_position_buffer(self, loc):
return self.qsa_rope_position_buffer[loc.long()]
def get_qsa_compressed_k_buffer(self, layer_id):
return self.compressed
def set_qsa_compressed_k_buffer(self, layer_id, loc, compressed_k):
self.compressed[loc.long()] = compressed_k.to(self.compressed.dtype)
def assert_bit_comparable(actual, expected, max_frac=1e-5, max_abs=0.02):
"""Eager RMSNorm (flashinfer CuTe DSL) reduces in an unreproducible order,
so ~1 row in 30k flips by 1-2 bf16 ulp; max_frac and max_abs bound that."""
diff = (actual.float() - expected.float()).abs()
mismatches = int((diff > 0).sum())
allowed = max(16, int(max_frac * actual.numel()))
assert mismatches <= allowed, f"{mismatches} mismatched elements"
if mismatches:
peak = diff.max().item()
assert peak <= max_abs, f"largest deviation {peak} exceeds {max_abs}"
def _eager_compress_reference(indexer, pool, group_locs, write_locs):
"""The pre-fusion compression chain, via the indexer's own helpers."""
key_groups = pool.get_qsa_key_state_buffer(0)[group_locs.long()]
pooled = average_pool_qsa_keys(key_groups)
rope_positions = indexer._rope_from_matrix(
pool.get_qsa_rope_position_buffer(group_locs[:, 0])
)
normalized = indexer.normalize_compressed_keys(pooled, rope_positions)
pool.set_qsa_compressed_k_buffer(0, write_locs, normalized)
@pytest.mark.parametrize("num_groups", [1, 5, 2000])
@pytest.mark.parametrize(
"mrope_section, mrope_interleaved",
[([24, 20, 20], True), ([24, 20, 20], False), (None, False)],
)
def test_fused_compress_matches_eager(num_groups, mrope_section, mrope_interleaved):
device = torch.device("cuda")
dtype = torch.bfloat16
torch.manual_seed(num_groups)
rotary = _make_rotary(mrope_section, mrope_interleaved, device, dtype)
indexer = _make_indexer(rotary, device, dtype)
pool_ref = FakePool(8192, 4096, device, dtype)
pool_new = FakePool(8192, 4096, device, dtype)
pool_new.key_state.copy_(
pool_ref.key_state.copy_(
torch.randn(8192, 1, HEAD_DIM, device=device, dtype=dtype)
)
)
positions = torch.randint(0, 30000, (8192, 3), device=device)
pool_new.qsa_rope_position_buffer.copy_(positions)
pool_ref.qsa_rope_position_buffer.copy_(positions)
# Random groups; slot 0 doubles as the CUDA-graph dummy write target, so
# allow repeats there too.
group_locs = torch.randint(0, 8192, (num_groups, RATIO), device=device).to(
torch.int32
)
write_locs = torch.randperm(4096, device=device)[:num_groups].to(torch.int32)
_eager_compress_reference(indexer, pool_ref, group_locs, write_locs)
indexer._fused_compress_store(pool_new, group_locs, write_locs)
assert_bit_comparable(pool_new.compressed, pool_ref.compressed)
@pytest.mark.parametrize("dtype", [torch.int32, torch.int64])
def test_expand_block_indices_int_inputs(dtype):
device = torch.device("cuda")
torch.manual_seed(0)
rows, block_topk, token_topk, ratio = 37, 512, 2048, 4
query_positions = torch.randint(0, 8000, (rows,), dtype=dtype, device=device)
sequence_lengths = (
query_positions + torch.randint(1, 9, (rows,), dtype=dtype, device=device)
).to(dtype)
# Production contract: top-k only selects blocks inside [0, seq_len//4),
# so no selected block ever masks out against sequence_lengths.
counts = torch.randint(0, block_topk + 1, (rows,))
block_indices = torch.full((rows, block_topk), -1, dtype=torch.int32)
seq_lens_host = sequence_lengths.cpu()
for r in range(rows):
limit = max(int(seq_lens_host[r]) // ratio, 1)
count = min(int(counts[r]), limit)
if count:
block_indices[r, :count] = torch.randperm(limit)[:count].to(torch.int32)
block_indices = block_indices.to(device)
out = expand_qsa_block_indices(
block_indices, query_positions, sequence_lengths, ratio, token_topk
)
ref = torch_expand_qsa_block_indices(
block_indices.cpu(),
query_positions.cpu(),
sequence_lengths.cpu(),
ratio,
token_topk,
)
assert torch.equal(out.cpu(), ref)
def test_decode_selection_equivalent():
"""Last-ulp norm flips must not change the selected blocks:
scores are fp32 sums of 128-dim dots, so a 1-ulp flip only matters on exact ties."""
from sglang.srt.layers.attention.qsa.kernel import qsa_fast_topk
from sglang.srt.layers.attention.qsa.mqa import torch_qsa_mqa_decode
device = torch.device("cuda")
dtype = torch.bfloat16
torch.manual_seed(7)
rotary = _make_rotary([24, 20, 20], True, device, dtype)
indexer = _make_indexer(rotary, device, dtype)
batch, max_pages, page_size = 4, 32, 64
max_model_len = max_pages * page_size
hidden = torch.randn(batch, HIDDEN, device=device, dtype=dtype)
positions = (
torch.arange(8000, 8000 + batch, device=device)
.unsqueeze(0)
.expand(3, -1)
.contiguous()
)
qk, _ = indexer.index_qk_proj(hidden)
# Eager index q.
q_ref = indexer.q_layernorm(qk[:, : NUM_Q_HEADS * HEAD_DIM].reshape(-1, HEAD_DIM))
q_ref = q_ref.reshape(batch, NUM_Q_HEADS, HEAD_DIM)
q_ref = indexer.apply_rope(positions, q_ref)
# Fused index q.
pool = FakePool(64, 4096, device, dtype)
cache_loc = torch.arange(1, batch + 1, device=device)
q_new, _, stored = indexer.project_qk(
hidden, positions, pool=pool, cache_loc=cache_loc
)
assert stored
compressed_cache = torch.randn(
64, page_size, 1, HEAD_DIM, device=device, dtype=dtype
)
page_table = torch.arange(max_pages, dtype=torch.int32, device=device).repeat(
batch, 1
)
context_lens = torch.full((batch,), 1500, dtype=torch.int32, device=device)
def select(q):
logits = torch_qsa_mqa_decode(
q, compressed_cache, page_table, context_lens, max_model_len
)
row_starts = torch.zeros_like(context_lens)
return qsa_fast_topk(logits, row_starts, context_lens, topk=512)
idx_ref = select(q_ref)
idx_new = select(q_new[:, :NUM_Q_HEADS].contiguous())
for row in range(batch):
ref_set = set(idx_ref[row][idx_ref[row] >= 0].tolist())
new_set = set(idx_new[row][idx_new[row] >= 0].tolist())
assert ref_set == new_set, f"row {row}: selection mismatch"
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-s"]))
@@ -0,0 +1,81 @@
import sys
import pytest
import torch
from sglang.kernels.ops.mamba.mamba_state_scatter_triton import (
fused_commit_track_indices,
)
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 _reference(accept_index, accept_lens, seq_lens, draft_token_num, track_interval):
"""Mirrors the eager branch of spec_utils._verify_commit_step_indices."""
bs = accept_lens.shape[0]
offset = torch.arange(
0,
bs * draft_token_num,
step=draft_token_num,
dtype=accept_lens.dtype,
device=accept_lens.device,
)
req_idx = torch.arange(bs, dtype=torch.int64, device=accept_lens.device)
last = accept_index[req_idx, (accept_lens - 1).to(torch.int64)] - offset
if track_interval <= 0:
return last, None
pre = seq_lens
post = seq_lens + accept_lens
mask = pre // track_interval != post // track_interval
point = post // track_interval * track_interval
ith = torch.clamp(point - pre - 1, min=0).to(torch.int64)
cand = accept_index[req_idx, ith] - offset
track = torch.where(mask, cand, torch.full_like(cand, -1))
return last, track
@pytest.mark.parametrize("bs", [1, 3, 48, 257])
@pytest.mark.parametrize("track_interval", [0, 64])
@pytest.mark.parametrize("tree_depth", [4, 3])
def test_verify_commit_steps_matches_eager(bs, track_interval, tree_depth):
"""The fused kernel must match eager on both outputs near tracking boundaries
and when accept_index rows (max_tree_depth) are narrower than draft_token_num."""
if not torch.cuda.is_available():
pytest.skip("needs CUDA")
torch.manual_seed(bs + track_interval + tree_depth)
device = "cuda"
draft_token_num = 4
accept_lens = torch.randint(
1, tree_depth + 1, (bs,), device=device, dtype=torch.int32
)
tree_nodes = torch.argsort(torch.rand(bs, draft_token_num, device=device), dim=1)[
:, :tree_depth
]
accept_index = (
torch.arange(bs, device=device, dtype=torch.int64).unsqueeze(1)
* draft_token_num
+ tree_nodes
).to(torch.int32)
# Cluster seq lens around tracking boundaries to exercise the crossing.
seq_lens = torch.randint(60, 70, (bs,), device=device, dtype=torch.int64)
exp_last, exp_track = _reference(
accept_index, accept_lens, seq_lens, draft_token_num, track_interval
)
got_last, got_track = fused_commit_track_indices(
accept_index,
accept_lens,
seq_lens if track_interval > 0 else None,
draft_token_num,
track_interval,
)
assert torch.equal(got_last, exp_last)
if track_interval > 0:
assert torch.equal(got_track, exp_track)
else:
assert got_track is None
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))
@@ -7,7 +7,9 @@ from sglang.srt.configs.model_config import (
ModelConfig,
get_hybrid_layer_ids,
is_embedding_gemma,
resolve_spec_hidden_size,
)
from sglang.srt.configs.qwen4_exp import Qwen4ExpTextConfig
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
@@ -69,6 +71,27 @@ class TestDraftModelConfig(CustomTestCase):
self.assertEqual(config.hf_config.num_nextn_predict_layers, 1)
self.assertEqual(config.hf_text_config.num_nextn_predict_layers, 1)
def test_qwen4_exp_spec_hidden_size_keeps_hc_width(self):
"""Qwen4-Exp's MTP draft consumes the hc-flattened target stream,
so spec_hidden_size must stay hidden_size * hc_mult; hy_v4 collapses first."""
hidden_size, hc_mult = 2560, 4
self.assertEqual(Qwen4ExpTextConfig(hc_count=hc_mult).hc_mult, hc_mult)
for arch in ("Qwen4ExpForConditionalGeneration", "Qwen4ExpForCausalLMMTP"):
hf_config = SimpleNamespace(architectures=[arch])
self.assertEqual(
resolve_spec_hidden_size(
hf_config=hf_config, hidden_size=hidden_size, hc_mult=hc_mult
),
(hidden_size * hc_mult, hidden_size * hc_mult),
)
hy_v4 = SimpleNamespace(architectures=["HYV4ForCausalLM"])
self.assertEqual(
resolve_spec_hidden_size(
hf_config=hy_v4, hidden_size=hidden_size, hc_mult=hc_mult
),
(hidden_size, None),
)
if __name__ == "__main__":
unittest.main()
@@ -631,6 +631,157 @@ class TestMamba(unittest.TestCase):
return tree, allocator, req_to_token_pool, make_dummy_req
# Qwen4-Exp's PLE N-gram window is 2 wide (ngram_size=3) and its "no history"
# sentinel is the eos id; pick a recognisable one for the tests.
NGRAM_CONTEXT_LEN = 2
NGRAM_EOS = 248044
def _setup_pool_with_ngram(self, ngram_context_len: int = NGRAM_CONTEXT_LEN):
server_args = ServerArgs(model_path="dummy", page_size=1)
server_args._mamba_cache_chunk_size = FLA_CHUNK_SIZE
set_global_server_args_for_scheduler(server_args)
with envs.SGLANG_MAMBA_SSM_DTYPE.override("bfloat16"):
shape = Mamba2StateShape.create(
tp_world_size=1,
intermediate_size=4096,
n_groups=16,
num_heads=32,
head_dim=128,
state_size=128,
conv_kernel=4,
)
cache_params = Mamba2CacheParams(shape=shape, layers=[0])
return HybridReqToTokenPool(
size=10,
mamba_size=20,
mamba_spec_state_size=10,
max_context_len=128,
device=get_device(),
enable_memory_saver=False,
cache_params=cache_params,
mamba_layer_ids=[0],
enable_mamba_extra_buffer=False,
speculative_num_draft_tokens=3,
ngram_context_len=ngram_context_len,
ngram_eos_token_id=self.NGRAM_EOS,
)
# Slot-sibling parity: each test below pins one way a mamba slot changes owner.
def test_slot_siblings_registered(self):
"""Enabled PLE side states register on the pool that owns the slots;
disabled ones stay off so the host-offload payload keeps its legacy shape."""
_, _, base_pool, _ = self._setup_tree_and_allocator()
# The default hybrid setup has no PLE config: no siblings ride along.
self.assertEqual(len(base_pool.mamba_pool._slot_siblings), 0)
pool = self._setup_pool_with_ngram()
self.assertEqual(len(pool.mamba_pool._slot_siblings), 1)
def test_ngram_clear_slots_resets_window(self):
"""A recycled slot must not carry its previous owner's N-gram window;
the sibling reset must ride the same deferred ``clear_slots`` call."""
pool = self._setup_pool_with_ngram()
mamba_pool = pool.mamba_pool
ngram = pool.ngram_pool
victim = pool.mamba_allocator.alloc(1)
ngram.context[victim.long()] = 777 # poison, as a real request's history
mamba_pool.clear_slots(victim)
self.assertTrue(
torch.all(ngram.context[victim.long()] == self.NGRAM_EOS),
f"clear_slots left a dirty N-gram row: {ngram.context[victim.long()]}",
)
def test_ngram_copy_from_copies_window(self):
"""copy_from carries the window, so radix cow gets the cached prefix's state."""
pool = self._setup_pool_with_ngram()
mamba_pool = pool.mamba_pool
ngram = pool.ngram_pool
src = pool.mamba_allocator.alloc(1)
dst = pool.mamba_allocator.alloc(1)
window = torch.tensor(
[[55, 66]], dtype=ngram.context.dtype, device=ngram.context.device
)
ngram.context[src.long()] = window
mamba_pool.copy_from(src, dst)
self.assertTrue(
torch.equal(ngram.context[dst.long()], window),
f"copy_from lost the N-gram window: got {ngram.context[dst.long()]}",
)
def test_ngram_cpu_offload_roundtrip(self):
"""The window survives a host offload round-trip along with mamba state."""
pool = self._setup_pool_with_ngram()
mamba_pool = pool.mamba_pool
ngram = pool.ngram_pool
indices = pool.mamba_allocator.alloc(2)
window = torch.tensor(
[[11, 12], [13, 14]],
dtype=ngram.context.dtype,
device=ngram.context.device,
)
ngram.context[indices.long()] = window
saved = mamba_pool.get_cpu_copy(indices)
ngram.context[indices.long()] = self.NGRAM_EOS # simulate slot reuse
mamba_pool.load_cpu_copy(saved, indices)
self.assertTrue(
torch.equal(ngram.context[indices.long()], window),
f"offload round-trip lost the window: got {ngram.context[indices.long()]}",
)
def test_ngram_pool_absent_keeps_legacy_offload_shape(self):
"""Disabled pool stays inert: legacy 2-tuple offload payload, no sibling."""
pool = self._setup_pool_with_ngram(ngram_context_len=0)
self.assertIsNone(pool.ngram_pool.context)
self.assertEqual(len(pool.mamba_pool._slot_siblings), 0)
src = pool.mamba_allocator.alloc(1)
payload = pool.mamba_pool.get_cpu_copy(src)
self.assertEqual(len(payload), 2)
pool.mamba_pool.load_cpu_copy(payload, src)
def test_mamba_track_aligned_lens_math(self):
"""Floor division must swallow the scheduler's `aligned + 1` (_force_track_h),
or the PLE side states snapshot one token past the mamba state."""
from types import SimpleNamespace
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
def aligned_for(chunk_size, track_seqlens, prefix_lens):
server_args = ServerArgs(model_path="dummy", page_size=1)
server_args._mamba_cache_chunk_size = chunk_size
set_global_server_args_for_scheduler(server_args)
fake = SimpleNamespace(
mamba_track_mask=torch.tensor([True] * len(track_seqlens)),
mamba_track_seqlens=torch.tensor(track_seqlens, dtype=torch.int64),
extend_prefix_lens=torch.tensor(prefix_lens, dtype=torch.int64),
)
return ForwardBatch.mamba_track_aligned_lens(fake).tolist()
# normal: track_seqlens = prefix + extend_input_len
self.assertEqual(
aligned_for(64, [100 + 64, 100 + 100, 100 + 127], [100, 100, 100]),
[64, 64, 64],
)
# _force_track_h with chunk > 64: track_seqlens = aligned + 1
self.assertEqual(aligned_for(128, [100 + 128 + 1], [100]), [128])
self.assertEqual(aligned_for(128, [100 + 256 + 1], [100]), [256])
# branching point inside the chunk, also handed over as +1
self.assertEqual(aligned_for(64, [100 + 64 + 1], [100]), [64])
# a masked-off row carries -1 and must come out non-positive, so the
# caller's clamp(min=0) routes it harmlessly
self.assertLessEqual(aligned_for(64, [-1], [100])[0], 0)
# restore the chunk size the rest of the suite expects
server_args = ServerArgs(model_path="dummy", page_size=1)
server_args._mamba_cache_chunk_size = FLA_CHUNK_SIZE
set_global_server_args_for_scheduler(server_args)
def test_mamba_pool_cpu_offload(self):
"""MambaPool.get_cpu_copy / load_cpu_copy round-trips conv and temporal state."""
_, _, req_to_token_pool, _ = self._setup_tree_and_allocator()
@@ -105,6 +105,23 @@ _mock_device.start()
class TestPrepareServerArgs(CustomTestCase):
def test_ple_embedding_offload_rejects_generic_weight_offload(self):
for generic_offload in (
{"cpu_offload_gb": 1},
{"offload_group_size": 1},
):
with (
self.subTest(generic_offload=generic_offload),
self.assertRaisesRegex(
ValueError, "ple-offload-embedding cannot be combined"
),
):
ServerArgs(
model_path="dummy",
ple_offload_embedding=True,
**generic_offload,
).resolve_once()
def test_weight_cache_daemon_allows_static_eplb(self):
args = ServerArgs(
model_path="dummy",
@@ -252,7 +252,10 @@ class TestEagleWorkerV2BackendFallback(CustomTestCase):
existing_backend = object()
decode_backend = object()
worker.server_args = _fake_server_args()
worker.draft_runner = SimpleNamespace(attn_backend=existing_backend)
worker.draft_runner = SimpleNamespace(
attn_backend=existing_backend,
model_config=SimpleNamespace(hf_config=SimpleNamespace()),
)
worker.topk = 1
worker.speculative_num_steps = 2
worker.seed_dsa_topk_from_draft_extend = False
@@ -274,7 +277,10 @@ class TestEagleWorkerV2BackendFallback(CustomTestCase):
decode_backend = object()
draft_extend_backend = object()
worker.server_args = _fake_server_args()
worker.draft_runner = SimpleNamespace(attn_backend=existing_backend)
worker.draft_runner = SimpleNamespace(
attn_backend=existing_backend,
model_config=SimpleNamespace(hf_config=SimpleNamespace()),
)
worker.topk = 1
worker.speculative_num_steps = 2
worker.seed_dsa_topk_from_draft_extend = True
@@ -88,6 +88,7 @@ class TestModelOverridableWhitelist(CustomTestCase):
"uses_mamba_radix_cache",
"mamba_radix_cache_strategy",
"mamba_full_memory_ratio",
"ple_offload_embedding",
"speculative_moe_runner_backend",
"speculative_moe_a2a_backend",
"disable_shared_experts_fusion",
@@ -605,12 +606,48 @@ class TestGoldenModelOverrides(_IsolatedPublish):
def test_control_arch_keeps_pristine_dtype(self):
sa = self._construct("LlamaForCausalLM", "llama")
self.assertEqual(self._resolved(sa, "dtype"), "auto")
self.assertIsNone(self._resolved(sa, "ple_offload_embedding"))
declared = {f for _s, d in sa._resolved_overrides for f in d}
self.assertNotIn("dtype", declared) # no arch declaration for Llama
# publish still projects the whitelisted leaf with the pristine
# value: readers only ever read flags.
self.assertEqual((self._publish(sa), self._leaf("dtype"))[1], "auto")
def test_qwen4_rejects_pd_and_unified_memory(self):
qwen4 = ("Qwen4ExpForConditionalGeneration", "qwen4_exp")
for kwargs, message in (
({"disaggregation_mode": "prefill"}, "PD disaggregation"),
({"disaggregation_mode": "decode"}, "PD disaggregation"),
({"enable_unified_memory": True}, "enable-unified-memory"),
):
with self.subTest(**kwargs):
with self.assertRaisesRegex(ValueError, message):
self._construct(*qwen4, **kwargs)
def test_qwen4_ple_offload_default(self):
qwen4 = ("Qwen4ExpForConditionalGeneration", "qwen4_exp")
with override_platform(is_cuda=True):
for kwargs, expected in (
({}, True),
({"dtype": "float16"}, False),
({"ple_offload_embedding": False}, False),
({"ple_offload_embedding": False, "cpu_offload_gb": 1}, False),
):
with self.subTest(kwargs=kwargs):
self.assertEqual(
self._resolved(
self._construct(*qwen4, **kwargs),
"ple_offload_embedding",
),
expected,
)
with self.assertRaisesRegex(ValueError, "cannot be combined"):
self._construct(*qwen4, cpu_offload_gb=1)
with override_platform(is_cuda=False, is_hip=True):
self.assertFalse(
self._resolved(self._construct(*qwen4), "ple_offload_embedding")
)
def test_minimax_m2_enables_tf32_matmul(self):
sa = self._construct("MiniMaxM2ForCausalLM", "llama")
self.assertTrue(self._resolved(sa, "enable_tf32_matmul"))