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:
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
@@ -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"]))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user