Deepseek V4 (#23882)
Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: fzyzcjy <ch271828n@outlook.com> Co-authored-by: ispobock <ispobaoke@gmail.com> Co-authored-by: Zhiqiang Xie <xiezhq@stanford.edu> Co-authored-by: yueming-yuan <yym022502@gmail.com> Co-authored-by: DarkSharpness <2040703891@qq.com> Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com> Co-authored-by: yhyang201 <yhyang201@users.noreply.github.com> Co-authored-by: yhyang201 <yhyang201@gmail.com> Co-authored-by: Qiaolin Yu <90088090+qiaolin-yu@users.noreply.github.com> Co-authored-by: Ethan (Yusheng) Su <11704492+yushengsu-thu@users.noreply.github.com> Co-authored-by: Mingyi <27337995+wisclmy0611@users.noreply.github.com> Co-authored-by: Cheng Wan <54331508+ch-wan@users.noreply.github.com> Co-authored-by: Yihao Wang <42559837+againstentropy@users.noreply.github.com>
This commit is contained in:
co-authored by
Baizhou Zhang
Claude Opus 4.7
fzyzcjy
ispobock
Zhiqiang Xie
yueming-yuan
DarkSharpness
Yuhao Yang
yhyang201
yhyang201
Qiaolin Yu
Ethan Su
Mingyi
Cheng Wan
Yihao Wang
parent
55224fff08
commit
35870d55ac
@@ -0,0 +1,317 @@
|
||||
"""Shared fixture for DeepSeek-V4 cookbook launch-command tests.
|
||||
|
||||
Each sibling ``test_<hardware>_<model_size>.py`` declares ONE
|
||||
``hardware x model_size`` cell from the cookbook (e.g. B200 x Flash)
|
||||
and contains one ``CustomTestCase`` subclass per recipe
|
||||
(Low-Latency / Balanced / Max-Throughput / CP, where supported).
|
||||
|
||||
Each subclass launches the server with the cookbook's exact flags and
|
||||
runs two sgl-eval evaluations (https://github.com/sgl-project/sgl-eval):
|
||||
- ``test_smoke_gsm8k`` — short, cheap GSM8K pass to verify the server
|
||||
can produce coherent math answers at all (smoke gate).
|
||||
- ``test_aime25`` — full AIME25 accuracy run (heavy; 16 repeats default).
|
||||
|
||||
Cookbook reference:
|
||||
https://docs.sglang.io/cookbook/autoregressive/DeepSeek/DeepSeek-V4
|
||||
|
||||
These are MANUAL tests (not CI). ``sgl-eval`` must be on PATH.
|
||||
|
||||
Per-variant defaults (set on the Flash/Pro intermediate base classes):
|
||||
Flash recipes -> AIME25 score threshold 0.93
|
||||
Pro recipes -> AIME25 score threshold 0.95
|
||||
GSM8K smoke threshold (0.93) is shared across Flash and Pro.
|
||||
|
||||
AIME25 knobs (env vars):
|
||||
DSV4_AIME25_NUM_REPEATS (default 16 -> --n-repeats)
|
||||
DSV4_AIME25_TEMPERATURE (default 1.0 -> --temperature)
|
||||
DSV4_AIME25_TOP_P (default 1.0 -> --top-p)
|
||||
DSV4_AIME25_MAX_TOKENS (default 65536 -> --max-tokens)
|
||||
DSV4_AIME25_NUM_THREADS (default 512 -> --num-threads)
|
||||
DSV4_AIME25_SCORE_METRIC (default "score"; sgl-eval JSON key under "aggregate")
|
||||
DSV4_AIME25_SCORE_THRESHOLD (default 0; >0 overrides per-variant default)
|
||||
|
||||
GSM8K smoke knobs (env vars):
|
||||
DSV4_GSM8K_NUM_EXAMPLES (default 50 -> --num-examples)
|
||||
DSV4_GSM8K_N_REPEATS (default 1 -> --n-repeats)
|
||||
DSV4_GSM8K_TEMPERATURE (default 0.6 -> --temperature)
|
||||
DSV4_GSM8K_TOP_P (default 0.95 -> --top-p)
|
||||
DSV4_GSM8K_MAX_TOKENS (default 8192 -> --max-tokens)
|
||||
DSV4_GSM8K_NUM_THREADS (default 64 -> --num-threads)
|
||||
DSV4_GSM8K_SCORE_METRIC (default "score"; sgl-eval JSON key under "aggregate")
|
||||
DSV4_GSM8K_SCORE_THRESHOLD (default 0.93; set to 0 to skip the assertion)
|
||||
|
||||
Shared knobs:
|
||||
DSV4_SGL_EVAL_OUT_DIR (default /tmp/sgl-eval-out -> --out-dir)
|
||||
DSV4_SGL_EVAL_BIN (default "sgl-eval"; override path to the CLI)
|
||||
DSV4_SERVER_LAUNCH_TIMEOUT (default 3600s; the sglang 600s default is
|
||||
too short for DSV4 model load + DeepGEMM
|
||||
warmup. 1800s is also tight for the heavier
|
||||
recipes (DP-attn + DeepEP); 3600s is the
|
||||
safe default. Bump again for first-run
|
||||
model downloads if needed.)
|
||||
|
||||
Multi-node knobs (only consumed by multi-node test classes; if either
|
||||
is unset, those classes ``SkipTest``):
|
||||
DSV4_NODE_RANK (per-node rank for --node-rank)
|
||||
DSV4_DIST_INIT_ADDR (e.g. 10.0.0.1:20000 for --dist-init-addr)
|
||||
|
||||
Always-on env (set by the base class for every recipe; per-recipe EXTRA_ENV
|
||||
wins on key conflict):
|
||||
SGLANG_JIT_DEEPGEMM_FAST_WARMUP=1 skip the slow DeepGEMM warmup grid
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Dict, List, Optional
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
SGL_EVAL_BIN = os.environ.get("DSV4_SGL_EVAL_BIN", "sgl-eval")
|
||||
SGL_EVAL_OUT_DIR = os.environ.get("DSV4_SGL_EVAL_OUT_DIR", "/tmp/sgl-eval-out")
|
||||
|
||||
# DSV4 server launch needs more than the 600s sglang default: model load alone
|
||||
# can take 5+ min and DeepGEMM warmup another ~5 min. First-run model download
|
||||
# adds ~10-30 min on top. 1800s covers steady-state; bump via env for downloads.
|
||||
SERVER_LAUNCH_TIMEOUT = int(os.environ.get("DSV4_SERVER_LAUNCH_TIMEOUT", "3600"))
|
||||
|
||||
# Defaults applied to every recipe's EXTRA_ENV. Per-recipe EXTRA_ENV wins on key
|
||||
# conflict.
|
||||
BASE_ENV: Dict[str, str] = {
|
||||
# Skip the slow exhaustive DeepGEMM warmup grid; covers the shapes DSV4
|
||||
# actually hits and shaves several minutes off server startup.
|
||||
"SGLANG_JIT_DEEPGEMM_FAST_WARMUP": "1",
|
||||
}
|
||||
|
||||
AIME25_NUM_REPEATS = int(os.environ.get("DSV4_AIME25_NUM_REPEATS", "16"))
|
||||
AIME25_TEMPERATURE = float(os.environ.get("DSV4_AIME25_TEMPERATURE", "1.0"))
|
||||
AIME25_TOP_P = float(os.environ.get("DSV4_AIME25_TOP_P", "1.0"))
|
||||
AIME25_MAX_TOKENS = int(os.environ.get("DSV4_AIME25_MAX_TOKENS", "65536"))
|
||||
AIME25_NUM_THREADS = int(os.environ.get("DSV4_AIME25_NUM_THREADS", "512"))
|
||||
AIME25_SCORE_METRIC = os.environ.get("DSV4_AIME25_SCORE_METRIC", "score")
|
||||
AIME25_SCORE_THRESHOLD = float(os.environ.get("DSV4_AIME25_SCORE_THRESHOLD", "0.0"))
|
||||
|
||||
GSM8K_NUM_EXAMPLES = int(os.environ.get("DSV4_GSM8K_NUM_EXAMPLES", "50"))
|
||||
GSM8K_N_REPEATS = int(os.environ.get("DSV4_GSM8K_N_REPEATS", "1"))
|
||||
GSM8K_TEMPERATURE = float(os.environ.get("DSV4_GSM8K_TEMPERATURE", "0.6"))
|
||||
GSM8K_TOP_P = float(os.environ.get("DSV4_GSM8K_TOP_P", "0.95"))
|
||||
GSM8K_MAX_TOKENS = int(os.environ.get("DSV4_GSM8K_MAX_TOKENS", "8192"))
|
||||
GSM8K_NUM_THREADS = int(os.environ.get("DSV4_GSM8K_NUM_THREADS", "64"))
|
||||
GSM8K_SCORE_METRIC = os.environ.get("DSV4_GSM8K_SCORE_METRIC", "score")
|
||||
GSM8K_SCORE_THRESHOLD = float(os.environ.get("DSV4_GSM8K_SCORE_THRESHOLD", "0.93"))
|
||||
|
||||
# DeepEP "large SMS" config — appears as `--deepep-config '{...}'` in every
|
||||
# DeepEP recipe except multi-node ones (where it is gated off in the JSX).
|
||||
DEEPEP_LARGE_SMS_CONFIG = (
|
||||
'{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}'
|
||||
)
|
||||
|
||||
|
||||
def multinode_args(nnodes: int) -> List[str]:
|
||||
"""Return CLI args for a multi-node launch, or skip the test.
|
||||
|
||||
Reads DSV4_NODE_RANK and DSV4_DIST_INIT_ADDR from the env. Raises
|
||||
``unittest.SkipTest`` when either is missing — call from inside
|
||||
``setUpClass`` so the whole class skips cleanly.
|
||||
"""
|
||||
rank = os.environ.get("DSV4_NODE_RANK")
|
||||
addr = os.environ.get("DSV4_DIST_INIT_ADDR")
|
||||
if rank is None or addr is None:
|
||||
raise unittest.SkipTest(
|
||||
"multi-node test requires DSV4_NODE_RANK and DSV4_DIST_INIT_ADDR"
|
||||
)
|
||||
return [
|
||||
"--nnodes",
|
||||
str(nnodes),
|
||||
"--node-rank",
|
||||
rank,
|
||||
"--dist-init-addr",
|
||||
addr,
|
||||
]
|
||||
|
||||
|
||||
class DSV4Aime25TestBase(CustomTestCase):
|
||||
"""Subclass via ``DSV4FlashAime25TestBase`` or ``DSV4ProAime25TestBase``,
|
||||
not directly. Per-recipe subclasses set MODEL / OTHER_ARGS / EXTRA_ENV.
|
||||
|
||||
SCORE_THRESHOLD is set by the Flash/Pro intermediate base classes:
|
||||
Flash 0.93, Pro 0.95.
|
||||
"""
|
||||
|
||||
MODEL: ClassVar[str] = ""
|
||||
OTHER_ARGS: ClassVar[List[str]] = []
|
||||
EXTRA_ENV: ClassVar[Dict[str, str]] = {}
|
||||
|
||||
SCORE_THRESHOLD: ClassVar[float] = 0.0
|
||||
|
||||
_BASE_CLASSES: ClassVar[set] = set()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if cls in cls._BASE_CLASSES:
|
||||
raise unittest.SkipTest("base class; subclass to run")
|
||||
if not cls.MODEL or not cls.OTHER_ARGS:
|
||||
raise unittest.SkipTest(f"{cls.__name__}: MODEL and OTHER_ARGS must be set")
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
env: Optional[Dict[str, str]] = {**BASE_ENV, **(cls.EXTRA_ENV or {})}
|
||||
cls.process = popen_launch_server(
|
||||
cls.MODEL,
|
||||
cls.base_url,
|
||||
timeout=SERVER_LAUNCH_TIMEOUT,
|
||||
other_args=list(cls.OTHER_ARGS),
|
||||
env=env,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if hasattr(cls, "process") and cls.process:
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_smoke_gsm8k(self):
|
||||
"""Quick GSM8K pass to verify the server is producing math answers."""
|
||||
self._run_sgl_eval(
|
||||
eval_name="gsm8k",
|
||||
n_repeats=GSM8K_N_REPEATS,
|
||||
temperature=GSM8K_TEMPERATURE,
|
||||
top_p=GSM8K_TOP_P,
|
||||
max_tokens=GSM8K_MAX_TOKENS,
|
||||
num_threads=GSM8K_NUM_THREADS,
|
||||
num_examples=GSM8K_NUM_EXAMPLES,
|
||||
metric=GSM8K_SCORE_METRIC,
|
||||
threshold=GSM8K_SCORE_THRESHOLD,
|
||||
)
|
||||
|
||||
def test_aime25(self):
|
||||
"""Full AIME25 accuracy run; threshold gated by Flash vs Pro base."""
|
||||
threshold = (
|
||||
AIME25_SCORE_THRESHOLD
|
||||
if AIME25_SCORE_THRESHOLD > 0
|
||||
else self.SCORE_THRESHOLD
|
||||
)
|
||||
self._run_sgl_eval(
|
||||
eval_name="aime25",
|
||||
n_repeats=AIME25_NUM_REPEATS,
|
||||
temperature=AIME25_TEMPERATURE,
|
||||
top_p=AIME25_TOP_P,
|
||||
max_tokens=AIME25_MAX_TOKENS,
|
||||
num_threads=AIME25_NUM_THREADS,
|
||||
num_examples=None,
|
||||
metric=AIME25_SCORE_METRIC,
|
||||
threshold=threshold,
|
||||
)
|
||||
|
||||
def _run_sgl_eval(
|
||||
self,
|
||||
eval_name,
|
||||
n_repeats,
|
||||
temperature,
|
||||
top_p,
|
||||
max_tokens,
|
||||
num_threads,
|
||||
num_examples,
|
||||
metric,
|
||||
threshold,
|
||||
):
|
||||
if shutil.which(SGL_EVAL_BIN) is None:
|
||||
self.skipTest(f"{SGL_EVAL_BIN!r} not found on PATH")
|
||||
|
||||
out_dir = Path(SGL_EVAL_OUT_DIR)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
glob_pattern = f"sgl_eval_{eval_name}_*.json"
|
||||
before = set(out_dir.glob(glob_pattern))
|
||||
|
||||
cmd = [
|
||||
SGL_EVAL_BIN,
|
||||
"run",
|
||||
eval_name,
|
||||
"--base-url",
|
||||
f"{self.base_url}/v1",
|
||||
"--n-repeats",
|
||||
str(n_repeats),
|
||||
"--temperature",
|
||||
str(temperature),
|
||||
"--top-p",
|
||||
str(top_p),
|
||||
"--max-tokens",
|
||||
str(max_tokens),
|
||||
"--num-threads",
|
||||
str(num_threads),
|
||||
"--out-dir",
|
||||
str(out_dir),
|
||||
]
|
||||
if num_examples is not None:
|
||||
cmd += ["--num-examples", str(num_examples)]
|
||||
|
||||
print(f"[{type(self).__name__}] + {' '.join(cmd)}", flush=True)
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
new = sorted(set(out_dir.glob(glob_pattern)) - before)
|
||||
if not new:
|
||||
self.fail(f"sgl-eval produced no new {eval_name} JSON in {out_dir}")
|
||||
result_path = new[-1]
|
||||
with open(result_path) as f:
|
||||
result = json.load(f)
|
||||
print(
|
||||
f"[{type(self).__name__}] sgl-eval {eval_name} result "
|
||||
f"({result_path.name}): {json.dumps(result, indent=2)}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
score = self._extract_score(result, metric)
|
||||
if threshold > 0:
|
||||
self.assertGreaterEqual(
|
||||
score,
|
||||
threshold,
|
||||
f"{eval_name} {metric}={score} below threshold {threshold}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_score(result, metric):
|
||||
"""Find ``metric`` (e.g. "pass@1") anywhere in the sgl-eval JSON tree."""
|
||||
|
||||
def walk(o):
|
||||
if isinstance(o, dict):
|
||||
if metric in o and isinstance(o[metric], (int, float)):
|
||||
return float(o[metric])
|
||||
for v in o.values():
|
||||
s = walk(v)
|
||||
if s is not None:
|
||||
return s
|
||||
elif isinstance(o, list):
|
||||
for v in o:
|
||||
s = walk(v)
|
||||
if s is not None:
|
||||
return s
|
||||
return None
|
||||
|
||||
score = walk(result)
|
||||
if score is None:
|
||||
raise AssertionError(f"metric {metric!r} not found in sgl-eval result JSON")
|
||||
return score
|
||||
|
||||
|
||||
class DSV4FlashAime25TestBase(DSV4Aime25TestBase):
|
||||
"""Base for DeepSeek-V4-Flash recipes: AIME25 threshold 0.93."""
|
||||
|
||||
SCORE_THRESHOLD = 0.93
|
||||
|
||||
|
||||
class DSV4ProAime25TestBase(DSV4Aime25TestBase):
|
||||
"""Base for DeepSeek-V4-Pro recipes: AIME25 threshold 0.95."""
|
||||
|
||||
SCORE_THRESHOLD = 0.95
|
||||
|
||||
|
||||
DSV4Aime25TestBase._BASE_CLASSES = {
|
||||
DSV4Aime25TestBase,
|
||||
DSV4FlashAime25TestBase,
|
||||
DSV4ProAime25TestBase,
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"""B200 (FP4) x DeepSeek-V4-Flash.
|
||||
|
||||
Covers the four cookbook recipes for this hardware x model_size cell:
|
||||
Low-Latency, Balanced, Max-Throughput, Context-Parallel (CP).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DEEPEP_LARGE_SMS_CONFIG, DSV4FlashAime25TestBase
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
|
||||
|
||||
class TestB200FlashLowLatency(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
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",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestB200FlashBalanced(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024"}
|
||||
|
||||
|
||||
class TestB200FlashMaxThroughput(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024"}
|
||||
|
||||
|
||||
class TestB200FlashCP(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--enable-nsa-prefill-context-parallel",
|
||||
"--nsa-prefill-cp-mode",
|
||||
"round-robin-split",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--mem-fraction-static",
|
||||
"0.78",
|
||||
"--max-running-requests",
|
||||
"1024",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
"SGLANG_OPT_USE_JIT_INDEXER_METADATA": "1",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,125 @@
|
||||
"""B200 (FP4) x DeepSeek-V4-Pro.
|
||||
|
||||
Covers the four cookbook recipes for this hardware x model_size cell:
|
||||
Low-Latency, Balanced, Max-Throughput, Context-Parallel (CP).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DEEPEP_LARGE_SMS_CONFIG, DSV4ProAime25TestBase
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Pro"
|
||||
|
||||
|
||||
class TestB200ProLowLatency(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--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",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestB200ProBalanced(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--dp",
|
||||
"8",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--mem-fraction-static",
|
||||
"0.82",
|
||||
"--cuda-graph-max-bs",
|
||||
"64",
|
||||
"--max-running-requests",
|
||||
"128",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256"}
|
||||
|
||||
|
||||
class TestB200ProMaxThroughput(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--dp",
|
||||
"8",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--mem-fraction-static",
|
||||
"0.82",
|
||||
"--cuda-graph-max-bs",
|
||||
"64",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256"}
|
||||
|
||||
|
||||
class TestB200ProCP(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--enable-nsa-prefill-context-parallel",
|
||||
"--nsa-prefill-cp-mode",
|
||||
"round-robin-split",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--mem-fraction-static",
|
||||
"0.78",
|
||||
"--cuda-graph-max-bs",
|
||||
"256",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
"SGLANG_OPT_USE_JIT_INDEXER_METADATA": "1",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,111 @@
|
||||
"""B300 x DeepSeek-V4-Flash.
|
||||
|
||||
The cookbook generator aliases B300 to B200, so the launch flags
|
||||
are identical to the B200(FP4) Flash cell. Kept as a separate file
|
||||
because the hardware target (and therefore the runtime environment)
|
||||
is different. Covers Low-Latency, Balanced, Max-Throughput, CP.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DEEPEP_LARGE_SMS_CONFIG, DSV4FlashAime25TestBase
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
|
||||
|
||||
class TestB300FlashLowLatency(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
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",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestB300FlashBalanced(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024"}
|
||||
|
||||
|
||||
class TestB300FlashMaxThroughput(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024"}
|
||||
|
||||
|
||||
class TestB300FlashCP(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--enable-nsa-prefill-context-parallel",
|
||||
"--nsa-prefill-cp-mode",
|
||||
"round-robin-split",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--mem-fraction-static",
|
||||
"0.78",
|
||||
"--max-running-requests",
|
||||
"1024",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
"SGLANG_OPT_USE_JIT_INDEXER_METADATA": "1",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,127 @@
|
||||
"""B300 x DeepSeek-V4-Pro.
|
||||
|
||||
The cookbook generator aliases B300 to B200, so the launch flags
|
||||
are identical to the B200(FP4) Pro cell. Kept as a separate file
|
||||
because the hardware target (and therefore the runtime environment)
|
||||
is different. Covers Low-Latency, Balanced, Max-Throughput, CP.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DEEPEP_LARGE_SMS_CONFIG, DSV4ProAime25TestBase
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Pro"
|
||||
|
||||
|
||||
class TestB300ProLowLatency(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--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",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestB300ProBalanced(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--dp",
|
||||
"8",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--mem-fraction-static",
|
||||
"0.82",
|
||||
"--cuda-graph-max-bs",
|
||||
"64",
|
||||
"--max-running-requests",
|
||||
"128",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256"}
|
||||
|
||||
|
||||
class TestB300ProMaxThroughput(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--dp",
|
||||
"8",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--mem-fraction-static",
|
||||
"0.82",
|
||||
"--cuda-graph-max-bs",
|
||||
"64",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256"}
|
||||
|
||||
|
||||
class TestB300ProCP(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--enable-nsa-prefill-context-parallel",
|
||||
"--nsa-prefill-cp-mode",
|
||||
"round-robin-split",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--mem-fraction-static",
|
||||
"0.78",
|
||||
"--cuda-graph-max-bs",
|
||||
"256",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
"SGLANG_OPT_USE_JIT_INDEXER_METADATA": "1",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,175 @@
|
||||
"""DSV4 Flash MTP test using EAGLE speculative algorithm.
|
||||
|
||||
DSV4 Flash MTP shares the EAGLE wire path: EAGLE algo + NextN head built
|
||||
into the target model weights. No separate draft model is needed (sglang
|
||||
auto-falls back `--speculative-draft-model-path` to the target model).
|
||||
|
||||
Test matrix mirrors test_eagle_infer_b.TestEAGLEServerBasic to maximize
|
||||
cuda-graph + buffer-pool coverage on the DSV4 path:
|
||||
- test_gsm8k (accuracy + spec path full forward)
|
||||
- test_max_token_one (degenerate spec step, still cuda-graph captured)
|
||||
- test_request_abort (cuda-graph buffer pool survives abort+restart)
|
||||
|
||||
Server launch matches `run_flash_dp4.sh`: tp=4, dp=4, deepep MoE backend,
|
||||
DSV4 FP8 (FP4 experts disabled).
|
||||
"""
|
||||
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.few_shot_gsm8k import run_eval as run_gsm8k_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
DSV4_FLASH_MODEL_PATH = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
|
||||
DSV4_FLASH_ENV = {
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
# MTP runs ~num_draft_tokens forward passes per step, so the deepep
|
||||
# dispatch input size scales by that factor. Default 256 (used by the
|
||||
# plain server) overflows once cuda-graph-max-bs * num_draft_tokens
|
||||
# > 256. 1024 covers bs=128 * 4 draft tokens with headroom.
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
DEEPEP_CONFIG = '{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}'
|
||||
|
||||
PROMPTS = [
|
||||
"[INST] You are a helpful assistant.\\nWhere are you from? [/INST]",
|
||||
"[INST] You are a helpful assistant.\\nSummarize gradient descent in 2 sentences. [/INST]",
|
||||
"[INST] You are a helpful assistant.\\nWhat is 17*23? [/INST]",
|
||||
"[INST] You are a helpful assistant.\\nList three primary colors. [/INST]",
|
||||
]
|
||||
|
||||
|
||||
class DSV4FlashMTPServerBase(CustomTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = DSV4_FLASH_MODEL_PATH
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
other_args = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
]
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=other_args,
|
||||
env=DSV4_FLASH_ENV,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def send_request(self):
|
||||
time.sleep(random.uniform(0, 2))
|
||||
for prompt in PROMPTS:
|
||||
resp = requests.post(
|
||||
self.base_url + "/generate",
|
||||
json={
|
||||
"text": prompt,
|
||||
"sampling_params": {"temperature": 0, "max_new_tokens": 256},
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def send_requests_abort(self):
|
||||
for prompt in PROMPTS:
|
||||
try:
|
||||
time.sleep(random.uniform(0, 2))
|
||||
requests.post(
|
||||
self.base_url + "/generate",
|
||||
json={
|
||||
"text": prompt,
|
||||
"sampling_params": {"temperature": 0, "max_new_tokens": 256},
|
||||
},
|
||||
timeout=0.5,
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
pass
|
||||
|
||||
|
||||
class TestDSV4FlashMTPBasic(DSV4FlashMTPServerBase):
|
||||
def test_gsm8k(self):
|
||||
"""Accuracy + spec path full forward."""
|
||||
requests.get(self.base_url + "/flush_cache")
|
||||
args = SimpleNamespace(
|
||||
num_shots=5,
|
||||
data_path=None,
|
||||
num_questions=200,
|
||||
max_new_tokens=512,
|
||||
parallel=128,
|
||||
host="http://127.0.0.1",
|
||||
port=int(self.base_url.split(":")[-1]),
|
||||
)
|
||||
metrics = run_gsm8k_eval(args)
|
||||
print(f"{metrics=}")
|
||||
self.assertGreater(metrics["accuracy"], 0.95)
|
||||
|
||||
def test_max_token_one(self):
|
||||
"""Degenerate spec step (still cuda-graph captured)."""
|
||||
requests.get(self.base_url + "/flush_cache")
|
||||
args = SimpleNamespace(
|
||||
num_shots=5,
|
||||
data_path=None,
|
||||
num_questions=100,
|
||||
max_new_tokens=1,
|
||||
parallel=128,
|
||||
host="http://127.0.0.1",
|
||||
port=int(self.base_url.split(":")[-1]),
|
||||
)
|
||||
metrics = run_gsm8k_eval(args)
|
||||
self.assertGreater(metrics["output_throughput"], 50)
|
||||
|
||||
def test_request_abort(self):
|
||||
"""Cuda-graph buffer pool must survive abort+restart cycles."""
|
||||
concurrency = 4
|
||||
threads = [
|
||||
threading.Thread(target=self.send_request) for _ in range(concurrency)
|
||||
] + [
|
||||
threading.Thread(target=self.send_requests_abort)
|
||||
for _ in range(concurrency)
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
self.assertIsNone(self.process.poll())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,125 @@
|
||||
"""DSV4-Flash 285B MTP performance tests on H200 TP=8.
|
||||
|
||||
Manual test (8× H200, 285B FP8 weights). Not registered in CI.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.bench_one_batch_server import BenchArgs as OneBatchBenchArgs
|
||||
from sglang.bench_one_batch_server import run_benchmark as run_one_batch_benchmark
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
DSV4_FLASH_MODEL_PATH = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
|
||||
DSV4_FLASH_BASE_ENV = {
|
||||
"SGLANG_ENABLE_SPEC_V2": "1",
|
||||
"SGLANG_OPT_USE_TOPK_V2": "1",
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
"SGLANG_JIT_DEEPGEMM_PRECOMPILE": "0",
|
||||
}
|
||||
|
||||
DSV4_FLASH_SERVER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
"--max-running-requests",
|
||||
"8",
|
||||
]
|
||||
|
||||
|
||||
def _launch_dsv4_flash_server(extra_env=None):
|
||||
env = dict(DSV4_FLASH_BASE_ENV)
|
||||
if extra_env:
|
||||
env.update(extra_env)
|
||||
return popen_launch_server(
|
||||
DSV4_FLASH_MODEL_PATH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH * 4,
|
||||
other_args=DSV4_FLASH_SERVER_ARGS,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
class TestDSV4FlashMTPSimulatedAcc(CustomTestCase):
|
||||
"""bs=1 latency at isl=4096 / 900000 with `SGLANG_SIMULATE_ACC_LEN=3`.
|
||||
|
||||
Reference (H200 Flash TP8):
|
||||
- isl=4096 → output 258.1 tok/s, accept 2.94
|
||||
- isl=900000 → output 222.9 tok/s, accept 2.90
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = _launch_dsv4_flash_server(
|
||||
extra_env={"SGLANG_SIMULATE_ACC_LEN": "3"}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if hasattr(cls, "process") and cls.process:
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def _run_one_batch(self, input_len):
|
||||
requests.get(self.base_url + "/flush_cache")
|
||||
server_args = ServerArgs(model_path=DSV4_FLASH_MODEL_PATH)
|
||||
bench_args = OneBatchBenchArgs(
|
||||
run_name=f"dsv4_flash_simacc_isl{input_len}",
|
||||
batch_size=(1,),
|
||||
input_len=(input_len,),
|
||||
output_len=(1024,),
|
||||
base_url=self.base_url,
|
||||
skip_warmup=True,
|
||||
result_filename=os.path.join(
|
||||
tempfile.gettempdir(), f"dsv4_flash_simacc_isl{input_len}.jsonl"
|
||||
),
|
||||
append_to_github_summary=False,
|
||||
)
|
||||
results, _ = run_one_batch_benchmark(server_args, bench_args)
|
||||
self.assertTrue(results, "bench_one_batch_server returned no results")
|
||||
return results[0]
|
||||
|
||||
def test_isl_4096(self):
|
||||
r = self._run_one_batch(4096)
|
||||
print(
|
||||
f"[flash simacc isl=4096] output_throughput={r.output_throughput:.2f} tok/s "
|
||||
f"latency={r.latency:.2f}s last_ttft={r.last_ttft:.2f}s "
|
||||
f"acc_length={r.acc_length:.2f}"
|
||||
)
|
||||
# Reference 258.1 tok/s / acc=2.94.
|
||||
self.assertGreater(r.output_throughput, 232.0)
|
||||
self.assertGreater(r.acc_length, 2.85)
|
||||
|
||||
def test_isl_900k(self):
|
||||
r = self._run_one_batch(900_000)
|
||||
print(
|
||||
f"[flash simacc isl=900k] output_throughput={r.output_throughput:.2f} tok/s "
|
||||
f"latency={r.latency:.2f}s last_ttft={r.last_ttft:.2f}s "
|
||||
f"acc_length={r.acc_length:.2f}"
|
||||
)
|
||||
# Reference 222.9 tok/s / acc=2.90.
|
||||
self.assertGreater(r.output_throughput, 200.0)
|
||||
self.assertGreater(r.acc_length, 2.85)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,151 @@
|
||||
"""DSV4-Flash 4-GPU server sanity matrix (TP4 variants)."""
|
||||
|
||||
import unittest
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.kits.server_sanity_kit import ServerSanityMixin
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
DSV4_FLASH_MODEL_PATH = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
|
||||
DSV4_FLASH_ENV = {
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
DEEPEP_CONFIG = '{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}'
|
||||
|
||||
|
||||
def _launch(other_args, env_extra=None, timeout_mult=1):
|
||||
env = dict(DSV4_FLASH_ENV)
|
||||
if env_extra:
|
||||
env.update(env_extra)
|
||||
return popen_launch_server(
|
||||
DSV4_FLASH_MODEL_PATH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH * timeout_mult,
|
||||
other_args=other_args,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
_EAGLE_SPEC_ARGS = [
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
]
|
||||
|
||||
|
||||
class TestDSV4FlashTP4DP4(ServerSanityMixin, CustomTestCase):
|
||||
"""TP4 + DP4 + deepep + EAGLE MTP."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = _launch(
|
||||
[
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
*_EAGLE_SPEC_ARGS,
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
|
||||
class TestDSV4FlashTP4EP(ServerSanityMixin, CustomTestCase):
|
||||
"""TP attn + EP MoE (no DP attn) — exercises the DeepEP + TP-attn path."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = _launch(
|
||||
[
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--ep",
|
||||
"4",
|
||||
# No --enable-dp-attention by design: covers TP-attn path.
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"64",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
*_EAGLE_SPEC_ARGS,
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
|
||||
class TestDSV4FlashTP4DP4ChunkedPrefillLarge(ServerSanityMixin, CustomTestCase):
|
||||
"""TP4 + DP4 with --chunked-prefill-size 16384 — large chunked prefill."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = _launch(
|
||||
[
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
*_EAGLE_SPEC_ARGS,
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,50 @@
|
||||
"""DSV4-Flash 8-GPU server sanity (TP8, no spec decoding)."""
|
||||
|
||||
import unittest
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.kits.server_sanity_kit import ServerSanityMixin
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
DSV4_FLASH_MODEL_PATH = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
|
||||
DSV4_FLASH_ENV = {
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
|
||||
class TestDSV4FlashTP8NoSpec(ServerSanityMixin, CustomTestCase):
|
||||
"""TP8, no spec decoding."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
DSV4_FLASH_MODEL_PATH,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=[
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--max-running-requests",
|
||||
"8",
|
||||
"--mem-fraction-static",
|
||||
"0.85",
|
||||
],
|
||||
env=DSV4_FLASH_ENV,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,148 @@
|
||||
"""DSV4 Flash PD-disagg with NIXL backend. Both sides run dp-attention
|
||||
+ deepep + EAGLE MTP so attn_tp_size and the V4 state pool layout are
|
||||
fully symmetric: same SWA item_len under matching attn_tp, and same
|
||||
NSA c4/c128 indexer ring buffer size under matching spec status. nixl
|
||||
`send_state` is page-by-index and has no V4 TP-slice / spec-asymmetric
|
||||
path, so any layout mismatch would trip the item_len assert in
|
||||
`nixl/conn.py`."""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sglang.test.few_shot_gsm8k import run_eval as run_gsm8k_eval
|
||||
from sglang.test.server_fixtures.disaggregation_fixture import (
|
||||
PDDisaggregationServerBase,
|
||||
)
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
popen_launch_pd_server,
|
||||
)
|
||||
|
||||
DSV4_FLASH_MODEL_PATH = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
|
||||
DSV4_FLASH_ENV = {
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
# MTP num_draft_tokens=4 scales dispatch by ~4x; 256 overflows at bs=128.
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
DEEPEP_CONFIG = '{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}'
|
||||
|
||||
|
||||
class TestDSV4FlashPDDisaggNIXL(PDDisaggregationServerBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.transfer_backend = ["--disaggregation-transfer-backend", "nixl"]
|
||||
cls.rdma_devices = []
|
||||
cls.model = DSV4_FLASH_MODEL_PATH
|
||||
|
||||
cls.start_prefill()
|
||||
cls.start_decode()
|
||||
|
||||
cls.wait_server_ready(cls.prefill_url + "/health", process=cls.process_prefill)
|
||||
cls.wait_server_ready(cls.decode_url + "/health", process=cls.process_decode)
|
||||
cls.launch_lb()
|
||||
|
||||
@classmethod
|
||||
def start_prefill(cls):
|
||||
prefill_args = [
|
||||
"--trust-remote-code",
|
||||
"--disaggregation-mode",
|
||||
"prefill",
|
||||
"--base-gpu-id",
|
||||
"0",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
*cls.transfer_backend,
|
||||
*cls.rdma_devices,
|
||||
]
|
||||
cls.process_prefill = popen_launch_pd_server(
|
||||
cls.model,
|
||||
cls.prefill_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=prefill_args,
|
||||
env=DSV4_FLASH_ENV,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def start_decode(cls):
|
||||
decode_args = [
|
||||
"--trust-remote-code",
|
||||
"--disaggregation-mode",
|
||||
"decode",
|
||||
"--base-gpu-id",
|
||||
"4",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
*cls.transfer_backend,
|
||||
*cls.rdma_devices,
|
||||
]
|
||||
cls.process_decode = popen_launch_pd_server(
|
||||
cls.model,
|
||||
cls.decode_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=decode_args,
|
||||
env=DSV4_FLASH_ENV,
|
||||
)
|
||||
|
||||
def test_gsm8k(self):
|
||||
"""End-to-end PD-disagg accuracy through the LB."""
|
||||
args = SimpleNamespace(
|
||||
num_shots=5,
|
||||
data_path=None,
|
||||
num_questions=200,
|
||||
max_new_tokens=512,
|
||||
parallel=64,
|
||||
host=f"http://{self.base_host}",
|
||||
port=int(self.lb_port),
|
||||
)
|
||||
metrics = run_gsm8k_eval(args)
|
||||
print(f"{metrics=}")
|
||||
self.assertGreater(metrics["accuracy"], 0.95)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,281 @@
|
||||
"""DSV4-Pro 1.6T MTP performance tests on B200 TP=8.
|
||||
|
||||
1. TestDSV4ProMTPSimulatedAcc — `SGLANG_SIMULATE_ACC_LEN=3` pins EAGLE accept
|
||||
length so latency comparisons are apples-to-apples. Runs `bench_one_batch_server`
|
||||
at bs=1 for isl=4096 and isl=900000 (osl=1024).
|
||||
|
||||
2. TestDSV4ProMTPHongloumeng — real EAGLE accept (no SIMULATE) on Chinese
|
||||
long-context input (`hongloumeng.txt`, ~627k DSV4 tokens). Builds a one-line
|
||||
custom JSONL dataset on the fly and drives `bench_serving --dataset-name custom`
|
||||
with one short slice (30k tokens) and the full long prompt.
|
||||
|
||||
Manual test (8× B200, 1.6T weights). Not registered in CI.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.bench_one_batch_server import BenchArgs as OneBatchBenchArgs
|
||||
from sglang.bench_one_batch_server import run_benchmark as run_one_batch_benchmark
|
||||
from sglang.bench_serving import run_benchmark as run_serving_benchmark
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
DSV4_PRO_MODEL_PATH = "deepseek-ai/DeepSeek-V4-Pro"
|
||||
|
||||
HONGLOUMENG_PATH = os.environ.get(
|
||||
"SGLANG_HONGLOUMENG_PATH",
|
||||
os.path.join(os.path.dirname(__file__), "hongloumeng.txt"),
|
||||
)
|
||||
|
||||
DSV4_PRO_BASE_ENV = {
|
||||
"SGLANG_ENABLE_SPEC_V2": "1",
|
||||
"SGLANG_OPT_USE_TOPK_V2": "1",
|
||||
"SGLANG_OPT_USE_CUSTOM_ALL_REDUCE_V2": "1",
|
||||
"SGLANG_JIT_DEEPGEMM_PRECOMPILE": "0",
|
||||
}
|
||||
|
||||
DSV4_PRO_SERVER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--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",
|
||||
"--mem-fraction-static",
|
||||
"0.82",
|
||||
"--max-running-requests",
|
||||
"8",
|
||||
]
|
||||
|
||||
|
||||
def _launch_dsv4_pro_server(extra_env=None):
|
||||
env = dict(DSV4_PRO_BASE_ENV)
|
||||
if extra_env:
|
||||
env.update(extra_env)
|
||||
return popen_launch_server(
|
||||
DSV4_PRO_MODEL_PATH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH * 4,
|
||||
other_args=DSV4_PRO_SERVER_ARGS,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
class TestDSV4ProMTPSimulatedAcc(CustomTestCase):
|
||||
"""bs=1 latency at isl=4096 / 900000 with `SGLANG_SIMULATE_ACC_LEN=3`.
|
||||
|
||||
Reference (B200 Pro TP8):
|
||||
- isl=4096 → output 194.6 tok/s, accept 2.96
|
||||
- isl=900000 → output 174.6 tok/s, accept 2.93
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = _launch_dsv4_pro_server(
|
||||
extra_env={"SGLANG_SIMULATE_ACC_LEN": "3"}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if hasattr(cls, "process") and cls.process:
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def _run_one_batch(self, input_len):
|
||||
requests.get(self.base_url + "/flush_cache")
|
||||
server_args = ServerArgs(model_path=DSV4_PRO_MODEL_PATH)
|
||||
bench_args = OneBatchBenchArgs(
|
||||
run_name=f"dsv4_pro_simacc_isl{input_len}",
|
||||
batch_size=(1,),
|
||||
input_len=(input_len,),
|
||||
output_len=(1024,),
|
||||
base_url=self.base_url,
|
||||
skip_warmup=True,
|
||||
result_filename=os.path.join(
|
||||
tempfile.gettempdir(), f"dsv4_pro_simacc_isl{input_len}.jsonl"
|
||||
),
|
||||
append_to_github_summary=False,
|
||||
)
|
||||
results, _ = run_one_batch_benchmark(server_args, bench_args)
|
||||
self.assertTrue(results, "bench_one_batch_server returned no results")
|
||||
return results[0]
|
||||
|
||||
def test_isl_4096(self):
|
||||
r = self._run_one_batch(4096)
|
||||
print(
|
||||
f"[pro simacc isl=4096] output_throughput={r.output_throughput:.2f} tok/s "
|
||||
f"latency={r.latency:.2f}s last_ttft={r.last_ttft:.2f}s "
|
||||
f"acc_length={r.acc_length:.2f}"
|
||||
)
|
||||
# Reference 194.6 tok/s / acc=2.96 — give 10% throughput margin and a
|
||||
# generous accept-length floor to absorb run-to-run jitter.
|
||||
self.assertGreater(r.output_throughput, 175.0)
|
||||
self.assertGreater(r.acc_length, 2.85)
|
||||
|
||||
def test_isl_900k(self):
|
||||
r = self._run_one_batch(900_000)
|
||||
print(
|
||||
f"[pro simacc isl=900k] output_throughput={r.output_throughput:.2f} tok/s "
|
||||
f"latency={r.latency:.2f}s last_ttft={r.last_ttft:.2f}s "
|
||||
f"acc_length={r.acc_length:.2f}"
|
||||
)
|
||||
# Reference 174.6 tok/s / acc=2.93.
|
||||
self.assertGreater(r.output_throughput, 155.0)
|
||||
self.assertGreater(r.acc_length, 2.85)
|
||||
|
||||
|
||||
def _build_hongloumeng_jsonl(num_tokens, tokenizer, out_path):
|
||||
"""Slice the first `num_tokens` DSV4 tokens of hongloumeng.txt into a
|
||||
one-line CustomDataset JSONL. Pass num_tokens=None to keep the full text.
|
||||
"""
|
||||
with open(HONGLOUMENG_PATH, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
if num_tokens is not None:
|
||||
ids = tokenizer.encode(text)
|
||||
text = tokenizer.decode(ids[:num_tokens])
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
json.dumps(
|
||||
{"conversations": [{"value": text}, {"value": "x"}]},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
return out_path
|
||||
|
||||
|
||||
class TestDSV4ProMTPHongloumeng(CustomTestCase):
|
||||
"""Real EAGLE accept on Chinese long-context (hongloumeng.txt).
|
||||
|
||||
Reference (B200 Pro TP8, no SIMULATE):
|
||||
- isl=30000 → output 124.4 tok/s, decode peak 184 tok/s, accept 2.47
|
||||
- isl=627059 → output 125.7 tok/s, decode peak 179 tok/s, accept 2.52
|
||||
"""
|
||||
|
||||
SHORT_TOKENS = 30_000
|
||||
LONG_TOKENS = None # full file (~627k DSV4 tokens)
|
||||
OUTPUT_TOKENS = 4096
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = _launch_dsv4_pro_server()
|
||||
|
||||
# Resolve tokenizer once; the server reports its own tokenizer path so
|
||||
# on-the-fly token-level slicing matches what the server will see.
|
||||
info = requests.get(cls.base_url + "/server_info", timeout=60).json()
|
||||
tokenizer_path = info.get("tokenizer_path") or DSV4_PRO_MODEL_PATH
|
||||
from sglang.srt.utils.hf_transformers_utils import get_tokenizer
|
||||
|
||||
cls.tokenizer = get_tokenizer(tokenizer_path)
|
||||
|
||||
cls.tmpdir = tempfile.mkdtemp(prefix="dsv4_hongloumeng_")
|
||||
cls.short_jsonl = _build_hongloumeng_jsonl(
|
||||
cls.SHORT_TOKENS,
|
||||
cls.tokenizer,
|
||||
os.path.join(cls.tmpdir, "hongloumeng_30k.jsonl"),
|
||||
)
|
||||
cls.long_jsonl = _build_hongloumeng_jsonl(
|
||||
cls.LONG_TOKENS,
|
||||
cls.tokenizer,
|
||||
os.path.join(cls.tmpdir, "hongloumeng_full.jsonl"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if hasattr(cls, "process") and cls.process:
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def _run_custom_bench(self, dataset_path):
|
||||
requests.get(self.base_url + "/flush_cache")
|
||||
args = SimpleNamespace(
|
||||
backend="sglang",
|
||||
base_url=self.base_url,
|
||||
host=None,
|
||||
port=None,
|
||||
dataset_name="custom",
|
||||
dataset_path=dataset_path,
|
||||
model=None,
|
||||
tokenizer=None,
|
||||
num_prompts=1,
|
||||
sharegpt_output_len=self.OUTPUT_TOKENS,
|
||||
sharegpt_context_len=None,
|
||||
random_input_len=4096,
|
||||
random_output_len=2048,
|
||||
random_range_ratio=0.0,
|
||||
request_rate=float("inf"),
|
||||
max_concurrency=1,
|
||||
warmup_requests=0,
|
||||
flush_cache=True,
|
||||
multi=None,
|
||||
output_file=None,
|
||||
disable_tqdm=False,
|
||||
disable_stream=False,
|
||||
return_logprob=False,
|
||||
return_routed_experts=False,
|
||||
seed=0,
|
||||
disable_ignore_eos=False,
|
||||
extra_request_body=None,
|
||||
apply_chat_template=False,
|
||||
profile=None,
|
||||
lora_name=None,
|
||||
lora_request_distribution="uniform",
|
||||
lora_zipf_alpha=1.5,
|
||||
prompt_suffix="",
|
||||
device="cuda",
|
||||
pd_separated=False,
|
||||
ready_check_timeout_sec=0,
|
||||
)
|
||||
return run_serving_benchmark(args)
|
||||
|
||||
def test_short_30k(self):
|
||||
res = self._run_custom_bench(self.short_jsonl)
|
||||
print(
|
||||
f"[hongloumeng 30k] output_throughput={res['output_throughput']:.2f} tok/s "
|
||||
f"accept_length={res['accept_length']:.2f} "
|
||||
f"mean_ttft_ms={res['mean_ttft_ms']:.0f} "
|
||||
f"mean_tpot_ms={res['mean_tpot_ms']:.2f}"
|
||||
)
|
||||
# Reference 124 tok/s / accept 2.47.
|
||||
self.assertGreater(res["output_throughput"], 105.0)
|
||||
self.assertGreater(res["accept_length"], 2.30)
|
||||
|
||||
def test_long_full(self):
|
||||
res = self._run_custom_bench(self.long_jsonl)
|
||||
print(
|
||||
f"[hongloumeng full] output_throughput={res['output_throughput']:.2f} tok/s "
|
||||
f"accept_length={res['accept_length']:.2f} "
|
||||
f"mean_ttft_ms={res['mean_ttft_ms']:.0f} "
|
||||
f"mean_tpot_ms={res['mean_tpot_ms']:.2f}"
|
||||
)
|
||||
# Reference 125 tok/s / accept 2.52. Cold prefill takes ~85s on 627k
|
||||
# tokens so the run is dominated by prefill, but decode steady-state
|
||||
# accept_length is the metric we care about.
|
||||
self.assertGreater(res["output_throughput"], 105.0)
|
||||
self.assertGreater(res["accept_length"], 2.30)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,165 @@
|
||||
"""DSV4 stress test for SWA radix cache + tombstone + retract interaction.
|
||||
|
||||
Reproduces the assert in `swa_radix_cache.cache_unfinished_req`:
|
||||
assert old_prefix_len <= len(new_indices)
|
||||
|
||||
Trip conditions (all required):
|
||||
1. Fork-only SWA leaf early-release on (`SGLANG_OPT_SWA_RELEASE_LEAF_LOCK_AFTER_WINDOW=1`)
|
||||
2. Multiple requests share a long prefix (so one req's tombstoned leaf
|
||||
poisons match_prefix for others walking the same radix path).
|
||||
3. Memory pressure forces retract while at least one req has tombstoned
|
||||
its leaf (decode_batch_idx >= sliding_window_size at retract time).
|
||||
|
||||
After main #19427 changed `old_prefix_len = req.cache_protected_len`
|
||||
(stable), tombstone-induced shrinks in match's `best_value_len` across
|
||||
chunked-prefill rounds can make stale `cache_protected_len` exceed
|
||||
current matchable length -> assert trips.
|
||||
|
||||
Test passes iff the scheduler does not crash under this stress workload.
|
||||
"""
|
||||
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
DSV4_FLASH_MODEL_PATH = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
|
||||
# Long shared prefix forces multi-chunk prefill and ensures cross-request
|
||||
# prefix-cache hits so one req's tombstone affects later reqs.
|
||||
SHARED_PREFIX_BLOCK = (
|
||||
"You are a careful, expert assistant. Answer concisely.\n"
|
||||
"Context: " + ("the quick brown fox jumps over the lazy dog. " * 600)
|
||||
)
|
||||
|
||||
QUESTION_TAILS = [
|
||||
" Q: What is 17*23?\n",
|
||||
" Q: List three primary colors.\n",
|
||||
" Q: Where is Mount Everest?\n",
|
||||
" Q: Summarize gradient descent in two sentences.\n",
|
||||
" Q: Name two bodies of water in Africa.\n",
|
||||
" Q: What language is spoken in Brazil?\n",
|
||||
]
|
||||
|
||||
|
||||
class TestDSV4FlashSWARadixRetract(CustomTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = DSV4_FLASH_MODEL_PATH
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
other_args = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
'{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}',
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
# Tight static memory so SWA pool fills up under load and
|
||||
# retract is forced.
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
]
|
||||
env = {
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
"SGLANG_OPT_SWA_RADIX_CACHE_COMPACT": "0",
|
||||
"SGLANG_TEST_RETRACT": "1",
|
||||
"SGLANG_TEST_RETRACT_INTERVAL": "3",
|
||||
}
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=other_args,
|
||||
env=env,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def _send_req(self, prompt: str, max_new_tokens: int):
|
||||
try:
|
||||
resp = requests.post(
|
||||
self.base_url + "/generate",
|
||||
json={
|
||||
"text": prompt,
|
||||
"sampling_params": {
|
||||
# Vary outputs slightly so reqs don't share decode
|
||||
# paths perfectly; we want some to finish, some to
|
||||
# be retracted under pressure.
|
||||
"temperature": 0.7,
|
||||
"max_new_tokens": max_new_tokens,
|
||||
},
|
||||
},
|
||||
timeout=600,
|
||||
)
|
||||
# Per-request success is not the gate; some requests are
|
||||
# expected to be retracted/aborted under heavy pressure.
|
||||
return resp.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def test_swa_tombstone_retract_does_not_crash(self):
|
||||
"""Stress: 64 concurrent long-prompt reqs with long generation force
|
||||
retract under SWA pool pressure. Reqs share a 30k+ token prefix so
|
||||
tombstoned leaves from retracted reqs are on the radix path of new
|
||||
reqs. Scheduler must not crash on the swa_radix_cache assert."""
|
||||
|
||||
random.seed(0)
|
||||
concurrency = 64
|
||||
# Long enough generation to push past sliding_window_size -> fires
|
||||
# `dec_swa_lock_only` -> tombstones leaves. Combined with SWA pool
|
||||
# pressure this guarantees retract while tombstones are live.
|
||||
max_new_tokens = 1024
|
||||
|
||||
threads = []
|
||||
for i in range(concurrency):
|
||||
tail = QUESTION_TAILS[i % len(QUESTION_TAILS)]
|
||||
# Add a small per-req suffix so reqs don't dedup at radix root
|
||||
# but still share the bulk of the prefix.
|
||||
prompt = SHARED_PREFIX_BLOCK + tail + f"(seed={i})"
|
||||
t = threading.Thread(target=self._send_req, args=(prompt, max_new_tokens))
|
||||
threads.append(t)
|
||||
t.start()
|
||||
# Stagger so requests enter prefill in waves; some are still in
|
||||
# decode (and have tombstoned leaves) when later waves of
|
||||
# chunked-prefill reqs walk the same radix path.
|
||||
time.sleep(0.05)
|
||||
|
||||
for t in threads:
|
||||
t.join(timeout=600)
|
||||
|
||||
# The only invariant: scheduler survived. Per-request completion is
|
||||
# best-effort under retract pressure.
|
||||
self.assertIsNone(self.process.poll())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,109 @@
|
||||
"""GB300 x DeepSeek-V4-Flash.
|
||||
|
||||
Single-node TP=4 path on the deepseek-ai MXFP4 repo. Covers
|
||||
Low-Latency, Balanced, Max-Throughput, Context-Parallel (CP).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DEEPEP_LARGE_SMS_CONFIG, DSV4FlashAime25TestBase
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
|
||||
|
||||
class TestGB300FlashLowLatency(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
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",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestGB300FlashBalanced(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024"}
|
||||
|
||||
|
||||
class TestGB300FlashMaxThroughput(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024"}
|
||||
|
||||
|
||||
class TestGB300FlashCP(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--enable-nsa-prefill-context-parallel",
|
||||
"--nsa-prefill-cp-mode",
|
||||
"round-robin-split",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--mem-fraction-static",
|
||||
"0.78",
|
||||
"--max-running-requests",
|
||||
"1024",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
"SGLANG_OPT_USE_JIT_INDEXER_METADATA": "1",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,127 @@
|
||||
"""GB300 x DeepSeek-V4-Pro.
|
||||
|
||||
Single-node TP=4 path. Note that GB300 Pro CP bumps
|
||||
mem-fraction-static to 0.88 (1.6T weights at TP=4 on 273 GB don't
|
||||
fit at the default 0.78). Covers Low-Latency, Balanced,
|
||||
Max-Throughput, Context-Parallel (CP).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DEEPEP_LARGE_SMS_CONFIG, DSV4ProAime25TestBase
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Pro"
|
||||
|
||||
|
||||
class TestGB300ProLowLatency(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
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",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestGB300ProBalanced(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--mem-fraction-static",
|
||||
"0.9",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256"}
|
||||
|
||||
|
||||
class TestGB300ProMaxThroughput(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--mem-fraction-static",
|
||||
"0.9",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256"}
|
||||
|
||||
|
||||
class TestGB300ProCP(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--enable-nsa-prefill-context-parallel",
|
||||
"--nsa-prefill-cp-mode",
|
||||
"round-robin-split",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
"--cuda-graph-max-bs",
|
||||
"256",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
"SGLANG_OPT_USE_JIT_INDEXER_METADATA": "1",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,71 @@
|
||||
"""H200 (FP4 / Marlin) x DeepSeek-V4-Flash.
|
||||
|
||||
The cookbook disables Context-Parallel for the H200 FP4 (Marlin)
|
||||
hardware, so this file only covers Low-Latency, Balanced, and
|
||||
Max-Throughput.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DSV4FlashAime25TestBase
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
|
||||
|
||||
class TestH200Fp4FlashLowLatency(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-runner-backend",
|
||||
"marlin",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestH200Fp4FlashBalanced(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-runner-backend",
|
||||
"marlin",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestH200Fp4FlashMaxThroughput(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-runner-backend",
|
||||
"marlin",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,77 @@
|
||||
"""H200 (FP4 / Marlin) x DeepSeek-V4-Pro.
|
||||
|
||||
The cookbook disables Context-Parallel for the H200 FP4 (Marlin)
|
||||
hardware, so this file only covers Low-Latency, Balanced, and
|
||||
Max-Throughput.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DSV4ProAime25TestBase
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Pro"
|
||||
|
||||
|
||||
class TestH200Fp4ProLowLatency(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--moe-runner-backend",
|
||||
"marlin",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestH200Fp4ProBalanced(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--moe-runner-backend",
|
||||
"marlin",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
class TestH200Fp4ProMaxThroughput(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"8",
|
||||
"--moe-runner-backend",
|
||||
"marlin",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
]
|
||||
EXTRA_ENV = {}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,121 @@
|
||||
"""H200 (FP8) x DeepSeek-V4-Flash.
|
||||
|
||||
Uses the FP8-repackaged repo (sgl-project/DeepSeek-V4-Flash-FP8) and
|
||||
the SGLANG_DSV4_FP4_EXPERTS=0 env that the cookbook generator emits
|
||||
for H200 FP8 cells. Covers Low-Latency, Balanced, Max-Throughput, CP.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DEEPEP_LARGE_SMS_CONFIG, DSV4FlashAime25TestBase
|
||||
|
||||
MODEL = "sgl-project/DeepSeek-V4-Flash-FP8"
|
||||
H200_FP8_ENV = {"SGLANG_DSV4_FP4_EXPERTS": "0"}
|
||||
|
||||
|
||||
class TestH200Fp8FlashLowLatency(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
]
|
||||
EXTRA_ENV = dict(H200_FP8_ENV)
|
||||
|
||||
|
||||
class TestH200Fp8FlashBalanced(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"128",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
**H200_FP8_ENV,
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256",
|
||||
}
|
||||
|
||||
|
||||
class TestH200Fp8FlashMaxThroughput(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
**H200_FP8_ENV,
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "256",
|
||||
}
|
||||
|
||||
|
||||
class TestH200Fp8FlashCP(DSV4FlashAime25TestBase):
|
||||
MODEL = MODEL
|
||||
OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"4",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--enable-nsa-prefill-context-parallel",
|
||||
"--nsa-prefill-cp-mode",
|
||||
"round-robin-split",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--mem-fraction-static",
|
||||
"0.78",
|
||||
"--max-running-requests",
|
||||
"1024",
|
||||
"--deepep-config",
|
||||
DEEPEP_LARGE_SMS_CONFIG,
|
||||
]
|
||||
EXTRA_ENV = {
|
||||
**H200_FP8_ENV,
|
||||
"SGLANG_OPT_USE_JIT_INDEXER_METADATA": "1",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""H200 (FP8) x DeepSeek-V4-Pro.
|
||||
|
||||
The cookbook ships this cell as a multi-node (2 nodes, TP=16) launch
|
||||
using the FP8-repackaged repo (sgl-project/DeepSeek-V4-Pro-FP8).
|
||||
Each test class skips itself unless DSV4_NODE_RANK and
|
||||
DSV4_DIST_INIT_ADDR are exported. Runtime expectation:
|
||||
|
||||
On every node:
|
||||
DSV4_NODE_RANK=<0 or 1> \\
|
||||
DSV4_DIST_INIT_ADDR=<head-node-ip>:20000 \\
|
||||
python test/manual/models/dsv4/test_h200_fp8_pro.py
|
||||
|
||||
Context-Parallel is marked TBD in the cookbook for this cell, so it
|
||||
is intentionally omitted.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _common import DSV4ProAime25TestBase, multinode_args
|
||||
|
||||
MODEL = "sgl-project/DeepSeek-V4-Pro-FP8"
|
||||
H200_FP8_PRO_ENV = {
|
||||
"SGLANG_DSV4_FP4_EXPERTS": "0",
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "128",
|
||||
}
|
||||
|
||||
|
||||
class TestH200Fp8ProLowLatency(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
EXTRA_ENV = dict(H200_FP8_PRO_ENV)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"16",
|
||||
"--dp",
|
||||
"16",
|
||||
"--enable-dp-attention",
|
||||
*multinode_args(2),
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--cuda-graph-max-bs",
|
||||
"8",
|
||||
"--max-running-requests",
|
||||
"32",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
]
|
||||
super().setUpClass()
|
||||
|
||||
|
||||
class TestH200Fp8ProBalanced(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
EXTRA_ENV = dict(H200_FP8_PRO_ENV)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"16",
|
||||
"--dp",
|
||||
"16",
|
||||
"--enable-dp-attention",
|
||||
*multinode_args(2),
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
"--cuda-graph-max-bs",
|
||||
"8",
|
||||
"--max-running-requests",
|
||||
"32",
|
||||
]
|
||||
super().setUpClass()
|
||||
|
||||
|
||||
class TestH200Fp8ProMaxThroughput(DSV4ProAime25TestBase):
|
||||
MODEL = MODEL
|
||||
EXTRA_ENV = dict(H200_FP8_PRO_ENV)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.OTHER_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--tp",
|
||||
"16",
|
||||
"--dp",
|
||||
"16",
|
||||
"--enable-dp-attention",
|
||||
*multinode_args(2),
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--mem-fraction-static",
|
||||
"0.88",
|
||||
"--cuda-graph-max-bs",
|
||||
"128",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
]
|
||||
super().setUpClass()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Regression for SWA alloc_extend page estimation.
|
||||
|
||||
Old gate in SWATokenToKVPoolAllocator.alloc_extend added one full page_size
|
||||
per request unconditionally, refusing extends that fit inside the request's
|
||||
last partial page. Fix replaces with get_num_new_pages-based gating.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.swa_memory_pool import SWATokenToKVPoolAllocator
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
|
||||
def _make_self(*, page_size: int, full_available: int, swa_available: int):
|
||||
full_indices = torch.tensor([10, 11], dtype=torch.int64)
|
||||
swa_indices = torch.tensor([20, 21], dtype=torch.int64)
|
||||
return SimpleNamespace(
|
||||
page_size=page_size,
|
||||
full_attn_allocator=SimpleNamespace(
|
||||
available_size=lambda: full_available,
|
||||
alloc_extend=MagicMock(return_value=full_indices),
|
||||
),
|
||||
swa_attn_allocator=SimpleNamespace(
|
||||
available_size=lambda: swa_available,
|
||||
alloc_extend=MagicMock(return_value=swa_indices),
|
||||
),
|
||||
translate_loc_from_full_to_swa=lambda last_loc: last_loc,
|
||||
full_to_swa_index_mapping=torch.zeros(64, dtype=torch.int64),
|
||||
)
|
||||
|
||||
|
||||
def _call(stub, *, prefix_lens_cpu, seq_lens_cpu, extend_num_tokens):
|
||||
return SWATokenToKVPoolAllocator.alloc_extend(
|
||||
stub,
|
||||
prefix_lens=prefix_lens_cpu,
|
||||
prefix_lens_cpu=prefix_lens_cpu,
|
||||
seq_lens=seq_lens_cpu,
|
||||
seq_lens_cpu=seq_lens_cpu,
|
||||
last_loc=torch.tensor(
|
||||
[int(p) - 1 for p in prefix_lens_cpu.tolist()], dtype=torch.int64
|
||||
),
|
||||
extend_num_tokens=extend_num_tokens,
|
||||
)
|
||||
|
||||
|
||||
class TestSWAAllocExtendPageEstimation(CustomTestCase):
|
||||
def test_zero_new_pages_must_succeed(self):
|
||||
# Old: 2 + 2*8 = 18 > 16 -> would refuse.
|
||||
# New: prefix 5 -> 6 stays in page 0, 0 new pages.
|
||||
stub = _make_self(page_size=8, full_available=16, swa_available=16)
|
||||
result = _call(
|
||||
stub,
|
||||
prefix_lens_cpu=torch.tensor([5, 5], dtype=torch.int64),
|
||||
seq_lens_cpu=torch.tensor([6, 6], dtype=torch.int64),
|
||||
extend_num_tokens=2,
|
||||
)
|
||||
self.assertIsNotNone(result)
|
||||
stub.full_attn_allocator.alloc_extend.assert_called_once()
|
||||
stub.swa_attn_allocator.alloc_extend.assert_called_once()
|
||||
|
||||
def test_one_new_page_fits(self):
|
||||
# Old: 6 + 2*8 = 22 > 16. New: 2 new pages == 16 // 8.
|
||||
stub = _make_self(page_size=8, full_available=16, swa_available=16)
|
||||
result = _call(
|
||||
stub,
|
||||
prefix_lens_cpu=torch.tensor([7, 7], dtype=torch.int64),
|
||||
seq_lens_cpu=torch.tensor([10, 10], dtype=torch.int64),
|
||||
extend_num_tokens=6,
|
||||
)
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_full_pool_genuinely_insufficient(self):
|
||||
stub = _make_self(page_size=8, full_available=8, swa_available=64)
|
||||
result = _call(
|
||||
stub,
|
||||
prefix_lens_cpu=torch.tensor([8, 8, 8, 8, 8], dtype=torch.int64),
|
||||
seq_lens_cpu=torch.tensor([9, 9, 9, 9, 9], dtype=torch.int64),
|
||||
extend_num_tokens=5,
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
stub.full_attn_allocator.alloc_extend.assert_not_called()
|
||||
|
||||
def test_swa_pool_genuinely_insufficient(self):
|
||||
stub = _make_self(page_size=8, full_available=64, swa_available=8)
|
||||
result = _call(
|
||||
stub,
|
||||
prefix_lens_cpu=torch.tensor([8, 8, 8, 8, 8], dtype=torch.int64),
|
||||
seq_lens_cpu=torch.tensor([9, 9, 9, 9, 9], dtype=torch.int64),
|
||||
extend_num_tokens=5,
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
stub.swa_attn_allocator.alloc_extend.assert_not_called()
|
||||
|
||||
def test_exactly_at_capacity_succeeds(self):
|
||||
stub = _make_self(page_size=8, full_available=16, swa_available=16)
|
||||
result = _call(
|
||||
stub,
|
||||
prefix_lens_cpu=torch.tensor([8, 8], dtype=torch.int64),
|
||||
seq_lens_cpu=torch.tensor([9, 9], dtype=torch.int64),
|
||||
extend_num_tokens=2,
|
||||
)
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_one_over_capacity_refuses(self):
|
||||
stub = _make_self(page_size=8, full_available=16, swa_available=16)
|
||||
result = _call(
|
||||
stub,
|
||||
prefix_lens_cpu=torch.tensor([8, 8, 8], dtype=torch.int64),
|
||||
seq_lens_cpu=torch.tensor([9, 9, 9], dtype=torch.int64),
|
||||
extend_num_tokens=3,
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_zero_new_pages_across_page_sizes(self):
|
||||
# Over-estimation gap grows with page_size; sweep to confirm fix
|
||||
# doesn't depend on the page_size=8 numbers above.
|
||||
for page_size in (16, 32, 64, 128):
|
||||
stub = _make_self(
|
||||
page_size=page_size,
|
||||
full_available=page_size * 2,
|
||||
swa_available=page_size * 2,
|
||||
)
|
||||
prefix = torch.tensor([page_size - 2] * 4, dtype=torch.int64)
|
||||
seq = torch.tensor([page_size - 1] * 4, dtype=torch.int64)
|
||||
result = _call(
|
||||
stub, prefix_lens_cpu=prefix, seq_lens_cpu=seq, extend_num_tokens=4
|
||||
)
|
||||
self.assertIsNotNone(result, f"page_size={page_size}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,463 @@
|
||||
"""Regression for SWA lock release lifecycle.
|
||||
|
||||
Hybrid-SWA early-release protocol: once a request's decode position passes
|
||||
the sliding window, drop its prefill SWA lock without touching the full
|
||||
lock, freeing SWA pages back to LRU.
|
||||
|
||||
Covers:
|
||||
- SWARadixCache.dec_swa_lock_only (leaf tombstone + free, internal protected->evictable)
|
||||
- SWARadixCache.dec_lock_ref(skip_swa=True)
|
||||
- SWARadixCache.evict swa branch for leaf with full_lock_ref > 0
|
||||
- SWARadixCache._delete_leaf skipping swa_evictable_size_ on tombstoned leaves
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
DecLockRefParams,
|
||||
EvictParams,
|
||||
InsertParams,
|
||||
MatchPrefixParams,
|
||||
)
|
||||
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
|
||||
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
|
||||
from sglang.srt.mem_cache.radix_cache import RadixKey
|
||||
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool, SWATokenToKVPoolAllocator
|
||||
from sglang.srt.mem_cache.swa_radix_cache import SWARadixCache
|
||||
from sglang.srt.utils import get_device
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
|
||||
def _build_tree(
|
||||
*,
|
||||
sliding_window_size: int = 4,
|
||||
page_size: int = 1,
|
||||
kv_size: int = 128,
|
||||
kv_size_swa: int = 64,
|
||||
):
|
||||
head_num, head_dim, num_layers, global_interval = 8, 128, 24, 4
|
||||
dtype = torch.bfloat16
|
||||
device = get_device()
|
||||
full_ids = list(range(0, num_layers, global_interval))
|
||||
swa_ids = [i for i in range(num_layers) if i not in set(full_ids)]
|
||||
|
||||
pool = ReqToTokenPool(
|
||||
size=8, max_context_len=256, device=device, enable_memory_saver=False
|
||||
)
|
||||
kv_pool = SWAKVPool(
|
||||
size=kv_size,
|
||||
size_swa=kv_size_swa,
|
||||
page_size=page_size,
|
||||
dtype=dtype,
|
||||
head_num=head_num,
|
||||
head_dim=head_dim,
|
||||
swa_attention_layer_ids=swa_ids,
|
||||
full_attention_layer_ids=full_ids,
|
||||
enable_kvcache_transpose=False,
|
||||
device=device,
|
||||
)
|
||||
allocator = SWATokenToKVPoolAllocator(
|
||||
size=kv_size,
|
||||
size_swa=kv_size_swa,
|
||||
page_size=page_size,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
kvcache=kv_pool,
|
||||
need_sort=False,
|
||||
)
|
||||
tree = SWARadixCache(
|
||||
params=CacheInitParams(
|
||||
req_to_token_pool=pool,
|
||||
token_to_kv_pool_allocator=allocator,
|
||||
page_size=page_size,
|
||||
disable=False,
|
||||
is_eagle=False,
|
||||
sliding_window_size=sliding_window_size,
|
||||
),
|
||||
)
|
||||
return tree, allocator, pool
|
||||
|
||||
|
||||
def _swa_alloc(allocator, need_size):
|
||||
"""Allocate from SWA allocator for any page_size.
|
||||
|
||||
SWATokenToKVPoolAllocator.alloc() asserts page_size == 1; for page_size > 1
|
||||
we drive the underlying paged allocators directly (mirrors the helper in
|
||||
test_swa_eviction_boundary.py). Required: need_size is a multiple of
|
||||
page_size when page_size > 1.
|
||||
"""
|
||||
if allocator.page_size == 1:
|
||||
return allocator.alloc(need_size)
|
||||
|
||||
assert need_size % allocator.page_size == 0, (
|
||||
f"page_size > 1 requires page-aligned alloc, got {need_size=} "
|
||||
f"with {allocator.page_size=}"
|
||||
)
|
||||
if need_size > allocator.full_attn_allocator.available_size():
|
||||
return None
|
||||
if need_size > allocator.swa_attn_allocator.available_size():
|
||||
return None
|
||||
full_indices = allocator.full_attn_allocator.alloc(need_size)
|
||||
swa_indices = allocator.swa_attn_allocator.alloc(need_size)
|
||||
assert full_indices is not None and swa_indices is not None
|
||||
allocator.full_to_swa_index_mapping[full_indices] = swa_indices
|
||||
return full_indices
|
||||
|
||||
|
||||
def _insert_chain(tree, allocator, token_ids):
|
||||
indices = _swa_alloc(allocator, len(token_ids))
|
||||
assert indices is not None
|
||||
tree.insert(InsertParams(key=RadixKey(token_ids), value=indices))
|
||||
match = tree.match_prefix(MatchPrefixParams(key=RadixKey(token_ids)))
|
||||
return match.last_device_node
|
||||
|
||||
|
||||
def _release_swa_lock_chain_in_place(tree, leaf, swa_uuid_for_lock):
|
||||
# Mirrors dec_swa_lock_only's non-tombstone arm (protected->evictable on
|
||||
# internal nodes) but skips the leaf-free + tombstone step, to construct
|
||||
# the post-revival state where SWA was already early-released yet the
|
||||
# leaf is back in swa_lru_list with full_lock_ref still > 0.
|
||||
node = leaf
|
||||
while node is not tree.root_node:
|
||||
if node.swa_lock_ref > 0:
|
||||
if node.swa_lock_ref == 1:
|
||||
tree.swa_protected_size_ -= len(node.value)
|
||||
tree.swa_evictable_size_ += len(node.value)
|
||||
node.swa_lock_ref -= 1
|
||||
if swa_uuid_for_lock and node.swa_uuid == swa_uuid_for_lock:
|
||||
break
|
||||
node = node.parent
|
||||
|
||||
|
||||
class TestSWALockReleaseLifecycle(CustomTestCase):
|
||||
"""Each test pins one component of the early-release fix; method names
|
||||
are prefixed with the API surface they exercise so pytest output groups
|
||||
them naturally."""
|
||||
|
||||
def test_dec_swa_lock_only_leaf_tombstones_and_frees(self):
|
||||
tree, allocator, _ = _build_tree(sliding_window_size=4)
|
||||
leaf = _insert_chain(tree, allocator, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
self.assertEqual(len(leaf.value), 8)
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf)
|
||||
swa_uuid = inc_res.swa_uuid_for_lock
|
||||
self.assertIsNotNone(swa_uuid)
|
||||
|
||||
swa_avail_before = allocator.swa_available_size()
|
||||
full_avail_before = allocator.full_available_size()
|
||||
self.assertEqual(leaf.swa_lock_ref, 1)
|
||||
self.assertEqual(leaf.full_lock_ref, 1)
|
||||
self.assertFalse(leaf.swa_tombstone)
|
||||
self.assertTrue(tree.swa_lru_list.in_list(leaf))
|
||||
|
||||
tree.dec_swa_lock_only(leaf, swa_uuid_for_lock=swa_uuid)
|
||||
|
||||
self.assertTrue(leaf.swa_tombstone)
|
||||
self.assertFalse(tree.swa_lru_list.in_list(leaf))
|
||||
self.assertEqual(leaf.swa_lock_ref, 0)
|
||||
self.assertEqual(
|
||||
allocator.swa_available_size(), swa_avail_before + len(leaf.value)
|
||||
)
|
||||
self.assertEqual(leaf.full_lock_ref, 1)
|
||||
self.assertEqual(allocator.full_available_size(), full_avail_before)
|
||||
|
||||
# sanity_check forbids live locks; release the full half before checking.
|
||||
tree.dec_lock_ref(
|
||||
leaf, DecLockRefParams(swa_uuid_for_lock=swa_uuid), skip_swa=True
|
||||
)
|
||||
tree.sanity_check()
|
||||
|
||||
def test_dec_swa_lock_only_internal_no_tombstone_no_free(self):
|
||||
# Two siblings force an internal node at the shared prefix.
|
||||
tree, allocator, _ = _build_tree(sliding_window_size=4)
|
||||
leaf_a = _insert_chain(tree, allocator, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
_insert_chain(tree, allocator, [1, 2, 3, 4, 5, 6, 7, 9])
|
||||
|
||||
# Post-split: leaf_a now carries [8] only, parent holds the shared 7.
|
||||
self.assertEqual(len(leaf_a.value), 1)
|
||||
internal = leaf_a.parent
|
||||
self.assertGreater(len(internal.children), 1)
|
||||
self.assertEqual(len(internal.value), 7)
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf_a)
|
||||
swa_uuid = inc_res.swa_uuid_for_lock
|
||||
# window=4, value 1 (leaf) + 7 (internal): swa lock chain ends at internal.
|
||||
self.assertEqual(swa_uuid, internal.swa_uuid)
|
||||
|
||||
swa_protected_before = tree.swa_protected_size_
|
||||
swa_evictable_before = tree.swa_evictable_size_
|
||||
swa_avail_before = allocator.swa_available_size()
|
||||
|
||||
tree.dec_swa_lock_only(leaf_a, swa_uuid_for_lock=swa_uuid)
|
||||
|
||||
self.assertFalse(internal.swa_tombstone)
|
||||
self.assertTrue(tree.swa_lru_list.in_list(internal))
|
||||
self.assertEqual(internal.swa_lock_ref, 0)
|
||||
self.assertEqual(
|
||||
tree.swa_protected_size_, swa_protected_before - (len(leaf_a.value) + 7)
|
||||
)
|
||||
self.assertEqual(tree.swa_evictable_size_, swa_evictable_before + 7)
|
||||
self.assertEqual(
|
||||
allocator.swa_available_size(), swa_avail_before + len(leaf_a.value)
|
||||
)
|
||||
|
||||
tree.dec_lock_ref(
|
||||
leaf_a, DecLockRefParams(swa_uuid_for_lock=swa_uuid), skip_swa=True
|
||||
)
|
||||
tree.sanity_check()
|
||||
|
||||
def test_dec_lock_ref_skip_swa_true_drops_full_only(self):
|
||||
tree, allocator, _ = _build_tree(sliding_window_size=4)
|
||||
leaf = _insert_chain(tree, allocator, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf)
|
||||
swa_uuid = inc_res.swa_uuid_for_lock
|
||||
|
||||
tree.dec_swa_lock_only(leaf, swa_uuid_for_lock=swa_uuid)
|
||||
self.assertTrue(leaf.swa_tombstone)
|
||||
self.assertEqual(leaf.full_lock_ref, 1)
|
||||
|
||||
swa_avail_after_release = allocator.swa_available_size()
|
||||
swa_protected_after_release = tree.swa_protected_size_
|
||||
|
||||
# Without skip_swa, dec_lock_ref would assert on the swa_tombstone leaf.
|
||||
tree.dec_lock_ref(
|
||||
leaf, DecLockRefParams(swa_uuid_for_lock=swa_uuid), skip_swa=True
|
||||
)
|
||||
|
||||
self.assertEqual(leaf.full_lock_ref, 0)
|
||||
self.assertEqual(allocator.swa_available_size(), swa_avail_after_release)
|
||||
self.assertEqual(tree.swa_protected_size_, swa_protected_after_release)
|
||||
tree.sanity_check()
|
||||
|
||||
def test_dec_lock_ref_skip_swa_false_drops_both(self):
|
||||
# Default skip_swa=False must keep legacy behavior intact.
|
||||
tree, allocator, _ = _build_tree(sliding_window_size=4)
|
||||
leaf = _insert_chain(tree, allocator, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf)
|
||||
swa_uuid = inc_res.swa_uuid_for_lock
|
||||
|
||||
full_avail_before = allocator.full_available_size()
|
||||
swa_avail_before = allocator.swa_available_size()
|
||||
|
||||
tree.dec_lock_ref(leaf, DecLockRefParams(swa_uuid_for_lock=swa_uuid))
|
||||
|
||||
self.assertEqual(leaf.full_lock_ref, 0)
|
||||
self.assertEqual(leaf.swa_lock_ref, 0)
|
||||
self.assertEqual(tree.full_protected_size_, 0)
|
||||
self.assertEqual(tree.swa_protected_size_, 0)
|
||||
# dec_lock_ref releases locks but doesn't free; eviction does.
|
||||
self.assertEqual(allocator.full_available_size(), full_avail_before)
|
||||
self.assertEqual(allocator.swa_available_size(), swa_avail_before)
|
||||
tree.sanity_check()
|
||||
|
||||
def test_evict_swa_leaf_with_full_lock_tombstones_in_place(self):
|
||||
# Large window so inc_lock_ref locks the entire SWA chain.
|
||||
tree, allocator, _ = _build_tree(sliding_window_size=64)
|
||||
leaf = _insert_chain(tree, allocator, [1, 2, 3, 4])
|
||||
self.assertEqual(len(leaf.value), 4)
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf)
|
||||
_release_swa_lock_chain_in_place(tree, leaf, inc_res.swa_uuid_for_lock)
|
||||
|
||||
self.assertEqual(leaf.full_lock_ref, 1)
|
||||
self.assertEqual(leaf.swa_lock_ref, 0)
|
||||
self.assertFalse(leaf.swa_tombstone)
|
||||
self.assertTrue(tree.swa_lru_list.in_list(leaf))
|
||||
|
||||
swa_avail_before = allocator.swa_available_size()
|
||||
swa_evictable_before = tree.swa_evictable_size_
|
||||
|
||||
# num_tokens=0 skips the full eviction loop; swa loop hits the new branch.
|
||||
evict_res = tree.evict(EvictParams(num_tokens=0, swa_num_tokens=4))
|
||||
|
||||
self.assertGreaterEqual(evict_res.swa_num_tokens_evicted, 4)
|
||||
self.assertTrue(leaf.swa_tombstone)
|
||||
self.assertFalse(tree.swa_lru_list.in_list(leaf))
|
||||
self.assertEqual(leaf.full_lock_ref, 1)
|
||||
self.assertEqual(
|
||||
allocator.swa_available_size(), swa_avail_before + len(leaf.value)
|
||||
)
|
||||
# Full lock prevents _delete_leaf, so the node stays attached.
|
||||
self.assertIs(leaf.parent.children[leaf.key.child_key(tree.page_size)], leaf)
|
||||
self.assertEqual(
|
||||
tree.swa_evictable_size_, swa_evictable_before - len(leaf.value)
|
||||
)
|
||||
|
||||
tree.dec_lock_ref(
|
||||
leaf,
|
||||
DecLockRefParams(swa_uuid_for_lock=inc_res.swa_uuid_for_lock),
|
||||
skip_swa=True,
|
||||
)
|
||||
tree.sanity_check()
|
||||
|
||||
def test_delete_leaf_skips_swa_size_on_tombstone(self):
|
||||
# Tombstone removes the count once; _delete_leaf must not subtract again.
|
||||
tree, allocator, _ = _build_tree(sliding_window_size=4)
|
||||
leaf = _insert_chain(tree, allocator, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf)
|
||||
swa_uuid = inc_res.swa_uuid_for_lock
|
||||
|
||||
tree.dec_swa_lock_only(leaf, swa_uuid_for_lock=swa_uuid)
|
||||
self.assertTrue(leaf.swa_tombstone)
|
||||
|
||||
swa_evictable_before_delete = tree.swa_evictable_size_
|
||||
tree.full_lru_list.remove_node(leaf)
|
||||
tree._delete_leaf(leaf)
|
||||
|
||||
self.assertEqual(tree.swa_evictable_size_, swa_evictable_before_delete)
|
||||
|
||||
def test_dec_swa_lock_only_leaf_page_size_variants(self):
|
||||
"""Single-leaf tombstone+free across all (page_size, window) regimes.
|
||||
|
||||
Sweep covers:
|
||||
- window multiple of page_size (page_size=2, window=4)
|
||||
- page_size > window (page_size=8, window=4)
|
||||
- window not multiple of page (page_size=4, window=6)
|
||||
|
||||
With page_size > 1, _swa_alloc routes through the paged allocators;
|
||||
free_swa(leaf.value) must release exactly len(leaf.value) tokens
|
||||
(page-aligned) regardless of how page_size relates to the window.
|
||||
"""
|
||||
for page_size, window in [(2, 4), (8, 4), (4, 6)]:
|
||||
with self.subTest(page_size=page_size, window=window):
|
||||
tree, allocator, _ = _build_tree(
|
||||
sliding_window_size=window,
|
||||
page_size=page_size,
|
||||
kv_size=max(128, 32 * page_size),
|
||||
kv_size_swa=max(64, 16 * page_size),
|
||||
)
|
||||
n_tokens = max(window, 2 * page_size)
|
||||
n_tokens = (n_tokens + page_size - 1) // page_size * page_size
|
||||
leaf = _insert_chain(tree, allocator, list(range(1, n_tokens + 1)))
|
||||
self.assertEqual(len(leaf.value), n_tokens)
|
||||
self.assertEqual(len(leaf.value) % page_size, 0)
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf)
|
||||
swa_uuid = inc_res.swa_uuid_for_lock
|
||||
self.assertIsNotNone(
|
||||
swa_uuid,
|
||||
f"inc_lock_ref must reach the window with leaf.value="
|
||||
f"{len(leaf.value)} >= window={window}",
|
||||
)
|
||||
|
||||
swa_avail_before = allocator.swa_available_size()
|
||||
full_avail_before = allocator.full_available_size()
|
||||
|
||||
tree.dec_swa_lock_only(leaf, swa_uuid_for_lock=swa_uuid)
|
||||
|
||||
self.assertTrue(leaf.swa_tombstone)
|
||||
self.assertFalse(tree.swa_lru_list.in_list(leaf))
|
||||
self.assertEqual(leaf.swa_lock_ref, 0)
|
||||
self.assertEqual(
|
||||
allocator.swa_available_size(),
|
||||
swa_avail_before + len(leaf.value),
|
||||
"free_swa must release the leaf's full page-aligned slot count",
|
||||
)
|
||||
self.assertEqual(leaf.full_lock_ref, 1)
|
||||
self.assertEqual(allocator.full_available_size(), full_avail_before)
|
||||
|
||||
tree.dec_lock_ref(
|
||||
leaf,
|
||||
DecLockRefParams(swa_uuid_for_lock=swa_uuid),
|
||||
skip_swa=True,
|
||||
)
|
||||
tree.sanity_check()
|
||||
|
||||
def test_dec_swa_lock_only_internal_page_size_gt_1(self):
|
||||
"""Internal-node chain release with page_size > 1.
|
||||
|
||||
Two siblings sharing a page-aligned prefix force a radix split on a
|
||||
page boundary. The swa lock chain therefore spans leaf -> internal,
|
||||
and dec_swa_lock_only must:
|
||||
- tombstone the leaf and free len(leaf.value) SWA tokens
|
||||
- flip the internal node from protected -> evictable (no free,
|
||||
no tombstone)
|
||||
"""
|
||||
page_size, window = 2, 6
|
||||
tree, allocator, _ = _build_tree(
|
||||
sliding_window_size=window, page_size=page_size
|
||||
)
|
||||
# Shared prefix len 4 (2 pages); divergent suffix len 2 (1 page each).
|
||||
leaf_a = _insert_chain(tree, allocator, [1, 2, 3, 4, 5, 6])
|
||||
_insert_chain(tree, allocator, [1, 2, 3, 4, 7, 8])
|
||||
|
||||
self.assertEqual(len(leaf_a.value), 2)
|
||||
internal = leaf_a.parent
|
||||
self.assertGreater(len(internal.children), 1)
|
||||
self.assertEqual(len(internal.value), 4)
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf_a)
|
||||
swa_uuid = inc_res.swa_uuid_for_lock
|
||||
# leaf_a (2) + internal (4) = 6 >= window=6, so uuid stops at internal.
|
||||
self.assertEqual(swa_uuid, internal.swa_uuid)
|
||||
|
||||
swa_protected_before = tree.swa_protected_size_
|
||||
swa_evictable_before = tree.swa_evictable_size_
|
||||
swa_avail_before = allocator.swa_available_size()
|
||||
|
||||
tree.dec_swa_lock_only(leaf_a, swa_uuid_for_lock=swa_uuid)
|
||||
|
||||
# Leaf side: tombstoned and pages freed.
|
||||
self.assertTrue(leaf_a.swa_tombstone)
|
||||
self.assertFalse(tree.swa_lru_list.in_list(leaf_a))
|
||||
self.assertEqual(
|
||||
allocator.swa_available_size(),
|
||||
swa_avail_before + len(leaf_a.value),
|
||||
)
|
||||
# Internal side: protected -> evictable, still in lru, no free.
|
||||
self.assertFalse(internal.swa_tombstone)
|
||||
self.assertTrue(tree.swa_lru_list.in_list(internal))
|
||||
self.assertEqual(internal.swa_lock_ref, 0)
|
||||
self.assertEqual(
|
||||
tree.swa_protected_size_,
|
||||
swa_protected_before - (len(leaf_a.value) + len(internal.value)),
|
||||
)
|
||||
self.assertEqual(
|
||||
tree.swa_evictable_size_,
|
||||
swa_evictable_before + len(internal.value),
|
||||
)
|
||||
|
||||
tree.dec_lock_ref(
|
||||
leaf_a, DecLockRefParams(swa_uuid_for_lock=swa_uuid), skip_swa=True
|
||||
)
|
||||
tree.sanity_check()
|
||||
|
||||
def test_full_lifecycle_inc_dec_swa_dec_lock_balances(self):
|
||||
tree, allocator, _ = _build_tree(sliding_window_size=4)
|
||||
leaf = _insert_chain(tree, allocator, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
|
||||
full_protected0 = tree.full_protected_size_
|
||||
swa_protected0 = tree.swa_protected_size_
|
||||
full_avail0 = allocator.full_available_size()
|
||||
swa_avail0 = allocator.swa_available_size()
|
||||
|
||||
inc_res = tree.inc_lock_ref(leaf)
|
||||
swa_uuid = inc_res.swa_uuid_for_lock
|
||||
|
||||
self.assertGreater(tree.full_protected_size_, full_protected0)
|
||||
self.assertGreater(tree.swa_protected_size_, swa_protected0)
|
||||
|
||||
tree.dec_swa_lock_only(leaf, swa_uuid_for_lock=swa_uuid)
|
||||
|
||||
self.assertEqual(tree.swa_protected_size_, swa_protected0)
|
||||
self.assertGreater(tree.full_protected_size_, full_protected0)
|
||||
|
||||
tree.dec_lock_ref(
|
||||
leaf, DecLockRefParams(swa_uuid_for_lock=swa_uuid), skip_swa=True
|
||||
)
|
||||
|
||||
self.assertEqual(tree.full_protected_size_, full_protected0)
|
||||
self.assertEqual(tree.swa_protected_size_, swa_protected0)
|
||||
self.assertEqual(allocator.full_available_size(), full_avail0)
|
||||
self.assertEqual(allocator.swa_available_size(), swa_avail0 + len(leaf.value))
|
||||
|
||||
tree.sanity_check()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,82 @@
|
||||
"""B200 per-commit CI: DeepSeek-V4-Flash FP4 (LowLatency recipe).
|
||||
|
||||
Launches TP=4 with flashinfer_mxfp4 MoE runner + EAGLE speculative decoding.
|
||||
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)
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.server_sanity_kit import ServerSanityMixin
|
||||
from sglang.test.run_eval import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
try_cached_model,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=900, suite="stage-c-test-dsv4-4-gpu-b200")
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
SERVER_LAUNCH_TIMEOUT = 3600
|
||||
|
||||
|
||||
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):
|
||||
args = SimpleNamespace(
|
||||
base_url=self.base_url,
|
||||
model=self.model,
|
||||
eval_name="gsm8k",
|
||||
api="completion",
|
||||
max_tokens=512,
|
||||
num_examples=200,
|
||||
num_threads=128,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"[DSV4 Flash FP4 B200] GSM8K {metrics=}")
|
||||
self.assertGreater(metrics["score"], 0.93)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,133 @@
|
||||
"""B200 nightly CI: DeepSeek-V4-Flash FP4 (Balanced + MaxThroughput recipes).
|
||||
|
||||
Two server configurations exercise the DeepEP all-to-all + DP-attention path
|
||||
that the per-commit LowLatency test does not cover.
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.server_sanity_kit import ServerSanityMixin
|
||||
from sglang.test.run_eval import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
try_cached_model,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=3600, suite="nightly-4-gpu-b200", nightly=True)
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
SERVER_LAUNCH_TIMEOUT = 3600
|
||||
DEEPEP_CONFIG = '{"normal_dispatch":{"num_sms":96},"normal_combine":{"num_sms":96}}'
|
||||
|
||||
_DEEPEP_ENV = {
|
||||
"SGLANG_DEEPEP_NUM_MAX_DISPATCH_TOKENS_PER_RANK": "1024",
|
||||
}
|
||||
|
||||
|
||||
def _gsm8k_check(test_case):
|
||||
args = SimpleNamespace(
|
||||
base_url=test_case.base_url,
|
||||
model=test_case.model,
|
||||
eval_name="gsm8k",
|
||||
api="completion",
|
||||
max_tokens=512,
|
||||
num_examples=200,
|
||||
num_threads=128,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"[{type(test_case).__name__}] GSM8K {metrics=}")
|
||||
test_case.assertGreater(metrics["score"], 0.93)
|
||||
|
||||
|
||||
class TestDSV4FlashFP4B200Balanced(ServerSanityMixin, CustomTestCase):
|
||||
"""Balanced recipe: TP=4, DP=4, DeepEP, EAGLE (1-step spec)."""
|
||||
|
||||
@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",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"1",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"2",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
],
|
||||
env=_DEEPEP_ENV,
|
||||
)
|
||||
|
||||
@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 TestDSV4FlashFP4B200MaxThroughput(ServerSanityMixin, CustomTestCase):
|
||||
"""MaxThroughput recipe: TP=4, DP=4, DeepEP, no speculation."""
|
||||
|
||||
@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",
|
||||
"--dp",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--moe-a2a-backend",
|
||||
"deepep",
|
||||
"--deepep-config",
|
||||
DEEPEP_CONFIG,
|
||||
],
|
||||
env=_DEEPEP_ENV,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if hasattr(cls, "process") and cls.process:
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_gsm8k(self):
|
||||
_gsm8k_check(self)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""H200 per-commit CI: DeepSeek-V4-Flash FP4 Marlin (LowLatency recipe).
|
||||
|
||||
Launches TP=4 with Marlin FP4 MoE runner + EAGLE speculative decoding.
|
||||
Runs 12 ServerSanity probes (correctness, streaming, concurrency, determinism)
|
||||
plus a GSM8K accuracy gate.
|
||||
|
||||
Registry: stage-c-test-dsv4-8-gpu-h200 (per-commit, 8x H200 — only 4 used by TP=4)
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.server_sanity_kit import ServerSanityMixin
|
||||
from sglang.test.run_eval import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
try_cached_model,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=900, suite="stage-c-test-dsv4-8-gpu-h200")
|
||||
|
||||
MODEL = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
SERVER_LAUNCH_TIMEOUT = 3600
|
||||
|
||||
|
||||
class TestDSV4FlashFP4H200(ServerSanityMixin, CustomTestCase):
|
||||
"""LowLatency recipe: TP=4, Marlin FP4, 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",
|
||||
"marlin",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if hasattr(cls, "process") and cls.process:
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_gsm8k(self):
|
||||
args = SimpleNamespace(
|
||||
base_url=self.base_url,
|
||||
model=self.model,
|
||||
eval_name="gsm8k",
|
||||
api="completion",
|
||||
max_tokens=512,
|
||||
num_examples=200,
|
||||
num_threads=128,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"[DSV4 Flash FP4 Marlin H200] GSM8K {metrics=}")
|
||||
self.assertGreater(metrics["score"], 0.93)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -56,7 +56,7 @@ COMMON_ENV_VARS = {
|
||||
"SGLANG_TOPK_TRANSFORM_512_TORCH": "1",
|
||||
"SGLANG_OPT_USE_TILELANG_INDEXER": "true",
|
||||
"SGLANG_HACK_FLASHMLA_BACKEND": "tilelang",
|
||||
"SGLANG_REASONING_EFFORT": "max",
|
||||
"SGLANG_DSV4_REASONING_EFFORT": "max",
|
||||
}
|
||||
|
||||
# FP4 variant: FP4 mixed-precision experts.
|
||||
@@ -82,7 +82,7 @@ class TestDeepseekV4Fp4(CustomTestCase):
|
||||
"8",
|
||||
"--disable-radix-cache",
|
||||
"--attention-backend",
|
||||
"compressed",
|
||||
"dsv4",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--page-size",
|
||||
|
||||
@@ -56,7 +56,7 @@ COMMON_ENV_VARS = {
|
||||
"SGLANG_TOPK_TRANSFORM_512_TORCH": "1",
|
||||
"SGLANG_OPT_USE_TILELANG_INDEXER": "true",
|
||||
"SGLANG_HACK_FLASHMLA_BACKEND": "tilelang",
|
||||
"SGLANG_REASONING_EFFORT": "max",
|
||||
"SGLANG_DSV4_REASONING_EFFORT": "max",
|
||||
}
|
||||
|
||||
# FP8 variant: dense-FP8 experts via the Triton MoE FP8 path.
|
||||
@@ -82,7 +82,7 @@ class TestDeepseekV4Fp8(CustomTestCase):
|
||||
"8",
|
||||
"--disable-radix-cache",
|
||||
"--attention-backend",
|
||||
"compressed",
|
||||
"dsv4",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--page-size",
|
||||
|
||||
@@ -58,7 +58,7 @@ COMMON_ENV_VARS = {
|
||||
"SGLANG_TOPK_TRANSFORM_512_TORCH": "1",
|
||||
"SGLANG_OPT_USE_TILELANG_INDEXER": "true",
|
||||
"SGLANG_HACK_FLASHMLA_BACKEND": "tilelang",
|
||||
"SGLANG_REASONING_EFFORT": "max",
|
||||
"SGLANG_DSV4_REASONING_EFFORT": "max",
|
||||
}
|
||||
|
||||
# FP4 variant: FP4 mixed-precision experts.
|
||||
@@ -84,7 +84,7 @@ class TestDeepseekV4ProFp4(CustomTestCase):
|
||||
"8",
|
||||
"--disable-radix-cache",
|
||||
"--attention-backend",
|
||||
"compressed",
|
||||
"dsv4",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--page-size",
|
||||
|
||||
@@ -58,7 +58,7 @@ COMMON_ENV_VARS = {
|
||||
"SGLANG_TOPK_TRANSFORM_512_TORCH": "1",
|
||||
"SGLANG_OPT_USE_TILELANG_INDEXER": "true",
|
||||
"SGLANG_HACK_FLASHMLA_BACKEND": "tilelang",
|
||||
"SGLANG_REASONING_EFFORT": "max",
|
||||
"SGLANG_DSV4_REASONING_EFFORT": "max",
|
||||
}
|
||||
|
||||
# FP8 variant: dense-FP8 experts via the Triton MoE FP8 path.
|
||||
@@ -84,7 +84,7 @@ class TestDeepseekV4ProFp8(CustomTestCase):
|
||||
"8",
|
||||
"--disable-radix-cache",
|
||||
"--attention-backend",
|
||||
"compressed",
|
||||
"dsv4",
|
||||
"--max-running-requests",
|
||||
"256",
|
||||
"--page-size",
|
||||
|
||||
@@ -46,7 +46,7 @@ class _MockTokenizerManager:
|
||||
reasoning_parser=None,
|
||||
stream_response_default_include_usage=False,
|
||||
)
|
||||
# Mock hf_config for _use_dpsk_v32_encoding check
|
||||
# Mock hf_config for _resolve_chat_encoding_spec check
|
||||
mock_hf_config = Mock()
|
||||
mock_hf_config.architectures = ["LlamaForCausalLM"]
|
||||
self.model_config.hf_config = mock_hf_config
|
||||
@@ -685,21 +685,204 @@ class ServingChatTestCase(unittest.TestCase):
|
||||
mock_hf_config.architectures = ["DeepseekV32ForCausalLM"]
|
||||
tm.model_config.hf_config = mock_hf_config
|
||||
|
||||
# Case 1: No chat template + DeepSeek V3.2 arch -> should use dpsk encoding
|
||||
# Case 1: No chat template + DeepSeek V3.2 arch -> should use dsv32 encoding
|
||||
tm.tokenizer.chat_template = None
|
||||
serving_chat = OpenAIServingChat(tm, TemplateManager())
|
||||
self.assertTrue(serving_chat.use_dpsk_v32_encoding)
|
||||
self.assertEqual(serving_chat.chat_encoding_spec, "dsv32")
|
||||
|
||||
# Case 2: Chat template exists -> should NOT use dpsk encoding
|
||||
# Case 2: Chat template exists -> should NOT use dsv32 encoding
|
||||
tm.tokenizer.chat_template = "some template"
|
||||
serving_chat = OpenAIServingChat(tm, TemplateManager())
|
||||
self.assertFalse(serving_chat.use_dpsk_v32_encoding)
|
||||
self.assertIsNone(serving_chat.chat_encoding_spec)
|
||||
|
||||
# Case 3: Not DeepSeek V3.2 architecture -> should NOT use dpsk encoding
|
||||
# Case 3: Not DeepSeek V3.2 architecture -> should NOT use dsv32 encoding
|
||||
tm.tokenizer.chat_template = None
|
||||
mock_hf_config.architectures = ["LlamaForCausalLM"]
|
||||
serving_chat = OpenAIServingChat(tm, TemplateManager())
|
||||
self.assertFalse(serving_chat.use_dpsk_v32_encoding)
|
||||
self.assertIsNone(serving_chat.chat_encoding_spec)
|
||||
|
||||
# Case 4: DeepseekV4 arch -> always dsv4, even with chat_template
|
||||
# (release ships a stale V3 jinja we deliberately override).
|
||||
mock_hf_config.architectures = ["DeepseekV4ForCausalLM"]
|
||||
tm.tokenizer.chat_template = "stale v3 jinja"
|
||||
serving_chat = OpenAIServingChat(tm, TemplateManager())
|
||||
self.assertEqual(serving_chat.chat_encoding_spec, "dsv4")
|
||||
|
||||
tm.tokenizer.chat_template = None
|
||||
serving_chat = OpenAIServingChat(tm, TemplateManager())
|
||||
self.assertEqual(serving_chat.chat_encoding_spec, "dsv4")
|
||||
|
||||
# ------------- dsv4 task + latest_reminder -------------
|
||||
def test_dsv4_task_field_schema(self):
|
||||
"""Top-level `task` accepts the 6 DS task tokens and rejects others."""
|
||||
for valid in ("action", "query", "authority", "domain", "title", "read_url"):
|
||||
req = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
task=valid,
|
||||
)
|
||||
self.assertEqual(req.task, valid)
|
||||
|
||||
# None / unset is fine
|
||||
self.assertIsNone(self.basic_req.task)
|
||||
|
||||
# Bogus value rejected at validation time
|
||||
from pydantic import ValidationError
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
task="bogus",
|
||||
)
|
||||
|
||||
def test_latest_reminder_role_accepted(self):
|
||||
"""`latest_reminder` is a first-class message role on generic param."""
|
||||
from sglang.srt.entrypoints.openai.protocol import (
|
||||
ChatCompletionMessageGenericParam,
|
||||
)
|
||||
|
||||
msg = ChatCompletionMessageGenericParam(
|
||||
role="latest_reminder", content="Be terse."
|
||||
)
|
||||
self.assertEqual(msg.role, "latest_reminder")
|
||||
|
||||
# Full request with reminder before user parses cleanly.
|
||||
req = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[
|
||||
{"role": "latest_reminder", "content": "Be terse."},
|
||||
{"role": "user", "content": "Hi"},
|
||||
],
|
||||
)
|
||||
self.assertEqual(req.messages[0].role, "latest_reminder")
|
||||
self.assertEqual(req.messages[1].role, "user")
|
||||
|
||||
def test_attach_task_to_last_user_message(self):
|
||||
"""Helper attaches task to the nearest user/developer message."""
|
||||
from sglang.srt.entrypoints.openai import encoding_dsv4
|
||||
|
||||
messages = [{"role": "user", "content": "Hi"}]
|
||||
encoding_dsv4.attach_task_to_last_user_message(messages, "domain")
|
||||
self.assertEqual(messages[0]["task"], "domain")
|
||||
|
||||
# Prefers the LAST user message across a multi-turn conversation.
|
||||
messages = [
|
||||
{"role": "user", "content": "first"},
|
||||
{"role": "assistant", "content": "ok"},
|
||||
{"role": "user", "content": "second"},
|
||||
]
|
||||
encoding_dsv4.attach_task_to_last_user_message(messages, "query")
|
||||
self.assertNotIn("task", messages[0])
|
||||
self.assertEqual(messages[2]["task"], "query")
|
||||
|
||||
# `developer` role is treated like `user` (matches encoder semantics).
|
||||
messages = [{"role": "developer", "content": "dev"}]
|
||||
encoding_dsv4.attach_task_to_last_user_message(messages, "authority")
|
||||
self.assertEqual(messages[0]["task"], "authority")
|
||||
|
||||
# No user/developer present -> raises.
|
||||
with self.assertRaises(ValueError):
|
||||
encoding_dsv4.attach_task_to_last_user_message(
|
||||
[{"role": "system", "content": "s"}], "domain"
|
||||
)
|
||||
|
||||
def test_dsv4_content_parts_list_normalized(self):
|
||||
"""OpenAI list-of-parts content flattens to text before reaching the encoder."""
|
||||
from sglang.srt.entrypoints.openai import encoding_dsv4
|
||||
from sglang.srt.parser.jinja_template_utils import (
|
||||
process_content_for_template_format,
|
||||
)
|
||||
|
||||
req = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "say hi"}],
|
||||
}
|
||||
],
|
||||
)
|
||||
messages = [m.model_dump() for m in req.messages]
|
||||
# Mirror the boundary normalization _process_messages does for any
|
||||
# non-None chat_encoding_spec.
|
||||
for i, msg in enumerate(messages):
|
||||
if isinstance(msg.get("content"), list):
|
||||
messages[i] = process_content_for_template_format(
|
||||
msg, "string", [], [], [], []
|
||||
)
|
||||
out = encoding_dsv4.encode_messages(messages, thinking_mode="chat")
|
||||
self.assertIn("<|User|>say hi", out)
|
||||
|
||||
# Multiple text parts concat with single space; non-text parts dropped.
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "describe"},
|
||||
{"type": "image_url", "image_url": {"url": "x"}},
|
||||
],
|
||||
}
|
||||
]
|
||||
for i, msg in enumerate(messages):
|
||||
if isinstance(msg.get("content"), list):
|
||||
messages[i] = process_content_for_template_format(
|
||||
msg, "string", [], [], [], []
|
||||
)
|
||||
out = encoding_dsv4.encode_messages(messages, thinking_mode="chat")
|
||||
self.assertIn("<|User|>describe", out)
|
||||
self.assertNotIn("image_url", out)
|
||||
|
||||
def test_dsv4_task_and_reminder_encode_end_to_end(self):
|
||||
"""Task + latest_reminder plumb through to the dsv4 encoder correctly."""
|
||||
from sglang.srt.entrypoints.openai import encoding_dsv4
|
||||
|
||||
# 1) task='domain' in chat mode -> `<|domain|>` appended, no Assistant
|
||||
# prefix (this is a single-shot classification, not a chat turn).
|
||||
req = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "What is SGLang?"}],
|
||||
task="domain",
|
||||
)
|
||||
messages = [m.model_dump() for m in req.messages]
|
||||
encoding_dsv4.attach_task_to_last_user_message(messages, req.task)
|
||||
out = encoding_dsv4.encode_messages(messages, thinking_mode="chat")
|
||||
self.assertIn("<|domain|>", out)
|
||||
self.assertTrue(out.rstrip().endswith("<|domain|>"))
|
||||
self.assertNotIn("<|Assistant|>", out)
|
||||
|
||||
# 2) task='action' in thinking mode -> Assistant + <think> + <|action|>
|
||||
# (action is the one task that still runs a reasoning pass).
|
||||
req = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[{"role": "user", "content": "Hi"}],
|
||||
task="action",
|
||||
)
|
||||
messages = [m.model_dump() for m in req.messages]
|
||||
encoding_dsv4.attach_task_to_last_user_message(messages, req.task)
|
||||
out = encoding_dsv4.encode_messages(messages, thinking_mode="thinking")
|
||||
self.assertIn("<|Assistant|>", out)
|
||||
self.assertIn("<think>", out)
|
||||
self.assertTrue(out.rstrip().endswith("<|action|>"))
|
||||
|
||||
# 3) latest_reminder preceding user -> reminder renders before user,
|
||||
# Assistant prefix still comes after user.
|
||||
req = ChatCompletionRequest(
|
||||
model="x",
|
||||
messages=[
|
||||
{"role": "latest_reminder", "content": "Be terse."},
|
||||
{"role": "user", "content": "Hello"},
|
||||
],
|
||||
)
|
||||
messages = [m.model_dump() for m in req.messages]
|
||||
out = encoding_dsv4.encode_messages(messages, thinking_mode="chat")
|
||||
self.assertIn("<|latest_reminder|>Be terse.", out)
|
||||
self.assertIn("<|User|>Hello", out)
|
||||
self.assertLess(
|
||||
out.index("<|latest_reminder|>"),
|
||||
out.index("<|User|>"),
|
||||
)
|
||||
self.assertIn("<|Assistant|>", out)
|
||||
|
||||
def test_streaming_abort_yields_error(self):
|
||||
"""Test that an abort finish reason during streaming correctly yields an error and stops."""
|
||||
|
||||
@@ -45,6 +45,7 @@ def _make_req(rid="test-req-0", origin_input_ids=None, output_ids=None):
|
||||
origin_input_ids=origin_input_ids,
|
||||
output_ids=output_ids,
|
||||
fill_ids=origin_input_ids + output_ids,
|
||||
seqlen=len(origin_input_ids) + len(output_ids),
|
||||
req_pool_idx=None,
|
||||
kv_allocated_len=0,
|
||||
kv_committed_len=0,
|
||||
|
||||
@@ -443,18 +443,25 @@ class TestPrefillAdder(CustomTestCase):
|
||||
self.assertEqual(result3, AddReqResult.OTHER)
|
||||
|
||||
def _build_hybrid_swa_chunked_req(
|
||||
self, *, page_size, rem_swa, rem_chunk=2048, extend_input_len=500
|
||||
self,
|
||||
*,
|
||||
page_size,
|
||||
rem_swa,
|
||||
rem_chunk=2048,
|
||||
extend_input_len=500,
|
||||
is_hybrid_swa=True,
|
||||
full_available=100_000,
|
||||
):
|
||||
self.mock_token_allocator.swa_available_size.return_value = rem_swa
|
||||
self.mock_token_allocator.full_available_size.return_value = 100_000
|
||||
self.mock_token_allocator.available_size.return_value = 100_000
|
||||
self.mock_token_allocator.full_available_size.return_value = full_available
|
||||
self.mock_token_allocator.available_size.return_value = full_available
|
||||
self.mock_tree_cache.sliding_window_size = 128
|
||||
adder = self.create_adder(
|
||||
self.create_running_batch(),
|
||||
page_size=page_size,
|
||||
rem_chunk_tokens=rem_chunk,
|
||||
)
|
||||
adder.is_hybrid_swa = True
|
||||
adder.is_hybrid_swa = is_hybrid_swa
|
||||
|
||||
req = self.create_mock_req("chunked", priority=0, max_new_tokens=128)
|
||||
req.extend_input_len = extend_input_len
|
||||
@@ -499,6 +506,44 @@ class TestPrefillAdder(CustomTestCase):
|
||||
self.assertEqual(req.extend_input_len, original_len)
|
||||
self.assertEqual(len(adder.can_run_list), 0)
|
||||
|
||||
def test_swa_budget_for_req(self):
|
||||
cases = [
|
||||
# (extend, rem_chunk, window, page, expected, label)
|
||||
(64, None, 128, 16, 128 + 16, "no_cap_floor_active"),
|
||||
(200, None, 256, 32, 256 + 32, "no_cap_floor_active_other_dims"),
|
||||
(300, None, 128, 16, 300 + 16, "no_cap_floor_inactive"),
|
||||
(200, 50, 64, 8, 64 + 8, "cap_binds_then_floor"),
|
||||
(300, 500, 64, 64, 300 + 64, "cap_does_not_bind"),
|
||||
(0, None, 128, 16, 128 + 16, "extend_zero_floor_only"),
|
||||
]
|
||||
for extend, rem_chunk, window, page, expected, label in cases:
|
||||
with self.subTest(label=label):
|
||||
self.mock_tree_cache.sliding_window_size = window
|
||||
adder = self.create_adder(
|
||||
self.create_running_batch(),
|
||||
page_size=page,
|
||||
rem_chunk_tokens=rem_chunk,
|
||||
)
|
||||
self.assertEqual(adder._swa_budget_for_req(extend), expected)
|
||||
|
||||
def test_add_chunked_req_non_hybrid_no_swa_reservation(self):
|
||||
# Non-hybrid path: the SWA-pool reservation must NOT apply, otherwise
|
||||
# the fix would regress non-SWA models.
|
||||
PAGE_SIZE = 16
|
||||
adder, req = self._build_hybrid_swa_chunked_req(
|
||||
page_size=PAGE_SIZE,
|
||||
rem_swa=10,
|
||||
rem_chunk=500,
|
||||
extend_input_len=200,
|
||||
is_hybrid_swa=False,
|
||||
full_available=300,
|
||||
)
|
||||
|
||||
result = adder.add_chunked_req(req)
|
||||
self.assertIsNone(result)
|
||||
req.set_extend_input_len.assert_called_once_with(200)
|
||||
self.assertIn(req, adder.can_run_list)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -110,6 +110,7 @@ def _make_req(req_pool_idx, token_ids, cache_protected_len, tree):
|
||||
extra_key=None,
|
||||
last_node=tree.root_node,
|
||||
swa_uuid_for_lock=None,
|
||||
swa_prefix_lock_released=False,
|
||||
prefix_indices=torch.tensor([], dtype=torch.int64, device=tree.device),
|
||||
_kv_committed_len=len(token_ids),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,9 @@ import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
DecLockRefParams,
|
||||
EvictParams,
|
||||
EvictResult,
|
||||
InsertParams,
|
||||
@@ -16,80 +18,113 @@ from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool, SWATokenToKVPoolAllo
|
||||
from sglang.srt.mem_cache.swa_radix_cache import SWARadixCache
|
||||
from sglang.srt.utils import get_device
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=9, suite="stage-b-test-1-gpu-large")
|
||||
register_amd_ci(est_time=10, suite="stage-b-test-1-gpu-small-amd")
|
||||
|
||||
|
||||
class _DummyReq:
|
||||
def __init__(self):
|
||||
self._kv_committed_len = 0
|
||||
self.swa_prefix_lock_released = False
|
||||
|
||||
def pop_committed_kv_cache(self):
|
||||
return self._kv_committed_len
|
||||
|
||||
|
||||
def _build_swa_tree(
|
||||
is_eagle: bool,
|
||||
page_size: int = 1,
|
||||
req_size: int = 8,
|
||||
max_context_len: int = 64,
|
||||
kv_size: int = 64,
|
||||
kv_size_swa: int = 32,
|
||||
sliding_window_size: int = 4,
|
||||
):
|
||||
head_num = 8
|
||||
head_dim = 128
|
||||
num_layers = 24
|
||||
global_interval = 4
|
||||
dtype = torch.bfloat16
|
||||
device = get_device()
|
||||
full_attention_layer_ids = [i for i in range(0, num_layers, global_interval)]
|
||||
full_attention_layer_ids_set = set(full_attention_layer_ids)
|
||||
swa_attention_layer_ids = [
|
||||
i for i in range(num_layers) if i not in full_attention_layer_ids_set
|
||||
]
|
||||
|
||||
req_to_token_pool = ReqToTokenPool(
|
||||
size=req_size,
|
||||
max_context_len=max_context_len,
|
||||
device=device,
|
||||
enable_memory_saver=False,
|
||||
)
|
||||
kv_pool = SWAKVPool(
|
||||
size=kv_size,
|
||||
size_swa=kv_size_swa,
|
||||
page_size=page_size,
|
||||
dtype=dtype,
|
||||
head_num=head_num,
|
||||
head_dim=head_dim,
|
||||
swa_attention_layer_ids=swa_attention_layer_ids,
|
||||
full_attention_layer_ids=full_attention_layer_ids,
|
||||
enable_kvcache_transpose=False,
|
||||
device=device,
|
||||
)
|
||||
allocator = SWATokenToKVPoolAllocator(
|
||||
size=kv_size,
|
||||
size_swa=kv_size_swa,
|
||||
page_size=page_size,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
kvcache=kv_pool,
|
||||
need_sort=False,
|
||||
)
|
||||
tree = SWARadixCache(
|
||||
params=CacheInitParams(
|
||||
req_to_token_pool=req_to_token_pool,
|
||||
token_to_kv_pool_allocator=allocator,
|
||||
page_size=page_size,
|
||||
disable=False,
|
||||
is_eagle=is_eagle,
|
||||
sliding_window_size=sliding_window_size,
|
||||
),
|
||||
)
|
||||
return tree, allocator, req_to_token_pool
|
||||
|
||||
|
||||
def _swa_alloc(allocator, need_size):
|
||||
"""SWA-pool alloc that also works for page_size > 1 (built-in alloc asserts page_size == 1)."""
|
||||
if allocator.page_size == 1:
|
||||
return allocator.alloc(need_size)
|
||||
|
||||
assert need_size % allocator.page_size == 0
|
||||
full_indices = allocator.full_attn_allocator.alloc(need_size)
|
||||
swa_indices = allocator.swa_attn_allocator.alloc(need_size)
|
||||
assert full_indices is not None and swa_indices is not None
|
||||
allocator.full_to_swa_index_mapping[full_indices] = swa_indices
|
||||
return full_indices
|
||||
|
||||
|
||||
def _insert(tree, allocator, token_ids):
|
||||
indices = _swa_alloc(allocator, len(token_ids))
|
||||
assert indices is not None
|
||||
tree.insert(InsertParams(key=RadixKey(token_ids), value=indices))
|
||||
|
||||
|
||||
def _insert_chain(tree, allocator, token_ids):
|
||||
_insert(tree, allocator, token_ids)
|
||||
match = tree.match_prefix(MatchPrefixParams(key=RadixKey(token_ids)))
|
||||
return match.last_device_node
|
||||
|
||||
|
||||
def _expected_tail_size(window: int, page_size: int) -> int:
|
||||
"""Mirror of _maybe_split_leaf_for_swa_lock's tail_size formula."""
|
||||
return (window + page_size - 1) // page_size * page_size
|
||||
|
||||
|
||||
class TestSWA(unittest.TestCase):
|
||||
class _DummyReq:
|
||||
def __init__(self):
|
||||
self._kv_committed_len = 0
|
||||
|
||||
def pop_committed_kv_cache(self):
|
||||
return self._kv_committed_len
|
||||
|
||||
def _build_swa_tree(
|
||||
self,
|
||||
is_eagle: bool,
|
||||
page_size: int = 1,
|
||||
req_size: int = 8,
|
||||
max_context_len: int = 64,
|
||||
kv_size: int = 64,
|
||||
kv_size_swa: int = 32,
|
||||
sliding_window_size: int = 4,
|
||||
):
|
||||
head_num = 8
|
||||
head_dim = 128
|
||||
num_layers = 24
|
||||
global_interval = 4
|
||||
dtype = torch.bfloat16
|
||||
device = get_device()
|
||||
full_attention_layer_ids = [i for i in range(0, num_layers, global_interval)]
|
||||
full_attention_layer_ids_set = set(full_attention_layer_ids)
|
||||
swa_attention_layer_ids = [
|
||||
i for i in range(num_layers) if i not in full_attention_layer_ids_set
|
||||
]
|
||||
|
||||
req_to_token_pool = ReqToTokenPool(
|
||||
size=req_size,
|
||||
max_context_len=max_context_len,
|
||||
device=device,
|
||||
enable_memory_saver=False,
|
||||
)
|
||||
kv_pool = SWAKVPool(
|
||||
size=kv_size,
|
||||
size_swa=kv_size_swa,
|
||||
page_size=page_size,
|
||||
dtype=dtype,
|
||||
head_num=head_num,
|
||||
head_dim=head_dim,
|
||||
swa_attention_layer_ids=swa_attention_layer_ids,
|
||||
full_attention_layer_ids=full_attention_layer_ids,
|
||||
enable_kvcache_transpose=False,
|
||||
device=device,
|
||||
)
|
||||
allocator = SWATokenToKVPoolAllocator(
|
||||
size=kv_size,
|
||||
size_swa=kv_size_swa,
|
||||
page_size=page_size,
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
kvcache=kv_pool,
|
||||
need_sort=False,
|
||||
)
|
||||
tree = SWARadixCache(
|
||||
params=CacheInitParams(
|
||||
req_to_token_pool=req_to_token_pool,
|
||||
token_to_kv_pool_allocator=allocator,
|
||||
page_size=page_size,
|
||||
disable=False,
|
||||
is_eagle=is_eagle,
|
||||
sliding_window_size=sliding_window_size,
|
||||
),
|
||||
)
|
||||
return tree, allocator, req_to_token_pool
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
pass
|
||||
@@ -475,10 +510,10 @@ class TestSWA(unittest.TestCase):
|
||||
self.assertEqual(list(last_node.key), [(5, 60), (60, 70)])
|
||||
|
||||
def test_swa_cache_finished_req_eagle_uses_cache_protected_len_and_bigram_key(self):
|
||||
tree, allocator, req_to_token_pool = self._build_swa_tree(is_eagle=True)
|
||||
tree, allocator, req_to_token_pool = _build_swa_tree(is_eagle=True)
|
||||
|
||||
# Case 1: is_insert=True should pass bigram key and use cache_protected_len.
|
||||
req = self._DummyReq()
|
||||
req = _DummyReq()
|
||||
req.req_pool_idx = 0
|
||||
req.origin_input_ids = [1, 2, 3, 4, 5, 6]
|
||||
req.output_ids = []
|
||||
@@ -513,7 +548,7 @@ class TestSWA(unittest.TestCase):
|
||||
|
||||
# Case 2: is_insert=False should free [cache_protected_len:page_aligned_len]
|
||||
# even when len(prefix_indices) is intentionally larger.
|
||||
req2 = self._DummyReq()
|
||||
req2 = _DummyReq()
|
||||
req2.req_pool_idx = 1
|
||||
req2.origin_input_ids = [11, 12, 13, 14, 15, 16]
|
||||
req2.output_ids = []
|
||||
@@ -546,5 +581,112 @@ class TestSWA(unittest.TestCase):
|
||||
self.assertEqual(freed_lens, [4, 1])
|
||||
|
||||
|
||||
# Optimization: SGLANG_OPT_SWA_SPLIT_LEAF_ON_INSERT.
|
||||
# Splits a freshly-inserted leaf at the (page-aligned) sliding-window
|
||||
# boundary so a future inc_lock_ref protects only ~sliding_window_size SWA
|
||||
# tokens instead of the whole chunked-prefill chain.
|
||||
class TestSWASplitLeafOnInsert(CustomTestCase):
|
||||
def _insert_and_lock(self, *, window, page_size, leaf_len, flag_on):
|
||||
tree, allocator, _ = _build_swa_tree(
|
||||
is_eagle=False,
|
||||
kv_size=128,
|
||||
kv_size_swa=64,
|
||||
sliding_window_size=window,
|
||||
page_size=page_size,
|
||||
)
|
||||
token_ids = list(range(leaf_len))
|
||||
with envs.SGLANG_OPT_SWA_SPLIT_LEAF_ON_INSERT.override(flag_on):
|
||||
leaf = _insert_chain(tree, allocator, token_ids)
|
||||
result = tree.inc_lock_ref(leaf)
|
||||
return tree, leaf, result
|
||||
|
||||
def test_flag_off_protects_full_leaf(self):
|
||||
tree, leaf, _ = self._insert_and_lock(
|
||||
window=4, page_size=1, leaf_len=12, flag_on=False
|
||||
)
|
||||
self.assertEqual(len(leaf.value), 12)
|
||||
self.assertEqual(tree.swa_protected_size_, 12)
|
||||
|
||||
def test_flag_on_caps_protection_at_window(self):
|
||||
# (window, page_size, leaf_len, expected_tail_size); leaf_len picked
|
||||
# > tail_size and page-aligned for page_size > 1.
|
||||
cases = [
|
||||
(4, 1, 12, 4),
|
||||
(4, 1, 5, 4),
|
||||
(1, 1, 5, 1),
|
||||
(4, 2, 12, 4),
|
||||
(8, 2, 12, 8),
|
||||
(4, 4, 12, 4),
|
||||
# window NOT page-aligned -> tail rounds up to page boundary.
|
||||
(3, 2, 12, 4),
|
||||
(5, 4, 12, 8),
|
||||
(3, 4, 12, 4),
|
||||
]
|
||||
for window, page_size, leaf_len, expected_tail in cases:
|
||||
with self.subTest(window=window, page_size=page_size, leaf_len=leaf_len):
|
||||
self.assertEqual(_expected_tail_size(window, page_size), expected_tail)
|
||||
tree, leaf, _ = self._insert_and_lock(
|
||||
window=window,
|
||||
page_size=page_size,
|
||||
leaf_len=leaf_len,
|
||||
flag_on=True,
|
||||
)
|
||||
self.assertEqual(len(leaf.value), expected_tail)
|
||||
self.assertEqual(tree.swa_protected_size_, expected_tail)
|
||||
|
||||
def test_flag_on_no_split_when_leaf_within_window(self):
|
||||
# leaf_len <= tail_size: split must no-op.
|
||||
cases = [
|
||||
(4, 1, 4),
|
||||
(4, 1, 3),
|
||||
(4, 2, 4),
|
||||
(3, 2, 4),
|
||||
(8, 2, 4),
|
||||
(4, 4, 4),
|
||||
]
|
||||
for window, page_size, leaf_len in cases:
|
||||
with self.subTest(window=window, page_size=page_size, leaf_len=leaf_len):
|
||||
tree, leaf, _ = self._insert_and_lock(
|
||||
window=window,
|
||||
page_size=page_size,
|
||||
leaf_len=leaf_len,
|
||||
flag_on=True,
|
||||
)
|
||||
self.assertEqual(len(leaf.value), leaf_len)
|
||||
self.assertEqual(tree.swa_protected_size_, leaf_len)
|
||||
|
||||
def test_match_prefix_returns_full_chain_after_split(self):
|
||||
tree, allocator, _ = _build_swa_tree(
|
||||
is_eagle=False,
|
||||
kv_size=128,
|
||||
kv_size_swa=64,
|
||||
sliding_window_size=4,
|
||||
page_size=1,
|
||||
)
|
||||
token_ids = list(range(12))
|
||||
with envs.SGLANG_OPT_SWA_SPLIT_LEAF_ON_INSERT.override(True):
|
||||
inserted_leaf = _insert_chain(tree, allocator, token_ids)
|
||||
self.assertEqual(len(inserted_leaf.value), 4)
|
||||
match = tree.match_prefix(MatchPrefixParams(key=RadixKey(token_ids)))
|
||||
self.assertEqual(match.device_indices.shape[0], 12)
|
||||
self.assertIs(match.last_device_node, inserted_leaf)
|
||||
|
||||
def test_dec_lock_ref_after_split_balances_to_zero(self):
|
||||
tree, leaf, result = self._insert_and_lock(
|
||||
window=4, page_size=1, leaf_len=12, flag_on=True
|
||||
)
|
||||
self.assertEqual(tree.swa_protected_size_, 4)
|
||||
self.assertEqual(tree.full_protected_size_, 12)
|
||||
|
||||
tree.dec_lock_ref(
|
||||
leaf,
|
||||
params=DecLockRefParams(swa_uuid_for_lock=result.swa_uuid_for_lock),
|
||||
)
|
||||
|
||||
self.assertEqual(tree.swa_protected_size_, 0)
|
||||
self.assertEqual(tree.full_protected_size_, 0)
|
||||
tree.sanity_check()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -188,14 +188,14 @@ class TestPostprocessTensors(CustomTestCase):
|
||||
b = torch.randn(4)
|
||||
raw = {"a.weight": a, "b.bias": b}
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors(raw),
|
||||
_postprocess_tensors(raw, set()),
|
||||
[("a.weight", True, a), ("b.bias", True, b)],
|
||||
)
|
||||
|
||||
def test_weight_alone_without_scale_inv_does_not_trigger_dequant(self):
|
||||
w = torch.randn(4)
|
||||
raw = {"x.weight": w}
|
||||
_assert_triples_close(_postprocess_tensors(raw), [("x.weight", True, w)])
|
||||
_assert_triples_close(_postprocess_tensors(raw, set()), [("x.weight", True, w)])
|
||||
|
||||
# --- non-persistent buffer skip ---
|
||||
|
||||
@@ -207,7 +207,7 @@ class TestPostprocessTensors(CustomTestCase):
|
||||
"model.layers.0.weight": plain,
|
||||
}
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors(raw),
|
||||
_postprocess_tensors(raw, set()),
|
||||
[
|
||||
("model.rotary_emb.cos_sin_cache", False, cache),
|
||||
("model.layers.0.weight", True, plain),
|
||||
@@ -217,14 +217,14 @@ class TestPostprocessTensors(CustomTestCase):
|
||||
def test_skips_inv_freq_substring(self):
|
||||
t = torch.randn(4)
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors({"model.rotary_emb.inv_freq": t}),
|
||||
_postprocess_tensors({"model.rotary_emb.inv_freq": t}, set()),
|
||||
[("model.rotary_emb.inv_freq", False, t)],
|
||||
)
|
||||
|
||||
def test_skips_weight_fp32_substring(self):
|
||||
t = torch.randn(4)
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors({"model.layers.0.mlp.gate._weight_fp32": t}),
|
||||
_postprocess_tensors({"model.layers.0.mlp.gate._weight_fp32": t}, set()),
|
||||
[("model.layers.0.mlp.gate._weight_fp32", False, t)],
|
||||
)
|
||||
|
||||
@@ -232,7 +232,7 @@ class TestPostprocessTensors(CustomTestCase):
|
||||
# Pattern can appear anywhere in the name, not just at the end.
|
||||
t = torch.randn(4)
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors({"weird.cos_sin_cache.foo.bar": t}),
|
||||
_postprocess_tensors({"weird.cos_sin_cache.foo.bar": t}, set()),
|
||||
[("weird.cos_sin_cache.foo.bar", False, t)],
|
||||
)
|
||||
|
||||
@@ -248,7 +248,7 @@ class TestPostprocessTensors(CustomTestCase):
|
||||
qweight, sf_fp32, block_size=[128, 128], dtype=torch.bfloat16
|
||||
)
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors(raw),
|
||||
_postprocess_tensors(raw, set()),
|
||||
[
|
||||
("x.weight", True, expected_dequant),
|
||||
("x.weight", False, qweight),
|
||||
@@ -264,7 +264,7 @@ class TestPostprocessTensors(CustomTestCase):
|
||||
qweight, sf_fp32, block_size=[128, 128], dtype=torch.bfloat16
|
||||
)
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors(raw),
|
||||
_postprocess_tensors(raw, set()),
|
||||
[
|
||||
("x.weight", True, expected_dequant),
|
||||
("x.weight", False, qweight),
|
||||
@@ -285,7 +285,7 @@ class TestPostprocessTensors(CustomTestCase):
|
||||
)
|
||||
# All dequant entries come first, then a raw pass over every key.
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors(raw),
|
||||
_postprocess_tensors(raw, set()),
|
||||
[
|
||||
("x.weight", True, expected_dequant),
|
||||
("x.weight", False, qweight),
|
||||
@@ -299,7 +299,7 @@ class TestPostprocessTensors(CustomTestCase):
|
||||
# through as a normal entry with should_compare=True.
|
||||
s = torch.zeros(1, 1, dtype=torch.int32)
|
||||
_assert_triples_close(
|
||||
_postprocess_tensors({"x.weight_scale_inv": s}),
|
||||
_postprocess_tensors({"x.weight_scale_inv": s}, set()),
|
||||
[("x.weight_scale_inv", True, s)],
|
||||
)
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ PER_COMMIT_SUITES = {
|
||||
"stage-c-test-8-gpu-b200",
|
||||
"stage-c-test-deepep-4-gpu-h100",
|
||||
"stage-c-test-deepep-8-gpu-h200",
|
||||
"stage-c-test-dsv4-4-gpu-b200",
|
||||
"stage-c-test-dsv4-8-gpu-h200",
|
||||
],
|
||||
HWBackend.NPU: [
|
||||
"stage-a-test-1-gpu-small",
|
||||
|
||||
Reference in New Issue
Block a user