From 8f87a9e9aa35fab7116b186cc883812132031282 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Sun, 16 Nov 2025 02:59:46 +0800 Subject: [PATCH 01/13] init code --- python/sglang/test/ci/ut_visitor.py | 140 ++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 python/sglang/test/ci/ut_visitor.py diff --git a/python/sglang/test/ci/ut_visitor.py b/python/sglang/test/ci/ut_visitor.py new file mode 100644 index 000000000000..ff8036a24a98 --- /dev/null +++ b/python/sglang/test/ci/ut_visitor.py @@ -0,0 +1,140 @@ +import argparse +import ast +import logging +from enum import Enum, auto + +from sglang.test.test_utils import CustomTestCase + +logger = logging.getLogger(__name__) + + +class HWBackend(Enum): + SKIP = auto() + CUDA = auto() + AMD = auto() + + +REGISTER_MAPPING = { + "skip_ci": HWBackend.SKIP, + "register_cuda_ci": HWBackend.CUDA, + "register_amd_ci": HWBackend.AMD, +} + + +def skip_ci(reason: str): + def wrapper(fn): + return fn + + return wrapper + + +def register_cuda_ci(esimation_time: float, ci_stage: str): + def wrapper(fn): + return fn + + return wrapper + + +def register_amd_ci(esimation_time: float, ci_stage: str): + def wrapper(fn): + return fn + + return wrapper + + +class HWConfig: + backend: HWBackend + estimation_time: float + stage: str + + +class CITest: + filename: str + testname: str + hw_configs: list[HWConfig] + + +class TestCaseVisitor(ast.NodeVisitor): + def __init__(self): + self.has_custom_test_case = False + self.ut_registries = [] + + def _collect_ci(self, dec): + if not isinstance(dec, ast.Call): + logger.info(f"skip non-call decorator: {ast.dump(dec)}") + return None + + if not isinstance(dec.func, ast.Name): + logger.info(f"skip non-name decorator: {ast.dump(dec)}") + return None + + if dec.func.id not in REGISTER_MAPPING: + return None + + hw = REGISTER_MAPPING[dec.func.id] + if hw == HWBackend.SKIP: + return HWConfig(backend=hw, estimation_time=0, stage="") + + # parse arguments + est_time = None + ci_stage = None + for kw in dec.keywords: + if kw.arg == "esimation_time": + if isinstance(kw.value, ast.Constant): + est_time = kw.value.value + elif kw.arg == "ci_stage": + if isinstance(kw.value, ast.Constant): + ci_stage = kw.value.value + + for i, arg in enumerate(dec.args): + if isinstance(arg, ast.Constant): + if i == 0: + est_time = arg.value + elif i == 1: + ci_stage = arg.value + + assert ( + est_time is not None + ), "esimation_time is required and should be a constant" + assert ci_stage is not None, "ci_stage is required and should be a constant" + return HWConfig(backend=hw, estimation_time=est_time, stage=ci_stage) + + def visit_ClassDef(self, node): + for base in node.bases: + is_ci_test_case = isinstance(base, ast.Name) and base.id == "CustomTestCase" + + if not is_ci_test_case: + continue + + self.has_custom_test_case = True + for dec in node.decorator_list: + hw_config = self._collect_ci(dec) + if hw_config is not None: + self.ut_registries.append(hw_config) + + self.generic_visit(node) + + +class TestGGUF(CustomTestCase): + def test_models(self): + pass + + +def test_main(): + parser = argparse.ArgumentParser() + parser.add_argument("--file", type=str, required=True) + args = parser.parse_args() + + with open(args.file, "r") as f: + file_content = f.read() + tree = ast.parse(file_content, filename=args.file) + visitor = TestCaseVisitor() + visitor.visit(tree) + + print(f"{visitor.has_custom_test_case=}") + for reg in visitor.ut_registries: + print(f"{reg=}") + + +if __name__ == "__main__": + test_main() From d2d481c9bb1ad66c9bd4ea69aff9651094596136 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Sun, 16 Nov 2025 03:06:15 +0800 Subject: [PATCH 02/13] fix decorator parser --- python/sglang/test/ci/ut_visitor.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/python/sglang/test/ci/ut_visitor.py b/python/sglang/test/ci/ut_visitor.py index ff8036a24a98..389567399a9a 100644 --- a/python/sglang/test/ci/ut_visitor.py +++ b/python/sglang/test/ci/ut_visitor.py @@ -1,6 +1,7 @@ import argparse import ast import logging +from dataclasses import dataclass, field from enum import Enum, auto from sglang.test.test_utils import CustomTestCase @@ -14,6 +15,20 @@ class HWBackend(Enum): AMD = auto() +@dataclass +class HWConfig: + backend: HWBackend + estimation_time: float + stage: str + + +@dataclass +class CITest: + filename: str + testname: str + hw_configs: list[HWConfig] = field(default_factory=list) + + REGISTER_MAPPING = { "skip_ci": HWBackend.SKIP, "register_cuda_ci": HWBackend.CUDA, @@ -42,18 +57,6 @@ def wrapper(fn): return wrapper -class HWConfig: - backend: HWBackend - estimation_time: float - stage: str - - -class CITest: - filename: str - testname: str - hw_configs: list[HWConfig] - - class TestCaseVisitor(ast.NodeVisitor): def __init__(self): self.has_custom_test_case = False @@ -115,6 +118,7 @@ def visit_ClassDef(self, node): self.generic_visit(node) +@register_cuda_ci(esimation_time=300, ci_stage="1-gpu") class TestGGUF(CustomTestCase): def test_models(self): pass From 6b1bd011ca89933aa0a6c7a8129a7157d5507b3a Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Sun, 16 Nov 2025 03:09:08 +0800 Subject: [PATCH 03/13] fix --- python/sglang/test/ci/ut_visitor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/sglang/test/ci/ut_visitor.py b/python/sglang/test/ci/ut_visitor.py index 389567399a9a..fd05059eba29 100644 --- a/python/sglang/test/ci/ut_visitor.py +++ b/python/sglang/test/ci/ut_visitor.py @@ -4,8 +4,6 @@ from dataclasses import dataclass, field from enum import Enum, auto -from sglang.test.test_utils import CustomTestCase - logger = logging.getLogger(__name__) @@ -118,6 +116,9 @@ def visit_ClassDef(self, node): self.generic_visit(node) +CustomTestCase = object + + @register_cuda_ci(esimation_time=300, ci_stage="1-gpu") class TestGGUF(CustomTestCase): def test_models(self): From 7cc3b0d1207368851fde29255d8d7c49107ac62f Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Sun, 16 Nov 2025 11:34:07 +0800 Subject: [PATCH 04/13] clean run_suite --- test/srt/run_suite.py | 46 ++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/test/srt/run_suite.py b/test/srt/run_suite.py index add4e7e117b0..9e9a33f8118b 100644 --- a/test/srt/run_suite.py +++ b/test/srt/run_suite.py @@ -617,7 +617,32 @@ def _sanity_check_suites(suites): ) -if __name__ == "__main__": +def run_suite_v1(args): + print(f"{args=}") + + _sanity_check_suites(suites) + + if args.suite == "all": + files = glob.glob("**/test_*.py", recursive=True) + else: + files = suites[args.suite] + + if args.auto_partition_size: + files = auto_partition(files, args.auto_partition_id, args.auto_partition_size) + else: + files = files[args.range_begin : args.range_end] + + print("The running tests are ", [f.name for f in files]) + + exit_code = run_unittest_files(files, args.timeout_per_file, args.continue_on_error) + exit(exit_code) + + +def run_suite_v2(args): + pass + + +def main(): arg_parser = argparse.ArgumentParser() arg_parser.add_argument( "--timeout-per-file", @@ -661,21 +686,10 @@ def _sanity_check_suites(suites): help="Continue running remaining tests even if one fails (useful for nightly tests)", ) args = arg_parser.parse_args() - print(f"{args=}") - _sanity_check_suites(suites) + # run_suite_v1(args) + run_suite_v2(args) - if args.suite == "all": - files = glob.glob("**/test_*.py", recursive=True) - else: - files = suites[args.suite] - if args.auto_partition_size: - files = auto_partition(files, args.auto_partition_id, args.auto_partition_size) - else: - files = files[args.range_begin : args.range_end] - - print("The running tests are ", [f.name for f in files]) - - exit_code = run_unittest_files(files, args.timeout_per_file, args.continue_on_error) - exit(exit_code) +if __name__ == "__main__": + main() From 238e1a9ce2168cc55a159883214406295dd08797 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Sun, 16 Nov 2025 11:50:20 +0800 Subject: [PATCH 05/13] update visitor --- python/sglang/test/ci/ut_visitor.py | 83 +++++++++++++++-------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/python/sglang/test/ci/ut_visitor.py b/python/sglang/test/ci/ut_visitor.py index fd05059eba29..e356459f8198 100644 --- a/python/sglang/test/ci/ut_visitor.py +++ b/python/sglang/test/ci/ut_visitor.py @@ -1,4 +1,3 @@ -import argparse import ast import logging from dataclasses import dataclass, field @@ -14,7 +13,7 @@ class HWBackend(Enum): @dataclass -class HWConfig: +class CIRegistry: backend: HWBackend estimation_time: float stage: str @@ -24,7 +23,7 @@ class HWConfig: class CITest: filename: str testname: str - hw_configs: list[HWConfig] = field(default_factory=list) + ci_registry: list[CIRegistry] = field(default_factory=list) REGISTER_MAPPING = { @@ -34,7 +33,7 @@ class CITest: } -def skip_ci(reason: str): +def skip_ci(): def wrapper(fn): return fn @@ -56,11 +55,11 @@ def wrapper(fn): class TestCaseVisitor(ast.NodeVisitor): - def __init__(self): - self.has_custom_test_case = False - self.ut_registries = [] + def __init__(self, filename: str): + self.filename = filename + self.ci_tests: list[CITest] = [] - def _collect_ci(self, dec): + def _collect_ci_registry(self, dec): if not isinstance(dec, ast.Call): logger.info(f"skip non-call decorator: {ast.dump(dec)}") return None @@ -74,7 +73,7 @@ def _collect_ci(self, dec): hw = REGISTER_MAPPING[dec.func.id] if hw == HWBackend.SKIP: - return HWConfig(backend=hw, estimation_time=0, stage="") + return CIRegistry(backend=hw, estimation_time=0, stage="") # parse arguments est_time = None @@ -98,7 +97,7 @@ def _collect_ci(self, dec): est_time is not None ), "esimation_time is required and should be a constant" assert ci_stage is not None, "ci_stage is required and should be a constant" - return HWConfig(backend=hw, estimation_time=est_time, stage=ci_stage) + return CIRegistry(backend=hw, estimation_time=est_time, stage=ci_stage) def visit_ClassDef(self, node): for base in node.bases: @@ -107,39 +106,45 @@ def visit_ClassDef(self, node): if not is_ci_test_case: continue - self.has_custom_test_case = True + ci_test = CITest(filename=self.filename, testname=node.name) + self.ci_tests.append(ci_test) + for dec in node.decorator_list: - hw_config = self._collect_ci(dec) - if hw_config is not None: - self.ut_registries.append(hw_config) + ci_registry = self._collect_ci_registry(dec) + if ci_registry is not None: + ci_test.ci_registry.append(ci_registry) self.generic_visit(node) -CustomTestCase = object - - -@register_cuda_ci(esimation_time=300, ci_stage="1-gpu") -class TestGGUF(CustomTestCase): - def test_models(self): - pass - - -def test_main(): - parser = argparse.ArgumentParser() - parser.add_argument("--file", type=str, required=True) - args = parser.parse_args() - - with open(args.file, "r") as f: +def ut_parse_one_file(file_path: str): + with open(file_path, "r") as f: file_content = f.read() - tree = ast.parse(file_content, filename=args.file) - visitor = TestCaseVisitor() + tree = ast.parse(file_content, filename=file_path) + visitor = TestCaseVisitor(file_path) visitor.visit(tree) - - print(f"{visitor.has_custom_test_case=}") - for reg in visitor.ut_registries: - print(f"{reg=}") - - -if __name__ == "__main__": - test_main() + return visitor + + +def collect_all_tests(files: list[str], sanity_check: bool = True) -> list[CITest]: + ci_tests = [] + for file in files: + visitor = ut_parse_one_file(file) + if sanity_check and len(visitor.ci_tests) == 0: + raise ValueError(f"No CustomTestCase found in {file}") + + for reg in visitor.ci_tests: + if sanity_check: + if len(reg.ci_registry) == 0: + raise ValueError( + f"No CI registry found in CustomTestCase {reg.testname} in {file}" + ) + + if len(reg.ci_registry) > 1 and any( + r.backend == HWBackend.SKIP for r in reg.ci_registry + ): + raise ValueError( + f"Conflicting CI registry found in CustomTestCase {reg.testname} in {file}" + ) + + ci_tests.extend(visitor.ci_tests) From fc0dfb85bbd9fa775a86e6238d533ed55d4f2456 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Sun, 16 Nov 2025 12:05:30 +0800 Subject: [PATCH 06/13] update --- python/sglang/test/ci/ut_visitor.py | 38 ++++++++++++++++++----------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/python/sglang/test/ci/ut_visitor.py b/python/sglang/test/ci/ut_visitor.py index e356459f8198..6991c2a36e54 100644 --- a/python/sglang/test/ci/ut_visitor.py +++ b/python/sglang/test/ci/ut_visitor.py @@ -130,21 +130,31 @@ def collect_all_tests(files: list[str], sanity_check: bool = True) -> list[CITes ci_tests = [] for file in files: visitor = ut_parse_one_file(file) - if sanity_check and len(visitor.ci_tests) == 0: - raise ValueError(f"No CustomTestCase found in {file}") + if len(visitor.ci_tests) == 0: + msg = f"No test cases found in {file}" + if sanity_check: + raise ValueError(msg) + else: + logger.warning(msg) + continue for reg in visitor.ci_tests: - if sanity_check: - if len(reg.ci_registry) == 0: - raise ValueError( - f"No CI registry found in CustomTestCase {reg.testname} in {file}" - ) - - if len(reg.ci_registry) > 1 and any( - r.backend == HWBackend.SKIP for r in reg.ci_registry - ): - raise ValueError( - f"Conflicting CI registry found in CustomTestCase {reg.testname} in {file}" - ) + if len(reg.ci_registry) == 0: + msg = f"No CI registry found in CustomTestCase {reg.testname} in {file}" + if sanity_check: + raise ValueError(msg) + else: + logger.warning(msg) + continue + + if len(reg.ci_registry) > 1 and any( + r.backend == HWBackend.SKIP for r in reg.ci_registry + ): + msg = f"Conflicting CI registry found in CustomTestCase {reg.testname} in {file}" + if sanity_check: + raise ValueError(msg) + else: + logger.warning(msg) + continue ci_tests.extend(visitor.ci_tests) From a2d7d7ddf9f037c3bcca03989369896aa3436e55 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Sun, 16 Nov 2025 21:59:54 +0800 Subject: [PATCH 07/13] fix --- python/sglang/test/ci/ut_visitor.py | 160 ---------------------------- python/sglang/test/ci_register.py | 104 ++++++++++++++++++ test/srt/run_suite.py | 13 ++- 3 files changed, 112 insertions(+), 165 deletions(-) delete mode 100644 python/sglang/test/ci/ut_visitor.py create mode 100644 python/sglang/test/ci_register.py diff --git a/python/sglang/test/ci/ut_visitor.py b/python/sglang/test/ci/ut_visitor.py deleted file mode 100644 index 6991c2a36e54..000000000000 --- a/python/sglang/test/ci/ut_visitor.py +++ /dev/null @@ -1,160 +0,0 @@ -import ast -import logging -from dataclasses import dataclass, field -from enum import Enum, auto - -logger = logging.getLogger(__name__) - - -class HWBackend(Enum): - SKIP = auto() - CUDA = auto() - AMD = auto() - - -@dataclass -class CIRegistry: - backend: HWBackend - estimation_time: float - stage: str - - -@dataclass -class CITest: - filename: str - testname: str - ci_registry: list[CIRegistry] = field(default_factory=list) - - -REGISTER_MAPPING = { - "skip_ci": HWBackend.SKIP, - "register_cuda_ci": HWBackend.CUDA, - "register_amd_ci": HWBackend.AMD, -} - - -def skip_ci(): - def wrapper(fn): - return fn - - return wrapper - - -def register_cuda_ci(esimation_time: float, ci_stage: str): - def wrapper(fn): - return fn - - return wrapper - - -def register_amd_ci(esimation_time: float, ci_stage: str): - def wrapper(fn): - return fn - - return wrapper - - -class TestCaseVisitor(ast.NodeVisitor): - def __init__(self, filename: str): - self.filename = filename - self.ci_tests: list[CITest] = [] - - def _collect_ci_registry(self, dec): - if not isinstance(dec, ast.Call): - logger.info(f"skip non-call decorator: {ast.dump(dec)}") - return None - - if not isinstance(dec.func, ast.Name): - logger.info(f"skip non-name decorator: {ast.dump(dec)}") - return None - - if dec.func.id not in REGISTER_MAPPING: - return None - - hw = REGISTER_MAPPING[dec.func.id] - if hw == HWBackend.SKIP: - return CIRegistry(backend=hw, estimation_time=0, stage="") - - # parse arguments - est_time = None - ci_stage = None - for kw in dec.keywords: - if kw.arg == "esimation_time": - if isinstance(kw.value, ast.Constant): - est_time = kw.value.value - elif kw.arg == "ci_stage": - if isinstance(kw.value, ast.Constant): - ci_stage = kw.value.value - - for i, arg in enumerate(dec.args): - if isinstance(arg, ast.Constant): - if i == 0: - est_time = arg.value - elif i == 1: - ci_stage = arg.value - - assert ( - est_time is not None - ), "esimation_time is required and should be a constant" - assert ci_stage is not None, "ci_stage is required and should be a constant" - return CIRegistry(backend=hw, estimation_time=est_time, stage=ci_stage) - - def visit_ClassDef(self, node): - for base in node.bases: - is_ci_test_case = isinstance(base, ast.Name) and base.id == "CustomTestCase" - - if not is_ci_test_case: - continue - - ci_test = CITest(filename=self.filename, testname=node.name) - self.ci_tests.append(ci_test) - - for dec in node.decorator_list: - ci_registry = self._collect_ci_registry(dec) - if ci_registry is not None: - ci_test.ci_registry.append(ci_registry) - - self.generic_visit(node) - - -def ut_parse_one_file(file_path: str): - with open(file_path, "r") as f: - file_content = f.read() - tree = ast.parse(file_content, filename=file_path) - visitor = TestCaseVisitor(file_path) - visitor.visit(tree) - return visitor - - -def collect_all_tests(files: list[str], sanity_check: bool = True) -> list[CITest]: - ci_tests = [] - for file in files: - visitor = ut_parse_one_file(file) - if len(visitor.ci_tests) == 0: - msg = f"No test cases found in {file}" - if sanity_check: - raise ValueError(msg) - else: - logger.warning(msg) - continue - - for reg in visitor.ci_tests: - if len(reg.ci_registry) == 0: - msg = f"No CI registry found in CustomTestCase {reg.testname} in {file}" - if sanity_check: - raise ValueError(msg) - else: - logger.warning(msg) - continue - - if len(reg.ci_registry) > 1 and any( - r.backend == HWBackend.SKIP for r in reg.ci_registry - ): - msg = f"Conflicting CI registry found in CustomTestCase {reg.testname} in {file}" - if sanity_check: - raise ValueError(msg) - else: - logger.warning(msg) - continue - - ci_tests.extend(visitor.ci_tests) diff --git a/python/sglang/test/ci_register.py b/python/sglang/test/ci_register.py new file mode 100644 index 000000000000..8203c4f1bdc5 --- /dev/null +++ b/python/sglang/test/ci_register.py @@ -0,0 +1,104 @@ +import ast +import warnings +from dataclasses import dataclass +from enum import Enum, auto +from typing import List + + +class HWBackend(Enum): + CUDA = auto() + AMD = auto() + + +@dataclass +class CIRegistry: + backend: HWBackend + estimation_time: float + stage: str + + +def register_cuda_ci(estimation_time: float, ci_stage: str): + pass + + +def register_amd_ci(estimation_time: float, ci_stage: str): + pass + + +REGISTER_MAPPING = { + "register_cuda_ci": HWBackend.CUDA, + "register_amd_ci": HWBackend.AMD, +} + + +class RegistryVisitor(ast.NodeVisitor): + def __init__(self, filename: str): + self.filename = filename + self.registries: list[CIRegistry] = [] + + def _collect_ci_registry(self, func_call: ast.Call): + if not isinstance(func_call.func, ast.Name): + return None + + if func_call.func.id not in REGISTER_MAPPING: + return None + + hw = REGISTER_MAPPING[func_call.func.id] + est_time = None + ci_stage = None + for kw in func_call.keywords: + if kw.arg == "esimation_time": + if isinstance(kw.value, ast.Constant): + est_time = kw.value.value + elif kw.arg == "ci_stage": + if isinstance(kw.value, ast.Constant): + ci_stage = kw.value.value + + for i, arg in enumerate(func_call.args): + if isinstance(arg, ast.Constant): + if i == 0: + est_time = arg.value + elif i == 1: + ci_stage = arg.value + assert ( + est_time is not None + ), "esimation_time is required and should be a constant" + assert ci_stage is not None, "ci_stage is required and should be a constant" + return CIRegistry(backend=hw, estimation_time=est_time, stage=ci_stage) + + def visit_Module(self, node): + for stmt in node.body: + if not isinstance(stmt, ast.Expr) or not isinstance(stmt.value, ast.Call): + continue + + cr = self._collect_ci_registry(stmt.value) + if cr is not None: + self.registries.append(cr) + + self.generic_visit(node) + + +def ut_parse_one_file(filename: str) -> List[CIRegistry]: + 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 + + +def collect_tests(files: list[str], sanity_check: bool = True) -> List[CIRegistry]: + ci_tests = [] + for file in files: + registries = 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 + + ci_tests.extend(registries) + + return ci_tests diff --git a/test/srt/run_suite.py b/test/srt/run_suite.py index 9e9a33f8118b..07ccd82b4039 100644 --- a/test/srt/run_suite.py +++ b/test/srt/run_suite.py @@ -3,8 +3,6 @@ from dataclasses import dataclass from pathlib import Path -from sglang.test.test_utils import run_unittest_files - @dataclass class TestFile: @@ -618,6 +616,8 @@ def _sanity_check_suites(suites): def run_suite_v1(args): + from sglang.test.test_utils import run_unittest_files + print(f"{args=}") _sanity_check_suites(suites) @@ -639,7 +639,10 @@ def run_suite_v1(args): def run_suite_v2(args): - pass + from sglang.test.ci_register import collect_tests + + files = glob.glob("**/test_*.py", recursive=True) + collect_tests(files, sanity_check=False) def main(): @@ -687,8 +690,8 @@ def main(): ) args = arg_parser.parse_args() - # run_suite_v1(args) - run_suite_v2(args) + run_suite_v1(args) + # run_suite_v2(args) if __name__ == "__main__": From 88554029b13515fa41e031e324d070874244679d Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Mon, 17 Nov 2025 00:02:25 +0800 Subject: [PATCH 08/13] with new run_suite.py --- python/sglang/test/{ => ci}/ci_register.py | 7 +- python/sglang/test/ci/ci_utils.py | 134 ++++++++++++++++++ python/sglang/test/test_utils.py | 127 ----------------- test/lang/run_suite.py | 2 +- test/{lang => per_commit}/test_srt_backend.py | 9 +- test/run_suite.py | 39 +++++ test/srt/run_suite.py | 19 +-- 7 files changed, 184 insertions(+), 153 deletions(-) rename python/sglang/test/{ => ci}/ci_register.py (93%) create mode 100644 python/sglang/test/ci/ci_utils.py rename test/{lang => per_commit}/test_srt_backend.py (91%) create mode 100644 test/run_suite.py diff --git a/python/sglang/test/ci_register.py b/python/sglang/test/ci/ci_register.py similarity index 93% rename from python/sglang/test/ci_register.py rename to python/sglang/test/ci/ci_register.py index 8203c4f1bdc5..6beea6a5e110 100644 --- a/python/sglang/test/ci_register.py +++ b/python/sglang/test/ci/ci_register.py @@ -13,6 +13,7 @@ class HWBackend(Enum): @dataclass class CIRegistry: backend: HWBackend + filename: str estimation_time: float stage: str @@ -47,7 +48,7 @@ def _collect_ci_registry(self, func_call: ast.Call): est_time = None ci_stage = None for kw in func_call.keywords: - if kw.arg == "esimation_time": + if kw.arg == "estimation_time": if isinstance(kw.value, ast.Constant): est_time = kw.value.value elif kw.arg == "ci_stage": @@ -64,7 +65,9 @@ def _collect_ci_registry(self, func_call: ast.Call): est_time is not None ), "esimation_time is required and should be a constant" assert ci_stage is not None, "ci_stage is required and should be a constant" - return CIRegistry(backend=hw, estimation_time=est_time, stage=ci_stage) + return CIRegistry( + backend=hw, filename=self.filename, estimation_time=est_time, stage=ci_stage + ) def visit_Module(self, node): for stmt in node.body: diff --git a/python/sglang/test/ci/ci_utils.py b/python/sglang/test/ci/ci_utils.py new file mode 100644 index 000000000000..447a14a54bb2 --- /dev/null +++ b/python/sglang/test/ci/ci_utils.py @@ -0,0 +1,134 @@ +import os +import subprocess +import threading +import time +from dataclasses import dataclass +from typing import Callable, List, Optional + +from sglang.srt.utils.common import kill_process_tree + + +@dataclass +class TestFile: + name: str + estimated_time: float = 60 + + +def run_with_timeout( + func: Callable, + args: tuple = (), + kwargs: Optional[dict] = None, + timeout: float = None, +): + """Run a function with timeout.""" + ret_value = [] + + def _target_func(): + ret_value.append(func(*args, **(kwargs or {}))) + + t = threading.Thread(target=_target_func) + t.start() + t.join(timeout=timeout) + if t.is_alive(): + raise TimeoutError() + + if not ret_value: + raise RuntimeError() + + return ret_value[0] + + +def run_unittest_files( + files: List[TestFile], timeout_per_file: float, continue_on_error: bool = False +): + """ + Run a list of test files. + + Args: + files: List of TestFile objects to run + timeout_per_file: Timeout in seconds for each test file + continue_on_error: If True, continue running remaining tests even if one fails. + If False, stop at first failure (default behavior for PR tests). + """ + tic = time.perf_counter() + success = True + passed_tests = [] + failed_tests = [] + + for i, file in enumerate(files): + filename, estimated_time = file.name, file.estimated_time + process = None + + def run_one_file(filename): + nonlocal process + + filename = os.path.join(os.getcwd(), filename) + print( + f".\n.\nBegin ({i}/{len(files) - 1}):\npython3 {filename}\n.\n.\n", + flush=True, + ) + tic = time.perf_counter() + + process = subprocess.Popen( + ["python3", filename], stdout=None, stderr=None, env=os.environ + ) + process.wait() + elapsed = time.perf_counter() - tic + + print( + f".\n.\nEnd ({i}/{len(files) - 1}):\n{filename=}, {elapsed=:.0f}, {estimated_time=}\n.\n.\n", + flush=True, + ) + return process.returncode + + try: + ret_code = run_with_timeout( + run_one_file, args=(filename,), timeout=timeout_per_file + ) + if ret_code != 0: + print( + f"\n✗ FAILED: {filename} returned exit code {ret_code}\n", + flush=True, + ) + success = False + failed_tests.append((filename, f"exit code {ret_code}")) + if not continue_on_error: + # Stop at first failure for PR tests + break + # Otherwise continue to next test for nightly tests + else: + passed_tests.append(filename) + except TimeoutError: + kill_process_tree(process.pid) + time.sleep(5) + print( + f"\n✗ TIMEOUT: {filename} after {timeout_per_file} seconds\n", + flush=True, + ) + success = False + failed_tests.append((filename, f"timeout after {timeout_per_file}s")) + if not continue_on_error: + # Stop at first timeout for PR tests + break + # Otherwise continue to next test for nightly tests + + if success: + print(f"Success. Time elapsed: {time.perf_counter() - tic:.2f}s", flush=True) + else: + print(f"Fail. Time elapsed: {time.perf_counter() - tic:.2f}s", flush=True) + + # Print summary + print(f"\n{'='*60}", flush=True) + print(f"Test Summary: {len(passed_tests)}/{len(files)} passed", flush=True) + print(f"{'='*60}", flush=True) + if passed_tests: + print("✓ PASSED:", flush=True) + for test in passed_tests: + print(f" {test}", flush=True) + if failed_tests: + print("\n✗ FAILED:", flush=True) + for test, reason in failed_tests: + print(f" {test} ({reason})", flush=True) + print(f"{'='*60}\n", flush=True) + + return 0 if success else -1 diff --git a/python/sglang/test/test_utils.py b/python/sglang/test/test_utils.py index 9c11c4a77abc..b34544d57782 100644 --- a/python/sglang/test/test_utils.py +++ b/python/sglang/test/test_utils.py @@ -14,7 +14,6 @@ import time import unittest from concurrent.futures import ThreadPoolExecutor -from dataclasses import dataclass from datetime import datetime from functools import partial, wraps from pathlib import Path @@ -705,132 +704,6 @@ def popen_launch_pd_server( return process -def run_with_timeout( - func: Callable, - args: tuple = (), - kwargs: Optional[dict] = None, - timeout: float = None, -): - """Run a function with timeout.""" - ret_value = [] - - def _target_func(): - ret_value.append(func(*args, **(kwargs or {}))) - - t = threading.Thread(target=_target_func) - t.start() - t.join(timeout=timeout) - if t.is_alive(): - raise TimeoutError() - - if not ret_value: - raise RuntimeError() - - return ret_value[0] - - -@dataclass -class TestFile: - name: str - estimated_time: float = 60 - - -def run_unittest_files( - files: List[TestFile], timeout_per_file: float, continue_on_error: bool = False -): - """ - Run a list of test files. - - Args: - files: List of TestFile objects to run - timeout_per_file: Timeout in seconds for each test file - continue_on_error: If True, continue running remaining tests even if one fails. - If False, stop at first failure (default behavior for PR tests). - """ - tic = time.perf_counter() - success = True - passed_tests = [] - failed_tests = [] - - for i, file in enumerate(files): - filename, estimated_time = file.name, file.estimated_time - process = None - - def run_one_file(filename): - nonlocal process - - filename = os.path.join(os.getcwd(), filename) - print( - f".\n.\nBegin ({i}/{len(files) - 1}):\npython3 {filename}\n.\n.\n", - flush=True, - ) - tic = time.perf_counter() - - process = subprocess.Popen( - ["python3", filename], stdout=None, stderr=None, env=os.environ - ) - process.wait() - elapsed = time.perf_counter() - tic - - print( - f".\n.\nEnd ({i}/{len(files) - 1}):\n{filename=}, {elapsed=:.0f}, {estimated_time=}\n.\n.\n", - flush=True, - ) - return process.returncode - - try: - ret_code = run_with_timeout( - run_one_file, args=(filename,), timeout=timeout_per_file - ) - if ret_code != 0: - print( - f"\n✗ FAILED: {filename} returned exit code {ret_code}\n", - flush=True, - ) - success = False - failed_tests.append((filename, f"exit code {ret_code}")) - if not continue_on_error: - # Stop at first failure for PR tests - break - # Otherwise continue to next test for nightly tests - else: - passed_tests.append(filename) - except TimeoutError: - kill_process_tree(process.pid) - time.sleep(5) - print( - f"\n✗ TIMEOUT: {filename} after {timeout_per_file} seconds\n", - flush=True, - ) - success = False - failed_tests.append((filename, f"timeout after {timeout_per_file}s")) - if not continue_on_error: - # Stop at first timeout for PR tests - break - # Otherwise continue to next test for nightly tests - - if success: - print(f"Success. Time elapsed: {time.perf_counter() - tic:.2f}s", flush=True) - else: - print(f"Fail. Time elapsed: {time.perf_counter() - tic:.2f}s", flush=True) - - # Print summary - print(f"\n{'='*60}", flush=True) - print(f"Test Summary: {len(passed_tests)}/{len(files)} passed", flush=True) - print(f"{'='*60}", flush=True) - if passed_tests: - print("✓ PASSED:", flush=True) - for test in passed_tests: - print(f" {test}", flush=True) - if failed_tests: - print("\n✗ FAILED:", flush=True) - for test, reason in failed_tests: - print(f" {test} ({reason})", flush=True) - print(f"{'='*60}\n", flush=True) - - return 0 if success else -1 - - def get_similarities(vec1, vec2): return F.cosine_similarity(torch.tensor(vec1), torch.tensor(vec2), dim=0) diff --git a/test/lang/run_suite.py b/test/lang/run_suite.py index 14690d935b2e..a1e4d9d0785c 100644 --- a/test/lang/run_suite.py +++ b/test/lang/run_suite.py @@ -1,7 +1,7 @@ import argparse import glob -from sglang.test.test_utils import TestFile, run_unittest_files +from sglang.test.ci.ci_utils import TestFile, run_unittest_files suites = { "per-commit": [ diff --git a/test/lang/test_srt_backend.py b/test/per_commit/test_srt_backend.py similarity index 91% rename from test/lang/test_srt_backend.py rename to test/per_commit/test_srt_backend.py index 0e05eb9069b9..e7f75fa6caa2 100644 --- a/test/lang/test_srt_backend.py +++ b/test/per_commit/test_srt_backend.py @@ -1,12 +1,7 @@ -""" -Usage: -python3 -m unittest test_srt_backend.TestSRTBackend.test_gen_min_new_tokens -python3 -m unittest test_srt_backend.TestSRTBackend.test_hellaswag_select -""" - import unittest import sglang as sgl +from sglang.test.ci.ci_register import register_cuda_ci from sglang.test.test_programs import ( test_decode_int, test_decode_json_regex, @@ -24,6 +19,8 @@ ) from sglang.test.test_utils import DEFAULT_MODEL_NAME_FOR_TEST, CustomTestCase +register_cuda_ci(estimation_time=80, ci_stage="stage-a-test-1") + class TestSRTBackend(CustomTestCase): backend = None diff --git a/test/run_suite.py b/test/run_suite.py new file mode 100644 index 000000000000..356c207952c7 --- /dev/null +++ b/test/run_suite.py @@ -0,0 +1,39 @@ +import glob +from typing import List + +from sglang.test.ci.ci_register import CIRegistry, HWBackend, collect_tests +from sglang.test.ci.ci_utils import TestFile, run_unittest_files + +LABEL_MAPPING = {HWBackend.CUDA: ["stage-a-test-1"]} + + +def _filter_tests( + ci_tests: List[CIRegistry], hw: HWBackend, suite: str +) -> List[CIRegistry]: + ci_tests = [t for t in ci_tests if t.backend == hw] + ret = [] + for t in ci_tests: + assert t.stage in LABEL_MAPPING[hw], f"Unknown stage {t.stage} for backend {hw}" + if t.stage == suite: + ret.append(t) + return ret + + +def run_per_commit(hw: HWBackend, suite: str): + files = glob.glob("per_commit/**/*.py", recursive=True) + ci_tests = _filter_tests(collect_tests(files), hw, suite) + test_files = [TestFile(t.filename, t.estimation_time) for t in ci_tests] + + run_unittest_files( + test_files, + timeout_per_file=1200, + continue_on_error=False, + ) + + +def main(): + run_per_commit(HWBackend.CUDA, "stage-a-test-1") + + +if __name__ == "__main__": + main() diff --git a/test/srt/run_suite.py b/test/srt/run_suite.py index 189a0de38f07..7d8ff420f4db 100644 --- a/test/srt/run_suite.py +++ b/test/srt/run_suite.py @@ -1,14 +1,8 @@ import argparse import glob -from dataclasses import dataclass from pathlib import Path - -@dataclass -class TestFile: - name: str - estimated_time: float = 60 - +from sglang.test.ci.ci_utils import TestFile, run_unittest_files # NOTE: please sort the test cases alphabetically by the test file name suites = { @@ -616,8 +610,6 @@ def _sanity_check_suites(suites): def run_suite_v1(args): - from sglang.test.test_utils import run_unittest_files - print(f"{args=}") _sanity_check_suites(suites) @@ -638,13 +630,6 @@ def run_suite_v1(args): exit(exit_code) -def run_suite_v2(args): - from sglang.test.ci_register import collect_tests - - files = glob.glob("**/test_*.py", recursive=True) - collect_tests(files, sanity_check=False) - - def main(): arg_parser = argparse.ArgumentParser() arg_parser.add_argument( @@ -690,8 +675,8 @@ def main(): ) args = arg_parser.parse_args() + # FIXME: this will be deprecated soon run_suite_v1(args) - # run_suite_v2(args) if __name__ == "__main__": From 71f7382aea90e1b6b7d3a636b281cb1a6ee8bc5c Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Mon, 17 Nov 2025 00:06:22 +0800 Subject: [PATCH 09/13] move tests --- .github/workflows/pr-test.yml | 6 +++--- test/lang/run_suite.py | 36 ----------------------------------- 2 files changed, 3 insertions(+), 39 deletions(-) delete mode 100644 test/lang/run_suite.py diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 33f53fc0e150..3a83755637c1 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -365,7 +365,7 @@ jobs: # =============================================== primary ==================================================== - unit-test-frontend: + stage-a-test-1: needs: [check-changes, sgl-kernel-build-wheels] if: always() && !failure() && !cancelled() && ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) @@ -391,8 +391,8 @@ jobs: - name: Run test timeout-minutes: 10 run: | - cd test/lang - python3 run_suite.py --suite per-commit + cd test/ + python3 run_suite.py unit-test-backend-1-gpu: needs: [check-changes, unit-test-frontend, sgl-kernel-build-wheels] diff --git a/test/lang/run_suite.py b/test/lang/run_suite.py deleted file mode 100644 index a1e4d9d0785c..000000000000 --- a/test/lang/run_suite.py +++ /dev/null @@ -1,36 +0,0 @@ -import argparse -import glob - -from sglang.test.ci.ci_utils import TestFile, run_unittest_files - -suites = { - "per-commit": [ - TestFile("test_srt_backend.py"), - ], -} - - -if __name__ == "__main__": - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument( - "--timeout-per-file", - type=int, - default=1000, - help="The time limit for running one file in seconds.", - ) - arg_parser.add_argument( - "--suite", - type=str, - default=list(suites.keys())[0], - choices=list(suites.keys()) + ["all"], - help="The suite to run", - ) - args = arg_parser.parse_args() - - if args.suite == "all": - files = glob.glob("**/test_*.py", recursive=True) - else: - files = suites[args.suite] - - exit_code = run_unittest_files(files, args.timeout_per_file) - exit(exit_code) From fbb66a703e96be8392b992321045b5dfa426d3a9 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Mon, 17 Nov 2025 00:11:30 +0800 Subject: [PATCH 10/13] fix --- test/srt/run_suite.py | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/test/srt/run_suite.py b/test/srt/run_suite.py index 7c79e43e04a3..8cef2b53d8e5 100644 --- a/test/srt/run_suite.py +++ b/test/srt/run_suite.py @@ -609,25 +609,6 @@ def _sanity_check_suites(suites): ) -def run_suite_v1(args): - print(f"{args=}") - - _sanity_check_suites(suites) - - if args.suite == "all": - files = glob.glob("**/test_*.py", recursive=True) - else: - files = suites[args.suite] - - if args.auto_partition_size: - files = auto_partition(files, args.auto_partition_id, args.auto_partition_size) - - print("The running tests are ", [f.name for f in files]) - - exit_code = run_unittest_files(files, args.timeout_per_file, args.continue_on_error) - exit(exit_code) - - def main(): arg_parser = argparse.ArgumentParser() arg_parser.add_argument( @@ -660,9 +641,22 @@ def main(): help="Continue running remaining tests even if one fails (useful for nightly tests)", ) args = arg_parser.parse_args() + print(f"{args=}") + + _sanity_check_suites(suites) + + if args.suite == "all": + files = glob.glob("**/test_*.py", recursive=True) + else: + files = suites[args.suite] + + if args.auto_partition_size: + files = auto_partition(files, args.auto_partition_id, args.auto_partition_size) - # FIXME: this will be deprecated soon - run_suite_v1(args) + print("The running tests are ", [f.name for f in files]) + + exit_code = run_unittest_files(files, args.timeout_per_file, args.continue_on_error) + exit(exit_code) if __name__ == "__main__": From 7255579716bc73b4d41e2e8ccfb3e1eaa939f775 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Mon, 17 Nov 2025 00:18:23 +0800 Subject: [PATCH 11/13] fix ci scripts --- .github/workflows/pr-test-amd.yml | 8 ++++---- .github/workflows/pr-test.yml | 4 ++-- scripts/ci_monitor/ci_analyzer.py | 2 +- scripts/ci_monitor/ci_analyzer_balance.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-test-amd.yml b/.github/workflows/pr-test-amd.yml index e722b24cdf82..b005f76593e4 100644 --- a/.github/workflows/pr-test-amd.yml +++ b/.github/workflows/pr-test-amd.yml @@ -98,7 +98,7 @@ jobs: # =============================================== primary ==================================================== - unit-test-frontend-amd: + stage-a-test-1-amd: needs: [check-changes] if: always() && !failure() && !cancelled() && ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) @@ -126,10 +126,10 @@ jobs: - name: Run test timeout-minutes: 10 run: | - docker exec -w /sglang-checkout/test/lang ci_sglang python3 run_suite.py --suite per-commit + docker exec -w /sglang-checkout/test ci_sglang python3 run_suite.py unit-test-backend-1-gpu-amd: - needs: [check-changes, unit-test-frontend-amd] + needs: [check-changes, stage-a-test-1-amd] if: always() && !failure() && !cancelled() && ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) strategy: @@ -420,7 +420,7 @@ jobs: sgl-kernel-unit-test-amd, - unit-test-frontend-amd, + stage-a-test-1-amd, unit-test-backend-1-gpu-amd, unit-test-backend-2-gpu-amd, unit-test-backend-8-gpu-amd, diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 3a83755637c1..5b6e79fcabf1 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -395,7 +395,7 @@ jobs: python3 run_suite.py unit-test-backend-1-gpu: - needs: [check-changes, unit-test-frontend, sgl-kernel-build-wheels] + needs: [check-changes, stage-a-test-1, sgl-kernel-build-wheels] if: always() && !failure() && !cancelled() && ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) runs-on: 1-gpu-runner @@ -965,7 +965,7 @@ jobs: multimodal-gen-test, - unit-test-frontend, + stage-a-test-1, unit-test-backend-1-gpu, unit-test-backend-2-gpu, unit-test-backend-4-gpu, diff --git a/scripts/ci_monitor/ci_analyzer.py b/scripts/ci_monitor/ci_analyzer.py index 8f7dc7e2d247..5ea434862ec1 100755 --- a/scripts/ci_monitor/ci_analyzer.py +++ b/scripts/ci_monitor/ci_analyzer.py @@ -74,7 +74,7 @@ def analyze_ci_failures(self, runs: List[Dict]) -> Dict: "sgl-kernel-build-wheels", ], "unit-test": [ - "unit-test-frontend", + "stage-a-test-1", "unit-test-backend-1-gpu", "unit-test-backend-2-gpu", "unit-test-backend-4-gpu", diff --git a/scripts/ci_monitor/ci_analyzer_balance.py b/scripts/ci_monitor/ci_analyzer_balance.py index 818339fe1bb6..e0779990c815 100755 --- a/scripts/ci_monitor/ci_analyzer_balance.py +++ b/scripts/ci_monitor/ci_analyzer_balance.py @@ -174,7 +174,7 @@ def collect_test_balance_data(self, runs: List[Dict]) -> Dict[str, Dict]: abnormal_tests_filtered = 0 target_job_prefixes = [ - "unit-test-frontend", + "stage-a-test-1", "unit-test-backend-1-gpu", "unit-test-backend-2-gpu", "unit-test-backend-4-gpu", From 0c4f7ae69d7b7a1941ac889e3fa0ab30f4adb61e Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Mon, 17 Nov 2025 00:22:56 +0800 Subject: [PATCH 12/13] fix --- scripts/ci_monitor/ci_analyzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci_monitor/ci_analyzer.py b/scripts/ci_monitor/ci_analyzer.py index 5ea434862ec1..7e0aa7726089 100755 --- a/scripts/ci_monitor/ci_analyzer.py +++ b/scripts/ci_monitor/ci_analyzer.py @@ -172,7 +172,7 @@ def analyze_ci_failures(self, runs: List[Dict]) -> Dict: "sgl-kernel-unit-test", "sgl-kernel-mla-test", "sgl-kernel-benchmark-test", - "unit-test-frontend", + "stage-a-test-1", "unit-test-backend-1-gpu", "unit-test-backend-2-gpu", "unit-test-backend-4-gpu", From e2d75f42284b400dced8ab7c96cf6e51a8f28e88 Mon Sep 17 00:00:00 2001 From: Liangsheng Yin Date: Mon, 17 Nov 2025 12:12:25 +0800 Subject: [PATCH 13/13] fix --- .github/workflows/pr-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 5b6e79fcabf1..dfe00791b578 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -562,7 +562,7 @@ jobs: python3 run_suite.py --suite per-commit-8-gpu-h20 --auto-partition-id ${{ matrix.part }} --auto-partition-size 2 performance-test-1-gpu-part-1: - needs: [check-changes, sgl-kernel-build-wheels] + needs: [check-changes, sgl-kernel-build-wheels, stage-a-test-1] if: always() && !failure() && !cancelled() && ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) runs-on: 1-gpu-runner @@ -623,7 +623,7 @@ jobs: python3 -m unittest test_bench_serving.TestBenchServing.test_lora_online_latency_with_concurrent_adapter_updates performance-test-1-gpu-part-2: - needs: [check-changes, sgl-kernel-build-wheels] + needs: [check-changes, sgl-kernel-build-wheels, stage-a-test-1] if: always() && !failure() && !cancelled() && ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) runs-on: 1-gpu-runner @@ -676,7 +676,7 @@ jobs: python3 -m unittest test_bench_serving.TestBenchServing.test_vlm_online_latency performance-test-1-gpu-part-3: - needs: [check-changes, sgl-kernel-build-wheels] + needs: [check-changes, sgl-kernel-build-wheels, stage-a-test-1] if: always() && !failure() && !cancelled() && ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) runs-on: 1-gpu-runner @@ -770,7 +770,7 @@ jobs: python3 -m unittest test_bench_serving.TestBenchServing.test_pp_long_context_prefill accuracy-test-1-gpu: - needs: [check-changes, sgl-kernel-build-wheels] + needs: [check-changes, sgl-kernel-build-wheels, stage-a-test-1] if: always() && !failure() && !cancelled() && ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) runs-on: 1-gpu-runner