GLM-5.3-Flash support (#36507)

Co-authored-by: zRzRzRzRzRzRzR <Yuxuan.Zhang2@liverpool.ac.uk>
Co-authored-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Co-authored-by: zanes-ops <zanes@nvidia.com>
Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com>
Co-authored-by: Jian Chen <jianchen0311@gmail.com>
Co-authored-by: zijiexia <37504505+zijiexia@users.noreply.github.com>
Co-authored-by: andyluo7 <43718156+andyluo7@users.noreply.github.com>
Co-authored-by: Ehsan Akhgari <ehsan.akhgari@gmail.com>
Co-authored-by: kpham-sgl <khoa.pham@radixark.ai>
Co-authored-by: BBuf <1182563586@qq.com>
Co-authored-by: Raiden Makoto <81530826+Raiden-Makoto@users.noreply.github.com>
This commit is contained in:
Xinyuan Tong
2026-09-06 02:27:59 -07:00
committed by GitHub
co-authored by zRzRzRzRzRzRzR Shijin Zhang zanes-ops Baizhou Zhang Jian Chen zijiexia andyluo7 Ehsan Akhgari kpham-sgl BBuf Raiden Makoto
parent a9944aec01
commit 97c6978369
103 changed files with 7741 additions and 559 deletions
@@ -219,6 +219,7 @@ class MockModelRunner:
self.sliding_window_size = None
self.page_size = self.config["page_size"]
self.max_running_requests = max_batch_size
# Create req_to_token_pool
self.req_to_token_pool = type(
@@ -1240,6 +1241,7 @@ class TestDSAIndexer(CustomTestCase):
backend.use_fused_topk = True
backend.dsa_topk_backend = topk_backend
backend.dsa_index_topk = 2048
backend.dsa_index_kpool = 1
backend.dsa_decode_impl = "fa3"
backend.req_to_token = torch.empty(
2, 4096, dtype=torch.int32, device=self.device
@@ -26,7 +26,7 @@ try:
except ImportError:
KERNELS_AVAILABLE = False
register_cuda_ci(est_time=6, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_cuda_ci(est_time=20, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_amd_ci(est_time=10, suite="nightly-amd-kernel-1-gpu", nightly=True)
@@ -234,5 +234,40 @@ def test_mtp_single_step_decode(N: int):
assert state_fail_rate < 0.01, f"State mismatch: fail_rate={state_fail_rate:.2f}%"
@pytest.mark.skipif(not KERNELS_AVAILABLE, reason="Kernels not available")
def test_verify_scratch_pitch_uses_allocated_steps():
# Gear below the allocated step dim must not spill into the neighbor block.
N, T, ALLOCATED = 2, 4, 8
H, HV, K, V = 16, 32, 128, 128
A_log, dt_bias, a, b, q, k, v, state, indices, cu_seqlens = _make_tensors(
N, T, H, HV, K, V
)
buffer = torch.full(
(N + 1, ALLOCATED, HV, V, K), float("nan"), dtype=torch.float32, device="cuda"
)
run_fused_mtp(
A_log,
dt_bias,
q,
k,
v,
a,
b,
state,
indices,
cu_seqlens,
disable_state_update=True,
intermediate_states_buffer=buffer,
intermediate_state_indices=indices,
cache_steps=T,
)
assert not torch.isnan(buffer[:N, :T]).any()
assert torch.isnan(buffer[N:]).all()
assert torch.isnan(buffer[:N, T:]).all()
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))
+11 -13
View File
@@ -197,12 +197,12 @@ def _run_pair_fp8(H_Q, H_KV, D, B, S, fp8_dtype, dev="cuda", seed=0):
def _run_pair_paged(
H_Q, H_KV, D, B, S, page_size, dev="cuda", dt=torch.float16, seed=0
):
"""Standard vs Lean on a **paged** 4-D KV buffer ``[num_pages, page_size, head, dim]``.
"""Standard vs Lean with page-aware addressing over a dense 3-D KV buffer.
The KV cache is stored in pages and addressed through scattered slot ids in ``kv_indices``
(a permutation), so the kernel's page-aware address math (``kv_loc // page_size`` /
``kv_loc % page_size``) is genuinely exercised — not the contiguous fast path. Both arms read
the identical buffer + indices, so their outputs must agree. Returns (o_std, o_lean).
The dense ``[max_slots, head, dim]`` cache is addressed through scattered slot ids in
``kv_indices`` (a permutation). With ``page_size > 1``, the kernel still exercises its
page-aware address math (``kv_loc // page_size`` / ``kv_loc % page_size``). Both arms read the
identical buffer + indices, so their outputs must agree. Returns (o_std, o_lean).
"""
torch.manual_seed(seed)
D_V = D
@@ -212,11 +212,9 @@ def _run_pair_paged(
assert tot % page_size == 0, (
"test setup: total tokens must be a multiple of page_size"
)
num_pages = tot // page_size
# 4-D paged KV buffers [num_pages, page_size, head, dim] (the shared-pool layout).
k = torch.randn(num_pages, page_size, H_KV, D, dtype=dt, device=dev)
v = torch.randn(num_pages, page_size, H_KV, D_V, dtype=dt, device=dev)
# Unified memory exposes dense 3-D KV views even when the allocator uses pages.
k = torch.randn(tot, H_KV, D, dtype=dt, device=dev)
v = torch.randn(tot, H_KV, D_V, dtype=dt, device=dev)
kv_indptr = torch.arange(0, (B + 1) * S, step=S, device=dev, dtype=torch.int32)
# Scatter slots across pages so page_id/tok_in_p vary within every BLOCK_N tile.
@@ -312,9 +310,9 @@ class TestLeanAttentionParity(CustomTestCase):
)
def test_paged_kv_parity(self):
# Lean must read a paged 4-D KV buffer the same way the standard kernel does. Guards
# the page-aware address math (kv_loc // page_size, kv_loc % page_size); a regression
# to the contiguous-only form would scramble reads and drop cos well below 1.
# Lean must apply page-aware address math to dense KV views the same way the standard
# kernel does. A regression in kv_loc // page_size or kv_loc % page_size would scramble
# the scattered reads and drop cos well below 1.
for name, H_Q, H_KV, D in GQA_SHAPES:
for page_size in (16, 64):
with self.subTest(model=name, page_size=page_size):
@@ -0,0 +1,138 @@
"""B200 per-commit coverage for the GLM-5.3-Flash serving recipes.
Runs the Low Latency, DFlash2, and High Throughput TP4/EP4 recipes on four
B200 GPUs. All recipes must retain GSM8K accuracy; the Low Latency recipe also
checks EAGLE speculative acceptance and single-request decode performance.
"""
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.kits.spec_decoding_kit import SpecDecodingMixin
from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST,
CustomTestCase,
_wait_for_gpu_idle_in_ci,
popen_launch_server,
try_cached_model,
)
register_cuda_ci(est_time=2400, stage="base-c", runner_config="4-gpu-b200")
MODEL_PATH = "zai-org/GLM-5.3-Flash"
DFLASH2_DRAFT_MODEL_PATH = "incoai/GLM-5.3-Flash-DFlash2"
SERVER_LAUNCH_TIMEOUT = 3600
GPU_IDLE_TIMEOUT = 120
COMMON_SERVER_ARGS = [
"--tp-size",
"4",
"--ep-size",
"4",
"--dsa-prefill-backend",
"trtllm",
"--dsa-decode-backend",
"trtllm",
"--kv-cache-dtype",
"fp8_e4m3",
"--moe-runner-backend",
"deep_gemm",
"--reasoning-parser",
"glm45",
"--tool-call-parser",
"glm47",
]
def _stop_server(process):
if process:
kill_process_tree(process.pid)
_wait_for_gpu_idle_in_ci(timeout=GPU_IDLE_TIMEOUT)
class _GLM53FlashB200Base(CustomTestCase):
server_args: list[str]
@classmethod
def setUpClass(cls):
cls.model = try_cached_model(MODEL_PATH)
cls.base_url = DEFAULT_URL_FOR_TEST
cls.process = None
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=SERVER_LAUNCH_TIMEOUT,
other_args=cls.server_args,
)
@classmethod
def tearDownClass(cls):
_stop_server(getattr(cls, "process", None))
class TestGLM53FlashB200LowLatency(
SpecDecodingMixin,
GSM8KMixin,
_GLM53FlashB200Base,
):
gsm8k_score_threshold = 0.93
# Match the established DSA+MTP accuracy workload. The generic 200-question,
# 5-shot defaults leave a single question worth 0.5 percentage points and
# make this tight quality floor unnecessarily sensitive to kernel numerics.
gsm8k_num_examples = 500
gsm8k_num_shots = 20
accept_length_thres = 4.0
bs_1_speed_thres = 250
server_args = [
*COMMON_SERVER_ARGS,
"--speculative-algorithm",
"EAGLE",
"--speculative-num-steps",
"5",
"--speculative-eagle-topk",
"1",
"--speculative-num-draft-tokens",
"6",
"--speculative-adaptive",
]
class TestGLM53FlashB200HighThroughput(
GSM8KMixin,
_GLM53FlashB200Base,
):
gsm8k_score_threshold = 0.93
gsm8k_num_examples = 500
gsm8k_num_shots = 20
server_args = [
*COMMON_SERVER_ARGS,
"--enable-dp-attention",
"--dp-size",
"4",
"--moe-a2a-backend",
"deepep",
]
class TestGLM53FlashB200DFlash2(
GSM8KMixin,
_GLM53FlashB200Base,
):
gsm8k_score_threshold = 0.93
gsm8k_num_examples = 500
gsm8k_num_shots = 20
server_args = [
*COMMON_SERVER_ARGS,
"--speculative-algorithm",
"DFLASH",
"--speculative-draft-model-path",
DFLASH2_DRAFT_MODEL_PATH,
"--speculative-draft-attention-backend",
"fa4",
]
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,119 @@
"""H200 per-commit coverage for the GLM-5.3-Flash serving recipes.
Runs the Low Latency and High Throughput TP8/EP8 recipes on eight H200 GPUs.
Both recipes must retain GSM8K accuracy; the Low Latency recipe also checks
EAGLE speculative acceptance and single-request decode performance.
"""
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.kits.spec_decoding_kit import SpecDecodingMixin
from sglang.test.test_utils import (
DEFAULT_URL_FOR_TEST,
CustomTestCase,
_wait_for_gpu_idle_in_ci,
popen_launch_server,
try_cached_model,
)
register_cuda_ci(est_time=2400, stage="extra-b", runner_config="8-gpu-h200")
MODEL_PATH = "zai-org/GLM-5.3-Flash"
SERVER_LAUNCH_TIMEOUT = 3600
GPU_IDLE_TIMEOUT = 120
COMMON_SERVER_ARGS = [
"--tp-size",
"8",
"--ep-size",
"8",
"--dsa-prefill-backend",
"tilelang",
"--dsa-decode-backend",
"tilelang",
"--kv-cache-dtype",
"bf16",
"--moe-runner-backend",
"deep_gemm",
"--reasoning-parser",
"glm45",
"--tool-call-parser",
"glm47",
]
def _stop_server(process):
if process:
kill_process_tree(process.pid)
_wait_for_gpu_idle_in_ci(timeout=GPU_IDLE_TIMEOUT)
class _GLM53FlashH200Base(CustomTestCase):
server_args: list[str]
@classmethod
def setUpClass(cls):
cls.model = try_cached_model(MODEL_PATH)
cls.base_url = DEFAULT_URL_FOR_TEST
cls.process = None
cls.process = popen_launch_server(
cls.model,
cls.base_url,
timeout=SERVER_LAUNCH_TIMEOUT,
other_args=cls.server_args,
)
@classmethod
def tearDownClass(cls):
_stop_server(getattr(cls, "process", None))
class TestGLM53FlashH200LowLatency(
SpecDecodingMixin,
GSM8KMixin,
_GLM53FlashH200Base,
):
gsm8k_score_threshold = 0.93
# Match the established DSA+MTP accuracy workload. The generic 200-question,
# 5-shot defaults leave a single question worth 0.5 percentage points and
# make this tight quality floor unnecessarily sensitive to kernel numerics.
gsm8k_num_examples = 500
gsm8k_num_shots = 20
accept_length_thres = 4.0
bs_1_speed_thres = 200
server_args = [
*COMMON_SERVER_ARGS,
"--speculative-algorithm",
"EAGLE",
"--speculative-num-steps",
"5",
"--speculative-eagle-topk",
"1",
"--speculative-num-draft-tokens",
"6",
"--speculative-adaptive",
]
class TestGLM53FlashH200HighThroughput(
GSM8KMixin,
_GLM53FlashH200Base,
):
gsm8k_score_threshold = 0.93
gsm8k_num_examples = 500
gsm8k_num_shots = 20
server_args = [
*COMMON_SERVER_ARGS,
"--enable-dp-attention",
"--dp-size",
"8",
"--moe-a2a-backend",
"deepep",
]
if __name__ == "__main__":
unittest.main()
@@ -60,6 +60,7 @@ class _Scheduler(SchedulerDisaggregationPrefillMixin):
self.send_kv_chunk = Mock()
self.output_streamer = Mock()
self.metrics_reporter = SimpleNamespace(report_prefill_stats=Mock())
self.maybe_send_health_check_signal = Mock()
self.req_to_metadata_buffer_idx_allocator = Mock()
self.enable_hicache_storage = True
self.chunked_req = None
@@ -1520,6 +1520,7 @@ if _HAS_MLX:
self.kv = ReqKvInfo()
self.mamba_branching_seqlen = None
self.inflight_middle_chunks = 0
self.mamba_branching_seqlen = None
class FakeTpWorker:
def __init__(self, next_token_ids):
@@ -74,16 +74,10 @@ def _inputs(seq_lens, head_num, page_size, max_kv_splits, seed):
total = sum(seq_lens)
n_slots = total + 64
if page_size == 1:
pool = torch.randn(
n_slots, 1, LK, dtype=torch.bfloat16, device=dev, generator=gen
)
else:
n_pages = (n_slots + page_size - 1) // page_size
pool = torch.randn(
n_pages, page_size, 1, LK, dtype=torch.bfloat16, device=dev, generator=gen
)
n_slots = n_pages * page_size
if page_size > 1:
n_slots = ((n_slots + page_size - 1) // page_size) * page_size
# Unified memory exposes dense 3-D KV views even when the allocator uses pages.
pool = torch.randn(n_slots, 1, LK, dtype=torch.bfloat16, device=dev, generator=gen)
kv_indptr = torch.zeros(batch + 1, dtype=torch.int32, device=dev)
kv_indptr[1:] = torch.cumsum(
@@ -476,6 +476,7 @@ def test_disaggregated_prefill_consumes_auxiliary_output_after_commit():
disagg_prefill_inflight_queue=[],
send_kv_chunk=Mock(),
metrics_reporter=SimpleNamespace(report_prefill_stats=Mock()),
maybe_send_health_check_signal=Mock(),
)
with patch("sglang.srt.disaggregation.prefill.maybe_cache_unfinished_req"):
@@ -492,6 +493,7 @@ def test_disaggregated_prefill_consumes_auxiliary_output_after_commit():
host_output,
[0],
)
scheduler.maybe_send_health_check_signal.assert_called_once_with()
def test_logprob_only_reuses_preprocessing_without_observer_lifecycle():
@@ -30,10 +30,12 @@ class _Allocator:
def get_kvcache(self):
return self._kv
def get_cpu_copy(self, indices, mamba_indices=None):
def get_cpu_copy(self, indices, mamba_indices=None, req_pool_index=None):
return "kv"
def load_cpu_copy(self, cpu_tensors, indices, mamba_indices=None):
def load_cpu_copy(
self, cpu_tensors, indices, mamba_indices=None, req_pool_index=None
):
self.loaded_kv = cpu_tensors
@@ -124,9 +124,11 @@ def _make_model_runner(
mc.get_num_kv_heads = lambda tp_size, dcp_size=1: num_kv_heads
mc.get_swa_num_kv_heads = lambda tp_size: swa_num_kv_heads or num_kv_heads
mc.hf_config = SimpleNamespace(architectures=["LlamaForCausalLM"])
mc.hf_config.model_type = "llama"
mc.hf_config.get_text_config = lambda: mc.hf_config
mc.linear_attn_registry_result = None
mc.context_len = 8192
mc.is_draft_model = False
mr.model_config = mc
mr.kv_cache_dtype = "fake_bf16"
@@ -0,0 +1,210 @@
"""Regression test for sgl-project/sglang#37548.
DeepseekModelNextN.forward must use forward_batch.mm_input_embeds for multimodal
positions (where input_ids hold MM_PAD_SHIFT_VALUE+hash sentinels far above
vocab_size) instead of calling embed_tokens on those sentinel values, which
causes a CUDA index-out-of-bounds gather.
"""
import unittest
from unittest.mock import MagicMock, patch
import torch
from sglang.srt.managers.schedule_batch import MM_PAD_SHIFT_VALUE
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=30, suite="base-a-test-cpu")
VOCAB_SIZE = 154880
HIDDEN_SIZE = 64 # tiny for CPU test
def _make_forward_batch(
input_ids: torch.Tensor,
mm_input_embeds: torch.Tensor = None,
extend_seq_lens: torch.Tensor = None,
extend_start_loc: torch.Tensor = None,
has_mm: bool = True,
):
"""Build a minimal mock ForwardBatch for DeepseekModelNextN.forward."""
fb = MagicMock()
fb.mm_input_embeds = mm_input_embeds
fb.contains_mm_inputs.return_value = has_mm
fb.forward_mode.is_extend.return_value = True
fb.forward_mode.is_draft_extend_v2.return_value = False
fb.forward_mode.is_idle.return_value = False
fb.extend_seq_lens = extend_seq_lens
fb.extend_start_loc = extend_start_loc
fb.spec_info.hidden_states = torch.randn(input_ids.shape[0], HIDDEN_SIZE)
return fb
def _make_model_nextn(vocab_size: int, hidden_size: int):
"""Build a mock DeepseekModelNextN with a real embed_tokens layer."""
from sglang.srt.models.deepseek_nextn import DeepseekModelNextN
model = DeepseekModelNextN.__new__(DeepseekModelNextN)
torch.nn.Module.__init__(model)
# Minimal attributes needed by forward
model.vocab_size = vocab_size
model.embed_tokens = torch.nn.Embedding(vocab_size, hidden_size)
model.enorm = torch.nn.RMSNorm(hidden_size)
model.hnorm = torch.nn.RMSNorm(hidden_size)
model.eh_proj = torch.nn.Linear(2 * hidden_size, hidden_size, bias=False)
model.rot_weight = None
model.alt_stream = None
model.quant_config = None
model.cp_rank = None
model.cp_size = None
model.dsa_enable_prefill_cp = False
model.mla_enable_prefill_cp = False
model.mtp_block = MagicMock(side_effect=lambda **kw: (kw["hidden_states"], None))
return model
class TestDeepseekNextNMmEmbed(CustomTestCase):
"""DeepseekModelNextN must not call embed_tokens on MM sentinel token ids."""
def test_mm_sentinel_ids_do_not_cause_oob(self):
"""input_ids containing MM_PAD_SHIFT_VALUE+hash must not reach embed_tokens."""
num_tokens = 10
mm_start, mm_end = 3, 7 # MM sentinel positions
input_ids = torch.arange(num_tokens, dtype=torch.long)
# Insert MM sentinel values
for i in range(mm_start, mm_end):
input_ids[i] = MM_PAD_SHIFT_VALUE + i
# Build mm_input_embeds matching the target-produced embeddings
mm_embeds = torch.randn(num_tokens, HIDDEN_SIZE)
extend_seq_lens = torch.tensor([num_tokens])
extend_start_loc = torch.tensor([0])
fb = _make_forward_batch(
input_ids,
mm_input_embeds=mm_embeds.clone(),
extend_seq_lens=extend_seq_lens,
extend_start_loc=extend_start_loc,
)
model = _make_model_nextn(VOCAB_SIZE, HIDDEN_SIZE)
# Use MagicMock to track embed_tokens calls
mock_embed = MagicMock(side_effect=model.embed_tokens)
object.__setattr__(model, "embed_tokens", mock_embed)
with (
patch(
"sglang.srt.models.deepseek_nextn.is_cp_v2_active", return_value=False
),
patch(
"sglang.srt.models.deepseek_nextn.dsa_use_prefill_cp",
return_value=False,
),
patch(
"sglang.srt.models.deepseek_nextn.mla_use_prefill_cp",
return_value=False,
),
patch(
"sglang.srt.models.deepseek_nextn.fused_eh_norm",
side_effect=lambda h, p, ew, hw, eps: torch.cat(
[model.enorm(h), model.hnorm(p)], dim=-1
),
),
patch(
"sglang.srt.models.deepseek_nextn.get_global_expert_distribution_recorder"
),
patch("sglang.srt.models.deepseek_nextn.is_cuda", False),
patch("sglang.srt.models.deepseek_nextn.is_npu", False),
patch("sglang.srt.models.deepseek_nextn.envs") as mock_envs,
patch("sglang.srt.models.deepseek_nextn.get_model") as mock_get_model,
patch("sglang.srt.models.deepseek_nextn.get_parallel") as mock_get_parallel,
patch("sglang.srt.models.deepseek_nextn.get_spec") as mock_get_spec,
):
mock_envs.SGLANG_NPU_USE_MULTI_STREAM.get.return_value = False
mock_get_model.return_value.quantization = None
positions = torch.arange(num_tokens, dtype=torch.long)
try:
model.forward(input_ids, positions, fb)
except Exception:
pass # We only care about embed_tokens call args
# embed_tokens should only be called for last_indices (the appended
# next-token), not with the full input_ids containing MM sentinels.
for call in mock_embed.call_args_list:
call_ids = call[0][0]
max_id = call_ids.max().item()
self.assertLess(
max_id,
VOCAB_SIZE,
f"embed_tokens was called with id {max_id} >= vocab_size "
f"{VOCAB_SIZE}. MM sentinel values (MM_PAD_SHIFT_VALUE+hash) "
f"must not reach embed_tokens.",
)
def test_no_mm_falls_back_to_embed_tokens(self):
"""Without mm_input_embeds, embed_tokens is called normally."""
num_tokens = 5
input_ids = torch.arange(num_tokens, dtype=torch.long)
fb = _make_forward_batch(
input_ids,
mm_input_embeds=None,
extend_seq_lens=torch.tensor([num_tokens]),
extend_start_loc=torch.tensor([0]),
has_mm=False,
)
model = _make_model_nextn(VOCAB_SIZE, HIDDEN_SIZE)
mock_embed = MagicMock(side_effect=model.embed_tokens)
object.__setattr__(model, "embed_tokens", mock_embed)
embed_calls = mock_embed.call_args_list
with (
patch(
"sglang.srt.models.deepseek_nextn.is_cp_v2_active", return_value=False
),
patch(
"sglang.srt.models.deepseek_nextn.dsa_use_prefill_cp",
return_value=False,
),
patch(
"sglang.srt.models.deepseek_nextn.mla_use_prefill_cp",
return_value=False,
),
patch(
"sglang.srt.models.deepseek_nextn.fused_eh_norm",
side_effect=lambda h, p, ew, hw, eps: torch.cat(
[model.enorm(h), model.hnorm(p)], dim=-1
),
),
patch(
"sglang.srt.models.deepseek_nextn.get_global_expert_distribution_recorder"
),
patch("sglang.srt.models.deepseek_nextn.is_cuda", False),
patch("sglang.srt.models.deepseek_nextn.is_npu", False),
patch("sglang.srt.models.deepseek_nextn.envs") as mock_envs,
patch("sglang.srt.models.deepseek_nextn.get_model") as mock_get_model,
patch("sglang.srt.models.deepseek_nextn.get_parallel") as mock_get_parallel,
patch("sglang.srt.models.deepseek_nextn.get_spec") as mock_get_spec,
):
mock_envs.SGLANG_NPU_USE_MULTI_STREAM.get.return_value = False
mock_get_model.return_value.quantization = None
positions = torch.arange(num_tokens, dtype=torch.long)
try:
model.forward(input_ids, positions, fb)
except Exception:
pass
# embed_tokens should be called with the full input_ids
self.assertTrue(mock_embed.call_count > 0, "embed_tokens should be called")
full_ids_call = mock_embed.call_args_list[0][0][0]
self.assertEqual(full_ids_call.numel(), num_tokens)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,40 @@
"""Regression for DFLASH aux-hidden capture on mHC models.
GLM-5.3-Flash runs with mhc=True. MHCLayerCommunicator folds the residual
into the widened hidden state and returns residual=None, so CUDA-graph
capture used to crash on `hidden_states + residual`. DFLASH also has to
contract that widened state back to the draft hidden size; skipping the
contract is a silent shape/quality bug the crash-guard alone would miss.
"""
import unittest
from types import SimpleNamespace
import torch
from torch import nn
from sglang.srt.models.glm5_next import Glm5NextModel
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
class TestGlm5NextDflashCapture(CustomTestCase):
def test_dflash_contracts_mhc_hidden_state_without_residual(self):
model = Glm5NextModel.__new__(Glm5NextModel)
nn.Module.__init__(model)
model.config = SimpleNamespace(mhc=True, hc_mult=4)
model.dflash_capture = True
hidden_states = torch.arange(24, dtype=torch.float32).reshape(2, 12)
actual = model._prepare_aux_hidden_state(hidden_states, None)
expected = hidden_states.unflatten(-1, (4, -1)).mean(dim=-2)
torch.testing.assert_close(actual, expected)
self.assertEqual(tuple(actual.shape), (2, 3))
if __name__ == "__main__":
unittest.main()
@@ -29,7 +29,6 @@ class TestTemplateContentFormatDetection(CustomTestCase):
select_template = (
"{{ messages | selectattr('tool_call_id', 'equalto', 'call-a') | list }}"
)
self.assertTrue(jinja_template_may_reorder_tool_results(attribute_template))
self.assertTrue(jinja_template_may_reorder_tool_results(item_template))
self.assertTrue(jinja_template_may_reorder_tool_results(get_template))
@@ -8,12 +8,13 @@ from sglang.srt.speculative.adaptive_spec_params import (
resolve_candidate_steps_from_config,
)
from sglang.test.ci.ci_register import register_cpu_ci, register_xpu_ci
from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=6, suite="base-a-test-cpu")
register_xpu_ci(est_time=10, suite="stage-a-test-1-gpu-xpu")
class TestAdaptiveStepSlot(unittest.TestCase):
class TestAdaptiveStepSlot(CustomTestCase):
def _make_params_from_config(self, initial_steps: int, config: dict):
return AdaptiveStepSlot(initial_steps=initial_steps, cfg=config)
@@ -244,11 +245,11 @@ class TestAdaptiveStepSlot(unittest.TestCase):
self.assertEqual(params.ceiling_coeff, 0)
class TestAdaptiveSpeculativeParams(unittest.TestCase):
class TestAdaptiveSpeculativeParams(CustomTestCase):
def test_default_config_loads(self):
params = AdaptiveSpeculativeParams(initial_steps=3)
self.assertEqual(params._bs_list, [1, 8, 32, 64])
self.assertEqual(params._slots[1].candidate_steps, [1, 3, 7])
self.assertEqual(params._slots[1].candidate_steps, [1, 3, 5, 7])
self.assertEqual(params._slots[8].candidate_steps, [0, 1, 3])
self.assertEqual(params._slots[32].candidate_steps, [0, 1])
self.assertEqual(params._slots[64].candidate_steps, [0])
@@ -339,18 +340,18 @@ class TestAdaptiveSpeculativeParams(unittest.TestCase):
self.assertEqual(params._slots[1].up_hysteresis, 0.1)
class TestBatchSizeRouting(unittest.TestCase):
class TestBatchSizeRouting(CustomTestCase):
"""BS-aware routing: batch size selects the slot, CUDA-graph BS pads first."""
def _params(self):
# Slots: bs=1 -> [1,3,7], bs=8 -> [1,3], bs=32 -> [1].
# Slots: bs=1 -> [1,3,5,7], bs=8 -> [0,1,3], bs=32 -> [0,1].
return AdaptiveSpeculativeParams(initial_steps=3)
def test_routes_to_floor_slot_without_cuda_graph(self):
params = self._params()
# A batch maps to the largest slot BS <= batch (floor), capped at the top slot.
self.assertEqual(params._route(1).candidate_steps, [1, 3, 7])
self.assertEqual(params._route(7).candidate_steps, [1, 3, 7])
self.assertEqual(params._route(1).candidate_steps, [1, 3, 5, 7])
self.assertEqual(params._route(7).candidate_steps, [1, 3, 5, 7])
self.assertEqual(params._route(8).candidate_steps, [0, 1, 3])
self.assertEqual(params._route(31).candidate_steps, [0, 1, 3])
self.assertEqual(params._route(32).candidate_steps, [0, 1])
@@ -373,6 +374,8 @@ class TestBatchSizeRouting(unittest.TestCase):
self.assertEqual(params.cuda_graph_bs_for_step(1), [4, 8, 16, 32])
# step=3 lives in the bs=1 and bs=8 slots: graphs 4,8,16 floor into them.
self.assertEqual(params.cuda_graph_bs_for_step(3), [4, 8, 16])
# step=5 lives only in the bs=1 slot: only graph BS 4 floors into it.
self.assertEqual(params.cuda_graph_bs_for_step(5), [4])
# step=7 lives only in the bs=1 slot: only graph BS 4 floors into it.
self.assertEqual(params.cuda_graph_bs_for_step(7), [4])
@@ -392,12 +395,10 @@ class TestBatchSizeRouting(unittest.TestCase):
self.assertEqual(params.get_steps_for_batch(32), 1)
class TestResolveCandidateSteps(unittest.TestCase):
class TestResolveCandidateSteps(CustomTestCase):
def test_default_config(self):
steps = resolve_candidate_steps_from_config()
self.assertIn(1, steps)
self.assertIn(3, steps)
self.assertIn(7, steps)
self.assertEqual(steps, [0, 1, 3, 5, 7])
def test_config_file(self):
with tempfile.NamedTemporaryFile("w", suffix=".json") as f:
@@ -204,6 +204,7 @@ class TestEagleWorkerV2BackendFallback(CustomTestCase):
worker.speculative_num_steps = 1
worker.speculative_num_draft_tokens = 2
worker.device = DEVICE
worker.plan_stream = None
worker.tree_mask_mode = None
worker.seed_dsa_topk_from_draft_extend = seed_enabled
worker.index_share_for_mtp_iteration = True