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
268 lines
9.9 KiB
Python
268 lines
9.9 KiB
Python
"""Sum est_time per per-commit suite and emit one $GITHUB_OUTPUT line
|
|
keyed by suite name. Consumed by pr-test.yml stage jobs as
|
|
`fromJson(needs.check-changes.outputs.partitions)['<suite>']`.
|
|
|
|
partitions={"base-b-test-1-gpu-small": {"size": 8, "arr": [0,...,7], "max_parallel": 2}, ...}
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
import os
|
|
from collections import defaultdict
|
|
from datetime import datetime, timedelta
|
|
|
|
import yaml # PyYAML; preinstalled on ubuntu-latest GHA runners.
|
|
|
|
REPO_ROOT = os.path.dirname(
|
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
)
|
|
|
|
# Load ci_register.py directly: `import sglang.test...` pulls torch/orjson via
|
|
# sglang.__init__ but check-changes runs on bare ubuntu-latest. ci_register
|
|
# itself is stdlib-only (AST).
|
|
_CI_REGISTER_PATH = os.path.join(
|
|
REPO_ROOT, "python", "sglang", "test", "ci", "ci_register.py"
|
|
)
|
|
_spec = importlib.util.spec_from_file_location("ci_register", _CI_REGISTER_PATH)
|
|
_ci_register = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(_ci_register)
|
|
collect_tests = _ci_register.collect_tests
|
|
HWBackend = _ci_register.HWBackend
|
|
|
|
# pr-test-amd.yml / pr-test-npu.yml have their own dispatch.
|
|
_TARGET_BACKENDS = {HWBackend.CUDA, HWBackend.CPU}
|
|
|
|
# base-a is the critical-path entry gate; pin its fanout to sanity-coverage
|
|
# defaults instead of est_time. max_parallel = size (no throttle).
|
|
_BASE_A_OVERRIDES = {
|
|
"base-a-test-cpu": 8,
|
|
"base-a-test-1-gpu-small": 1,
|
|
}
|
|
|
|
_REUSABLE_STAGE_USES = "./.github/workflows/_pr-test-stage.yml"
|
|
|
|
|
|
def load_run_timeouts(pr_test_yml_path: str) -> dict:
|
|
"""Map `self_name -> run_timeout_minutes` from one pr-test*.yml. The input
|
|
is required in `_pr-test-stage.yml` -- KeyError surfaces missing.
|
|
Inline base-a-test-cpu is skipped (uses `_BASE_A_OVERRIDES`)."""
|
|
with open(pr_test_yml_path) as f:
|
|
wf = yaml.safe_load(f)
|
|
timeouts = {}
|
|
for job_id, job in (wf.get("jobs") or {}).items():
|
|
if not isinstance(job, dict) or job.get("uses") != _REUSABLE_STAGE_USES:
|
|
continue
|
|
with_ = job.get("with") or {}
|
|
suite = with_.get("self_name", job_id)
|
|
timeouts[suite] = int(with_["run_timeout_minutes"])
|
|
if not timeouts:
|
|
raise RuntimeError(
|
|
f"load_run_timeouts: no jobs matched uses={_REUSABLE_STAGE_USES!r} "
|
|
f"in {pr_test_yml_path}. The reusable workflow path likely "
|
|
"changed -- update _REUSABLE_STAGE_USES."
|
|
)
|
|
return timeouts
|
|
|
|
|
|
def per_shard_target_seconds(suite: str, run_timeouts: dict) -> float:
|
|
"""Per-shard wall budget = 0.75 * stage timeout. 0.75 is the inverse
|
|
of LPT's 4/3 worst-case approximation ratio, so the most imbalanced
|
|
LPT shard fills exactly the timeout."""
|
|
return 0.75 * run_timeouts[suite] * 60
|
|
|
|
|
|
def discover_files(repo_root: str) -> list[str]:
|
|
test_dir = os.path.join(repo_root, "test")
|
|
files = [
|
|
f
|
|
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")
|
|
]
|
|
jit_kernel_dir = os.path.join(repo_root, "python", "sglang", "jit_kernel")
|
|
files += glob.glob(
|
|
os.path.join(jit_kernel_dir, "tests", "**", "test_*.py"), recursive=True
|
|
)
|
|
files += glob.glob(
|
|
os.path.join(jit_kernel_dir, "benchmark", "**", "bench_*.py"), recursive=True
|
|
)
|
|
return files
|
|
|
|
|
|
def load_partition_model(path):
|
|
"""Read sglang-ci-stats' model.json; None on missing/unparsable.
|
|
Cross-repo schema -- guard against non-dict top-level."""
|
|
if not path or not os.path.exists(path):
|
|
return None
|
|
try:
|
|
with open(path) as f:
|
|
data = json.load(f)
|
|
except (OSError, json.JSONDecodeError):
|
|
return None
|
|
return data if isinstance(data, dict) else None
|
|
|
|
|
|
def compute_max_parallel(size: int) -> int:
|
|
return max(size // 3, 1)
|
|
|
|
|
|
def compute_partitions(
|
|
tests, repo_root, run_timeouts, partition_model=None, full_parallel=False
|
|
):
|
|
"""Group per-commit tests by suite and emit partition metadata.
|
|
|
|
`run_timeouts`: `suite -> minutes` from `load_run_timeouts`.
|
|
`partition_model`: optional sglang-ci-stats `model.json`; per-file
|
|
`est` and per-suite `(coeff, bias)` each fall back independently to
|
|
in-source `est_time` / `(1.0, 0.0)`.
|
|
`full_parallel=True` lifts the matrix-fanout throttle.
|
|
"""
|
|
# Allowlist: stages pr-test.yml dispatches. Stress / weekly /
|
|
# nightly-* live in test/registered/ but pr-test doesn't run them.
|
|
dispatched_suites = set(run_timeouts) | set(_BASE_A_OVERRIDES)
|
|
suite_tests = defaultdict(list)
|
|
for t in tests:
|
|
if t.backend not in _TARGET_BACKENDS:
|
|
continue
|
|
if t.nightly or t.disabled is not None:
|
|
continue
|
|
if t.effective_suite not in dispatched_suites:
|
|
continue
|
|
suite_tests[t.effective_suite].append(t)
|
|
|
|
est_table = (partition_model or {}).get("est", {})
|
|
fit_table = (partition_model or {}).get("fit", {})
|
|
|
|
result = {}
|
|
for suite, group in suite_tests.items():
|
|
live_est = est_table.get(suite, {})
|
|
total = 0.0
|
|
for t in group:
|
|
relpath = os.path.relpath(t.filename, repo_root)
|
|
total += live_est.get(relpath, t.est_time)
|
|
|
|
fit = fit_table.get(suite) or {}
|
|
coeff = fit.get("coeff", 1.0)
|
|
bias = fit.get("bias", 0.0)
|
|
|
|
# Each shard pays `bias` once, so size >= coeff*total / (target-bias).
|
|
if suite in _BASE_A_OVERRIDES:
|
|
size = _BASE_A_OVERRIDES[suite]
|
|
max_parallel = size
|
|
else:
|
|
target = per_shard_target_seconds(suite, run_timeouts)
|
|
budget = target - bias
|
|
if budget <= 0:
|
|
raise RuntimeError(
|
|
f"Suite {suite!r}: fit bias={bias}s >= target={target}s. "
|
|
"Investigate the fit or raise the stage's run_timeout_minutes."
|
|
)
|
|
ideal_size = math.ceil(coeff * total / budget)
|
|
# ideal_size > len(group) -> slowest single file alone exceeds
|
|
# the per-shard budget; surface via raise instead of empty shard.
|
|
if ideal_size > len(group):
|
|
raise RuntimeError(
|
|
f"Suite {suite!r}: needs {ideal_size} shards but has only "
|
|
f"{len(group)} test file(s). target={target:.0f}s, "
|
|
f"coeff={coeff}, bias={bias}s, total_est={total:.0f}s."
|
|
)
|
|
size = max(1, ideal_size)
|
|
max_parallel = size if full_parallel else compute_max_parallel(size)
|
|
result[suite] = {
|
|
"size": size,
|
|
"arr": list(range(size)),
|
|
"max_parallel": max_parallel,
|
|
}
|
|
return result
|
|
|
|
|
|
def format_fit_window(model: dict) -> str:
|
|
"""Render the model's fit window as `[start, end)` for the step summary.
|
|
|
|
Surfacing the whole span keeps the lower-bound date from reading as a
|
|
staleness marker (it trails today by fit_window_days)."""
|
|
start = model.get("fit_window_start")
|
|
days = model.get("fit_window_days")
|
|
if not start or not isinstance(days, int):
|
|
return f"fit_window_start={start}"
|
|
end = (datetime.strptime(start[:10], "%Y-%m-%d") + timedelta(days=days)).strftime(
|
|
"%Y-%m-%d"
|
|
)
|
|
return f"fit over [{start[:10]}, {end}) ({days}d window)"
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--repo-root", default=REPO_ROOT)
|
|
parser.add_argument(
|
|
"--output-format",
|
|
choices=("gha", "json"),
|
|
default="gha",
|
|
help="`gha` emits `partitions=<json>` for $GITHUB_OUTPUT; `json` is raw",
|
|
)
|
|
parser.add_argument(
|
|
"--full-parallel",
|
|
choices=("true", "false"),
|
|
default="false",
|
|
help="Lift the max_parallel throttle (set by schedule / `high priority`)",
|
|
)
|
|
parser.add_argument(
|
|
"--partition-model-file",
|
|
default=None,
|
|
help="Path to sglang-ci-stats model.json (omit/missing -> static fallback)",
|
|
)
|
|
parser.add_argument(
|
|
"--pr-test-yml",
|
|
default=os.path.join(REPO_ROOT, ".github", "workflows", "pr-test.yml"),
|
|
help="Path to pr-test*.yml; per-stage `run_timeout_minutes` is read from here.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
files = discover_files(args.repo_root)
|
|
# Warn-not-fail on unregistered files: run_suite.py catches this at
|
|
# test-execution time with sanity_check=True; dispatch should keep going.
|
|
all_tests = collect_tests(files, sanity_check=False)
|
|
partition_model = load_partition_model(args.partition_model_file)
|
|
run_timeouts = load_run_timeouts(args.pr_test_yml)
|
|
|
|
result = compute_partitions(
|
|
all_tests,
|
|
repo_root=args.repo_root,
|
|
run_timeouts=run_timeouts,
|
|
partition_model=partition_model,
|
|
full_parallel=(args.full_parallel == "true"),
|
|
)
|
|
payload = json.dumps(result, separators=(",", ":"), sort_keys=True)
|
|
if args.output_format == "gha":
|
|
print(f"partitions={payload}")
|
|
else:
|
|
print(payload)
|
|
|
|
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
if summary_path:
|
|
with open(summary_path, "a") as f:
|
|
f.write("## Partitions\n\n")
|
|
if partition_model is None:
|
|
src_note = "no live model -- static est_time + (coeff=1, bias=0)"
|
|
else:
|
|
src_note = (
|
|
f"live model: {format_fit_window(partition_model)}, "
|
|
f"`n_runs={partition_model.get('n_runs')}`"
|
|
)
|
|
f.write(
|
|
f"`full_parallel={args.full_parallel}` "
|
|
f"(`size//3` throttle is lifted when true); {src_note}\n\n"
|
|
)
|
|
f.write("| Suite | size | max_parallel |\n")
|
|
f.write("|---|---:|---:|\n")
|
|
for suite, info in sorted(result.items()):
|
|
f.write(f"| `{suite}` | {info['size']} | {info['max_parallel']} |\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|