[CI] Trim redundant nightly test registrations (#34070)
Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com>
This commit is contained in:
co-authored by
Baizhou Zhang
parent
dd5d82bead
commit
f6a6f5bf1e
@@ -1,90 +0,0 @@
|
||||
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.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-8-gpu-b200", nightly=True)
|
||||
|
||||
FULL_DEEPSEEK_V3_MODEL_PATH = "deepseek-ai/DeepSeek-V3-0324"
|
||||
SERVER_LAUNCH_TIMEOUT = 1000
|
||||
|
||||
|
||||
class TestDeepseekR1Fp8Flashinfer(CustomTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = try_cached_model(FULL_DEEPSEEK_V3_MODEL_PATH)
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
other_args = [
|
||||
"--trust-remote-code",
|
||||
"--disable-radix-cache",
|
||||
"--max-running-requests",
|
||||
"512",
|
||||
"--chunked-prefill-size",
|
||||
"8192",
|
||||
"--mem-fraction-static",
|
||||
"0.9",
|
||||
"--cuda-graph-max-bs-decode",
|
||||
"128",
|
||||
"--max-prefill-tokens",
|
||||
"8192",
|
||||
"--kv-cache-dtype",
|
||||
"fp8_e4m3",
|
||||
"--quantization",
|
||||
"fp8",
|
||||
"--tensor-parallel-size",
|
||||
"8",
|
||||
"--data-parallel-size",
|
||||
"1",
|
||||
"--expert-parallel-size",
|
||||
"1",
|
||||
"--scheduler-recv-interval",
|
||||
"10",
|
||||
"--stream-interval",
|
||||
"10",
|
||||
"--attention-backend",
|
||||
"trtllm_mla",
|
||||
"--fp8-gemm-backend",
|
||||
"flashinfer_trtllm",
|
||||
"--moe-runner-backend",
|
||||
"flashinfer_trtllm",
|
||||
"--enable-symm-mem",
|
||||
"--model-loader-extra-config",
|
||||
'{"enable_multithread_load": true,"num_threads": 64}',
|
||||
]
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=SERVER_LAUNCH_TIMEOUT,
|
||||
other_args=other_args,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
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=512,
|
||||
num_threads=512,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"Eval accuracy of GSM8K: {metrics=}")
|
||||
|
||||
self.assertGreater(metrics["score"], 0.92)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,69 +0,0 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sglang.srt.utils import get_device_sm, kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.run_eval import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
# modelopt_fp4 requires SM 100+ (Blackwell)
|
||||
register_cuda_ci(est_time=300, suite="nightly-1-gpu", nightly=True)
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
get_device_sm() < 100, "Test requires CUDA SM 100 or higher (Blackwell)"
|
||||
)
|
||||
class TestFlashinferTrtllmGenMoeBackend(CustomTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = "nvidia/Qwen3-30B-A3B-NVFP4"
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=[
|
||||
"--moe-runner-backend",
|
||||
"flashinfer_trtllm",
|
||||
"--quantization",
|
||||
"modelopt_fp4",
|
||||
"--trust-remote-code",
|
||||
"--disable-radix-cache",
|
||||
"--max-running-requests",
|
||||
"1024",
|
||||
"--chunked-prefill-size",
|
||||
"16384",
|
||||
"--mem-fraction-static",
|
||||
"0.89",
|
||||
"--max-prefill-tokens",
|
||||
"16384",
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
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=1319,
|
||||
num_threads=1319,
|
||||
num_shots=8,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"{metrics=}")
|
||||
self.assertGreater(metrics["score"], 0.88)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,191 +0,0 @@
|
||||
import json
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
from sglang.benchmark.serving import run_benchmark
|
||||
from sglang.benchmark.utils import parse_custom_headers
|
||||
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
get_benchmark_args,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=300, suite="nightly-1-gpu", nightly=True)
|
||||
register_amd_ci(est_time=300, suite="nightly-amd-1-gpu", nightly=True)
|
||||
|
||||
MODEL = "Qwen/Qwen3-0.6B"
|
||||
NUM_CONVERSATIONS, NUM_TURNS = 4, 3
|
||||
|
||||
|
||||
class TestBenchServingFunctionality(CustomTestCase):
|
||||
def test_gsp_multi_turn(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
process = popen_launch_server(
|
||||
MODEL,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=[
|
||||
"--mem-fraction-static",
|
||||
"0.7",
|
||||
"--log-requests",
|
||||
"--log-requests-level",
|
||||
"3",
|
||||
"--log-requests-format",
|
||||
"json",
|
||||
"--log-requests-target",
|
||||
"stdout",
|
||||
temp_dir,
|
||||
],
|
||||
)
|
||||
try:
|
||||
args = get_benchmark_args(
|
||||
base_url=DEFAULT_URL_FOR_TEST,
|
||||
backend="sglang-oai-chat",
|
||||
tokenizer=MODEL,
|
||||
dataset_name="generated-shared-prefix",
|
||||
num_prompts=NUM_CONVERSATIONS,
|
||||
request_rate=float("inf"),
|
||||
gsp_num_groups=2,
|
||||
gsp_prompts_per_group=2,
|
||||
gsp_system_prompt_len=64,
|
||||
gsp_question_len=16,
|
||||
gsp_output_len=16,
|
||||
gsp_num_turns=NUM_TURNS,
|
||||
)
|
||||
args.warmup_requests = 0
|
||||
res = run_benchmark(args)
|
||||
self.assertEqual(res["completed"], NUM_CONVERSATIONS * NUM_TURNS)
|
||||
|
||||
time.sleep(1)
|
||||
logs = "".join(f.read_text() for f in Path(temp_dir).glob("*.log"))
|
||||
self._verify_multi_turn_logs(logs)
|
||||
finally:
|
||||
kill_process_tree(process.pid)
|
||||
|
||||
def _verify_multi_turn_logs(self, content: str):
|
||||
reqs = []
|
||||
for line in content.splitlines():
|
||||
idx = line.find("{")
|
||||
if idx == -1:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line[idx:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if obj.get("event") != "request.finished":
|
||||
continue
|
||||
text = obj.get("obj", {}).get("text")
|
||||
rid = obj.get("rid", "")
|
||||
if text and not rid.startswith(HEALTH_CHECK_RID_PREFIX):
|
||||
reqs.append(text)
|
||||
|
||||
self.assertGreaterEqual(len(reqs), NUM_CONVERSATIONS * NUM_TURNS)
|
||||
|
||||
# Verify prefix relationships
|
||||
reqs_sorted = sorted(reqs, key=len)
|
||||
prefix_count = 0
|
||||
for i, text in enumerate(reqs_sorted):
|
||||
for j in range(i + 1, len(reqs_sorted)):
|
||||
if reqs_sorted[j].startswith(text):
|
||||
prefix_count += 1
|
||||
break
|
||||
|
||||
expected = NUM_CONVERSATIONS * (NUM_TURNS - 1)
|
||||
self.assertGreaterEqual(
|
||||
prefix_count, expected, f"Expected at least {expected} prefix pairs"
|
||||
)
|
||||
|
||||
|
||||
class TestBenchServingCustomHeaders(CustomTestCase):
|
||||
def test_parse_custom_headers(self):
|
||||
headers = parse_custom_headers(["MyHeader=MY_VALUE", "Another=value=hello"])
|
||||
self.assertEqual(headers, {"MyHeader": "MY_VALUE", "Another": "value=hello"})
|
||||
|
||||
headers = parse_custom_headers(["InvalidNoEquals"])
|
||||
self.assertEqual(headers, {})
|
||||
|
||||
headers = parse_custom_headers(["=NoKey"])
|
||||
self.assertEqual(headers, {})
|
||||
|
||||
# TODO: Using well-implemented mock server, e.g. the on in sgl-router
|
||||
def test_custom_headers_sent_to_server(self):
|
||||
import queue
|
||||
|
||||
received_requests = queue.Queue()
|
||||
|
||||
class HeaderEchoHandler(BaseHTTPRequestHandler):
|
||||
def _handle(self):
|
||||
received_requests.put(
|
||||
{
|
||||
"method": self.command,
|
||||
"path": self.path,
|
||||
"headers": dict(self.headers),
|
||||
}
|
||||
)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
if self.path == "/v1/models":
|
||||
self.wfile.write(json.dumps({"data": [{"id": "gpt2"}]}).encode())
|
||||
elif self.path == "/generate":
|
||||
self.wfile.write(
|
||||
json.dumps(
|
||||
{"text": "ok", "meta_info": {"completion_tokens": 1}}
|
||||
).encode()
|
||||
)
|
||||
else:
|
||||
self.wfile.write(json.dumps({}).encode())
|
||||
|
||||
do_GET = do_POST = _handle
|
||||
|
||||
server = HTTPServer(("127.0.0.1", 0), HeaderEchoHandler)
|
||||
port = server.server_address[1]
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
try:
|
||||
args = get_benchmark_args(
|
||||
base_url=f"http://127.0.0.1:{port}",
|
||||
backend="sglang",
|
||||
dataset_name="random",
|
||||
tokenizer="gpt2",
|
||||
num_prompts=1,
|
||||
random_input_len=8,
|
||||
random_output_len=8,
|
||||
header=["X-Custom-Test=TestValue123", "X-Another=AnotherVal"],
|
||||
)
|
||||
args.warmup_requests = 0
|
||||
args.disable_tqdm = True
|
||||
run_benchmark(args)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
server.shutdown()
|
||||
|
||||
all_reqs = []
|
||||
while not received_requests.empty():
|
||||
all_reqs.append(received_requests.get_nowait())
|
||||
|
||||
generate_reqs = [r for r in all_reqs if r["path"] == "/generate"]
|
||||
self.assertGreater(
|
||||
len(generate_reqs),
|
||||
0,
|
||||
f"No /generate request. All: {[r['path'] for r in all_reqs]}",
|
||||
)
|
||||
headers = generate_reqs[0]["headers"]
|
||||
self.assertEqual(headers.get("X-Custom-Test"), "TestValue123")
|
||||
self.assertEqual(headers.get("X-Another"), "AnotherVal")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,71 +0,0 @@
|
||||
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.run_eval import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=900, suite="nightly-4-gpu-b200", nightly=True)
|
||||
|
||||
GLM52_FP4_MODEL = "nvidia/GLM-5.2-NVFP4"
|
||||
|
||||
|
||||
class TestPCGGlm52Fp4(CustomTestCase):
|
||||
"""PCG prefill on GLM-5.2-NVFP4 (DSA model, TP=4, B200).
|
||||
|
||||
GLM-5.2 uses GlmMoeDsaForCausalLM (DSA attention). This test verifies that
|
||||
piecewise CUDA graph works correctly after the DSA indexer was updated to
|
||||
cache k_fp8/k_scale for PCG-compatible prefill.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = GLM52_FP4_MODEL
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=[
|
||||
"--tp-size",
|
||||
"4",
|
||||
"--trust-remote-code",
|
||||
"--reasoning-parser",
|
||||
"glm45",
|
||||
"--tool-call-parser",
|
||||
"glm47",
|
||||
"--quantization",
|
||||
"modelopt_fp4",
|
||||
"--disable-flashinfer-autotune",
|
||||
"--cuda-graph-backend-prefill=tc_piecewise",
|
||||
"--model-loader-extra-config",
|
||||
'{"enable_multithread_load": true, "num_threads": 64}',
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_gsm8k(self):
|
||||
args = SimpleNamespace(
|
||||
base_url=self.base_url,
|
||||
model=self.model,
|
||||
eval_name="gsm8k",
|
||||
num_examples=200,
|
||||
num_threads=200,
|
||||
max_tokens=4096,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"{metrics=}")
|
||||
self.assertGreater(metrics["score"], 0.92)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,75 +0,0 @@
|
||||
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.run_eval import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=900, suite="nightly-8-gpu-h200", nightly=True)
|
||||
|
||||
GLM52_FP8_MODEL = "zai-org/GLM-5.2-FP8"
|
||||
|
||||
|
||||
class TestBCGGlm52Fp8TP8(CustomTestCase):
|
||||
"""Breakable CUDA graph prefill on GLM-5.2-FP8 (DSA model, TP=8, H200)."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = GLM52_FP8_MODEL
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=[
|
||||
"--tp-size",
|
||||
"8",
|
||||
"--trust-remote-code",
|
||||
"--reasoning-parser",
|
||||
"glm45",
|
||||
"--tool-call-parser",
|
||||
"glm47",
|
||||
"--mem-fraction-static",
|
||||
"0.8",
|
||||
"--disable-flashinfer-autotune",
|
||||
"--cuda-graph-backend-prefill=breakable",
|
||||
# Small chunks => many prefill iterations, each <= the 2048
|
||||
# capture max, so every prefill batch replays the BCG graph and
|
||||
# exercises the DSA split-op / dual-stream / MLA-fusion paths.
|
||||
"--chunked-prefill-size",
|
||||
"512",
|
||||
"--model-loader-extra-config",
|
||||
'{"enable_multithread_load": true, "num_threads": 64}',
|
||||
],
|
||||
env={
|
||||
"SGLANG_ENABLE_PCG_DSV2_DUAL_STREAM": "1",
|
||||
},
|
||||
)
|
||||
|
||||
@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",
|
||||
num_examples=200,
|
||||
num_threads=200,
|
||||
max_tokens=4096,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
print(f"{metrics=}")
|
||||
self.assertGreater(metrics["score"], 0.92)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,122 +0,0 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.run_eval import run_eval
|
||||
from sglang.test.server_fixtures.disaggregation_fixture import (
|
||||
PDDisaggregationServerBase,
|
||||
)
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
is_in_ci,
|
||||
popen_launch_pd_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=900, suite="nightly-8-gpu-b200", nightly=True)
|
||||
|
||||
MIMO_V2_MODEL_PATH = "XiaomiMiMo/MiMo-V2.5"
|
||||
GSM8K_BASELINE_ACCURACY = 0.93
|
||||
|
||||
|
||||
@unittest.skipIf(is_in_ci(), "Temporarily disable the flaky test.")
|
||||
class TestDisaggregationDWDPMiMo(PDDisaggregationServerBase):
|
||||
"""PD disagg with DWDP prefill (4 GPUs) and DP-attention decode (4 GPUs)."""
|
||||
|
||||
NUM_PREFILL_GPUS = 4
|
||||
NUM_DECODE_GPUS = 4
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.model = MIMO_V2_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",
|
||||
"--disaggregation-bootstrap-port",
|
||||
cls.bootstrap_port,
|
||||
"--tp",
|
||||
str(cls.NUM_PREFILL_GPUS),
|
||||
"--dwdp-size",
|
||||
str(cls.NUM_PREFILL_GPUS),
|
||||
"--mm-enable-dp-encoder",
|
||||
"--attention-backend",
|
||||
"fa4",
|
||||
"--mem-fraction-static",
|
||||
"0.78",
|
||||
]
|
||||
prefill_args += 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,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def start_decode(cls):
|
||||
decode_args = [
|
||||
"--trust-remote-code",
|
||||
"--disaggregation-mode",
|
||||
"decode",
|
||||
"--disaggregation-bootstrap-port",
|
||||
cls.bootstrap_port,
|
||||
"--tp",
|
||||
str(cls.NUM_DECODE_GPUS),
|
||||
"--dp",
|
||||
str(cls.NUM_DECODE_GPUS),
|
||||
"--enable-dp-attention",
|
||||
"--moe-dense-tp-size",
|
||||
"1",
|
||||
"--ep-size",
|
||||
str(cls.NUM_DECODE_GPUS),
|
||||
"--attention-backend",
|
||||
"fa4",
|
||||
"--mem-fraction-static",
|
||||
"0.78",
|
||||
"--base-gpu-id",
|
||||
str(cls.NUM_PREFILL_GPUS),
|
||||
]
|
||||
decode_args += 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,
|
||||
)
|
||||
|
||||
def test_gsm8k(self):
|
||||
metrics = run_eval(
|
||||
SimpleNamespace(
|
||||
base_url=self.base_url,
|
||||
model=self.model,
|
||||
eval_name="gsm8k",
|
||||
api="chat",
|
||||
num_shots=5,
|
||||
num_examples=200,
|
||||
max_tokens=4096,
|
||||
num_threads=8,
|
||||
repeat=1,
|
||||
temperature=0.0,
|
||||
top_p=1.0,
|
||||
host="http://127.0.0.1",
|
||||
port=int(self.base_url.split(":")[-1]),
|
||||
)
|
||||
)
|
||||
print(f"{metrics=}")
|
||||
self.assertGreaterEqual(metrics["score"], GSM8K_BASELINE_ACCURACY)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,63 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sglang.test.accuracy_test_runner import AccuracyTestParams
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.performance_test_runner import PerformanceTestParams
|
||||
from sglang.test.run_combined_tests import run_combined_tests
|
||||
from sglang.test.test_utils import ModelLaunchSettings
|
||||
|
||||
register_cuda_ci(
|
||||
est_time=7200,
|
||||
suite="nightly-4-gpu-gb300-kimi-k25",
|
||||
nightly=True,
|
||||
disabled="not needed",
|
||||
)
|
||||
|
||||
MODEL_PATH = "moonshotai/Kimi-K2.5"
|
||||
|
||||
COMMON_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--reasoning-parser=kimi_k2",
|
||||
"--tool-call-parser=kimi_k2",
|
||||
"--mem-fraction-static=0.8",
|
||||
"--enable-multimodal",
|
||||
"--enable-metrics",
|
||||
]
|
||||
|
||||
|
||||
class TestKimiK25(unittest.TestCase):
|
||||
"""Kimi-K2.5 (native INT4) on GB300 (4x GB300 NVL4, tp=4).
|
||||
|
||||
No EAGLE/MTP support for Kimi-K2.5 — only TP and TP+DP+DPA variants.
|
||||
"""
|
||||
|
||||
def test_kimi_k25(self):
|
||||
variants = [
|
||||
ModelLaunchSettings(
|
||||
MODEL_PATH,
|
||||
tp_size=4,
|
||||
extra_args=COMMON_ARGS,
|
||||
variant="TP4",
|
||||
),
|
||||
ModelLaunchSettings(
|
||||
MODEL_PATH,
|
||||
tp_size=4,
|
||||
extra_args=COMMON_ARGS + ["--dp-size=4", "--enable-dp-attention"],
|
||||
variant="TP4+DP4+DPA",
|
||||
),
|
||||
]
|
||||
|
||||
run_combined_tests(
|
||||
models=variants,
|
||||
test_name="Kimi-K2.5",
|
||||
accuracy_params=AccuracyTestParams(
|
||||
dataset="mmmu-pro", baseline_accuracy=0.69, repeat=1, max_tokens=32768
|
||||
),
|
||||
performance_params=PerformanceTestParams(
|
||||
result_dir="performance_results_gb300",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,83 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sglang.test.accuracy_test_runner import AccuracyTestParams
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.performance_test_runner import PerformanceTestParams
|
||||
from sglang.test.run_combined_tests import run_combined_tests
|
||||
from sglang.test.test_utils import ModelLaunchSettings
|
||||
|
||||
register_cuda_ci(
|
||||
est_time=7200,
|
||||
suite="nightly-4-gpu-gb300-qwen35-nvfp4",
|
||||
nightly=True,
|
||||
disabled="not needed",
|
||||
)
|
||||
|
||||
MODEL_PATH = "nvidia/Qwen3.5-397B-A17B-NVFP4"
|
||||
|
||||
COMMON_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--reasoning-parser=qwen3",
|
||||
"--tool-call-parser=qwen3_coder",
|
||||
"--quantization=modelopt_fp4",
|
||||
"--fp4-gemm-backend=flashinfer_cutlass",
|
||||
"--moe-runner-backend=flashinfer_trtllm",
|
||||
"--kv-cache-dtype=fp8_e4m3",
|
||||
"--enable-flashinfer-allreduce-fusion",
|
||||
"--attention-backend=trtllm_mha",
|
||||
"--mem-fraction-static=0.8",
|
||||
"--enable-multimodal",
|
||||
"--enable-metrics",
|
||||
]
|
||||
|
||||
MTP_ARGS = [
|
||||
"--speculative-algorithm=EAGLE",
|
||||
"--speculative-num-steps=3",
|
||||
"--speculative-eagle-topk=1",
|
||||
"--speculative-num-draft-tokens=4",
|
||||
"--mamba-scheduler-strategy=extra_buffer",
|
||||
"--page-size=64",
|
||||
]
|
||||
|
||||
|
||||
class TestQwen35Nvfp4(unittest.TestCase):
|
||||
"""Qwen3.5-397B NVFP4 on GB300 (4x GB300 NVL4, tp=4)."""
|
||||
|
||||
def test_qwen35_nvfp4(self):
|
||||
variants = [
|
||||
ModelLaunchSettings(
|
||||
MODEL_PATH,
|
||||
tp_size=4,
|
||||
extra_args=COMMON_ARGS,
|
||||
variant="TP4",
|
||||
),
|
||||
ModelLaunchSettings(
|
||||
MODEL_PATH,
|
||||
tp_size=4,
|
||||
extra_args=COMMON_ARGS + ["--dp-size=4", "--enable-dp-attention"],
|
||||
variant="TP4+DP4+DPA",
|
||||
),
|
||||
ModelLaunchSettings(
|
||||
MODEL_PATH,
|
||||
tp_size=4,
|
||||
extra_args=COMMON_ARGS
|
||||
+ ["--dp-size=4", "--enable-dp-attention"]
|
||||
+ MTP_ARGS,
|
||||
variant="TP4+DP4+DPA+MTP",
|
||||
),
|
||||
]
|
||||
|
||||
run_combined_tests(
|
||||
models=variants,
|
||||
test_name="Qwen3.5-397B-NVFP4",
|
||||
accuracy_params=AccuracyTestParams(
|
||||
dataset="mmmu-pro", baseline_accuracy=0.76, repeat=1, max_tokens=32768
|
||||
),
|
||||
performance_params=PerformanceTestParams(
|
||||
result_dir="performance_results_gb300",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,232 +0,0 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""
|
||||
Unit tests for LoRA support in embedding models.
|
||||
|
||||
Validates that EmbeddingReqInput correctly handles LoRA fields through
|
||||
normalization, batching, and request splitting.
|
||||
"""
|
||||
|
||||
import multiprocessing as mp
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from sglang.srt.entrypoints.openai.protocol import EmbeddingRequest
|
||||
from sglang.srt.managers.io_struct import EmbeddingReqInput, TokenizedEmbeddingReqInput
|
||||
from sglang.srt.sampling.sampling_params import SamplingParams
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.runners import SRTRunner
|
||||
from sglang.test.test_utils import DEFAULT_PORT_FOR_SRT_TEST_RUNNER, CustomTestCase
|
||||
|
||||
# Test configuration (same model/LoRA as test_lora_hf_sgl_logprob_diff.py)
|
||||
MODEL_PATH = "meta-llama/Llama-2-7b-hf"
|
||||
LORA_PATH = "yushengsu/sglang_lora_logprob_diff_without_tuning"
|
||||
LORA_BACKEND = "triton"
|
||||
SIMILARITY_THRESHOLD = 0.9999
|
||||
|
||||
register_cuda_ci(
|
||||
est_time=150,
|
||||
suite="nightly-1-gpu",
|
||||
)
|
||||
|
||||
|
||||
class TestEmbeddingLoraSupport(unittest.TestCase):
|
||||
"""Test LoRA support in embedding request structures."""
|
||||
|
||||
def test_engine_encode_validates_enable_lora(self):
|
||||
"""Test Engine.encode() validates enable_lora before processing lora_path."""
|
||||
# Use a simple non-gated model for this validation test
|
||||
with SRTRunner(
|
||||
MODEL_PATH,
|
||||
torch_dtype=torch.float16,
|
||||
model_type="embedding",
|
||||
port=DEFAULT_PORT_FOR_SRT_TEST_RUNNER,
|
||||
) as runner:
|
||||
# Should raise ValueError because enable_lora was not set for the server
|
||||
with self.assertRaises(ValueError) as context:
|
||||
runner.engine.encode(prompt="Test", lora_path="fake-adapter")
|
||||
|
||||
error_msg = str(context.exception)
|
||||
self.assertIn("not enabled", error_msg.lower())
|
||||
self.assertIn("--enable-lora", error_msg)
|
||||
self.assertIn("fake-adapter", error_msg)
|
||||
|
||||
def test_embedding_lora_fields(self):
|
||||
"""Test LoRA fields exist and work correctly across all embedding structures."""
|
||||
# EmbeddingReqInput: fields exist, normalization expands single to batch, indexing works
|
||||
req = EmbeddingReqInput(
|
||||
text=["Hello", "World"], lora_path="my-adapter", lora_id=["id1", "id2"]
|
||||
)
|
||||
self.assertIsNotNone(req.lora_path)
|
||||
req.normalize_batch_and_arguments()
|
||||
self.assertEqual(req.lora_path, ["my-adapter", "my-adapter"])
|
||||
self.assertEqual(req[0].lora_path, "my-adapter")
|
||||
self.assertEqual(req[1].lora_id, "id2")
|
||||
|
||||
# EmbeddingReqInput: mismatched list length raises error
|
||||
req = EmbeddingReqInput(text=["Hello", "World", "Test"], lora_path=["adapter1"])
|
||||
with self.assertRaises(ValueError):
|
||||
req.normalize_batch_and_arguments()
|
||||
|
||||
# TokenizedEmbeddingReqInput and EmbeddingRequest have lora fields
|
||||
tokenized = TokenizedEmbeddingReqInput(
|
||||
input_text="Hello",
|
||||
input_ids=[1, 2, 3],
|
||||
image_inputs={},
|
||||
token_type_ids=[],
|
||||
sampling_params=SamplingParams(),
|
||||
lora_id="my-lora-id",
|
||||
)
|
||||
self.assertEqual(tokenized.lora_id, "my-lora-id")
|
||||
self.assertEqual(
|
||||
EmbeddingRequest(
|
||||
input="Hello", model="test", lora_path="adapter"
|
||||
).lora_path,
|
||||
"adapter",
|
||||
)
|
||||
|
||||
|
||||
class TestEmbeddingLoraHFComparison(CustomTestCase):
|
||||
"""Compare HF+LoRA vs SGLang+LoRA embedding outputs."""
|
||||
|
||||
@classmethod
|
||||
def get_hf_embedding_with_lora(cls, model_path, lora_path, texts, torch_dtype):
|
||||
"""Get embeddings from HuggingFace model with LoRA adapter."""
|
||||
from peft import PeftModel
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
|
||||
# Load base model as CausalLM to match adapter's expected structure
|
||||
base_model = AutoModelForCausalLM.from_pretrained(
|
||||
model_path,
|
||||
torch_dtype=torch_dtype,
|
||||
trust_remote_code=True,
|
||||
).cuda()
|
||||
|
||||
# Load LoRA adapter
|
||||
model = PeftModel.from_pretrained(base_model, lora_path)
|
||||
model.eval()
|
||||
|
||||
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
||||
if tokenizer.pad_token is None:
|
||||
tokenizer.pad_token = tokenizer.eos_token
|
||||
|
||||
with torch.no_grad():
|
||||
inputs = tokenizer(
|
||||
texts, padding=True, truncation=True, return_tensors="pt"
|
||||
).to("cuda")
|
||||
|
||||
# Access the inner model (CausalLM wraps the base model)
|
||||
outputs = model.model(**inputs, output_hidden_states=True)
|
||||
hidden_states = outputs.hidden_states[-1]
|
||||
|
||||
# Last token pooling with L2 normalization (matching SGLang)
|
||||
attention_mask = inputs["attention_mask"]
|
||||
last_token_indices = attention_mask.sum(dim=1) - 1
|
||||
batch_size = hidden_states.shape[0]
|
||||
embeddings = hidden_states[
|
||||
torch.arange(batch_size, device="cuda"), last_token_indices
|
||||
]
|
||||
embeddings = embeddings / embeddings.norm(dim=1, keepdim=True)
|
||||
|
||||
# Cleanup
|
||||
del model, base_model
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
return embeddings.cpu().numpy()
|
||||
|
||||
@classmethod
|
||||
def get_sglang_embedding_with_lora(cls, model_path, lora_path, texts, torch_dtype):
|
||||
"""Get embeddings from SGLang with LoRA adapter."""
|
||||
with SRTRunner(
|
||||
model_path,
|
||||
torch_dtype=torch_dtype,
|
||||
model_type="embedding",
|
||||
lora_paths=[lora_path],
|
||||
lora_backend=LORA_BACKEND,
|
||||
port=DEFAULT_PORT_FOR_SRT_TEST_RUNNER,
|
||||
trust_remote_code=True,
|
||||
mem_fraction_static=0.88,
|
||||
) as runner:
|
||||
# Call engine.encode directly with lora_path
|
||||
response = runner.engine.encode(prompt=texts, lora_path=lora_path)
|
||||
if isinstance(response, list):
|
||||
embeddings = [r["embedding"] for r in response]
|
||||
else:
|
||||
embeddings = [response["embedding"]]
|
||||
|
||||
return np.array(embeddings)
|
||||
|
||||
@staticmethod
|
||||
def cosine_similarity(a, b):
|
||||
"""Compute cosine similarity between vectors."""
|
||||
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
|
||||
|
||||
def test_embedding_lora_hf_sglang_similarity(self):
|
||||
"""Test that HF+LoRA and SGLang+LoRA produce similar embeddings."""
|
||||
test_texts = [
|
||||
"Hello world",
|
||||
"This is a test sentence for embedding comparison",
|
||||
]
|
||||
|
||||
print(f"\nModel: {MODEL_PATH}")
|
||||
print(f"LoRA: {LORA_PATH}")
|
||||
|
||||
# Get SGLang embeddings first (before HF loads model into GPU)
|
||||
# This order matches test_lora_hf_sgl_logprob_diff.py and avoids OOM
|
||||
print("\nGetting SGLang embeddings...")
|
||||
sglang_embeddings = self.get_sglang_embedding_with_lora(
|
||||
MODEL_PATH, LORA_PATH, test_texts, torch.float16
|
||||
)
|
||||
|
||||
# Clear GPU memory
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# Get HF embeddings
|
||||
print("Getting HF embeddings...")
|
||||
hf_embeddings = self.get_hf_embedding_with_lora(
|
||||
MODEL_PATH, LORA_PATH, test_texts, torch.float16
|
||||
)
|
||||
|
||||
# Compare embeddings
|
||||
print("\nHF vs SGLang LoRA Embedding Comparison:")
|
||||
similarities = []
|
||||
for i, (hf_emb, sgl_emb) in enumerate(zip(hf_embeddings, sglang_embeddings)):
|
||||
sim = self.cosine_similarity(hf_emb, sgl_emb)
|
||||
similarities.append(sim)
|
||||
print(f" Text {i}: cosine similarity = {sim:.6f}")
|
||||
self.assertGreater(
|
||||
sim,
|
||||
SIMILARITY_THRESHOLD,
|
||||
f"Text {i} similarity {sim:.6f} below threshold {SIMILARITY_THRESHOLD}",
|
||||
)
|
||||
|
||||
avg_similarity = np.mean(similarities)
|
||||
print(f" Average similarity: {avg_similarity:.6f}")
|
||||
print(f" Threshold: {SIMILARITY_THRESHOLD}")
|
||||
|
||||
self.assertGreater(
|
||||
avg_similarity,
|
||||
SIMILARITY_THRESHOLD,
|
||||
f"Average similarity {avg_similarity:.4f} below threshold {SIMILARITY_THRESHOLD}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
mp.set_start_method("spawn")
|
||||
except RuntimeError:
|
||||
pass
|
||||
unittest.main()
|
||||
@@ -10,13 +10,8 @@ from unittest.mock import MagicMock
|
||||
|
||||
from sglang.srt.entrypoints.openai.serving_base import OpenAIServingBase
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from sglang.test.ci.ci_register import (
|
||||
register_amd_ci,
|
||||
register_cpu_ci,
|
||||
register_cuda_ci,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cpu_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="nightly-1-gpu", nightly=True)
|
||||
register_amd_ci(est_time=30, suite="nightly-amd-1-gpu", nightly=True)
|
||||
register_cpu_ci(est_time=8, suite="base-c-test-cpu")
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
|
||||
import multiprocessing as mp
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
from sglang.test.lora_utils import CI_MULTI_LORA_MODELS, run_lora_test_one_by_one
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=200, suite="nightly-1-gpu", nightly=True)
|
||||
register_amd_ci(est_time=200, suite="nightly-amd-1-gpu", nightly=True)
|
||||
|
||||
PROMPTS = [
|
||||
"AI is a field of computer science focused on",
|
||||
"""
|
||||
### Instruction:
|
||||
Tell me about llamas and alpacas
|
||||
### Response:
|
||||
Llamas are large, long-necked animals with a woolly coat. They have two toes on each foot instead of three like other camelids.
|
||||
### Question:
|
||||
What do you know about llamas?
|
||||
### Answer:
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
class TestLoRARadixCache(CustomTestCase):
|
||||
|
||||
def test_lora_radix_cache(self):
|
||||
# Here we need a model case with multiple adaptors for testing correctness of radix cache
|
||||
model_case = CI_MULTI_LORA_MODELS[0]
|
||||
|
||||
torch_dtype = torch.float16
|
||||
max_new_tokens = 32
|
||||
batch_prompts = (
|
||||
PROMPTS
|
||||
if not model_case.skip_long_prompt
|
||||
else [p for p in PROMPTS if len(p) < 1000]
|
||||
)
|
||||
|
||||
# Test lora with radix cache
|
||||
run_lora_test_one_by_one(
|
||||
batch_prompts,
|
||||
model_case,
|
||||
torch_dtype,
|
||||
max_new_tokens=max_new_tokens,
|
||||
disable_radix_cache=False,
|
||||
test_tag="lora-with-radix-cache",
|
||||
)
|
||||
|
||||
# Test lora without radix cache
|
||||
run_lora_test_one_by_one(
|
||||
batch_prompts,
|
||||
model_case,
|
||||
torch_dtype,
|
||||
max_new_tokens=max_new_tokens,
|
||||
disable_radix_cache=True,
|
||||
test_tag="lora-without-radix-cache",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
mp.set_start_method("spawn")
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
unittest.main(warnings="ignore")
|
||||
@@ -1,224 +0,0 @@
|
||||
# Copyright 2023-2025 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
|
||||
"""
|
||||
Test LoRA on models with tied lm_head (tie_word_embeddings=True).
|
||||
|
||||
When tie_word_embeddings=True, lm_head shares the same weight tensor as
|
||||
embed_tokens. PyTorch's named_modules() deduplicates by object identity,
|
||||
so lm_head won't appear as a separate module. This test validates that
|
||||
SGLang correctly handles this case by untying lm_head before LoRA wrapping.
|
||||
|
||||
The test:
|
||||
1. Programmatically creates a LoRA adapter with lm_head in target_modules
|
||||
using PEFT on a model with tie_word_embeddings=True (Qwen/Qwen2.5-0.5B).
|
||||
2. Compares logprobs between HuggingFace+PEFT and SGLang to ensure numerical
|
||||
consistency. This implicitly verifies no NaN values are produced and that
|
||||
LoRA is actually being applied (since HF+PEFT is the trusted reference).
|
||||
"""
|
||||
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
try:
|
||||
from peft import LoraConfig, get_peft_model
|
||||
except ImportError:
|
||||
import subprocess
|
||||
|
||||
subprocess.check_call(["pip", "install", "peft", "--no-deps"])
|
||||
from peft import LoraConfig, get_peft_model
|
||||
|
||||
from transformers import AutoModelForCausalLM
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.runners import HFRunner, SRTRunner
|
||||
from sglang.test.test_utils import DEFAULT_PORT_FOR_SRT_TEST_RUNNER, CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=120, suite="nightly-1-gpu", nightly=True)
|
||||
|
||||
# Use a small model with tie_word_embeddings=True
|
||||
BASE_MODEL = "Qwen/Qwen2.5-0.5B"
|
||||
|
||||
TEST_PROMPTS = [
|
||||
"AI is a field of computer science focused on",
|
||||
"The capital of France is",
|
||||
]
|
||||
|
||||
MAX_NEW_TOKENS = 16
|
||||
LOGPROB_THRESHOLD = 2e-1
|
||||
|
||||
|
||||
def create_lora_adapter_with_lm_head(base_model_name: str, output_dir: str):
|
||||
"""
|
||||
Programmatically create a LoRA adapter that targets lm_head,
|
||||
using a model with tie_word_embeddings=True.
|
||||
|
||||
The adapter uses randomly initialized LoRA weights (no training).
|
||||
This is sufficient to test that:
|
||||
- SGLang can load the adapter without errors
|
||||
- lm_head LoRA is applied (output differs from base model)
|
||||
- Logprobs match between HF and SGLang
|
||||
"""
|
||||
model = AutoModelForCausalLM.from_pretrained(
|
||||
base_model_name,
|
||||
torch_dtype=torch.float16,
|
||||
device_map="cpu",
|
||||
)
|
||||
|
||||
# Verify the model actually has tied embeddings
|
||||
assert (
|
||||
model.config.tie_word_embeddings
|
||||
), f"Expected tie_word_embeddings=True for {base_model_name}"
|
||||
|
||||
# Only target lm_head to isolate the test to the tied-embedding scenario.
|
||||
lora_config = LoraConfig(
|
||||
r=8,
|
||||
lora_alpha=16,
|
||||
target_modules=["lm_head"],
|
||||
lora_dropout=0,
|
||||
bias="none",
|
||||
task_type="CAUSAL_LM",
|
||||
)
|
||||
|
||||
peft_model = get_peft_model(model, lora_config)
|
||||
|
||||
# PEFT initializes lora_B to zeros by default, which makes the adapter
|
||||
# produce identical output to the base model. Initialize lora_B with
|
||||
# non-zero random weights so the adapter has a visible effect.
|
||||
with torch.no_grad():
|
||||
for name, param in peft_model.named_parameters():
|
||||
if "lora_B" in name:
|
||||
torch.nn.init.normal_(param, mean=0.0, std=0.02)
|
||||
|
||||
peft_model.save_pretrained(output_dir)
|
||||
|
||||
# Verify the saved adapter contains lm_head keys
|
||||
from safetensors import safe_open
|
||||
|
||||
safetensors_path = os.path.join(output_dir, "adapter_model.safetensors")
|
||||
f = safe_open(safetensors_path, framework="pt")
|
||||
lm_head_keys = [k for k in f.keys() if "lm_head" in k]
|
||||
assert (
|
||||
len(lm_head_keys) > 0
|
||||
), f"Expected lm_head LoRA weights in adapter, got keys: {sorted(f.keys())}"
|
||||
|
||||
print(f"Created LoRA adapter at {output_dir}")
|
||||
print(f" lm_head keys: {lm_head_keys}")
|
||||
|
||||
# Clean up the model to free memory
|
||||
del peft_model, model
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
class TestLoRATiedLMHead(CustomTestCase):
|
||||
"""
|
||||
Test that LoRA works correctly on models with tied lm_head.
|
||||
"""
|
||||
|
||||
_adapter_dir = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Create a temporary LoRA adapter with lm_head targeting."""
|
||||
super().setUpClass()
|
||||
cls._adapter_dir = tempfile.mkdtemp(prefix="sglang_test_lora_tied_lm_head_")
|
||||
create_lora_adapter_with_lm_head(BASE_MODEL, cls._adapter_dir)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Clean up the temporary adapter directory."""
|
||||
if cls._adapter_dir and os.path.exists(cls._adapter_dir):
|
||||
shutil.rmtree(cls._adapter_dir)
|
||||
super().tearDownClass()
|
||||
|
||||
def test_tied_lm_head_lora_hf_sgl_logprob_match(self):
|
||||
"""
|
||||
Compare logprobs between HuggingFace+PEFT and SGLang+LoRA
|
||||
for a tied lm_head adapter, ensuring numerical consistency.
|
||||
"""
|
||||
prompts = TEST_PROMPTS[:2]
|
||||
|
||||
# Run SGLang with LoRA
|
||||
with SRTRunner(
|
||||
BASE_MODEL,
|
||||
torch_dtype=torch.float16,
|
||||
model_type="generation",
|
||||
lora_paths=[self._adapter_dir],
|
||||
max_loras_per_batch=1,
|
||||
lora_backend="triton",
|
||||
lora_target_modules=["lm_head"],
|
||||
disable_cuda_graph=True,
|
||||
disable_radix_cache=True,
|
||||
mem_fraction_static=0.80,
|
||||
port=DEFAULT_PORT_FOR_SRT_TEST_RUNNER,
|
||||
) as srt_runner:
|
||||
srt_outputs = srt_runner.forward(
|
||||
prompts,
|
||||
max_new_tokens=MAX_NEW_TOKENS,
|
||||
lora_paths=[self._adapter_dir] * len(prompts),
|
||||
)
|
||||
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
# Run HuggingFace with LoRA (via PEFT)
|
||||
with HFRunner(
|
||||
BASE_MODEL,
|
||||
torch_dtype=torch.float16,
|
||||
model_type="generation",
|
||||
) as hf_runner:
|
||||
hf_outputs = hf_runner.forward(
|
||||
prompts,
|
||||
max_new_tokens=MAX_NEW_TOKENS,
|
||||
lora_paths=[self._adapter_dir] * len(prompts),
|
||||
)
|
||||
|
||||
# Compare prefill logprobs
|
||||
for i in range(len(prompts)):
|
||||
srt_logprobs = torch.tensor(srt_outputs.top_input_logprobs[i])
|
||||
hf_logprobs = torch.tensor(hf_outputs.top_input_logprobs[i])
|
||||
max_diff = torch.max(torch.abs(srt_logprobs - hf_logprobs)).item()
|
||||
print(f"Prompt {i} prefill logprob max_diff (SGLang vs HF): {max_diff:.6e}")
|
||||
self.assertLess(
|
||||
max_diff,
|
||||
LOGPROB_THRESHOLD,
|
||||
f"Prompt {i}: prefill logprob diff {max_diff:.6e} "
|
||||
f"exceeds threshold {LOGPROB_THRESHOLD:.0e}",
|
||||
)
|
||||
|
||||
# Compare decode logprobs
|
||||
for i in range(len(prompts)):
|
||||
srt_logprobs = torch.tensor(srt_outputs.top_output_logprobs[i])
|
||||
hf_logprobs = torch.tensor(hf_outputs.top_output_logprobs[i])
|
||||
max_diff = torch.max(torch.abs(srt_logprobs - hf_logprobs)).item()
|
||||
print(f"Prompt {i} decode logprob max_diff (SGLang vs HF): {max_diff:.6e}")
|
||||
self.assertLess(
|
||||
max_diff,
|
||||
LOGPROB_THRESHOLD,
|
||||
f"Prompt {i}: decode logprob diff {max_diff:.6e} "
|
||||
f"exceeds threshold {LOGPROB_THRESHOLD:.0e}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
mp.set_start_method("spawn")
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
unittest.main(warnings="ignore")
|
||||
@@ -1,34 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sglang.srt.utils import get_device_sm
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.eval_accuracy_kit import GSM8KMixin
|
||||
from sglang.test.server_fixtures.default_fixture import DefaultServerBase
|
||||
|
||||
register_cuda_ci(est_time=500, suite="nightly-4-gpu-b200", nightly=True)
|
||||
|
||||
QWEN3_NEXT_MODEL_FP4 = "nvidia/Qwen3-Next-80B-A3B-Instruct-NVFP4"
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
get_device_sm() < 100, "Test requires CUDA SM 100 or higher (Blackwell)"
|
||||
)
|
||||
class TestQwen3NextFp4(GSM8KMixin, DefaultServerBase):
|
||||
model = QWEN3_NEXT_MODEL_FP4
|
||||
gsm8k_accuracy_thres = 0.93
|
||||
other_args = [
|
||||
"--tp-size",
|
||||
"4",
|
||||
"--chunked-prefill-size",
|
||||
"2048",
|
||||
"--quantization",
|
||||
"modelopt_fp4",
|
||||
"--mamba-scheduler-strategy",
|
||||
"extra_buffer",
|
||||
"--mamba-track-interval",
|
||||
"128",
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,77 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sglang.test.accuracy_test_runner import AccuracyTestParams
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.performance_test_runner import PerformanceTestParams
|
||||
from sglang.test.run_combined_tests import run_combined_tests
|
||||
from sglang.test.test_utils import ModelLaunchSettings
|
||||
|
||||
# Runs on B200 via nightly-4-gpu-b200 suite
|
||||
register_cuda_ci(est_time=2000, suite="nightly-4-gpu-b200", nightly=True)
|
||||
|
||||
FULL_DEEPSEEK_V3_FP4_MODEL_PATH = "nvidia/DeepSeek-V3-0324-FP4"
|
||||
|
||||
|
||||
class TestDeepseekR1FP4Unified(unittest.TestCase):
|
||||
"""Unified test class for DeepSeek-V3-0324-FP4 performance and accuracy.
|
||||
|
||||
Two variants:
|
||||
- basic: Standard TP=4
|
||||
- mtp: TP=4 + EAGLE speculative decoding
|
||||
|
||||
Each variant runs BOTH:
|
||||
- Performance test (using NightlyBenchmarkRunner)
|
||||
- Accuracy test (using run_eval with mgsm_en)
|
||||
"""
|
||||
|
||||
def test_deepseek_r1_fp4_all_variants(self):
|
||||
"""Run performance and accuracy for all DeepSeek-R1-0528-NVFP4-v2 variants."""
|
||||
# Define base arguments shared by most variants
|
||||
base_args = [
|
||||
"--tp=4",
|
||||
"--trust-remote-code",
|
||||
"--model-loader-extra-config",
|
||||
'{"enable_multithread_load": true}',
|
||||
]
|
||||
mtp_args = [
|
||||
"--speculative-algorithm=EAGLE",
|
||||
"--speculative-num-steps=3",
|
||||
"--speculative-eagle-topk=1",
|
||||
"--speculative-num-draft-tokens=4",
|
||||
"--mem-frac=0.7",
|
||||
]
|
||||
|
||||
variants = [
|
||||
# Variant: "basic" - Standard TP=4
|
||||
ModelLaunchSettings(
|
||||
FULL_DEEPSEEK_V3_FP4_MODEL_PATH,
|
||||
tp_size=4,
|
||||
extra_args=base_args,
|
||||
variant="TP4",
|
||||
),
|
||||
# Variant: "mtp" - TP=4 + EAGLE speculative decoding
|
||||
ModelLaunchSettings(
|
||||
FULL_DEEPSEEK_V3_FP4_MODEL_PATH,
|
||||
tp_size=4,
|
||||
extra_args=base_args + mtp_args,
|
||||
variant="TP4+MTP",
|
||||
),
|
||||
]
|
||||
|
||||
run_combined_tests(
|
||||
models=variants,
|
||||
test_name="DeepSeek-V3-0324-FP4 Unified",
|
||||
accuracy_params=AccuracyTestParams(
|
||||
dataset="gsm8k",
|
||||
baseline_accuracy=0.935,
|
||||
num_examples=200,
|
||||
api="completion",
|
||||
),
|
||||
performance_params=PerformanceTestParams(
|
||||
result_dir="performance_results_deepseek_v3_fp4",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,60 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.nightly_utils import NightlyBenchmarkRunner
|
||||
from sglang.test.test_utils import DEFAULT_URL_FOR_TEST
|
||||
|
||||
register_cuda_ci(est_time=600, suite="nightly-4-gpu-b200", nightly=True)
|
||||
|
||||
RESULT_DIR = "performance_results_gpt_oss_4gpu"
|
||||
|
||||
|
||||
class TestNightlyGptOss4GpuPerformance(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.models = [
|
||||
(
|
||||
"openai/gpt-oss-120b",
|
||||
[
|
||||
"--tp",
|
||||
"4",
|
||||
"--cuda-graph-max-bs-decode",
|
||||
"200",
|
||||
"--mem-fraction-static",
|
||||
"0.93",
|
||||
],
|
||||
),
|
||||
]
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.batch_sizes = [1, 1, 8, 16, 64]
|
||||
cls.input_lens = (4096,)
|
||||
cls.output_lens = (512,)
|
||||
cls.runner = NightlyBenchmarkRunner(RESULT_DIR, cls.__name__, cls.base_url)
|
||||
cls.runner.setup_result_directory()
|
||||
|
||||
def test_bench_one_batch(self):
|
||||
all_model_succeed = True
|
||||
|
||||
for model_path, other_args in self.models:
|
||||
with self.subTest(model=model_path):
|
||||
results, success, _ = self.runner.run_benchmark_for_model(
|
||||
model_path=model_path,
|
||||
batch_sizes=self.batch_sizes,
|
||||
input_lens=self.input_lens,
|
||||
output_lens=self.output_lens,
|
||||
other_args=other_args,
|
||||
)
|
||||
|
||||
if not success:
|
||||
all_model_succeed = False
|
||||
|
||||
self.runner.add_report(results)
|
||||
|
||||
self.runner.write_final_report()
|
||||
|
||||
if not all_model_succeed:
|
||||
raise AssertionError("Some models failed the perf tests.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,162 +0,0 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Regression test for the fa_skip_kv_cache embedding fast path under piecewise
|
||||
CUDA graph.
|
||||
|
||||
PR #21971 added an embedding fast path (`fa_skip_kv_cache`) that serves attention
|
||||
with `flash_attn_varlen_func` on raw K/V. Under a piecewise CUDA graph the model
|
||||
forward runs at a padded token-bucket size, so `q` has more rows than
|
||||
`cu_seqlens_q` covers. `flash_attn_varlen_func` requires
|
||||
`q.shape[0] == cu_seqlens_q[-1]`; when that is violated the boundary query block
|
||||
corrupts the **last real token's** output. Because embedding models use LAST-token
|
||||
pooling, that corrupted row IS the returned embedding -> ~40% of *short* inputs
|
||||
came back fully NaN (long inputs, which fill the bucket, were unaffected).
|
||||
|
||||
This test feeds a spread of short inputs through `fa3 + piecewise + fa_skip_kv_cache`
|
||||
and asserts no embedding contains NaN, and that the embeddings match the
|
||||
non-piecewise path.
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang import Engine
|
||||
from sglang.srt.utils import get_device_sm
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
# Route to the nightly 1-GPU suite, which runs on the H100 pool (1-gpu-h100, SM90).
|
||||
# FA3 + the piecewise embedding path this regression covers only runs on
|
||||
# Ampere/Ada/Hopper (SM 80-90), so the test must land on the H100 pool to actually
|
||||
# execute (on the RTX 5090 SM120/Blackwell 1-gpu-small runner it would skip 100%).
|
||||
register_cuda_ci(est_time=600, suite="nightly-1-gpu", nightly=True)
|
||||
|
||||
# Lowest/highest CUDA SM that supports the FA3 + piecewise embedding path. FA3 is
|
||||
# unavailable on Blackwell (sm100 B200 / sm120 consumer e.g. RTX 5090); the gate is
|
||||
# applied at RUNTIME (see setUp) so the SM is read after CUDA is initialized on the
|
||||
# actual runner, never at import/collection time.
|
||||
_FA3_SM_MIN, _FA3_SM_MAX = 80, 90
|
||||
|
||||
# Overridable so the test can run against a locally-mounted model in dev.
|
||||
MODEL_PATH = os.environ.get("SGLANG_TEST_EMB_MODEL", "Qwen/Qwen3-Embedding-0.6B")
|
||||
|
||||
_WORDS = [
|
||||
"the",
|
||||
"quick",
|
||||
"brown",
|
||||
"fox",
|
||||
"jumps",
|
||||
"over",
|
||||
"lazy",
|
||||
"dog",
|
||||
"embedding",
|
||||
"vector",
|
||||
"token",
|
||||
"sample",
|
||||
]
|
||||
|
||||
|
||||
def _short_prompts():
|
||||
"""A spread of short inputs (~1..150 tokens).
|
||||
|
||||
The bug only triggers when a prefill is PADDED up to a piecewise bucket, i.e.
|
||||
for token counts that are not exactly a capture size. Using many lengths
|
||||
guarantees several land just below a bucket boundary (80/96/112/128 ...).
|
||||
"""
|
||||
return [" ".join(_WORDS[i % len(_WORDS)] for i in range(n)) for n in range(1, 150)]
|
||||
|
||||
|
||||
def _embed(prompts, **engine_kwargs):
|
||||
# fa_skip_kv_cache is enabled by: is_embedding + chunked_prefill_size == -1
|
||||
# + disable_radix_cache (+ a non-MLA model + the FA3 backend).
|
||||
engine = Engine(
|
||||
model_path=MODEL_PATH,
|
||||
is_embedding=True,
|
||||
attention_backend="fa3",
|
||||
chunked_prefill_size=-1,
|
||||
disable_radix_cache=True,
|
||||
**engine_kwargs,
|
||||
)
|
||||
try:
|
||||
# Encode one request per forward (batch size 1). The bug corrupts the
|
||||
# last real token, which sits exactly at the real/pad boundary; when many
|
||||
# requests are batched into one forward only the tail request hits the
|
||||
# boundary, which hides the per-request failure rate.
|
||||
embs = []
|
||||
for prompt in prompts:
|
||||
out = engine.encode(prompt)
|
||||
emb = out["embedding"] if isinstance(out, dict) else out[0]["embedding"]
|
||||
embs.append(torch.tensor(emb, dtype=torch.float32))
|
||||
return embs
|
||||
finally:
|
||||
engine.shutdown()
|
||||
|
||||
|
||||
# Enables the piecewise CUDA graph for prefill the way production does. After the
|
||||
# cuda-graph refactor (#23906) the piecewise config lives in cuda_graph_config; the
|
||||
# convenience kwargs below fold into cuda_graph_config[prefill]:
|
||||
# - cuda_graph_backend_prefill="tc_piecewise" -> prefill.backend (also the default)
|
||||
# - cuda_graph_max_bs_prefill=32768 -> prefill.max_bs (for tc_piecewise
|
||||
# prefill, max_bs/bs carries the captured TOKEN count -- the old
|
||||
# piecewise_cuda_graph_max_tokens)
|
||||
# - cuda_graph_tc_compiler="inductor" -> prefill.tc_compiler
|
||||
_PIECEWISE_KWARGS = dict(
|
||||
cuda_graph_backend_prefill="tc_piecewise",
|
||||
cuda_graph_max_bs_prefill=32768,
|
||||
cuda_graph_tc_compiler="inductor",
|
||||
)
|
||||
|
||||
|
||||
class TestFaSkipKvCachePiecewiseNoNaN(CustomTestCase):
|
||||
def setUp(self):
|
||||
# Gate at runtime: read the SM after CUDA is initialized on the runner. If
|
||||
# the hardware can't run FA3 (e.g. SM120 RTX 5090 / SM100 B200), skip --
|
||||
# a skip is NOT a CI failure, it just records the test as inapplicable here.
|
||||
sm = get_device_sm()
|
||||
if not (_FA3_SM_MIN <= sm <= _FA3_SM_MAX):
|
||||
self.skipTest(
|
||||
f"fa3 + piecewise embedding repro requires CUDA SM "
|
||||
f"{_FA3_SM_MIN}-{_FA3_SM_MAX} (Ampere/Ada/Hopper); got SM {sm}"
|
||||
)
|
||||
|
||||
def test_no_nan_with_piecewise(self):
|
||||
prompts = _short_prompts()
|
||||
embs = _embed(prompts, **_PIECEWISE_KWARGS)
|
||||
nan_idx = [i for i, e in enumerate(embs) if torch.isnan(e).any()]
|
||||
self.assertEqual(
|
||||
nan_idx,
|
||||
[],
|
||||
f"{len(nan_idx)}/{len(embs)} short-input embeddings contain NaN under "
|
||||
f"fa_skip_kv_cache + piecewise CUDA graph (e.g. prompt indices {nan_idx[:10]})",
|
||||
)
|
||||
|
||||
def test_matches_non_piecewise(self):
|
||||
prompts = _short_prompts()
|
||||
with_pcg = _embed(prompts, **_PIECEWISE_KWARGS)
|
||||
without_pcg = _embed(prompts, disable_prefill_cuda_graph=True)
|
||||
for i, (a, b) in enumerate(zip(with_pcg, without_pcg)):
|
||||
self.assertFalse(
|
||||
torch.isnan(a).any(),
|
||||
f"prompt {i}: NaN embedding with piecewise CUDA graph",
|
||||
)
|
||||
cos = torch.nn.functional.cosine_similarity(a, b, dim=0).item()
|
||||
self.assertGreater(
|
||||
cos, 0.99, f"prompt {i}: cosine {cos:.4f} < 0.99 vs non-piecewise"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,68 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sglang.test.accuracy_test_runner import AccuracyTestParams
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.performance_test_runner import PerformanceTestParams
|
||||
from sglang.test.run_combined_tests import run_combined_tests
|
||||
from sglang.test.test_utils import ModelLaunchSettings
|
||||
|
||||
# Kimi-K2.5 NVFP4 + EAGLE3 (MLA draft) speculative decoding on 4x B200, tp=4.
|
||||
register_cuda_ci(est_time=3000, suite="nightly-4-gpu-b200", nightly=True)
|
||||
|
||||
MODEL_PATH = "nvidia/Kimi-K2.5-NVFP4"
|
||||
DRAFT_MODEL_PATH = "lightseekorg/kimi-k2.5-eagle3-mla"
|
||||
|
||||
EXTRA_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--attention-backend=tokenspeed_mla",
|
||||
"--moe-runner-backend=flashinfer_trtllm",
|
||||
"--quantization=modelopt_fp4",
|
||||
"--kv-cache-dtype=fp8_e4m3",
|
||||
"--mem-fraction-static=0.85",
|
||||
"--max-running-requests=16",
|
||||
"--speculative-algorithm=EAGLE3",
|
||||
f"--speculative-draft-model-path={DRAFT_MODEL_PATH}",
|
||||
"--speculative-num-steps=3",
|
||||
"--speculative-eagle-topk=1",
|
||||
"--speculative-num-draft-tokens=4",
|
||||
"--speculative-draft-model-quantization=unquant",
|
||||
]
|
||||
|
||||
|
||||
class TestKimiK25Nvfp4Eagle(unittest.TestCase):
|
||||
"""Kimi-K2.5 NVFP4 with EAGLE3 speculative decoding on 4x B200 (tp=4).
|
||||
|
||||
Runs both an accuracy test (gsm8k) and a performance test (bs=1/8/16),
|
||||
and gates the speculative-decoding accept length.
|
||||
"""
|
||||
|
||||
def test_kimi_k25_nvfp4_eagle(self):
|
||||
variants = [
|
||||
ModelLaunchSettings(
|
||||
MODEL_PATH,
|
||||
tp_size=4,
|
||||
extra_args=EXTRA_ARGS,
|
||||
variant="TP4+EAGLE3",
|
||||
),
|
||||
]
|
||||
|
||||
run_combined_tests(
|
||||
models=variants,
|
||||
test_name="Kimi-K2.5-NVFP4 EAGLE3",
|
||||
# Thresholds from a measured tp=4 run: gsm8k 0.945, perf accept ~3.0-3.3.
|
||||
accuracy_params=AccuracyTestParams(
|
||||
dataset="gsm8k",
|
||||
baseline_accuracy=0.92,
|
||||
num_examples=200,
|
||||
api="completion",
|
||||
),
|
||||
performance_params=PerformanceTestParams(
|
||||
batch_sizes=[1, 8, 16],
|
||||
spec_accept_length_threshold=2.8,
|
||||
result_dir="performance_results_kimi_k25_nvfp4_eagle",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,70 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sglang.test.accuracy_test_runner import AccuracyTestParams
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.performance_test_runner import PerformanceTestParams
|
||||
from sglang.test.run_combined_tests import run_combined_tests
|
||||
from sglang.test.test_utils import ModelLaunchSettings
|
||||
|
||||
# Kimi-K2.6 NVFP4 (pure-MLA target, fp8 KV) + DFlash speculative decoding on 8x B200, tp=8.
|
||||
register_cuda_ci(est_time=3600, suite="nightly-8-gpu-b200", nightly=True)
|
||||
|
||||
MODEL_PATH = "nvidia/Kimi-K2.6-NVFP4"
|
||||
DRAFT_MODEL_PATH = "nvidia/Kimi-K2.6-DFlash"
|
||||
|
||||
# trtllm_mla verify only; cuteDSL fold verify depends on the flashinfer version.
|
||||
EXTRA_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--quantization=modelopt_fp4",
|
||||
"--moe-runner-backend=flashinfer_trtllm",
|
||||
"--fp4-gemm-backend=flashinfer_cutlass",
|
||||
"--attention-backend=trtllm_mla",
|
||||
"--kv-cache-dtype=fp8_e4m3",
|
||||
"--mem-fraction-static=0.85",
|
||||
"--max-running-requests=16",
|
||||
"--speculative-algorithm=DFLASH",
|
||||
f"--speculative-draft-model-path={DRAFT_MODEL_PATH}",
|
||||
"--speculative-num-draft-tokens=8",
|
||||
"--speculative-draft-attention-backend=fa4",
|
||||
"--speculative-draft-model-quantization=unquant",
|
||||
"--speculative-draft-window-size=4096",
|
||||
]
|
||||
|
||||
|
||||
class TestKimiK26Nvfp4Dflash(unittest.TestCase):
|
||||
"""Kimi-K2.6 NVFP4 (pure-MLA, fp8 KV) with DFlash speculative decoding on 8x B200 (tp=8).
|
||||
|
||||
Runs both an accuracy test (gsm8k) and a performance test (bs=1/8/16), and gates the
|
||||
speculative-decoding accept length. Guards the pure-MLA fp8-KV DFlash path.
|
||||
"""
|
||||
|
||||
def test_kimi_k26_nvfp4_dflash(self):
|
||||
variants = [
|
||||
ModelLaunchSettings(
|
||||
MODEL_PATH,
|
||||
tp_size=8,
|
||||
extra_args=EXTRA_ARGS,
|
||||
variant="TP8+DFLASH",
|
||||
),
|
||||
]
|
||||
|
||||
run_combined_tests(
|
||||
models=variants,
|
||||
test_name="Kimi-K2.6-NVFP4 DFlash",
|
||||
# Thresholds from a measured tp=8 run: gsm8k 0.936 (full set), accept length ~2.66.
|
||||
accuracy_params=AccuracyTestParams(
|
||||
dataset="gsm8k",
|
||||
baseline_accuracy=0.92,
|
||||
num_examples=200,
|
||||
api="completion",
|
||||
),
|
||||
performance_params=PerformanceTestParams(
|
||||
batch_sizes=[1, 8, 16],
|
||||
spec_accept_length_threshold=2.0,
|
||||
result_dir="performance_results_kimi_k26_nvfp4_dflash",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,41 +0,0 @@
|
||||
import unittest
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.kits.eval_accuracy_kit import MMLUMixin
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_MODEL_NAME_FOR_TEST,
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
# Note: AMD registration removed - test_cpp_radix_cache fails on AMD due to C++ radix tree issues
|
||||
register_cuda_ci(est_time=60, suite="nightly-1-gpu", nightly=True)
|
||||
|
||||
|
||||
class TestCppRadixCache(CustomTestCase, MMLUMixin):
|
||||
mmlu_score_threshold = 0.65
|
||||
mmlu_num_examples = 64
|
||||
mmlu_num_threads = 32
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
envs.SGLANG_EXPERIMENTAL_CPP_RADIX_TREE.set(True)
|
||||
cls.model = DEFAULT_MODEL_NAME_FOR_TEST
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,100 +0,0 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.run_eval import run_eval
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_DEEPSEEK_NVFP4_MODEL_FOR_TEST,
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
is_in_ci,
|
||||
popen_launch_server,
|
||||
write_github_step_summary,
|
||||
)
|
||||
|
||||
# 16 GPU test (4 TP x 4 DP), runs on 2x 8-GPU B200 nodes
|
||||
register_cuda_ci(est_time=600, suite="nightly-8-gpu-b200", nightly=True)
|
||||
|
||||
|
||||
def test_gsm8k(base_url: str, model: str):
|
||||
requests.get(base_url + "/flush_cache")
|
||||
|
||||
args = SimpleNamespace(
|
||||
base_url=base_url,
|
||||
model=model,
|
||||
eval_name="gsm8k",
|
||||
api="completion",
|
||||
max_tokens=512,
|
||||
num_examples=200,
|
||||
num_threads=128,
|
||||
)
|
||||
metrics = run_eval(args)
|
||||
server_info = requests.get(base_url + "/server_info").json()
|
||||
avg_spec_accept_length = server_info["internal_states"][0]["avg_spec_accept_length"]
|
||||
|
||||
print(f"{metrics=}")
|
||||
print(f"{avg_spec_accept_length=}")
|
||||
return metrics, avg_spec_accept_length
|
||||
|
||||
|
||||
class TestEagleDPAttnServerLarge(CustomTestCase):
|
||||
# FIXME: move this large mode test into nightly tests
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.model = DEFAULT_DEEPSEEK_NVFP4_MODEL_FOR_TEST
|
||||
cls.base_url = DEFAULT_URL_FOR_TEST
|
||||
other_args = [
|
||||
"--tp-size",
|
||||
"4",
|
||||
"--dp-size",
|
||||
"4",
|
||||
"--enable-dp-attention",
|
||||
"--attention-backend",
|
||||
"trtllm_mla",
|
||||
"--moe-runner-backend",
|
||||
"flashinfer_trtllm",
|
||||
"--quantization",
|
||||
"modelopt_fp4",
|
||||
"--speculative-algorithm",
|
||||
"EAGLE",
|
||||
"--speculative-num-steps",
|
||||
"3",
|
||||
"--speculative-eagle-topk",
|
||||
"1",
|
||||
"--speculative-num-draft-tokens",
|
||||
"4",
|
||||
"--kv-cache-dtype",
|
||||
"fp8_e4m3",
|
||||
"--model-loader-extra-config",
|
||||
'{"enable_multithread_load": true,"num_threads": 64}',
|
||||
]
|
||||
cls.process = popen_launch_server(
|
||||
cls.model,
|
||||
cls.base_url,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=other_args,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
|
||||
def test_a_gsm8k(self):
|
||||
metrics, avg_spec_accept_length = test_gsm8k(self.base_url, self.model)
|
||||
|
||||
self.assertGreater(metrics["score"], 0.94)
|
||||
self.assertGreater(avg_spec_accept_length, 2.7)
|
||||
if is_in_ci():
|
||||
write_github_step_summary(
|
||||
f"### test_gsm8k (deepseek-v3-fp4 mtp)\n"
|
||||
f'{metrics["score"]=:.3f}\n'
|
||||
f"{avg_spec_accept_length=:.2f}\n"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -20,7 +20,7 @@ from sglang.srt.utils.model_file_verifier import (
|
||||
generate_checksums,
|
||||
verify,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cpu_ci, register_cuda_ci
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
@@ -28,7 +28,6 @@ from sglang.test.test_utils import (
|
||||
)
|
||||
|
||||
# Note: AMD registration removed - test_model_file_verifier fails on AMD
|
||||
register_cuda_ci(est_time=120, suite="nightly-1-gpu", nightly=True)
|
||||
register_cpu_ci(est_time=540, suite="base-c-test-cpu")
|
||||
|
||||
MODEL_NAME = "Qwen/Qwen3-0.6B"
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=120, suite="nightly-1-gpu", nightly=True)
|
||||
register_amd_ci(est_time=120, suite="nightly-amd-1-gpu", nightly=True)
|
||||
|
||||
TEST_ROUTING_KEY = "test-routing-key-12345"
|
||||
TEST_CUSTOM_HEADER_NAME = "X-Test-Header"
|
||||
TEST_CUSTOM_HEADER_VALUE = "test-header-value-67890"
|
||||
TEST_MODEL_NAME = "Qwen/Qwen3-0.6B"
|
||||
|
||||
|
||||
class BaseTestRequestLogger:
|
||||
log_requests_format = None
|
||||
env_vars: dict[str, str] = {} # Env vars to set before server launch
|
||||
request_headers: dict[str, str] = {"X-SMG-Routing-Key": TEST_ROUTING_KEY}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._temp_dir_obj = tempfile.TemporaryDirectory()
|
||||
cls.temp_dir = cls._temp_dir_obj.name
|
||||
cls.stdout = io.StringIO()
|
||||
cls.stderr = io.StringIO()
|
||||
other_args = [
|
||||
"--log-requests",
|
||||
"--log-requests-level",
|
||||
"2",
|
||||
"--log-requests-format",
|
||||
cls.log_requests_format,
|
||||
"--skip-server-warmup",
|
||||
"--log-requests-target",
|
||||
"stdout",
|
||||
cls.temp_dir,
|
||||
]
|
||||
# Set env vars and save old values for restoration
|
||||
cls._old_env_vars = {}
|
||||
for key, value in cls.env_vars.items():
|
||||
cls._old_env_vars[key] = os.environ.get(key)
|
||||
os.environ[key] = value
|
||||
|
||||
cls.process = popen_launch_server(
|
||||
TEST_MODEL_NAME,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=other_args,
|
||||
return_stdout_stderr=(cls.stdout, cls.stderr),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
kill_process_tree(cls.process.pid)
|
||||
cls.stdout.close()
|
||||
cls.stderr.close()
|
||||
cls._temp_dir_obj.cleanup()
|
||||
# Restore env vars
|
||||
for key, old_value in cls._old_env_vars.items():
|
||||
if old_value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = old_value
|
||||
|
||||
def _verify_logs(self, content: str, source_name: str):
|
||||
raise NotImplementedError
|
||||
|
||||
def _verify_openai_logs(self, content: str, source_name: str):
|
||||
raise NotImplementedError
|
||||
|
||||
def _wait_until_verified(
|
||||
self,
|
||||
verify_fn,
|
||||
get_content_fn,
|
||||
source_name: str,
|
||||
timeout: float = 10.0,
|
||||
interval: float = 0.1,
|
||||
):
|
||||
deadline = time.time() + timeout
|
||||
last_error = None
|
||||
|
||||
while time.time() < deadline:
|
||||
content = get_content_fn()
|
||||
try:
|
||||
verify_fn(content, source_name)
|
||||
return
|
||||
except AssertionError as err:
|
||||
last_error = err
|
||||
time.sleep(interval)
|
||||
|
||||
if last_error is not None:
|
||||
raise last_error
|
||||
|
||||
def test_logging(self):
|
||||
response = requests.post(
|
||||
DEFAULT_URL_FOR_TEST + "/generate",
|
||||
json={
|
||||
"text": "Hello",
|
||||
"sampling_params": {"max_new_tokens": 8, "temperature": 0},
|
||||
},
|
||||
headers=self.request_headers,
|
||||
timeout=30,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self._wait_until_verified(
|
||||
self._verify_logs,
|
||||
lambda: self.stdout.getvalue() + self.stderr.getvalue(),
|
||||
"stdout",
|
||||
)
|
||||
self._wait_until_verified(
|
||||
self._verify_logs,
|
||||
lambda: "".join(f.read_text() for f in Path(self.temp_dir).glob("*.log")),
|
||||
"log files",
|
||||
)
|
||||
|
||||
log_files = list(Path(self.temp_dir).glob("*.log"))
|
||||
self.assertGreater(len(log_files), 0, "No log files found in temp directory")
|
||||
|
||||
def test_openai_chat_logging(self):
|
||||
response = requests.post(
|
||||
DEFAULT_URL_FOR_TEST + "/v1/chat/completions",
|
||||
json={
|
||||
"model": TEST_MODEL_NAME,
|
||||
"messages": [{"role": "user", "content": "hello request logger"}],
|
||||
"max_tokens": 8,
|
||||
"temperature": 0,
|
||||
},
|
||||
headers=self.request_headers,
|
||||
timeout=30,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self._wait_until_verified(
|
||||
self._verify_openai_logs,
|
||||
lambda: self.stdout.getvalue() + self.stderr.getvalue(),
|
||||
"stdout",
|
||||
)
|
||||
self._wait_until_verified(
|
||||
self._verify_openai_logs,
|
||||
lambda: "".join(f.read_text() for f in Path(self.temp_dir).glob("*.log")),
|
||||
"log files",
|
||||
)
|
||||
|
||||
log_files = list(Path(self.temp_dir).glob("*.log"))
|
||||
self.assertGreater(len(log_files), 0, "No log files found in temp directory")
|
||||
|
||||
|
||||
class TestRequestLoggerText(BaseTestRequestLogger, CustomTestCase):
|
||||
log_requests_format = "text"
|
||||
|
||||
def _verify_logs(self, content: str, source_name: str):
|
||||
self.assertIn("Receive:", content, f"'Receive:' not found in {source_name}")
|
||||
self.assertIn("Finish:", content, f"'Finish:' not found in {source_name}")
|
||||
self.assertIn(
|
||||
TEST_ROUTING_KEY, content, f"Routing key not found in {source_name}"
|
||||
)
|
||||
self.assertIn(
|
||||
"x-smg-routing-key", content, f"Header name not found in {source_name}"
|
||||
)
|
||||
|
||||
def _verify_openai_logs(self, content: str, source_name: str):
|
||||
self.assertIn(
|
||||
"Receive OpenAI:", content, f"OpenAI receive log not found in {source_name}"
|
||||
)
|
||||
self.assertIn("'messages':", content, f"Messages not found in {source_name}")
|
||||
self.assertIn(
|
||||
"hello request logger",
|
||||
content,
|
||||
f"OpenAI user prompt not found in {source_name}",
|
||||
)
|
||||
|
||||
|
||||
class TestRequestLoggerJson(BaseTestRequestLogger, CustomTestCase):
|
||||
log_requests_format = "json"
|
||||
|
||||
def _verify_logs(self, content: str, source_name: str):
|
||||
received_found = False
|
||||
finished_found = False
|
||||
for line in content.splitlines():
|
||||
idx = line.find("{")
|
||||
if idx == -1:
|
||||
continue
|
||||
try:
|
||||
data = json.loads(line[idx:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
rid = data.get("rid", "")
|
||||
if rid.startswith(HEALTH_CHECK_RID_PREFIX):
|
||||
continue
|
||||
|
||||
if data.get("event") == "request.received":
|
||||
self.assertIn("rid", data)
|
||||
self.assertIn("obj", data)
|
||||
self.assertEqual(
|
||||
data.get("headers", {}).get("x-smg-routing-key"), TEST_ROUTING_KEY
|
||||
)
|
||||
received_found = True
|
||||
elif data.get("event") == "request.finished":
|
||||
self.assertIn("rid", data)
|
||||
self.assertIn("obj", data)
|
||||
self.assertIn("out", data)
|
||||
self.assertEqual(
|
||||
data.get("headers", {}).get("x-smg-routing-key"), TEST_ROUTING_KEY
|
||||
)
|
||||
finished_found = True
|
||||
|
||||
self.assertTrue(
|
||||
received_found, f"request.received event not found in {source_name}"
|
||||
)
|
||||
self.assertTrue(
|
||||
finished_found, f"request.finished event not found in {source_name}"
|
||||
)
|
||||
|
||||
def _verify_openai_logs(self, content: str, source_name: str):
|
||||
openai_received_found = False
|
||||
for line in content.splitlines():
|
||||
idx = line.find("{")
|
||||
if idx == -1:
|
||||
continue
|
||||
try:
|
||||
data = json.loads(line[idx:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if data.get("event") != "request.received.openai":
|
||||
continue
|
||||
|
||||
obj = data.get("obj", {})
|
||||
self.assertEqual(obj.get("model"), TEST_MODEL_NAME)
|
||||
self.assertIsInstance(obj.get("messages"), list)
|
||||
self.assertGreater(len(obj.get("messages")), 0)
|
||||
self.assertEqual(obj["messages"][0].get("content"), "hello request logger")
|
||||
self.assertEqual(
|
||||
data.get("headers", {}).get("x-smg-routing-key"), TEST_ROUTING_KEY
|
||||
)
|
||||
openai_received_found = True
|
||||
break
|
||||
|
||||
self.assertTrue(
|
||||
openai_received_found,
|
||||
f"request.received.openai event not found in {source_name}",
|
||||
)
|
||||
|
||||
|
||||
class TestCustomHeaderViaEnvVar(BaseTestRequestLogger, CustomTestCase):
|
||||
"""Test that custom headers can be added via SGLANG_LOG_REQUEST_HEADERS env var."""
|
||||
|
||||
log_requests_format = "text"
|
||||
env_vars = {"SGLANG_LOG_REQUEST_HEADERS": TEST_CUSTOM_HEADER_NAME}
|
||||
request_headers = {
|
||||
"X-SMG-Routing-Key": TEST_ROUTING_KEY,
|
||||
TEST_CUSTOM_HEADER_NAME: TEST_CUSTOM_HEADER_VALUE,
|
||||
}
|
||||
|
||||
def _verify_logs(self, content: str, source_name: str):
|
||||
# Verify custom header is logged
|
||||
self.assertIn(
|
||||
TEST_CUSTOM_HEADER_NAME.lower(),
|
||||
content,
|
||||
f"Custom header name not found in {source_name}",
|
||||
)
|
||||
self.assertIn(
|
||||
TEST_CUSTOM_HEADER_VALUE,
|
||||
content,
|
||||
f"Custom header value not found in {source_name}",
|
||||
)
|
||||
# Verify default header is still logged (env var appends, not replaces)
|
||||
self.assertIn(
|
||||
"x-smg-routing-key",
|
||||
content,
|
||||
f"Default header should still be in whitelist in {source_name}",
|
||||
)
|
||||
self.assertIn(
|
||||
TEST_ROUTING_KEY,
|
||||
content,
|
||||
f"Default header value not found in {source_name}",
|
||||
)
|
||||
|
||||
def _verify_openai_logs(self, content: str, source_name: str):
|
||||
self.assertIn(
|
||||
"Receive OpenAI:", content, f"OpenAI receive log not found in {source_name}"
|
||||
)
|
||||
self.assertIn(
|
||||
TEST_CUSTOM_HEADER_NAME.lower(),
|
||||
content,
|
||||
f"Custom header name not found in {source_name}",
|
||||
)
|
||||
self.assertIn(
|
||||
TEST_CUSTOM_HEADER_VALUE,
|
||||
content,
|
||||
f"Custom header value not found in {source_name}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,79 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from sglang.srt.utils import kill_process_tree
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
from sglang.test.test_utils import (
|
||||
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
CustomTestCase,
|
||||
popen_launch_server,
|
||||
)
|
||||
|
||||
register_cuda_ci(est_time=120, suite="nightly-1-gpu", nightly=True)
|
||||
register_amd_ci(est_time=120, suite="nightly-amd-1-gpu", nightly=True)
|
||||
|
||||
|
||||
class TestSchedulerStatusLogger(CustomTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.temp_dir = tempfile.mkdtemp()
|
||||
cls.addClassCleanup(shutil.rmtree, cls.temp_dir)
|
||||
env = os.environ.copy()
|
||||
env["SGLANG_LOG_SCHEDULER_STATUS_TARGET"] = cls.temp_dir
|
||||
env["SGLANG_LOG_SCHEDULER_STATUS_INTERVAL"] = "1"
|
||||
cls.process = popen_launch_server(
|
||||
"Qwen/Qwen3-0.6B",
|
||||
DEFAULT_URL_FOR_TEST,
|
||||
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
||||
other_args=["--skip-server-warmup", "--enable-metrics"],
|
||||
env=env,
|
||||
)
|
||||
cls.addClassCleanup(kill_process_tree, cls.process.pid)
|
||||
|
||||
def test_scheduler_status_dump(self):
|
||||
response = requests.post(
|
||||
DEFAULT_URL_FOR_TEST + "/generate",
|
||||
json={
|
||||
"text": "Hello",
|
||||
"sampling_params": {"max_new_tokens": 8, "temperature": 0},
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
events = list(_find_log_events(self.temp_dir, "scheduler.status"))
|
||||
print(f"{events=}")
|
||||
self.assertGreater(len(events), 0, "scheduler.status event not found")
|
||||
data = events[0]
|
||||
for field in ["timestamp", "rank", "running_rids", "queued_rids"]:
|
||||
self.assertIn(field, data)
|
||||
self.assertIsInstance(data["running_rids"], list)
|
||||
self.assertIsInstance(data["queued_rids"], list)
|
||||
|
||||
|
||||
def _find_log_events(log_dir: str, event_name: str):
|
||||
for f in Path(log_dir).glob("*.log"):
|
||||
for line in f.read_text().splitlines():
|
||||
idx = line.find("{")
|
||||
if idx == -1:
|
||||
continue
|
||||
try:
|
||||
data = json.loads(line[idx:])
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if data.get("event") == event_name:
|
||||
yield data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user