[CI] Prune redundant CPU test overhead (#34309)
Co-authored-by: hnyls2002 <lsyincs@gmail.com>
This commit is contained in:
co-authored by
hnyls2002
parent
6ad3f2d8fd
commit
85cdf1178d
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-commit hook: reject CI-registered tests that live inside the importable
|
||||
`sglang` package (python/sglang/).
|
||||
|
||||
Registered tests and benchmarks must live under test/registered/ (e.g.
|
||||
test/registered/jit/ for JIT kernel tests and test/registered/jit/benchmark/
|
||||
for JIT kernel benchmarks) so they are not shipped in the wheel and are
|
||||
collected by run_suite.py's registered glob. A registered file placed inside
|
||||
the package would be shipped to users AND silently dropped by run_suite.py
|
||||
(which no longer globs the package) -- it would never run in CI. This guard
|
||||
turns that silent skip into a hard failure.
|
||||
|
||||
Reuses ut_parse_one_file() from ci_register.py (AST-based) so the registry
|
||||
detection matches run_suite.py's collect_tests() exactly.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Markers whose mere presence in the source is worth an AST parse. Anything
|
||||
# without one of these strings cannot register a test, so we skip parsing it.
|
||||
_MARKERS = (
|
||||
"register_cuda_ci",
|
||||
"register_amd_ci",
|
||||
"register_cpu_ci",
|
||||
"register_npu_ci",
|
||||
"register_xpu_ci",
|
||||
"register_musa_ci",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# Import ci_register directly to avoid pulling in all of sglang.
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ci_register",
|
||||
os.path.join("python", "sglang", "test", "ci", "ci_register.py"),
|
||||
)
|
||||
ci_register = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ci_register)
|
||||
|
||||
offenders = []
|
||||
for f in sorted(glob.glob("python/sglang/**/*.py", recursive=True)):
|
||||
try:
|
||||
with open(f, "r", encoding="utf-8") as fh:
|
||||
source = fh.read()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
if not any(marker in source for marker in _MARKERS):
|
||||
continue
|
||||
try:
|
||||
registries, _has_main_entry = ci_register.ut_parse_one_file(f)
|
||||
except Exception:
|
||||
# A malformed register call still indicates a misplaced test.
|
||||
offenders.append(f)
|
||||
continue
|
||||
if registries:
|
||||
offenders.append(f)
|
||||
|
||||
if offenders:
|
||||
print(
|
||||
"ERROR: CI-registered test(s)/benchmark(s) found inside the sglang package:"
|
||||
)
|
||||
print(
|
||||
" Registered tests and benchmarks must live under test/registered/\n"
|
||||
" (e.g. test/registered/jit/ for JIT kernel tests and\n"
|
||||
" test/registered/jit/benchmark/ for JIT kernel benchmarks) so they\n"
|
||||
" are not shipped in the wheel and are collected by run_suite.py.\n"
|
||||
)
|
||||
for f in offenders:
|
||||
print(f" {f}")
|
||||
print()
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,184 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-commit hook: validate CI registry calls under test/registered/.
|
||||
|
||||
1. Every test file must contain a CI registry call (register_cuda_ci,
|
||||
register_amd_ci, etc.).
|
||||
2. A CUDA test must register its suite via the modern
|
||||
`stage=`/`runner_config=` form. The legacy single-string `suite=` is reserved
|
||||
for the stress family (and for AMD/CPU/NPU suites); any other CUDA `suite=`
|
||||
resolves to a name no workflow invokes, so the test silently never runs.
|
||||
Two shapes are rejected:
|
||||
a. `{stage}-test-{runner_config}` -- the modern name stuffed back into the
|
||||
legacy form. Reported with the exact stage/runner split to use.
|
||||
b. an older `{stage}-{runner_config}` PR-test name (e.g. the pre-migration
|
||||
`base-b-kernel-unit-1-gpu-large`) -- no longer matches any workflow
|
||||
suite at all.
|
||||
The modern form resolves to the identical suite (CIRegistry.effective_suite
|
||||
is f"{stage}-test-{runner_config}") and is /rerun-test-able.
|
||||
|
||||
Reuses ut_parse_one_file() from ci_register.py (AST-based parsing)
|
||||
to match the same logic used by run_suite.py's collect_tests().
|
||||
"""
|
||||
|
||||
import ast
|
||||
import glob
|
||||
import importlib.util
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Suite names of the form `{stage}-test-{runner_config}` are exactly what the
|
||||
# modern stage=/runner_config= form produces, so a legacy suite= carrying this
|
||||
# shape is always expressible (and should be expressed) the modern way.
|
||||
_MODERN_SHAPE = re.compile(r"^(.+)-test-(.+)$")
|
||||
|
||||
# The only CUDA suite family still allowed on the legacy single-string `suite=`
|
||||
# form. Anything else needs stage=/runner_config=, or its effective_suite matches
|
||||
# no suite any workflow invokes and the test silently never runs.
|
||||
_LEGACY_CUDA_PREFIXES = ("stress",)
|
||||
|
||||
|
||||
def _defines_testcase(tree: ast.AST) -> bool:
|
||||
"""True if the file defines unittest classes, statically or via type()."""
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
if any("TestCase" in ast.unparse(b) for b in node.bases):
|
||||
return True
|
||||
elif isinstance(node, ast.Call):
|
||||
if (
|
||||
isinstance(node.func, ast.Name)
|
||||
and node.func.id == "type"
|
||||
and len(node.args) >= 2
|
||||
and isinstance(node.args[1], ast.Tuple)
|
||||
and any("TestCase" in ast.unparse(e) for e in node.args[1].elts)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _main_runs_tests(tree: ast.Module) -> bool:
|
||||
for stmt in tree.body:
|
||||
if not (
|
||||
isinstance(stmt, ast.If)
|
||||
and ast.unparse(stmt.test).replace("'", '"') == '__name__ == "__main__"'
|
||||
):
|
||||
continue
|
||||
body = ast.unparse(ast.Module(body=stmt.body, type_ignores=[]))
|
||||
if "unittest.main" in body or "pytest.main" in body:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# Import ci_register directly to avoid pulling in all of sglang
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ci_register",
|
||||
os.path.join("python", "sglang", "test", "ci", "ci_register.py"),
|
||||
)
|
||||
ci_register = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ci_register)
|
||||
cuda = ci_register.HWBackend.CUDA
|
||||
|
||||
# Same filter as run_suite.py: skip conftest.py, __init__.py, and utils.py
|
||||
files = sorted(
|
||||
f
|
||||
for f in glob.glob("test/registered/**/*.py", recursive=True)
|
||||
if os.path.basename(f) not in ("conftest.py", "__init__.py", "utils.py")
|
||||
)
|
||||
if not files:
|
||||
return 0
|
||||
|
||||
missing = []
|
||||
legacy_shape = [] # (file, suite, stage, runner_config) -- has a -test- split
|
||||
non_dispatchable = [] # (file, suite) -- legacy CUDA suite no workflow invokes
|
||||
dead_tests = [] # (file) -- TestCase classes that `python3 file.py` never runs
|
||||
for f in files:
|
||||
try:
|
||||
registries, _has_main_entry = ci_register.ut_parse_one_file(f)
|
||||
except Exception:
|
||||
# Skip files that can't be parsed (syntax errors, etc.)
|
||||
continue
|
||||
if len(registries) == 0:
|
||||
missing.append(f)
|
||||
continue
|
||||
# TestCase classes are dead unless __main__ runs them (CI does
|
||||
# `python3 file.py`); the ERROR text below explains the fix.
|
||||
with open(f, "r", encoding="utf-8") as fh:
|
||||
tree = ast.parse(fh.read(), filename=f)
|
||||
if _defines_testcase(tree) and not _main_runs_tests(tree):
|
||||
dead_tests.append(f)
|
||||
for r in registries:
|
||||
# Pure legacy form on a CUDA registry: suite set, stage/runner unset.
|
||||
if not (
|
||||
r.backend == cuda
|
||||
and r.suite is not None
|
||||
and r.stage is None
|
||||
and r.runner_config is None
|
||||
):
|
||||
continue
|
||||
if r.suite.split("-", 1)[0] in _LEGACY_CUDA_PREFIXES:
|
||||
continue
|
||||
m = _MODERN_SHAPE.match(r.suite)
|
||||
if m:
|
||||
legacy_shape.append((f, r.suite, m.group(1), m.group(2)))
|
||||
else:
|
||||
non_dispatchable.append((f, r.suite))
|
||||
|
||||
exit_code = 0
|
||||
if missing:
|
||||
print("ERROR: Files in test/registered/ missing CI registry call:")
|
||||
print(" Move manual-only tests to test/manual/.\n")
|
||||
for f in missing:
|
||||
print(f" {f}")
|
||||
print()
|
||||
exit_code = 1
|
||||
if legacy_shape:
|
||||
print(
|
||||
"ERROR: CUDA test(s) register a `{stage}-test-{runner_config}`-shaped "
|
||||
'suite via the legacy `suite="..."` form, which is not dispatchable '
|
||||
"via /rerun-test. Switch to the modern `stage=`/`runner_config=` form "
|
||||
"(same stage, same runner):\n"
|
||||
)
|
||||
for f, suite, stage, runner_config in legacy_shape:
|
||||
print(
|
||||
f" {f}\n"
|
||||
f' suite="{suite}"'
|
||||
f' -> stage="{stage}", runner_config="{runner_config}"'
|
||||
)
|
||||
print()
|
||||
exit_code = 1
|
||||
if non_dispatchable:
|
||||
print(
|
||||
'ERROR: CUDA test(s) register a legacy `suite="..."` that is neither a '
|
||||
"nightly/stress/weekly suite nor the modern `stage=`/`runner_config=` "
|
||||
"form. This name matches no suite the PR-test workflows invoke, so the "
|
||||
"test silently never runs. Switch to the modern form:\n"
|
||||
)
|
||||
for f, suite in non_dispatchable:
|
||||
print(
|
||||
f" {f}\n"
|
||||
f' suite="{suite}"'
|
||||
f' -> stage="...", runner_config="..."'
|
||||
)
|
||||
print()
|
||||
exit_code = 1
|
||||
if dead_tests:
|
||||
print(
|
||||
"ERROR: Test file(s) define TestCase classes that CI never runs: "
|
||||
"the registered file is executed as `python3 file.py`, but its "
|
||||
'`if __name__ == "__main__"` block does not call unittest.main() '
|
||||
"or pytest.main(), so the classes are silently skipped while the "
|
||||
"file reports success. Make __main__ run the tests (put any CLI "
|
||||
"entry point behind an explicit flag):\n"
|
||||
)
|
||||
for f in dead_tests:
|
||||
print(f" {f}")
|
||||
print()
|
||||
exit_code = 1
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that the rust-ext cache_key_prefix defaults stay in sync.
|
||||
|
||||
The build workflow saves cache entries under its default; the download action
|
||||
restores with its own. Neither file can reference the other, and a mismatch
|
||||
makes every pool silently fall back to source builds at install time.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
BUILD_WORKFLOW = ".github/workflows/_pr-test-rust-ext-build.yml"
|
||||
DOWNLOAD_ACTION = ".github/actions/download-rust-ext/action.yml"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with open(BUILD_WORKFLOW, encoding="utf-8") as f:
|
||||
workflow = yaml.safe_load(f)
|
||||
with open(DOWNLOAD_ACTION, encoding="utf-8") as f:
|
||||
action = yaml.safe_load(f)
|
||||
|
||||
# yaml 1.1 parses the `on:` key as boolean True
|
||||
triggers = workflow.get("on", workflow.get(True))
|
||||
save_prefix = triggers["workflow_call"]["inputs"]["cache_key_prefix"]["default"]
|
||||
restore_prefix = action["inputs"]["cache_key_prefix"]["default"]
|
||||
|
||||
if save_prefix != restore_prefix:
|
||||
print("ERROR: rust-ext cache_key_prefix defaults do not match.")
|
||||
print(f" {BUILD_WORKFLOW} saves under: {save_prefix}")
|
||||
print(f" {DOWNLOAD_ACTION} restores with: {restore_prefix}")
|
||||
print("Bump both together, or every pool falls back to source builds.")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that required status check job names are unique across workflows.
|
||||
|
||||
Duplicate job names on the same commit allow a passing job in one workflow
|
||||
to satisfy a required status check meant for a different workflow, bypassing
|
||||
branch protection.
|
||||
|
||||
See: https://github.com/sgl-project/sglang/pull/20208 for an example where
|
||||
pr-test-npu.yml's "pr-test-finish" job (which passed) caused GitHub to treat
|
||||
the required "pr-test-finish" check (from pr-test.yml, which failed) as met.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
import yaml
|
||||
|
||||
# Job names used as required status checks in branch protection.
|
||||
# These MUST be unique across all workflow files.
|
||||
PROTECTED_JOB_NAMES = {
|
||||
"pr-test-finish",
|
||||
"lint",
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
workflows = sorted(glob.glob(".github/workflows/*.yml"))
|
||||
job_to_files: dict[str, list[str]] = defaultdict(list)
|
||||
|
||||
for wf in workflows:
|
||||
with open(wf, encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
if not data or "jobs" not in data:
|
||||
continue
|
||||
for job in data["jobs"]:
|
||||
if job in PROTECTED_JOB_NAMES:
|
||||
job_to_files[job].append(wf)
|
||||
|
||||
duplicates = {job: files for job, files in job_to_files.items() if len(files) > 1}
|
||||
|
||||
if not duplicates:
|
||||
return 0
|
||||
|
||||
print("ERROR: Required status check job names must be unique across workflows.")
|
||||
print("Duplicates allow branch protection bypass via auto-merge.\n")
|
||||
for job, files in sorted(duplicates.items()):
|
||||
print(f" Job '{job}' appears in:")
|
||||
for f in files:
|
||||
print(f" - {f}")
|
||||
print()
|
||||
|
||||
print("Fix: rename the job in non-primary workflows to avoid collision.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -104,12 +104,6 @@ _FILE_EXTENSIONS = (
|
||||
".onnx",
|
||||
)
|
||||
|
||||
# Non-test helper files under test/registered/ (skipped by basename, matching
|
||||
# scripts/ci/check_registered_tests.py). run_suite.py skips `cpu/utils.py` by
|
||||
# path; excluding every `utils.py` by basename is a superset that drops no
|
||||
# CUDA-registered test (the other `utils.py` registers CPU only).
|
||||
_NON_TEST_BASENAMES = frozenset({"conftest.py", "__init__.py", "utils.py"})
|
||||
|
||||
|
||||
def looks_like_model_id(value: str, deny: Optional[Set[str]] = None) -> bool:
|
||||
"""Heuristic: does ``value`` look like a HuggingFace repo id?
|
||||
@@ -273,11 +267,13 @@ def collect_suite_files(
|
||||
ci_register = _load_ci_register(repo_root)
|
||||
backend = getattr(ci_register.HWBackend, backend_name.upper())
|
||||
|
||||
pattern = os.path.join(repo_root, "test", "registered", "**", "*.py")
|
||||
# Same exclusion as run_suite.py: pytest+package structure files.
|
||||
files = sorted(
|
||||
f
|
||||
for f in glob.glob(pattern, recursive=True)
|
||||
if os.path.basename(f) not in _NON_TEST_BASENAMES
|
||||
for f in glob.glob(
|
||||
os.path.join(repo_root, "test", "registered", "**", "*.py"), recursive=True
|
||||
)
|
||||
if os.path.basename(f) not in ("conftest.py", "__init__.py")
|
||||
)
|
||||
|
||||
suite_files: Dict[str, List[str]] = {}
|
||||
|
||||
@@ -88,7 +88,8 @@ def discover_files(repo_root: str) -> list[str]:
|
||||
for f in glob.glob(
|
||||
os.path.join(test_dir, "registered", "**", "*.py"), recursive=True
|
||||
)
|
||||
if not f.endswith("/conftest.py") and not f.endswith("/__init__.py")
|
||||
# Same exclusion as run_suite.py: pytest+package structure files.
|
||||
if os.path.basename(f) not in ("conftest.py", "__init__.py")
|
||||
]
|
||||
jit_kernel_dir = os.path.join(repo_root, "python", "sglang", "jit_kernel")
|
||||
files += glob.glob(
|
||||
|
||||
Reference in New Issue
Block a user