[DSV4] Cherry pick missing commits from deepseek_v4 branch and enhance tests (#24793)
Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com> Co-authored-by: yueming-yuan <yym022502@gmail.com>
This commit is contained in:
co-authored by
Xinyuan Tong
yueming-yuan
parent
4b23f6bdc5
commit
ef5e9f8aba
+65
-15
@@ -1,14 +1,10 @@
|
||||
"""B200 nightly CI: DeepSeek-V4-Flash FP4 (Balanced + MaxThroughput recipes).
|
||||
"""B200 per-commit CI: DeepSeek-V4-Flash FP4 (LowLatency recipe).
|
||||
|
||||
Two server configurations exercise the DeepEP all-to-all + DP-attention path
|
||||
that the per-commit LowLatency test does not cover.
|
||||
Launches TP=4 with flashinfer_mxfp4 MoE runner + EAGLE speculative decoding.
|
||||
Runs 12 ServerSanity probes (correctness, streaming, concurrency, determinism)
|
||||
plus a GSM8K accuracy gate.
|
||||
|
||||
Balanced: TP=4, DP=4, DeepEP, EAGLE (1 step)
|
||||
MaxThroughput: TP=4, DP=4, DeepEP, no speculation
|
||||
|
||||
Each class inherits 12 ServerSanity probes plus a GSM8K accuracy gate.
|
||||
|
||||
Registry: nightly-4-gpu-b200
|
||||
Registry: stage-c-test-dsv4-4-gpu-b200 (per-commit, 4x B200)
|
||||
"""
|
||||
|
||||
import unittest
|
||||
@@ -25,7 +21,7 @@ from sglang.test.test_utils import (
|
||||
try_cached_model,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=3600, suite="nightly-4-gpu-b200", nightly=True)
|
||||
register_cuda_ci(est_time=1800, suite="stage-c-test-dsv4-4-gpu-b200")
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
SERVER_LAUNCH_TIMEOUT = 3600
|
||||
@@ -35,6 +31,14 @@ _DEEPEP_ENV = {
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
_MEGAMOE_ENV = {
|
||||
"SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE": "1",
|
||||
"SGLANG_OPT_FIX_MEGA_MOE_MEMORY": "1",
|
||||
"SGLANG_OPT_FIX_NEXTN_MEGA_MOE": "1",
|
||||
"SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK": "4096",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "0",
|
||||
}
|
||||
|
||||
|
||||
def _gsm8k_check(test_case):
|
||||
args = SimpleNamespace(
|
||||
@@ -51,6 +55,46 @@ def _gsm8k_check(test_case):
|
||||
test_case.assertGreater(metrics["score"], 0.93)
|
||||
|
||||
|
||||
class TestDSV4FlashFP4B200(ServerSanityMixin, CustomTestCase):
|
||||
"""LowLatency recipe: TP=4, FP4 (mxfp4), EAGLE spec decoding."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = try_cached_model(MODEL)
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=SERVER_LAUNCH_TIMEOUT,
|
||||
other_args=[
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-runner-backend",
|
||||
"flashinfer_mxfp4",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
"--chunked-prefill-size",
|
||||
"4096",
|
||||
"--disable-flashinfer-autotune",
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if hasattr(cls, "process") and cls.process:
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_gsm8k(self):
|
||||
_gsm8k_check(self)
|
||||
|
||||
|
||||
class TestDSV4FlashFP4B200Balanced(ServerSanityMixin, CustomTestCase):
|
||||
"""Balanced recipe: TP=4, DP=4, DeepEP, EAGLE (1-step spec)."""
|
||||
|
||||
@@ -94,8 +138,8 @@ class TestDSV4FlashFP4B200Balanced(ServerSanityMixin, CustomTestCase):
|
||||
_gsm8k_check(self)
|
||||
|
||||
|
||||
class TestDSV4FlashFP4B200MaxThroughput(ServerSanityMixin, CustomTestCase):
|
||||
"""MaxThroughput recipe: TP=4, DP=4, DeepEP, no speculation."""
|
||||
class TestDSV4FlashFP4B200MegaMoE(ServerSanityMixin, CustomTestCase):
|
||||
"""Balanced recipe: TP=4, DP=4, MegaMoE."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -114,10 +158,16 @@ class TestDSV4FlashFP4B200MaxThroughput(ServerSanityMixin, CustomTestCase):
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
],
|
||||
env=_DEEPEP_ENV,
|
||||
env=_MEGAMOE_ENV,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
+2
@@ -24,7 +24,9 @@ from sglang.test.test_utils import (
|
||||
register_cuda_ci(est_time=900, suite="stage-c-test-dsv4-8-gpu-h200")
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
MODEL_FP8 = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
SERVER_LAUNCH_TIMEOUT = 3600
|
||||
DEEPEP_CONFIG = '{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}'
|
||||
|
||||
|
||||
class TestDSV4FlashFP4H200(ServerSanityMixin, CustomTestCase):
|
||||
+28
-16
@@ -1,10 +1,11 @@
|
||||
"""B200 per-commit CI: DeepSeek-V4-Flash FP4 (LowLatency recipe).
|
||||
"""H200 per-commit CI: DeepSeek-V4-Flash FP8 (LowLatency recipe).
|
||||
|
||||
Launches TP=4 with flashinfer_mxfp4 MoE runner + EAGLE speculative decoding.
|
||||
Launches TP=4 with DeepEP a2a backend + EAGLE speculative decoding,
|
||||
with FP4 experts disabled via SGLANG_DSV4_FP4_EXPERTS=0.
|
||||
Runs 12 ServerSanity probes (correctness, streaming, concurrency, determinism)
|
||||
plus a GSM8K accuracy gate.
|
||||
|
||||
Registry: stage-c-test-dsv4-4-gpu-b200 (per-commit, 4x B200)
|
||||
Registry: stage-c-test-dsv4-8-gpu-h200 (per-commit, 8x H200 — only 4 used by TP=4)
|
||||
"""
|
||||
|
||||
import unittest
|
||||
@@ -21,18 +22,19 @@ from sglang.test.test_utils import (
|
||||
try_cached_model,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=900, suite="stage-c-test-dsv4-4-gpu-b200")
|
||||
register_cuda_ci(est_time=900, suite="stage-c-test-dsv4-8-gpu-h200")
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
MODEL_FP8 = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
SERVER_LAUNCH_TIMEOUT = 3600
|
||||
DEEPEP_CONFIG = '{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}'
|
||||
|
||||
|
||||
class TestDSV4FlashFP4B200(ServerSanityMixin, CustomTestCase):
|
||||
"""LowLatency recipe: TP=4, FP4 (mxfp4), EAGLE spec decoding."""
|
||||
class TestDSV4FlashFP8H200(ServerSanityMixin, CustomTestCase):
|
||||
"""LowLatency recipe: TP=4, Marlin FP4, EAGLE spec decoding."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = try_cached_model(MODEL)
|
||||
cls.model = try_cached_model(MODEL_FP8)
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
@@ -42,20 +44,30 @@ class TestDSV4FlashFP4B200(ServerSanityMixin, CustomTestCase):
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-runner-backend",
|
||||
"flashinfer_mxfp4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
"--chunked-prefill-size",
|
||||
"4096",
|
||||
"--disable-flashinfer-autotune",
|
||||
"2",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"128",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
],
|
||||
env={
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256",
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -74,7 +86,7 @@ class TestDSV4FlashFP4B200(ServerSanityMixin, CustomTestCase):
|
||||
num_threads=128,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"[DSV4 Flash FP4 B200] GSM8K {metrics=}")
|
||||
print(f"[DSV4 Flash FP4 Marlin H200] GSM8K {metrics=}")
|
||||
self.assertGreater(metrics["score"], 0.93)
|
||||
|
||||
|
||||
@@ -220,6 +220,37 @@ class TestChatCompletionRequest(unittest.TestCase):
|
||||
self.assertFalse(request.chat_template_kwargs.get("thinking"))
|
||||
self.assertFalse(request.chat_template_kwargs.get("enable_thinking"))
|
||||
|
||||
def test_chat_completion_reasoning_effort_max(self):
|
||||
"""`max` is an sglang extension on chat completion's top-level
|
||||
`reasoning_effort` only; the Responses-API-style nested
|
||||
`reasoning.effort` path stays aligned with OpenAI's three levels."""
|
||||
from pydantic import ValidationError
|
||||
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
request = ChatCompletionRequest(
|
||||
model="test-model",
|
||||
messages=messages,
|
||||
reasoning_effort="max",
|
||||
)
|
||||
self.assertEqual(request.reasoning_effort, "max")
|
||||
|
||||
# Unknown values still rejected.
|
||||
with self.assertRaises(ValidationError):
|
||||
ChatCompletionRequest(
|
||||
model="test-model",
|
||||
messages=messages,
|
||||
reasoning_effort="ultra",
|
||||
)
|
||||
|
||||
# Nested reasoning.effort=max is NOT promoted by normalize_reasoning_inputs:
|
||||
# the Responses API path keeps the OpenAI low/medium/high contract.
|
||||
request = ChatCompletionRequest(
|
||||
model="test-model",
|
||||
messages=messages,
|
||||
reasoning={"effort": "max"},
|
||||
)
|
||||
self.assertNotEqual(request.reasoning_effort, "max")
|
||||
|
||||
def test_chat_completion_json_format(self):
|
||||
"""Test chat completion json format"""
|
||||
transcript = "Good morning! It's 7:00 AM, and I'm just waking up. Today is going to be a busy day, "
|
||||
|
||||
@@ -31,7 +31,7 @@ from sglang.srt.function_call.pythonic_detector import PythonicDetector
|
||||
from sglang.srt.function_call.qwen3_coder_detector import Qwen3CoderDetector
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
|
||||
register_cpu_ci(15, "stage-a-test-cpu")
|
||||
register_cpu_ci(est_time=15, suite="stage-a-test-cpu")
|
||||
|
||||
|
||||
class TestPythonicDetector(unittest.TestCase):
|
||||
@@ -1686,6 +1686,26 @@ class TestDeepSeekV32Detector(unittest.TestCase):
|
||||
grammar = xgr.Grammar.from_structural_tag(structural_tag)
|
||||
self.assertIsInstance(grammar, xgr.Grammar)
|
||||
|
||||
def test_self_closing_zero_arg_invoke(self):
|
||||
"""V32 inherits the same regex; verify self-closing parses to empty
|
||||
params here too (V32 model rarely emits this shape, but the parser
|
||||
must agree with V4 since V4 inherits from V32)."""
|
||||
submit_tool = Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="submit",
|
||||
parameters={"type": "object", "properties": {}},
|
||||
),
|
||||
)
|
||||
text = (
|
||||
'<|DSML|function_calls>\n<|DSML|invoke name="submit"/>\n'
|
||||
"</|DSML|function_calls>"
|
||||
)
|
||||
result = self.detector.detect_and_parse(text, [submit_tool])
|
||||
self.assertEqual(len(result.calls), 1)
|
||||
self.assertEqual(result.calls[0].name, "submit")
|
||||
self.assertEqual(json.loads(result.calls[0].parameters), {})
|
||||
|
||||
|
||||
class TestDeepSeekV4Detector(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -2111,6 +2131,96 @@ class TestDeepSeekV4Detector(unittest.TestCase):
|
||||
grammar = xgr.Grammar.from_structural_tag(structural_tag)
|
||||
self.assertIsInstance(grammar, xgr.Grammar)
|
||||
|
||||
def test_self_closing_zero_arg_invoke(self):
|
||||
"""V4 emits `<|DSML|invoke name="x"/>` for zero-arg tools; the
|
||||
detector must parse it as a complete tool call with empty params
|
||||
instead of leaking the raw markup back into normal_text."""
|
||||
submit_tool = Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="submit",
|
||||
description="Submit the final answer.",
|
||||
parameters={"type": "object", "properties": {}},
|
||||
),
|
||||
)
|
||||
|
||||
text = (
|
||||
"Final answer.\n"
|
||||
'<|DSML|tool_calls>\n<|DSML|invoke name="submit"/>\n'
|
||||
"</|DSML|tool_calls>"
|
||||
)
|
||||
result = self.detector.detect_and_parse(text, [submit_tool])
|
||||
self.assertEqual(len(result.calls), 1)
|
||||
self.assertEqual(result.calls[0].name, "submit")
|
||||
self.assertEqual(json.loads(result.calls[0].parameters), {})
|
||||
self.assertNotIn("DSML", result.normal_text)
|
||||
|
||||
def test_self_closing_mixed_with_long_form(self):
|
||||
"""Mix of long-form (with params) and self-closing tags in one block."""
|
||||
submit_tool = Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="submit",
|
||||
parameters={"type": "object", "properties": {}},
|
||||
),
|
||||
)
|
||||
text = (
|
||||
"<|DSML|tool_calls>\n"
|
||||
'<|DSML|invoke name="get_favorite_tourist_spot">\n'
|
||||
'<|DSML|parameter name="city" string="true">SF</|DSML|parameter>\n'
|
||||
"</|DSML|invoke>\n"
|
||||
'<|DSML|invoke name="submit"/>\n'
|
||||
"</|DSML|tool_calls>"
|
||||
)
|
||||
result = self.detector.detect_and_parse(text, self.tools + [submit_tool])
|
||||
self.assertEqual(len(result.calls), 2)
|
||||
self.assertEqual(result.calls[0].name, "get_favorite_tourist_spot")
|
||||
self.assertEqual(json.loads(result.calls[0].parameters), {"city": "SF"})
|
||||
self.assertEqual(result.calls[1].name, "submit")
|
||||
self.assertEqual(json.loads(result.calls[1].parameters), {})
|
||||
|
||||
def test_streaming_self_closing_invoke(self):
|
||||
"""Self-closing invoke must terminate cleanly even when `/>` arrives
|
||||
after the `name=` attribute crosses chunk boundaries."""
|
||||
submit_tool = Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="submit",
|
||||
parameters={"type": "object", "properties": {}},
|
||||
),
|
||||
)
|
||||
# Build the prompt and feed it through the tokenizer to exercise the
|
||||
# same chunk shapes the runtime sees.
|
||||
text = (
|
||||
"<|DSML|tool_calls>\n"
|
||||
'<|DSML|invoke name="submit"/>\n'
|
||||
"</|DSML|tool_calls>"
|
||||
)
|
||||
self.detector = DeepSeekV4Detector()
|
||||
input_ids = self.tokenizer.encode(text, add_special_tokens=False)
|
||||
chunks = [
|
||||
self.tokenizer.decode(input_ids[i : i + self.interval])
|
||||
for i in range(0, len(input_ids), self.interval)
|
||||
]
|
||||
|
||||
tool_calls_by_index = {}
|
||||
for chunk in chunks:
|
||||
result = self.detector.parse_streaming_increment(chunk, [submit_tool])
|
||||
for call in result.calls:
|
||||
if call.tool_index is None:
|
||||
continue
|
||||
slot = tool_calls_by_index.setdefault(
|
||||
call.tool_index, {"name": "", "parameters": ""}
|
||||
)
|
||||
if call.name:
|
||||
slot["name"] = call.name
|
||||
if call.parameters:
|
||||
slot["parameters"] += call.parameters
|
||||
|
||||
self.assertEqual(len(tool_calls_by_index), 1)
|
||||
self.assertEqual(tool_calls_by_index[0]["name"], "submit")
|
||||
self.assertEqual(json.loads(tool_calls_by_index[0]["parameters"]), {})
|
||||
|
||||
|
||||
class TestQwen3CoderDetector(unittest.TestCase):
|
||||
"""Test suite for Qwen3CoderDetector."""
|
||||
|
||||
Reference in New Issue
Block a user