94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
421 lines
14 KiB
Python
421 lines
14 KiB
Python
import ast
|
|
import warnings
|
|
from dataclasses import dataclass
|
|
from enum import Enum, auto
|
|
from typing import List, Optional, Tuple
|
|
|
|
__all__ = [
|
|
"HWBackend",
|
|
"CIRegistry",
|
|
"collect_tests",
|
|
"auto_partition",
|
|
"register_cpu_ci",
|
|
"register_cuda_ci",
|
|
"register_amd_ci",
|
|
"register_musa_ci",
|
|
"register_npu_ci",
|
|
"register_xpu_ci",
|
|
"register_musa_ci",
|
|
"register_mlx_ci",
|
|
"ut_parse_one_file",
|
|
]
|
|
|
|
# `suite` stays in positional slot 2 for backward compat with existing
|
|
# `register_cpu_ci(5, "base-a-test-cpu")` style positional calls. New fields
|
|
# (`stage`, `runner_config`) are kwarg-only.
|
|
_PARAM_ORDER = ("est_time", "suite", "nightly", "disabled")
|
|
_KWARG_ONLY = ("stage", "runner_config")
|
|
_ALL_PARAMS = _PARAM_ORDER + _KWARG_ONLY
|
|
_UNSET = object()
|
|
|
|
|
|
class HWBackend(Enum):
|
|
CPU = auto()
|
|
CUDA = auto()
|
|
AMD = auto()
|
|
NPU = auto()
|
|
XPU = auto()
|
|
MUSA = auto()
|
|
MLX = auto()
|
|
|
|
|
|
@dataclass
|
|
class CIRegistry:
|
|
backend: HWBackend
|
|
filename: str
|
|
est_time: float
|
|
stage: Optional[str] = None
|
|
runner_config: Optional[str] = None
|
|
# Legacy single-string suite; kept for nightly/stress/weekly + AMD/CPU/NPU
|
|
# suites whose names don't follow `{stage}-test-{runner_config}` shape.
|
|
suite: Optional[str] = None
|
|
nightly: bool = False
|
|
disabled: Optional[str] = None
|
|
|
|
@property
|
|
def effective_suite(self) -> Optional[str]:
|
|
if self.stage is not None and self.runner_config is not None:
|
|
return f"{self.stage}-test-{self.runner_config}"
|
|
return self.suite
|
|
|
|
|
|
def register_cpu_ci(
|
|
est_time: float,
|
|
suite: Optional[str] = None,
|
|
nightly: bool = False,
|
|
disabled: Optional[str] = None,
|
|
*,
|
|
stage: Optional[str] = None,
|
|
runner_config: Optional[str] = None,
|
|
):
|
|
"""Marker for CPU CI registration (parsed via AST; runtime no-op)."""
|
|
return None
|
|
|
|
|
|
def register_cuda_ci(
|
|
est_time: float,
|
|
suite: Optional[str] = None,
|
|
nightly: bool = False,
|
|
disabled: Optional[str] = None,
|
|
*,
|
|
stage: Optional[str] = None,
|
|
runner_config: Optional[str] = None,
|
|
):
|
|
"""Marker for CUDA CI registration (parsed via AST; runtime no-op)."""
|
|
return None
|
|
|
|
|
|
def register_amd_ci(
|
|
est_time: float,
|
|
suite: Optional[str] = None,
|
|
nightly: bool = False,
|
|
disabled: Optional[str] = None,
|
|
*,
|
|
stage: Optional[str] = None,
|
|
runner_config: Optional[str] = None,
|
|
):
|
|
"""Marker for AMD CI registration (parsed via AST; runtime no-op)."""
|
|
return None
|
|
|
|
|
|
def register_musa_ci(
|
|
est_time: float,
|
|
suite: Optional[str] = None,
|
|
nightly: bool = False,
|
|
disabled: Optional[str] = None,
|
|
*,
|
|
stage: Optional[str] = None,
|
|
runner_config: Optional[str] = None,
|
|
):
|
|
"""Marker for MUSA CI registration (parsed via AST; runtime no-op)."""
|
|
return None
|
|
|
|
|
|
def register_npu_ci(
|
|
est_time: float,
|
|
suite: Optional[str] = None,
|
|
nightly: bool = False,
|
|
disabled: Optional[str] = None,
|
|
*,
|
|
stage: Optional[str] = None,
|
|
runner_config: Optional[str] = None,
|
|
):
|
|
"""Marker for NPU CI registration (parsed via AST; runtime no-op)."""
|
|
return None
|
|
|
|
|
|
def register_xpu_ci(
|
|
est_time: float,
|
|
suite: Optional[str] = None,
|
|
nightly: bool = False,
|
|
disabled: Optional[str] = None,
|
|
*,
|
|
stage: Optional[str] = None,
|
|
runner_config: Optional[str] = None,
|
|
):
|
|
"""Marker for XPU CI registration (parsed via AST; runtime no-op)."""
|
|
return None
|
|
|
|
|
|
def register_musa_ci(
|
|
est_time: float,
|
|
suite: Optional[str] = None,
|
|
nightly: bool = False,
|
|
disabled: Optional[str] = None,
|
|
*,
|
|
stage: Optional[str] = None,
|
|
runner_config: Optional[str] = None,
|
|
):
|
|
"""Marker for MUSA CI registration (parsed via AST; runtime no-op)."""
|
|
return None
|
|
|
|
|
|
def register_mlx_ci(
|
|
est_time: float,
|
|
suite: Optional[str] = None,
|
|
nightly: bool = False,
|
|
disabled: Optional[str] = None,
|
|
*,
|
|
stage: Optional[str] = None,
|
|
runner_config: Optional[str] = None,
|
|
):
|
|
"""Marker for MLX CI registration (parsed via AST; runtime no-op)."""
|
|
return None
|
|
|
|
|
|
REGISTER_MAPPING = {
|
|
"register_cpu_ci": HWBackend.CPU,
|
|
"register_cuda_ci": HWBackend.CUDA,
|
|
"register_amd_ci": HWBackend.AMD,
|
|
"register_musa_ci": HWBackend.MUSA,
|
|
"register_npu_ci": HWBackend.NPU,
|
|
"register_xpu_ci": HWBackend.XPU,
|
|
"register_musa_ci": HWBackend.MUSA,
|
|
"register_mlx_ci": HWBackend.MLX,
|
|
}
|
|
|
|
|
|
class RegistryVisitor(ast.NodeVisitor):
|
|
def __init__(self, filename: str):
|
|
self.filename = filename
|
|
self.registries: list[CIRegistry] = []
|
|
self.has_main_entry: bool = False
|
|
|
|
def _constant_value(self, node: ast.AST) -> object:
|
|
if isinstance(node, ast.Constant):
|
|
return node.value
|
|
return _UNSET
|
|
|
|
def _parse_call_args(self, func_call: ast.Call) -> dict:
|
|
args = {name: _UNSET for name in _ALL_PARAMS}
|
|
seen = set()
|
|
|
|
if any(isinstance(arg, ast.Starred) for arg in func_call.args):
|
|
raise ValueError(
|
|
f"{self.filename}: starred arguments are not supported in {func_call.func.id}()"
|
|
)
|
|
if len(func_call.args) > len(_PARAM_ORDER):
|
|
raise ValueError(
|
|
f"{self.filename}: too many positional arguments in {func_call.func.id}()"
|
|
)
|
|
|
|
for name, arg in zip(_PARAM_ORDER, func_call.args):
|
|
seen.add(name)
|
|
args[name] = self._constant_value(arg)
|
|
|
|
for kw in func_call.keywords:
|
|
if kw.arg is None:
|
|
raise ValueError(
|
|
f"{self.filename}: **kwargs are not supported in {func_call.func.id}()"
|
|
)
|
|
if kw.arg not in args:
|
|
raise ValueError(
|
|
f"{self.filename}: unknown argument '{kw.arg}' in {func_call.func.id}()"
|
|
)
|
|
if kw.arg in seen:
|
|
raise ValueError(
|
|
f"{self.filename}: duplicated argument '{kw.arg}' in {func_call.func.id}()"
|
|
)
|
|
seen.add(kw.arg)
|
|
args[kw.arg] = self._constant_value(kw.value)
|
|
|
|
if args["est_time"] is _UNSET:
|
|
raise ValueError(
|
|
f"{self.filename}: est_time is a required constant in {func_call.func.id}()"
|
|
)
|
|
|
|
# The only valid (stage, runner_config, suite) shapes are:
|
|
# (set, set, unset) -> new-style pair
|
|
# (unset, unset, set) -> legacy single-string
|
|
# Any other combination is rejected with the actual triple in the error.
|
|
stage_set = args["stage"] is not _UNSET
|
|
runner_set = args["runner_config"] is not _UNSET
|
|
suite_set = args["suite"] is not _UNSET
|
|
valid_shape = (stage_set and runner_set and not suite_set) or (
|
|
not stage_set and not runner_set and suite_set
|
|
)
|
|
if not valid_shape:
|
|
raise ValueError(
|
|
f"{self.filename}: {func_call.func.id}() must specify exactly one of "
|
|
f"(stage, runner_config) pair or suite; got stage={stage_set}, "
|
|
f"runner_config={runner_set}, suite={suite_set}"
|
|
)
|
|
|
|
est_time = args["est_time"]
|
|
if not isinstance(est_time, (int, float)):
|
|
raise ValueError(
|
|
f"{self.filename}: est_time must be a number in {func_call.func.id}()"
|
|
)
|
|
|
|
suite = args["suite"] if suite_set else None
|
|
if suite is not None and not isinstance(suite, str):
|
|
raise ValueError(
|
|
f"{self.filename}: suite must be a string in {func_call.func.id}()"
|
|
)
|
|
|
|
stage = args["stage"] if stage_set else None
|
|
runner_config = args["runner_config"] if runner_set else None
|
|
for name, value in (("stage", stage), ("runner_config", runner_config)):
|
|
if value is not None and not isinstance(value, str):
|
|
raise ValueError(
|
|
f"{self.filename}: {name} must be a string in {func_call.func.id}()"
|
|
)
|
|
|
|
nightly_value = args["nightly"]
|
|
if nightly_value is _UNSET:
|
|
nightly = False
|
|
elif isinstance(nightly_value, bool):
|
|
nightly = nightly_value
|
|
else:
|
|
raise ValueError(
|
|
f"{self.filename}: nightly must be a boolean in {func_call.func.id}()"
|
|
)
|
|
|
|
disabled = args["disabled"] if args["disabled"] is not _UNSET else None
|
|
if disabled is not None and not isinstance(disabled, str):
|
|
raise ValueError(
|
|
f"{self.filename}: disabled must be a string in {func_call.func.id}()"
|
|
)
|
|
|
|
return {
|
|
"est_time": float(est_time),
|
|
"stage": stage,
|
|
"runner_config": runner_config,
|
|
"suite": suite,
|
|
"nightly": nightly,
|
|
"disabled": disabled,
|
|
}
|
|
|
|
def _collect_ci_registry(self, func_call: ast.Call):
|
|
if not isinstance(func_call.func, ast.Name):
|
|
return None
|
|
|
|
backend = REGISTER_MAPPING.get(func_call.func.id)
|
|
if backend is None:
|
|
return None
|
|
|
|
parsed = self._parse_call_args(func_call)
|
|
return CIRegistry(
|
|
backend=backend,
|
|
filename=self.filename,
|
|
**parsed,
|
|
)
|
|
|
|
@staticmethod
|
|
def _is_main_block_with_call(stmt: ast.If) -> bool:
|
|
"""True iff `stmt` is `if __name__ == "__main__":` with a body that
|
|
contains at least one call (i.e. actually runs something, not just
|
|
`pass`). This is what makes `python3 file.py` execute tests."""
|
|
test = stmt.test
|
|
if not isinstance(test, ast.Compare):
|
|
return False
|
|
if not (isinstance(test.left, ast.Name) and test.left.id == "__name__"):
|
|
return False
|
|
if len(test.ops) != 1 or not isinstance(test.ops[0], ast.Eq):
|
|
return False
|
|
if len(test.comparators) != 1:
|
|
return False
|
|
rhs = test.comparators[0]
|
|
if not (isinstance(rhs, ast.Constant) and rhs.value == "__main__"):
|
|
return False
|
|
for child in ast.walk(ast.Module(body=stmt.body, type_ignores=[])):
|
|
if isinstance(child, ast.Call):
|
|
return True
|
|
return False
|
|
|
|
def visit_Module(self, node):
|
|
for stmt in node.body:
|
|
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
|
|
cr = self._collect_ci_registry(stmt.value)
|
|
if cr is not None:
|
|
self.registries.append(cr)
|
|
elif isinstance(stmt, ast.If) and self._is_main_block_with_call(stmt):
|
|
self.has_main_entry = True
|
|
|
|
self.generic_visit(node)
|
|
|
|
|
|
def ut_parse_one_file(filename: str) -> Tuple[List[CIRegistry], bool]:
|
|
"""Parse a test file and return (registries, has_main_entry).
|
|
|
|
`has_main_entry` is True iff the file has `if __name__ == "__main__":`
|
|
with a call in its body -- required for `python3 file.py` to actually
|
|
run tests (the CI runner's invocation pattern).
|
|
"""
|
|
with open(filename, "r") as f:
|
|
file_content = f.read()
|
|
tree = ast.parse(file_content, filename=filename)
|
|
visitor = RegistryVisitor(filename=filename)
|
|
visitor.visit(tree)
|
|
return visitor.registries, visitor.has_main_entry
|
|
|
|
|
|
def auto_partition(
|
|
files: List[CIRegistry],
|
|
rank: int,
|
|
size: int,
|
|
live_est: Optional[dict] = None,
|
|
) -> List[CIRegistry]:
|
|
"""Partition files into `size` sublists with approximately equal sums of
|
|
estimated times using a greedy algorithm (LPT heuristic), and return the
|
|
partition for the specified rank.
|
|
|
|
`live_est`: optional `filename -> est seconds` overrides; missing
|
|
files fall back to in-source `est_time`.
|
|
"""
|
|
if not files or size <= 0:
|
|
return []
|
|
|
|
def est_of(f: CIRegistry) -> float:
|
|
if live_est is not None and f.filename in live_est:
|
|
return live_est[f.filename]
|
|
return f.est_time
|
|
|
|
# Sort by estimated_time descending; filename as tie-breaker for
|
|
# deterministic partitioning regardless of glob ordering.
|
|
sorted_files = sorted(files, key=lambda f: (-est_of(f), f.filename))
|
|
|
|
partitions: List[List[CIRegistry]] = [[] for _ in range(size)]
|
|
partition_sums = [0.0] * size
|
|
|
|
# Greedily assign each file to the partition with the smallest current total time
|
|
for file in sorted_files:
|
|
min_sum_idx = min(range(size), key=partition_sums.__getitem__)
|
|
partitions[min_sum_idx].append(file)
|
|
partition_sums[min_sum_idx] += est_of(file)
|
|
|
|
if rank < size:
|
|
return partitions[rank]
|
|
return []
|
|
|
|
|
|
def collect_tests(files: list[str], sanity_check: bool = True) -> List[CIRegistry]:
|
|
ci_tests = []
|
|
for file in files:
|
|
registries, has_main_entry = ut_parse_one_file(file)
|
|
if len(registries) == 0:
|
|
msg = f"No CI registry found in {file}"
|
|
if sanity_check:
|
|
raise ValueError(msg)
|
|
else:
|
|
warnings.warn(msg)
|
|
continue
|
|
|
|
# Every file with at least one enabled registry must have an
|
|
# executable `if __name__ == "__main__":` block; otherwise
|
|
# `python3 file.py -f` (how run_unittest_files invokes tests)
|
|
# silently exits and the file shows green without running.
|
|
has_enabled = any(r.disabled is None for r in registries)
|
|
if sanity_check and has_enabled and not has_main_entry:
|
|
raise ValueError(
|
|
f'{file}: missing `if __name__ == "__main__":` entry. '
|
|
f"Pytest-style tests in this file will silently skip under "
|
|
f"`python3 file.py -f`. Add `unittest.main()` (for "
|
|
f"unittest.TestCase) or `sys.exit(pytest.main([__file__, "
|
|
f'"-v"]))` (for pytest-style).'
|
|
)
|
|
|
|
ci_tests.extend(registries)
|
|
|
|
return ci_tests
|