+26









Liangsheng Yin
DarkSharpness
Xiaoyu Zhang
Mick
Yuhao Yang
Cheng Wan
Ke Bao
Baizhou Zhang
Chunan Zeng
Khoa Pham
Ziyi Xu
Zijie Xia
Yuwei An
zhangxiaohao
Yangmin Li
Julien Lin
Hao Phan
Thomas Wang
RolaoDenthu
pigeonsoup
HaiShaw
Xinyuan Tong
Pranjal Shankhdhar
Lee Nau
HMING
elvischenv
Byron Hsu
Byron Hsu
Claude Opus 5
Thomas Wang
Xinyi Song
Mohammad Miadh Angkad
Cheng Wan
BBuf
Hanming Lu
Xinyi Song
abddb1c7e9
Co-authored-by: DarkSharpness <76582120+DarkSharpness@users.noreply.github.com> Co-authored-by: Xiaoyu Zhang <1182563586@qq.com> Co-authored-by: Mick <mickjagger19@icloud.com> Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com> Co-authored-by: Cheng Wan <54331508+ch-wan@users.noreply.github.com> Co-authored-by: Ke Bao <ispobaoke@gmail.com> Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com> Co-authored-by: Chunan Zeng <zcnrex@gmail.com> Co-authored-by: Khoa Pham <khoa.pham@radixark.ai> Co-authored-by: Ziyi Xu <ziyi.xu@radixark.ai> Co-authored-by: Zijie Xia <37504505+zijiexia@users.noreply.github.com> Co-authored-by: Yuwei An <ayw.sirius19@gmail.com> Co-authored-by: zhangxiaohao <1024393531@qq.com> Co-authored-by: Yangmin Li <yangminl@nvidia.com> Co-authored-by: Julien Lin <jullin@nvidia.com> Co-authored-by: Hao Phan <htphan@nvidia.com> Co-authored-by: Thomas Wang <1am9trash@gmail.com> Co-authored-by: RolaoDenthu <xinyisong0111@gmail.com> Co-authored-by: pigeonsoup <32922982+pigeonsoup@users.noreply.github.com> Co-authored-by: HaiShaw <hixiao@gmail.com> Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com> Co-authored-by: Pranjal Shankhdhar <pranjal.ssh@gmail.com> Co-authored-by: Lee Nau <lee.nau@gmail.com> Co-authored-by: HMING <126185151+Hearum@users.noreply.github.com> Co-authored-by: elvischenv <219235043+elvischenv@users.noreply.github.com> Co-authored-by: Byron Hsu <byronhsu1230@gmail.com> Co-authored-by: Byron Hsu <byron+per@periodiclabs.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Thomas Wang <thomawan@amd.com> Co-authored-by: Xinyi Song <86638975+RolaoDenthu@users.noreply.github.com> Co-authored-by: Mohammad Miadh Angkad <176301910+mmangkad@users.noreply.github.com> Co-authored-by: Cheng Wan <cheng.wan@radixark.ai> Co-authored-by: BBuf <xiaoyu.zhang@radixark.ai> Co-authored-by: Hanming Lu <hanminglu@meta.com> Co-authored-by: Xinyi Song <xinyis10@illinois.edu>
477 lines
15 KiB
Python
477 lines
15 KiB
Python
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Dict, List, Optional
|
|
|
|
import tabulate
|
|
|
|
from sglang.test.ci.ci_register import (
|
|
CIRegistry,
|
|
HWBackend,
|
|
auto_partition,
|
|
collect_tests,
|
|
)
|
|
from sglang.test.ci.ci_utils import run_unittest_files
|
|
|
|
HW_MAPPING = {
|
|
"cpu": HWBackend.CPU,
|
|
"cuda": HWBackend.CUDA,
|
|
"amd": HWBackend.AMD,
|
|
"musa": HWBackend.MUSA,
|
|
"npu": HWBackend.NPU,
|
|
"xpu": HWBackend.XPU,
|
|
"mlx": HWBackend.MLX,
|
|
}
|
|
|
|
# Per-commit test suites (run on every PR).
|
|
# Includes both base-a/b/c (always-on; pr-test.yml) and extra-a/b
|
|
# (label-gated; pr-test-extra.yml). Tests are tagged per-commit regardless;
|
|
# pr-test-extra.yml's `run-ci-extra` PR label decides whether extra-* dispatches.
|
|
PER_COMMIT_SUITES = {
|
|
HWBackend.CPU: [
|
|
"base-a-test-cpu",
|
|
"base-b-test-cpu",
|
|
"base-c-test-cpu",
|
|
"base-b-test-cpu-arm64",
|
|
],
|
|
HWBackend.AMD: [
|
|
"stage-a-test-1-gpu-small-amd",
|
|
"stage-b-test-1-gpu-small-amd",
|
|
"stage-b-test-1-gpu-small-amd-nondeterministic",
|
|
"stage-b-test-1-gpu-small-amd-mi35x",
|
|
"stage-b-test-large-8-gpu-mi35x-disaggregation-amd",
|
|
"stage-b-test-1-gpu-large-amd",
|
|
"stage-b-test-2-gpu-large-amd",
|
|
"jit-kernel-unit-test-amd",
|
|
"jit-kernel-benchmark-test-amd",
|
|
"sgl-kernel-unit-test-2-gpu-amd",
|
|
"stage-c-test-4-gpu-amd",
|
|
"stage-c-test-large-8-gpu-amd",
|
|
"stage-c-test-large-8-gpu-amd-mi35x",
|
|
# extra-a: label-gated PR opt-in suites in pr-test-amd-extra.yml
|
|
# (mirror of CUDA extra-a; tests stay tagged per-commit but only
|
|
# dispatch when the PR carries the `run-ci-extra` label). 1-gpu-small
|
|
# carries the mock-model / kv_canary unit + single-GPU canary e2e
|
|
# tests; 1-gpu-large carries the subset of model e2e tests validated
|
|
# to pass on mi325 (quant fp8kv-triton, sessions streaming-session
|
|
# EAGLE3, spec standalone triton-backend variant); 2-gpu-large carries
|
|
# the multi-GPU (TP/PP/PD) mock-model + kv_canary e2e tests. The rest
|
|
# of CUDA extra-a tests fail on ROCm (missing flash_attn.cute/flash_ops
|
|
# kernels, OOM, or accuracy regressions — e.g. gemma4-mtp-31b dips
|
|
# below the gsm8k floor on the topk=3 leg) and stay CUDA-only for now.
|
|
"extra-a-test-1-gpu-small-amd",
|
|
"extra-a-test-1-gpu-large-amd",
|
|
"extra-a-test-2-gpu-large-amd",
|
|
],
|
|
HWBackend.MUSA: [],
|
|
HWBackend.CUDA: [
|
|
"base-a-test-1-gpu-small",
|
|
"base-b-test-1-gpu-small",
|
|
"base-b-test-1-gpu-large",
|
|
"base-b-test-2-gpu-large",
|
|
"base-b-test-4-gpu-b200",
|
|
"base-b-kernel-unit-test-1-gpu-large",
|
|
"base-b-kernel-unit-test-4-gpu-b200",
|
|
"base-b-kernel-unit-test-8-gpu-h200",
|
|
"base-b-kernel-benchmark-test-1-gpu-large",
|
|
"base-c-test-4-gpu-h100",
|
|
"base-c-test-4-gpu-b200",
|
|
"base-c-test-4-gpu-gb300",
|
|
"base-c-test-8-gpu-h20",
|
|
"base-c-test-8-gpu-h200",
|
|
"base-c-test-8-gpu-b200",
|
|
"base-c-test-8-gpu-b300",
|
|
"base-c-test-deepep-4-gpu-h100",
|
|
"base-c-test-deepep-4-gpu-b200",
|
|
"base-c-test-deepep-8-gpu-h200",
|
|
# extra-a / extra-b: label-gated PR opt-in suites in pr-test-extra.yml
|
|
# (tests still tagged per-commit but skipped on default PR runs).
|
|
"extra-a-test-1-gpu-small",
|
|
"extra-a-test-1-gpu-large",
|
|
"extra-a-test-2-gpu-large",
|
|
"extra-b-test-4-gpu-h100",
|
|
"extra-b-test-4-gpu-b200",
|
|
"extra-b-test-8-gpu-h200",
|
|
"extra-b-test-deepep-4-gpu-h100",
|
|
"extra-b-test-deepep-4-gpu-b200",
|
|
"extra-b-test-deepep-8-gpu-h200",
|
|
],
|
|
HWBackend.NPU: [
|
|
"base-a-test-1-gpu-small",
|
|
"stage-a-unit-test-npu",
|
|
"stage-b-test-1-npu-a3",
|
|
"stage-b-test-2-npu-a3",
|
|
"stage-b-test-4-npu-a3",
|
|
"stage-b-test-8-npu-a3",
|
|
"stage-b-test-16-npu-a3",
|
|
],
|
|
HWBackend.XPU: [
|
|
"stage-a-test-1-gpu-xpu",
|
|
"stage-b-test-1-gpu-xpu",
|
|
],
|
|
HWBackend.MLX: [
|
|
"stage-a-unit-test-mlx",
|
|
"stage-b-e2e-mlx",
|
|
],
|
|
}
|
|
|
|
# Nightly test suites (run nightly, organized by GPU configuration)
|
|
NIGHTLY_SUITES = {
|
|
HWBackend.CUDA: [
|
|
"nightly-1-gpu",
|
|
"nightly-2-gpu",
|
|
"nightly-4-gpu",
|
|
"nightly-4-gpu-b200",
|
|
"nightly-8-gpu",
|
|
"nightly-8-gpu-h200",
|
|
"nightly-8-gpu-h20",
|
|
"nightly-8-gpu-b200",
|
|
"nightly-8-gpu-h200-basic", # Basic tests for large models on H200
|
|
"nightly-8-gpu-b200-basic", # Basic tests for large models on B200
|
|
"nightly-8-gpu-common", # Common tests that run on both H200 and B200
|
|
"nightly-kernel-1-gpu",
|
|
"nightly-kernel-8-gpu-h200",
|
|
# Eval and perf suites (2-gpu)
|
|
"nightly-eval-text-2-gpu",
|
|
"nightly-eval-vlm-2-gpu",
|
|
"nightly-perf-text-2-gpu",
|
|
"nightly-perf-vlm-2-gpu",
|
|
# GB300 (4x GB300 NVL4) nightly suites
|
|
"nightly-4-gpu-gb300",
|
|
"nightly-4-gpu-gb300-deepseek-v4-pro-fp4",
|
|
"nightly-4-gpu-gb300-glm5-nvfp4",
|
|
"nightly-4-gpu-gb300-kimi-k25",
|
|
"nightly-4-gpu-gb300-kimi-k25-nvfp4",
|
|
"nightly-4-gpu-gb300-qwen35-fp8",
|
|
"nightly-4-gpu-gb300-qwen35-nvfp4",
|
|
# Nightly precision regression (per-layer hidden state comparison)
|
|
"nightly-precision-8-gpu-h200",
|
|
],
|
|
HWBackend.AMD: [
|
|
"nightly-amd",
|
|
"nightly-amd-1-gpu",
|
|
"nightly-amd-kernel-1-gpu",
|
|
"nightly-amd-1-gpu-mi35x",
|
|
"nightly-amd-1-gpu-zimage-turbo",
|
|
"nightly-amd-2-gpu-mi35x-deepseek-r1-mxfp4-tp2",
|
|
"nightly-amd-8-gpu-mi35x-deepseek-r1-mxfp4-tp4",
|
|
"nightly-amd-4-gpu",
|
|
"nightly-amd-8-gpu",
|
|
"nightly-amd-vlm",
|
|
# MI35x 8-GPU suite (different model configs)
|
|
"nightly-amd-8-gpu-mi35x",
|
|
],
|
|
HWBackend.MUSA: [
|
|
"nightly-musa-1-gpu",
|
|
],
|
|
HWBackend.CPU: [],
|
|
HWBackend.NPU: [
|
|
"nightly-1-npu-a3",
|
|
"nightly-2-npu-a3",
|
|
"nightly-4-npu-a3",
|
|
"nightly-8-npu-a3",
|
|
"nightly-16-npu-a3",
|
|
"full-1-npu-a3",
|
|
"full-2-npu-a3",
|
|
"full-4-npu-a3",
|
|
"full-8-npu-a3",
|
|
"full-16-npu-a3",
|
|
],
|
|
HWBackend.XPU: [
|
|
"nightly-xpu-1-gpu",
|
|
"nightly-xpu-2-gpu",
|
|
"nightly-xpu-4-gpu",
|
|
],
|
|
}
|
|
|
|
|
|
OTHER_SUITES = {
|
|
HWBackend.CPU: [
|
|
"default",
|
|
],
|
|
HWBackend.CUDA: [
|
|
"stress",
|
|
"weekly-8-gpu-h200",
|
|
],
|
|
}
|
|
|
|
|
|
_SUITE_CHECKED_BACKENDS = {
|
|
HWBackend.CUDA,
|
|
HWBackend.CPU,
|
|
HWBackend.MUSA,
|
|
HWBackend.XPU,
|
|
HWBackend.MLX,
|
|
}
|
|
|
|
|
|
def _valid_suites_by_backend() -> dict:
|
|
"""Build a mapping from backend to its set of valid suite names."""
|
|
result = {}
|
|
for suite_dict in (PER_COMMIT_SUITES, NIGHTLY_SUITES, OTHER_SUITES):
|
|
for backend, suites in suite_dict.items():
|
|
if backend not in result:
|
|
result[backend] = set()
|
|
result[backend].update(suites)
|
|
return result
|
|
|
|
|
|
def validate_all_suites(all_tests: List[CIRegistry]):
|
|
"""Fail fast if any test is registered to a suite that doesn't belong to its backend."""
|
|
valid_by_backend = _valid_suites_by_backend()
|
|
errors = []
|
|
for t in all_tests:
|
|
if t.backend not in _SUITE_CHECKED_BACKENDS:
|
|
continue
|
|
valid = valid_by_backend.get(t.backend, set())
|
|
if t.effective_suite not in valid:
|
|
errors.append(
|
|
f" {t.filename}: backend={t.backend.name}, suite='{t.effective_suite}'"
|
|
)
|
|
if errors:
|
|
raise ValueError("Tests registered to invalid suites:\n" + "\n".join(errors))
|
|
|
|
|
|
def filter_tests(
|
|
ci_tests: List[CIRegistry], hw: HWBackend, suite: str, nightly: bool = False
|
|
) -> List[CIRegistry]:
|
|
ci_tests = [
|
|
t
|
|
for t in ci_tests
|
|
if t.backend == hw and t.effective_suite == suite and t.nightly == nightly
|
|
]
|
|
|
|
valid_suites = (
|
|
NIGHTLY_SUITES.get(hw, []) if nightly else PER_COMMIT_SUITES.get(hw, [])
|
|
)
|
|
|
|
if suite not in valid_suites:
|
|
print(
|
|
f"Warning: Unknown suite {suite} for backend {hw.name}, nightly={nightly}"
|
|
)
|
|
|
|
enabled_tests = [t for t in ci_tests if t.disabled is None]
|
|
skipped_tests = [t for t in ci_tests if t.disabled is not None]
|
|
|
|
return enabled_tests, skipped_tests
|
|
|
|
|
|
def pretty_print_tests(
|
|
args, ci_tests: List[CIRegistry], skipped_tests: List[CIRegistry]
|
|
):
|
|
hw = HW_MAPPING[args.hw]
|
|
suite = args.suite
|
|
nightly = args.nightly
|
|
if args.auto_partition_size:
|
|
partition_info = (
|
|
f"{args.auto_partition_id + 1}/{args.auto_partition_size} "
|
|
f"(0-based id={args.auto_partition_id})"
|
|
)
|
|
else:
|
|
partition_info = "full"
|
|
|
|
headers = ["Hardware", "Suite", "Nightly", "Partition"]
|
|
rows = [[hw.name, suite, str(nightly), partition_info]]
|
|
msg = tabulate.tabulate(rows, headers=headers, tablefmt="psql") + "\n"
|
|
|
|
if skipped_tests:
|
|
msg += f"⚠️ Skipped {len(skipped_tests)} test(s):\n"
|
|
for t in skipped_tests:
|
|
reason = t.disabled or "disabled"
|
|
msg += f" - {t.filename} (reason: {reason})\n"
|
|
msg += "\n"
|
|
|
|
if len(ci_tests) == 0:
|
|
msg += f"No tests found for hw={hw.name}, suite={suite}, nightly={nightly}\n"
|
|
msg += "This is expected during incremental migration. Skipping.\n"
|
|
else:
|
|
total_est_time = sum(t.est_time for t in ci_tests)
|
|
msg += (
|
|
f"✅ Enabled {len(ci_tests)} test(s) (est total {total_est_time:.1f}s):\n"
|
|
)
|
|
for t in ci_tests:
|
|
msg += f" - {t.filename} (est_time={t.est_time})\n"
|
|
|
|
print(msg, flush=True)
|
|
|
|
|
|
def load_live_est(
|
|
partition_model_file: Optional[str], suite: str, repo_root: str
|
|
) -> Optional[Dict[str, float]]:
|
|
"""`CIRegistry.filename -> est seconds` from `model.json est[suite]`;
|
|
None on any miss (caller falls back to in-source `est_time`)."""
|
|
if not partition_model_file or not os.path.exists(partition_model_file):
|
|
return None
|
|
try:
|
|
with open(partition_model_file) as f:
|
|
partition_model = json.load(f)
|
|
except (OSError, json.JSONDecodeError):
|
|
return None
|
|
if not isinstance(partition_model, dict):
|
|
return None
|
|
suite_est = partition_model.get("est", {}).get(suite)
|
|
if not isinstance(suite_est, dict) or not suite_est:
|
|
return None
|
|
return {
|
|
os.path.join(repo_root, relpath): float(elapsed)
|
|
for relpath, elapsed in suite_est.items()
|
|
}
|
|
|
|
|
|
def run_a_suite(args):
|
|
hw = HW_MAPPING[args.hw]
|
|
suite = args.suite
|
|
nightly = args.nightly
|
|
auto_partition_id = args.auto_partition_id
|
|
auto_partition_size = args.auto_partition_size
|
|
|
|
# Use absolute paths so the script works from any working directory
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
repo_root = os.path.dirname(script_dir)
|
|
|
|
# Registered tests under test/registered/
|
|
files = [
|
|
f
|
|
for f in glob.glob(
|
|
os.path.join(script_dir, "registered", "**", "*.py"), recursive=True
|
|
)
|
|
if not f.endswith("/conftest.py")
|
|
and not f.endswith("/__init__.py")
|
|
and not f.endswith("/cpu/utils.py")
|
|
and not f.endswith("/run_tests.py")
|
|
]
|
|
|
|
# Strict: all discovered files must have proper registration
|
|
sanity_check = True
|
|
|
|
all_tests = collect_tests(files, sanity_check=sanity_check)
|
|
validate_all_suites(all_tests)
|
|
ci_tests, skipped_tests = filter_tests(all_tests, hw, suite, nightly)
|
|
|
|
if auto_partition_size:
|
|
live_est = load_live_est(args.partition_model_file, suite, repo_root)
|
|
if live_est is not None:
|
|
print(
|
|
f"LPT: {len(live_est)} live est entries from {args.partition_model_file}",
|
|
flush=True,
|
|
)
|
|
else:
|
|
print(
|
|
f"LPT: no live est ({args.partition_model_file!r}); using in-source est_time",
|
|
flush=True,
|
|
)
|
|
ci_tests = auto_partition(
|
|
ci_tests, auto_partition_id, auto_partition_size, live_est=live_est
|
|
)
|
|
|
|
pretty_print_tests(args, ci_tests, skipped_tests)
|
|
|
|
# Add extra timeout when retry is enabled
|
|
timeout = args.timeout_per_file
|
|
if args.enable_retry:
|
|
timeout += args.retry_timeout_increase
|
|
|
|
return run_unittest_files(
|
|
ci_tests,
|
|
timeout_per_file=timeout,
|
|
continue_on_error=args.continue_on_error,
|
|
enable_retry=args.enable_retry,
|
|
max_attempts=args.max_attempts,
|
|
retry_wait_seconds=args.retry_wait_seconds,
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Run CI test suites from test/registered/"
|
|
)
|
|
parser.add_argument(
|
|
"--hw",
|
|
type=str,
|
|
choices=HW_MAPPING.keys(),
|
|
required=True,
|
|
help="Hardware backend to run tests on.",
|
|
)
|
|
parser.add_argument("--suite", type=str, required=True, help="Test suite to run.")
|
|
parser.add_argument(
|
|
"--nightly",
|
|
action="store_true",
|
|
help="Run nightly tests instead of per-commit tests.",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout-per-file",
|
|
type=int,
|
|
default=1200,
|
|
help="The time limit for running one file in seconds (default: 1200).",
|
|
)
|
|
parser.add_argument(
|
|
"--continue-on-error",
|
|
action="store_true",
|
|
default=False,
|
|
help="Continue running remaining tests even if one fails (default: False, useful for nightly tests).",
|
|
)
|
|
parser.add_argument(
|
|
"--auto-partition-id",
|
|
type=int,
|
|
help="Use auto load balancing. The part id.",
|
|
)
|
|
parser.add_argument(
|
|
"--auto-partition-size",
|
|
type=int,
|
|
help="Use auto load balancing. The number of parts.",
|
|
)
|
|
parser.add_argument(
|
|
"--enable-retry",
|
|
action="store_true",
|
|
default=False,
|
|
help="Enable smart retry for accuracy/performance assertion failures (not code errors)",
|
|
)
|
|
parser.add_argument(
|
|
"--max-attempts",
|
|
type=int,
|
|
default=2,
|
|
help="Maximum number of attempts per file including initial run (default: 2)",
|
|
)
|
|
parser.add_argument(
|
|
"--retry-wait-seconds",
|
|
type=int,
|
|
default=60,
|
|
help="Seconds to wait between retries (default: 60)",
|
|
)
|
|
parser.add_argument(
|
|
"--retry-timeout-increase",
|
|
type=int,
|
|
default=600,
|
|
help="Additional timeout in seconds when retry is enabled (default: 600)",
|
|
)
|
|
parser.add_argument(
|
|
"--partition-model-file",
|
|
type=str,
|
|
default=None,
|
|
help="Path to sglang-ci-stats model.json for live LPT est; missing/malformed -> in-source est_time fallback.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# Validate auto-partition arguments
|
|
if (args.auto_partition_id is not None) != (args.auto_partition_size is not None):
|
|
parser.error(
|
|
"--auto-partition-id and --auto-partition-size must be specified together."
|
|
)
|
|
if args.auto_partition_size is not None:
|
|
if args.auto_partition_size <= 0:
|
|
parser.error("--auto-partition-size must be positive.")
|
|
if not 0 <= args.auto_partition_id < args.auto_partition_size:
|
|
parser.error(
|
|
f"--auto-partition-id must be in range [0, {args.auto_partition_size}), "
|
|
f"but got {args.auto_partition_id}"
|
|
)
|
|
|
|
exit_code = run_a_suite(args)
|
|
sys.exit(exit_code)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|