[Test] Consolidate test cleanup and CI taxonomy (net -11.4K lines) (#37436)
Co-authored-by: Mick Qian <mickqian@users.noreply.github.com>
This commit is contained in:
co-authored by
Mick Qian
parent
6a1ff90f2d
commit
4d23a4fa6d
@@ -17,7 +17,7 @@ from sglang.kernels.ops.diffusion import fused_rmsnorm_scale_shift_bitexact
|
||||
```
|
||||
|
||||
**Import from the package, never from a submodule.** The internal layout is
|
||||
free to move; the facade is not. `test_import_surface.py` enforces this, with
|
||||
free to move; the facade is not. Callers should use the facade, with
|
||||
a small allowlist for tests that deliberately exercise one backend.
|
||||
|
||||
Resolution is lazy (PEP 562): the backends have disjoint heavy dependencies
|
||||
@@ -189,7 +189,7 @@ inspecting model modules is its whole job.
|
||||
generated by the KDA workflow in `sglang.kernels.kda_kernels`, together
|
||||
with its source revision and any JIT CUDA source files.
|
||||
2. Export it from `__init__.py` (`_EXPORTS`) and register a `KernelSpec`
|
||||
(`_SPECS`) — `test_import_surface.py` checks both resolve.
|
||||
(`_SPECS`).
|
||||
3. Give it a `can_use_*` predicate; raise, don't return `None`.
|
||||
4. State the numerical contract in the module docstring, including which
|
||||
shapes it was verified on.
|
||||
|
||||
@@ -5,8 +5,8 @@ This module is the **only** supported import surface for these kernels::
|
||||
from sglang.kernels.ops.diffusion import fused_rmsnorm_scale_shift_bitexact
|
||||
|
||||
Importing a submodule directly (``...diffusion.norm.norm_triton``) couples the
|
||||
caller to the file layout; ``test_import_surface.py`` guards against it. The
|
||||
one exception is a test that deliberately exercises a single backend.
|
||||
caller to the file layout. The one exception is a test that deliberately
|
||||
exercises a single backend.
|
||||
|
||||
Layout -- ordinary implementations use one subpackage per **operator domain**
|
||||
(``norm``, ``modulate``, ``rope``, ``activation``, ``attention``, ``routing``,
|
||||
|
||||
@@ -1,691 +1,6 @@
|
||||
"""
|
||||
Test runner for multimodal_gen that manages test suites and parallel execution.
|
||||
|
||||
For diffusion 1-gpu/2-gpu suites, cases are partitioned by estimated runtime
|
||||
using LPT so each CI shard has a similar total runtime.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import tabulate
|
||||
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.test.partitioning import PartitionItem, assign_partition
|
||||
from sglang.multimodal_gen.test.runner.pytest_runner import (
|
||||
partition_items_by_index,
|
||||
run_pytest,
|
||||
)
|
||||
from sglang.multimodal_gen.test.server.testcase_configs import (
|
||||
BASELINE_CONFIG,
|
||||
DiffusionTestCase,
|
||||
)
|
||||
|
||||
# TODO: remove duplicated code
|
||||
if current_platform.is_npu():
|
||||
from sglang.multimodal_gen.test.server.ascend.testcase_configs_npu import (
|
||||
_UPDATE_WEIGHTS_FROM_DISK_TEST_FILE,
|
||||
COMPONENT_ACCURACY_SUITES,
|
||||
DEFAULT_EST_TIME_SECONDS,
|
||||
DEFAULT_STANDALONE_EST_TIME_SECONDS,
|
||||
FILE_SUITES,
|
||||
PARAMETRIZED_CASE_GROUPS,
|
||||
STANDALONE_FILE_EST_TIMES,
|
||||
STANDALONE_FILES,
|
||||
STARTUP_OVERHEAD_SECONDS,
|
||||
SUITES,
|
||||
)
|
||||
else:
|
||||
from sglang.multimodal_gen.test.server.gpu_cases import ( # noqa: F401 It is used by ci scripts
|
||||
_UPDATE_WEIGHTS_FROM_DISK_TEST_FILE,
|
||||
_UPDATE_WEIGHTS_MODEL_PAIR_ENV,
|
||||
_UPDATE_WEIGHTS_MODEL_PAIR_IDS,
|
||||
COMPONENT_ACCURACY_FILE_NUM_GPUS,
|
||||
COMPONENT_ACCURACY_SUITES,
|
||||
DEFAULT_EST_TIME_SECONDS,
|
||||
DEFAULT_STANDALONE_EST_TIME_SECONDS,
|
||||
FILE_SUITES,
|
||||
ONE_GPU_CASES,
|
||||
PARAMETRIZED_CASE_GROUPS,
|
||||
STANDALONE_FILE_EST_TIMES,
|
||||
STANDALONE_FILES,
|
||||
STARTUP_OVERHEAD_SECONDS,
|
||||
STRICT_SUITES,
|
||||
SUITES,
|
||||
TWO_GPU_CASES,
|
||||
)
|
||||
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PartitionAssignment:
|
||||
case_ids: list[str]
|
||||
standalone_files: list[str]
|
||||
estimated_time: float | None = None
|
||||
missing_standalone_estimates: list[str] | None = None
|
||||
|
||||
|
||||
def get_case_est_time(case_id: str) -> float:
|
||||
scenario = BASELINE_CONFIG.scenarios.get(case_id)
|
||||
if scenario is None:
|
||||
return DEFAULT_EST_TIME_SECONDS
|
||||
if scenario.estimated_full_test_time_s is not None:
|
||||
return scenario.estimated_full_test_time_s
|
||||
return scenario.expected_e2e_ms / 1000.0 + STARTUP_OVERHEAD_SECONDS
|
||||
|
||||
|
||||
def get_standalone_file_est_time(
|
||||
suite: str, standalone_file: str
|
||||
) -> tuple[float, bool]:
|
||||
suite_est_times = STANDALONE_FILE_EST_TIMES.get(suite, {})
|
||||
if standalone_file not in suite_est_times:
|
||||
return DEFAULT_STANDALONE_EST_TIME_SECONDS, True
|
||||
return suite_est_times[standalone_file], False
|
||||
|
||||
|
||||
def get_all_standalone_file_est_times() -> dict[str, dict[str, float]]:
|
||||
return copy.deepcopy(STANDALONE_FILE_EST_TIMES)
|
||||
|
||||
|
||||
def validate_standalone_file_est_times() -> dict[str, list[str]]:
|
||||
missing_by_suite: dict[str, list[str]] = {}
|
||||
for suite, standalone_files in STANDALONE_FILES.items():
|
||||
suite_est_times = STANDALONE_FILE_EST_TIMES.get(suite, {})
|
||||
missing = [
|
||||
standalone_file
|
||||
for standalone_file in standalone_files
|
||||
if standalone_file not in suite_est_times
|
||||
]
|
||||
if missing:
|
||||
missing_by_suite[suite] = missing
|
||||
return missing_by_suite
|
||||
|
||||
|
||||
def get_suite_files_rel(suite: str, parametrized_only: bool = False) -> list[str]:
|
||||
if parametrized_only and suite in PARAMETRIZED_CASE_GROUPS:
|
||||
return [filename for filename, _ in PARAMETRIZED_CASE_GROUPS[suite]]
|
||||
return SUITES[suite]
|
||||
|
||||
|
||||
def _normalize_standalone_key(standalone_file: str) -> str:
|
||||
return f"standalone:{standalone_file}"
|
||||
|
||||
|
||||
def parse_partition_plan(
|
||||
suite: str,
|
||||
partition_id: int,
|
||||
total_partitions: int,
|
||||
plan_json: str,
|
||||
) -> PartitionAssignment:
|
||||
plan = json.loads(plan_json)
|
||||
if plan.get("suite") != suite:
|
||||
raise ValueError(
|
||||
f"Partition plan suite mismatch: expected {suite!r}, "
|
||||
f"got {plan.get('suite')!r}"
|
||||
)
|
||||
|
||||
partition_count = plan.get("partition_count")
|
||||
if partition_count != total_partitions:
|
||||
raise ValueError(
|
||||
f"Partition count mismatch for suite {suite!r}: "
|
||||
f"plan={partition_count}, matrix={total_partitions}"
|
||||
)
|
||||
|
||||
partitions = plan.get("partitions", [])
|
||||
selected_partition = None
|
||||
for partition in partitions:
|
||||
if partition.get("part") == partition_id:
|
||||
selected_partition = partition
|
||||
break
|
||||
|
||||
if selected_partition is None:
|
||||
raise ValueError(
|
||||
f"Partition {partition_id} not found in plan for suite {suite!r}"
|
||||
)
|
||||
|
||||
return PartitionAssignment(
|
||||
case_ids=list(selected_partition.get("case_ids", [])),
|
||||
standalone_files=list(selected_partition.get("standalone_files", [])),
|
||||
estimated_time=selected_partition.get("estimated_time"),
|
||||
missing_standalone_estimates=list(
|
||||
selected_partition.get("missing_standalone_estimates", [])
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def build_local_partition_assignment(
|
||||
suite: str,
|
||||
partition_id: int,
|
||||
total_partitions: int,
|
||||
) -> PartitionAssignment:
|
||||
"""Assign this shard's work when CI did not precompute a partition plan.
|
||||
|
||||
Lanes with a hardcoded ``--total-partitions`` (the AMD ones) cannot give
|
||||
every standalone file a shard of its own, so standalone files are LPT
|
||||
balanced together with the parametrized cases instead.
|
||||
"""
|
||||
items = [
|
||||
PartitionItem(kind="case", item_id=case.id, est_time=get_case_est_time(case.id))
|
||||
for case in _get_dynamic_suite_cases(suite)
|
||||
]
|
||||
for standalone_file in STANDALONE_FILES.get(suite, []):
|
||||
items.append(
|
||||
PartitionItem(
|
||||
kind="standalone",
|
||||
item_id=standalone_file,
|
||||
est_time=get_standalone_file_est_time(suite, standalone_file)[0],
|
||||
)
|
||||
)
|
||||
|
||||
my_items = assign_partition(items, partition_id, total_partitions)
|
||||
return PartitionAssignment(
|
||||
case_ids=[item.item_id for item in my_items if item.kind == "case"],
|
||||
standalone_files=[
|
||||
item.item_id for item in my_items if item.kind == "standalone"
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _merge_execution_results(
|
||||
executed_cases: list[str],
|
||||
case_results: dict[str, str],
|
||||
new_executed_cases: list[str],
|
||||
new_case_results: dict[str, str],
|
||||
) -> None:
|
||||
executed_cases.extend(
|
||||
case_id for case_id in new_executed_cases if case_id not in executed_cases
|
||||
)
|
||||
case_results.update(new_case_results)
|
||||
|
||||
|
||||
def _format_standalone_estimate_snippet(
|
||||
suite: str, standalone_file: str, measured_full_test_time_s: float
|
||||
) -> str:
|
||||
return (
|
||||
f'"{suite}": {{\n "{standalone_file}": {measured_full_test_time_s:.1f},\n}}'
|
||||
)
|
||||
|
||||
|
||||
def _print_missing_standalone_estimate_message(
|
||||
suite: str,
|
||||
standalone_file: str,
|
||||
measured_full_test_time_s: float,
|
||||
) -> None:
|
||||
snippet = _format_standalone_estimate_snippet(
|
||||
suite, standalone_file, measured_full_test_time_s
|
||||
)
|
||||
logger.error(
|
||||
f"\n{'=' * 60}\n"
|
||||
f'Add standalone estimate for suite "{suite}" and file "{standalone_file}":\n\n'
|
||||
f"File: python/sglang/multimodal_gen/test/run_suite.py\n\n"
|
||||
f"Current partition used fallback estimate: "
|
||||
f"{DEFAULT_STANDALONE_EST_TIME_SECONDS:.1f}s\n\n"
|
||||
f"{snippet}\n"
|
||||
f"{'=' * 60}\n"
|
||||
)
|
||||
|
||||
|
||||
def _run_standalone_file(
|
||||
suite: str,
|
||||
standalone_rel: str,
|
||||
target_dir: Path,
|
||||
extra_filter: str | None = None,
|
||||
) -> tuple[int, list[str], dict[str, str], dict]:
|
||||
if standalone_rel == _UPDATE_WEIGHTS_FROM_DISK_TEST_FILE:
|
||||
_maybe_pin_update_weights_model_pair([standalone_rel])
|
||||
|
||||
est_time, used_fallback_estimate = get_standalone_file_est_time(
|
||||
suite, standalone_rel
|
||||
)
|
||||
standalone_file = _resolve_suite_files(target_dir, [standalone_rel], strict=True)[0]
|
||||
junit_xml_path = str(
|
||||
target_dir / f"junit_results_{suite}_{Path(standalone_rel).stem}.xml"
|
||||
)
|
||||
start_time = time.perf_counter()
|
||||
exit_code, _, _ = run_pytest(
|
||||
[standalone_file],
|
||||
filter_expr=extra_filter,
|
||||
junit_xml_path=junit_xml_path,
|
||||
)
|
||||
measured_full_test_time_s = round(time.perf_counter() - start_time, 1)
|
||||
standalone_key = _normalize_standalone_key(standalone_rel)
|
||||
measurement = {
|
||||
"suite": suite,
|
||||
"standalone_file": standalone_rel,
|
||||
"measured_full_test_time_s": measured_full_test_time_s,
|
||||
"used_fallback_estimate": used_fallback_estimate,
|
||||
"fallback_estimate_s": DEFAULT_STANDALONE_EST_TIME_SECONDS,
|
||||
"had_configured_estimate": not used_fallback_estimate,
|
||||
"configured_or_fallback_estimate_s": est_time,
|
||||
}
|
||||
if used_fallback_estimate:
|
||||
_print_missing_standalone_estimate_message(
|
||||
suite, standalone_rel, measured_full_test_time_s
|
||||
)
|
||||
return (
|
||||
exit_code,
|
||||
[standalone_key],
|
||||
{standalone_key: "pass" if exit_code == 0 else "fail"},
|
||||
measurement,
|
||||
)
|
||||
|
||||
|
||||
def parse_args():
|
||||
suite_choices = sorted(set(FILE_SUITES) | set(PARAMETRIZED_CASE_GROUPS))
|
||||
parser = argparse.ArgumentParser(description="Run multimodal_gen test suite")
|
||||
parser.add_argument(
|
||||
"--suite",
|
||||
type=str,
|
||||
required=True,
|
||||
choices=suite_choices,
|
||||
help="The test suite to run.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--partition-id",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Index of the current partition (for parallel execution)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--total-partitions",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Total number of partitions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-dir",
|
||||
type=str,
|
||||
default="server",
|
||||
help="Base directory for tests relative to this script's parent",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--filter",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Pytest filter expression (passed to pytest -k)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--continue-on-error",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Continue running remaining tests even if one fails.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--partition-plan-json",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Full partition plan JSON for the current suite.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def write_execution_report(
|
||||
suite: str,
|
||||
partition_id: int,
|
||||
total_partitions: int,
|
||||
executed_cases: list[str],
|
||||
is_standalone: bool = False,
|
||||
standalone_file: str | None = None,
|
||||
case_results: dict[str, str] | None = None,
|
||||
missing_standalone_estimates: list[str] | None = None,
|
||||
standalone_measurements: list[dict] | None = None,
|
||||
) -> str:
|
||||
report = {
|
||||
"suite": suite,
|
||||
"partition_id": partition_id,
|
||||
"total_partitions": total_partitions,
|
||||
"is_standalone": is_standalone,
|
||||
"standalone_file": standalone_file,
|
||||
"executed_cases": executed_cases,
|
||||
"case_results": case_results or {},
|
||||
"missing_standalone_estimates": missing_standalone_estimates or [],
|
||||
"standalone_measurements": standalone_measurements or [],
|
||||
}
|
||||
|
||||
report_filename = f"execution_report_{suite}_{partition_id}.json"
|
||||
report_path = Path(__file__).parent / report_filename
|
||||
with open(report_path, "w", encoding="utf-8") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
logger.info("Execution report written to: %s", report_path)
|
||||
return str(report_path)
|
||||
|
||||
|
||||
def run_component_accuracy_files(files, filter_expr=None, continue_on_error=False):
|
||||
exit_code = 0
|
||||
for file_path in files:
|
||||
file_name = Path(file_path).name
|
||||
num_gpus = COMPONENT_ACCURACY_FILE_NUM_GPUS.get(file_name, 1)
|
||||
if num_gpus > 1:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"torch.distributed.run",
|
||||
f"--nproc_per_node={num_gpus}",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-s",
|
||||
"-v",
|
||||
]
|
||||
else:
|
||||
cmd = [sys.executable, "-m", "pytest", "-s", "-v"]
|
||||
|
||||
if filter_expr:
|
||||
cmd.extend(["-k", filter_expr])
|
||||
cmd.append(file_path)
|
||||
|
||||
print(f"Running command: {' '.join(cmd)}")
|
||||
file_exit_code = subprocess.call(cmd)
|
||||
if file_exit_code == 5:
|
||||
print(
|
||||
"No tests collected (exit code 5). This is expected when filters "
|
||||
"deselect all tests in a file. Treating as success."
|
||||
)
|
||||
file_exit_code = 0
|
||||
if file_exit_code != 0 and exit_code == 0:
|
||||
exit_code = file_exit_code
|
||||
if file_exit_code != 0 and not continue_on_error:
|
||||
return file_exit_code
|
||||
return exit_code
|
||||
|
||||
|
||||
def _is_in_ci() -> bool:
|
||||
return os.environ.get("SGLANG_IS_IN_CI", "").lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def _maybe_pin_update_weights_model_pair(suite_files_rel: list[str]) -> None:
|
||||
if not _is_in_ci():
|
||||
return
|
||||
if _UPDATE_WEIGHTS_FROM_DISK_TEST_FILE not in suite_files_rel:
|
||||
return
|
||||
if os.environ.get(_UPDATE_WEIGHTS_MODEL_PAIR_ENV):
|
||||
print(
|
||||
f"Using preset {_UPDATE_WEIGHTS_MODEL_PAIR_ENV}="
|
||||
f"{os.environ[_UPDATE_WEIGHTS_MODEL_PAIR_ENV]}"
|
||||
)
|
||||
return
|
||||
|
||||
selected_pair = random.choice(_UPDATE_WEIGHTS_MODEL_PAIR_IDS)
|
||||
os.environ[_UPDATE_WEIGHTS_MODEL_PAIR_ENV] = selected_pair
|
||||
print(f"Selected {_UPDATE_WEIGHTS_MODEL_PAIR_ENV}={selected_pair} for this CI run")
|
||||
|
||||
|
||||
def _resolve_suite_files(
|
||||
target_dir: Path, suite_files_rel: list[str], strict: bool
|
||||
) -> list[str]:
|
||||
suite_files_abs = []
|
||||
for f_rel in suite_files_rel:
|
||||
f_abs = target_dir / f_rel
|
||||
if not f_abs.exists():
|
||||
msg = f"Test file {f_rel} not found in {target_dir}."
|
||||
if strict:
|
||||
print(f"Error: {msg}")
|
||||
sys.exit(1)
|
||||
print(f"Warning: {msg} Skipping.")
|
||||
continue
|
||||
suite_files_abs.append(str(f_abs))
|
||||
return suite_files_abs
|
||||
|
||||
|
||||
def _run_file_suite(args, target_dir: Path) -> int:
|
||||
suite_files_rel = FILE_SUITES[args.suite]
|
||||
_maybe_pin_update_weights_model_pair(suite_files_rel)
|
||||
suite_files_abs = _resolve_suite_files(
|
||||
target_dir, suite_files_rel, args.suite in STRICT_SUITES
|
||||
)
|
||||
|
||||
if not suite_files_abs:
|
||||
print(f"No valid test files found for suite '{args.suite}'.")
|
||||
return 1 if args.suite in STRICT_SUITES else 0
|
||||
|
||||
exit_code, _, _ = run_pytest(
|
||||
suite_files_abs,
|
||||
filter_expr=args.filter,
|
||||
junit_xml_path=None,
|
||||
)
|
||||
return exit_code
|
||||
|
||||
|
||||
def _get_dynamic_suite_cases(suite: str) -> list[DiffusionTestCase]:
|
||||
cases = []
|
||||
for _, case_group in PARAMETRIZED_CASE_GROUPS[suite]:
|
||||
cases.extend(case_group)
|
||||
return cases
|
||||
|
||||
|
||||
def _get_parametrized_files_for_case_ids(
|
||||
suite: str, case_ids: set[str], target_dir: Path
|
||||
) -> list[str]:
|
||||
files = []
|
||||
for filename, case_group in PARAMETRIZED_CASE_GROUPS[suite]:
|
||||
if any(case.id in case_ids for case in case_group):
|
||||
file_path = target_dir / filename
|
||||
if file_path.exists():
|
||||
files.append(str(file_path))
|
||||
else:
|
||||
logger.warning("Test file %s not found in %s", filename, target_dir)
|
||||
return files
|
||||
|
||||
|
||||
def _run_dynamic_suite(args, target_dir: Path) -> int:
|
||||
if args.partition_plan_json:
|
||||
assignment = parse_partition_plan(
|
||||
suite=args.suite,
|
||||
partition_id=args.partition_id,
|
||||
total_partitions=args.total_partitions,
|
||||
plan_json=args.partition_plan_json,
|
||||
)
|
||||
else:
|
||||
assignment = build_local_partition_assignment(
|
||||
suite=args.suite,
|
||||
partition_id=args.partition_id,
|
||||
total_partitions=args.total_partitions,
|
||||
)
|
||||
return _run_partition_assignment(args, target_dir, assignment)
|
||||
|
||||
|
||||
def _run_partition_assignment(
|
||||
args, target_dir: Path, assignment: PartitionAssignment
|
||||
) -> int:
|
||||
rows = [[args.suite, f"{args.partition_id + 1}/{args.total_partitions}"]]
|
||||
print(tabulate.tabulate(rows, headers=["Suite", "Partition"], tablefmt="psql"))
|
||||
|
||||
total_est_time = 0.0
|
||||
executed_cases: list[str] = []
|
||||
case_results: dict[str, str] = {}
|
||||
missing_standalone_estimates: list[str] = []
|
||||
standalone_measurements: list[dict] = []
|
||||
overall_exit_code = 0
|
||||
|
||||
if assignment.case_ids:
|
||||
case_id_set = set(assignment.case_ids)
|
||||
total_est_time += sum(
|
||||
get_case_est_time(case_id) for case_id in assignment.case_ids
|
||||
)
|
||||
suite_files = _get_parametrized_files_for_case_ids(
|
||||
args.suite, case_id_set, target_dir
|
||||
)
|
||||
if not suite_files:
|
||||
print(f"No valid parametrized test files found for suite '{args.suite}'.")
|
||||
return 0
|
||||
|
||||
partition_filter = " or ".join(
|
||||
f"[{case_id}]" for case_id in assignment.case_ids
|
||||
)
|
||||
filter_expr = (
|
||||
f"({partition_filter}) and ({args.filter})"
|
||||
if args.filter
|
||||
else partition_filter
|
||||
)
|
||||
|
||||
print(
|
||||
f"Running {len(assignment.case_ids)} parametrized cases with estimated total "
|
||||
f"{sum(get_case_est_time(case_id) for case_id in assignment.case_ids):.1f}s:"
|
||||
)
|
||||
for case_id in assignment.case_ids:
|
||||
print(f" - case: {case_id} ({get_case_est_time(case_id):.1f}s)")
|
||||
print(f"Test files: {[Path(f).name for f in suite_files]}")
|
||||
print(f"Filter expression: {filter_expr}")
|
||||
|
||||
junit_xml_path = str(
|
||||
target_dir / f"junit_results_{args.suite}_{args.partition_id}.xml"
|
||||
)
|
||||
exit_code, new_executed_cases, new_case_results = run_pytest(
|
||||
suite_files,
|
||||
filter_expr=filter_expr,
|
||||
junit_xml_path=junit_xml_path,
|
||||
)
|
||||
_merge_execution_results(
|
||||
executed_cases, case_results, new_executed_cases, new_case_results
|
||||
)
|
||||
# A failing case must not swallow this shard's standalone files: they
|
||||
# are separate pytest runs, and they only share a shard because the
|
||||
# shard count is fixed. --continue-on-error still decides whether a
|
||||
# failing standalone file stops the ones queued behind it.
|
||||
if exit_code != 0 and overall_exit_code == 0:
|
||||
overall_exit_code = exit_code
|
||||
|
||||
if assignment.standalone_files:
|
||||
standalone_estimate = sum(
|
||||
get_standalone_file_est_time(args.suite, standalone_file)[0]
|
||||
for standalone_file in assignment.standalone_files
|
||||
)
|
||||
total_est_time += standalone_estimate
|
||||
print(
|
||||
f"Running {len(assignment.standalone_files)} standalone file(s) with estimated total "
|
||||
f"{standalone_estimate:.1f}s:"
|
||||
)
|
||||
for standalone_file in assignment.standalone_files:
|
||||
est_time, used_fallback_estimate = get_standalone_file_est_time(
|
||||
args.suite, standalone_file
|
||||
)
|
||||
fallback_suffix = (
|
||||
f", fallback estimate {DEFAULT_STANDALONE_EST_TIME_SECONDS:.1f}s"
|
||||
if used_fallback_estimate
|
||||
else ""
|
||||
)
|
||||
print(
|
||||
f" - standalone: {standalone_file} ({est_time:.1f}s{fallback_suffix})"
|
||||
)
|
||||
|
||||
for standalone_file in assignment.standalone_files:
|
||||
exit_code, new_executed_cases, new_case_results, measurement = (
|
||||
_run_standalone_file(
|
||||
args.suite,
|
||||
standalone_file,
|
||||
target_dir,
|
||||
extra_filter=args.filter,
|
||||
)
|
||||
)
|
||||
if measurement["used_fallback_estimate"]:
|
||||
missing_standalone_estimates.append(standalone_file)
|
||||
standalone_measurements.append(measurement)
|
||||
_merge_execution_results(
|
||||
executed_cases,
|
||||
case_results,
|
||||
new_executed_cases,
|
||||
new_case_results,
|
||||
)
|
||||
if exit_code != 0 and overall_exit_code == 0:
|
||||
overall_exit_code = exit_code
|
||||
if exit_code != 0 and not args.continue_on_error:
|
||||
break
|
||||
|
||||
if not assignment.case_ids and not assignment.standalone_files:
|
||||
print(f"No work assigned to partition {args.partition_id}. Exiting success.")
|
||||
|
||||
print(f"Partition estimated total time: {total_est_time:.1f}s")
|
||||
write_execution_report(
|
||||
suite=args.suite,
|
||||
partition_id=args.partition_id,
|
||||
total_partitions=args.total_partitions,
|
||||
executed_cases=executed_cases,
|
||||
is_standalone=False,
|
||||
standalone_file=None,
|
||||
case_results=case_results,
|
||||
missing_standalone_estimates=missing_standalone_estimates,
|
||||
standalone_measurements=standalone_measurements,
|
||||
)
|
||||
return overall_exit_code
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
validate_standalone_file_est_times()
|
||||
test_root_dir = Path(__file__).resolve().parent
|
||||
target_dir = test_root_dir / args.base_dir
|
||||
|
||||
if not target_dir.exists():
|
||||
print(f"Error: Target directory {target_dir} does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
if args.suite in COMPONENT_ACCURACY_SUITES:
|
||||
suite_files_rel = FILE_SUITES[args.suite]
|
||||
suite_files_abs = _resolve_suite_files(
|
||||
target_dir, suite_files_rel, args.suite in STRICT_SUITES
|
||||
)
|
||||
|
||||
if not suite_files_abs:
|
||||
print(f"No valid test files found for suite '{args.suite}'.")
|
||||
sys.exit(1 if args.suite in STRICT_SUITES else 0)
|
||||
|
||||
my_files = partition_items_by_index(
|
||||
suite_files_abs, args.partition_id, args.total_partitions
|
||||
)
|
||||
partition_info = (
|
||||
f"{args.partition_id + 1}/{args.total_partitions} "
|
||||
f"(0-based id={args.partition_id})"
|
||||
)
|
||||
headers = ["Suite", "Partition"]
|
||||
rows = [[args.suite, partition_info]]
|
||||
msg = tabulate.tabulate(rows, headers=headers, tablefmt="psql") + "\n"
|
||||
msg += f"Enabled {len(my_files)} file(s):\n"
|
||||
for file_path in my_files:
|
||||
msg += f" - {file_path}\n"
|
||||
print(msg, flush=True)
|
||||
print(
|
||||
f"Suite: {args.suite} | Partition: {args.partition_id}/{args.total_partitions}"
|
||||
)
|
||||
print(f"Selected {len(suite_files_abs)} files:")
|
||||
for f in suite_files_abs:
|
||||
print(f" - {os.path.basename(f)}")
|
||||
|
||||
if not my_files:
|
||||
print("No files assigned to this partition. Exiting success.")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Running {len(my_files)} files in this shard: {', '.join(my_files)}")
|
||||
|
||||
exit_code = run_component_accuracy_files(
|
||||
my_files,
|
||||
filter_expr=args.filter,
|
||||
continue_on_error=args.continue_on_error,
|
||||
)
|
||||
|
||||
msg = "\n" + tabulate.tabulate(rows, headers=headers, tablefmt="psql") + "\n"
|
||||
msg += f"Executed {len(my_files)} file(s):\n"
|
||||
for file_path in my_files:
|
||||
msg += f" - {file_path}\n"
|
||||
print(msg, flush=True)
|
||||
elif args.suite in PARAMETRIZED_CASE_GROUPS:
|
||||
exit_code = _run_dynamic_suite(args, target_dir)
|
||||
else:
|
||||
exit_code = _run_file_suite(args, target_dir)
|
||||
|
||||
sys.exit(exit_code)
|
||||
"""Compatibility entry point; CI dispatches diffusion via ``test/run_suite.py``."""
|
||||
|
||||
from sglang.multimodal_gen.test.runner.diffusion_suite_runner import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
"""Internal diffusion-suite adapter used by ``test/run_suite.py`` bridges.
|
||||
|
||||
For diffusion 1-gpu/2-gpu suites, cases are partitioned by estimated runtime
|
||||
using LPT so each CI shard has a similar total runtime.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import tabulate
|
||||
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.test.partitioning import PartitionItem, assign_partition
|
||||
from sglang.multimodal_gen.test.runner.pytest_runner import (
|
||||
partition_items_by_index,
|
||||
run_pytest,
|
||||
)
|
||||
from sglang.multimodal_gen.test.server.testcase_configs import (
|
||||
BASELINE_CONFIG,
|
||||
DiffusionTestCase,
|
||||
)
|
||||
|
||||
# TODO: remove duplicated code
|
||||
if current_platform.is_npu():
|
||||
from sglang.multimodal_gen.test.server.ascend.testcase_configs_npu import (
|
||||
_UPDATE_WEIGHTS_FROM_DISK_TEST_FILE,
|
||||
COMPONENT_ACCURACY_SUITES,
|
||||
DEFAULT_EST_TIME_SECONDS,
|
||||
DEFAULT_STANDALONE_EST_TIME_SECONDS,
|
||||
FILE_SUITES,
|
||||
PARAMETRIZED_CASE_GROUPS,
|
||||
STANDALONE_FILE_EST_TIMES,
|
||||
STANDALONE_FILES,
|
||||
STARTUP_OVERHEAD_SECONDS,
|
||||
SUITES,
|
||||
)
|
||||
else:
|
||||
from sglang.multimodal_gen.test.server.gpu_cases import ( # noqa: F401 It is used by ci scripts
|
||||
_UPDATE_WEIGHTS_FROM_DISK_TEST_FILE,
|
||||
_UPDATE_WEIGHTS_MODEL_PAIR_ENV,
|
||||
_UPDATE_WEIGHTS_MODEL_PAIR_IDS,
|
||||
COMPONENT_ACCURACY_FILE_NUM_GPUS,
|
||||
COMPONENT_ACCURACY_SUITES,
|
||||
DEFAULT_EST_TIME_SECONDS,
|
||||
DEFAULT_STANDALONE_EST_TIME_SECONDS,
|
||||
FILE_SUITES,
|
||||
ONE_GPU_CASES,
|
||||
PARAMETRIZED_CASE_GROUPS,
|
||||
STANDALONE_FILE_EST_TIMES,
|
||||
STANDALONE_FILES,
|
||||
STARTUP_OVERHEAD_SECONDS,
|
||||
STRICT_SUITES,
|
||||
SUITES,
|
||||
TWO_GPU_CASES,
|
||||
)
|
||||
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PartitionAssignment:
|
||||
case_ids: list[str]
|
||||
standalone_files: list[str]
|
||||
estimated_time: float | None = None
|
||||
missing_standalone_estimates: list[str] | None = None
|
||||
|
||||
|
||||
def get_case_est_time(case_id: str) -> float:
|
||||
scenario = BASELINE_CONFIG.scenarios.get(case_id)
|
||||
if scenario is None:
|
||||
return DEFAULT_EST_TIME_SECONDS
|
||||
if scenario.estimated_full_test_time_s is not None:
|
||||
return scenario.estimated_full_test_time_s
|
||||
return scenario.expected_e2e_ms / 1000.0 + STARTUP_OVERHEAD_SECONDS
|
||||
|
||||
|
||||
def get_standalone_file_est_time(
|
||||
suite: str, standalone_file: str
|
||||
) -> tuple[float, bool]:
|
||||
suite_est_times = STANDALONE_FILE_EST_TIMES.get(suite, {})
|
||||
if standalone_file not in suite_est_times:
|
||||
return DEFAULT_STANDALONE_EST_TIME_SECONDS, True
|
||||
return suite_est_times[standalone_file], False
|
||||
|
||||
|
||||
def get_all_standalone_file_est_times() -> dict[str, dict[str, float]]:
|
||||
return copy.deepcopy(STANDALONE_FILE_EST_TIMES)
|
||||
|
||||
|
||||
def validate_standalone_file_est_times() -> dict[str, list[str]]:
|
||||
missing_by_suite: dict[str, list[str]] = {}
|
||||
for suite, standalone_files in STANDALONE_FILES.items():
|
||||
suite_est_times = STANDALONE_FILE_EST_TIMES.get(suite, {})
|
||||
missing = [
|
||||
standalone_file
|
||||
for standalone_file in standalone_files
|
||||
if standalone_file not in suite_est_times
|
||||
]
|
||||
if missing:
|
||||
missing_by_suite[suite] = missing
|
||||
return missing_by_suite
|
||||
|
||||
|
||||
def get_suite_files_rel(suite: str, parametrized_only: bool = False) -> list[str]:
|
||||
if parametrized_only and suite in PARAMETRIZED_CASE_GROUPS:
|
||||
return [filename for filename, _ in PARAMETRIZED_CASE_GROUPS[suite]]
|
||||
return SUITES[suite]
|
||||
|
||||
|
||||
def _normalize_standalone_key(standalone_file: str) -> str:
|
||||
return f"standalone:{standalone_file}"
|
||||
|
||||
|
||||
def parse_partition_plan(
|
||||
suite: str,
|
||||
partition_id: int,
|
||||
total_partitions: int,
|
||||
plan_json: str,
|
||||
) -> PartitionAssignment:
|
||||
plan = json.loads(plan_json)
|
||||
if plan.get("suite") != suite:
|
||||
raise ValueError(
|
||||
f"Partition plan suite mismatch: expected {suite!r}, "
|
||||
f"got {plan.get('suite')!r}"
|
||||
)
|
||||
|
||||
partition_count = plan.get("partition_count")
|
||||
if partition_count != total_partitions:
|
||||
raise ValueError(
|
||||
f"Partition count mismatch for suite {suite!r}: "
|
||||
f"plan={partition_count}, matrix={total_partitions}"
|
||||
)
|
||||
|
||||
partitions = plan.get("partitions", [])
|
||||
selected_partition = None
|
||||
for partition in partitions:
|
||||
if partition.get("part") == partition_id:
|
||||
selected_partition = partition
|
||||
break
|
||||
|
||||
if selected_partition is None:
|
||||
raise ValueError(
|
||||
f"Partition {partition_id} not found in plan for suite {suite!r}"
|
||||
)
|
||||
|
||||
return PartitionAssignment(
|
||||
case_ids=list(selected_partition.get("case_ids", [])),
|
||||
standalone_files=list(selected_partition.get("standalone_files", [])),
|
||||
estimated_time=selected_partition.get("estimated_time"),
|
||||
missing_standalone_estimates=list(
|
||||
selected_partition.get("missing_standalone_estimates", [])
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def build_local_partition_assignment(
|
||||
suite: str,
|
||||
partition_id: int,
|
||||
total_partitions: int,
|
||||
) -> PartitionAssignment:
|
||||
"""Assign this shard's work when CI did not precompute a partition plan.
|
||||
|
||||
Lanes with a hardcoded ``--total-partitions`` (the AMD ones) cannot give
|
||||
every standalone file a shard of its own, so standalone files are LPT
|
||||
balanced together with the parametrized cases instead.
|
||||
"""
|
||||
items = [
|
||||
PartitionItem(kind="case", item_id=case.id, est_time=get_case_est_time(case.id))
|
||||
for case in _get_dynamic_suite_cases(suite)
|
||||
]
|
||||
for standalone_file in STANDALONE_FILES.get(suite, []):
|
||||
items.append(
|
||||
PartitionItem(
|
||||
kind="standalone",
|
||||
item_id=standalone_file,
|
||||
est_time=get_standalone_file_est_time(suite, standalone_file)[0],
|
||||
)
|
||||
)
|
||||
|
||||
my_items = assign_partition(items, partition_id, total_partitions)
|
||||
return PartitionAssignment(
|
||||
case_ids=[item.item_id for item in my_items if item.kind == "case"],
|
||||
standalone_files=[
|
||||
item.item_id for item in my_items if item.kind == "standalone"
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _merge_execution_results(
|
||||
executed_cases: list[str],
|
||||
case_results: dict[str, str],
|
||||
new_executed_cases: list[str],
|
||||
new_case_results: dict[str, str],
|
||||
) -> None:
|
||||
executed_cases.extend(
|
||||
case_id for case_id in new_executed_cases if case_id not in executed_cases
|
||||
)
|
||||
case_results.update(new_case_results)
|
||||
|
||||
|
||||
def _format_standalone_estimate_snippet(
|
||||
suite: str, standalone_file: str, measured_full_test_time_s: float
|
||||
) -> str:
|
||||
return (
|
||||
f'"{suite}": {{\n "{standalone_file}": {measured_full_test_time_s:.1f},\n}}'
|
||||
)
|
||||
|
||||
|
||||
def _print_missing_standalone_estimate_message(
|
||||
suite: str,
|
||||
standalone_file: str,
|
||||
measured_full_test_time_s: float,
|
||||
) -> None:
|
||||
snippet = _format_standalone_estimate_snippet(
|
||||
suite, standalone_file, measured_full_test_time_s
|
||||
)
|
||||
logger.error(
|
||||
f"\n{'=' * 60}\n"
|
||||
f'Add standalone estimate for suite "{suite}" and file "{standalone_file}":\n\n'
|
||||
"File: python/sglang/multimodal_gen/test/server/gpu_cases.py\n\n"
|
||||
f"Current partition used fallback estimate: "
|
||||
f"{DEFAULT_STANDALONE_EST_TIME_SECONDS:.1f}s\n\n"
|
||||
f"{snippet}\n"
|
||||
f"{'=' * 60}\n"
|
||||
)
|
||||
|
||||
|
||||
def _run_standalone_file(
|
||||
suite: str,
|
||||
standalone_rel: str,
|
||||
target_dir: Path,
|
||||
extra_filter: str | None = None,
|
||||
) -> tuple[int, list[str], dict[str, str], dict]:
|
||||
if standalone_rel == _UPDATE_WEIGHTS_FROM_DISK_TEST_FILE:
|
||||
_maybe_pin_update_weights_model_pair([standalone_rel])
|
||||
|
||||
est_time, used_fallback_estimate = get_standalone_file_est_time(
|
||||
suite, standalone_rel
|
||||
)
|
||||
standalone_file = _resolve_suite_files(target_dir, [standalone_rel], strict=True)[0]
|
||||
junit_xml_path = str(
|
||||
target_dir / f"junit_results_{suite}_{Path(standalone_rel).stem}.xml"
|
||||
)
|
||||
start_time = time.perf_counter()
|
||||
exit_code, _, _ = run_pytest(
|
||||
[standalone_file],
|
||||
filter_expr=extra_filter,
|
||||
junit_xml_path=junit_xml_path,
|
||||
)
|
||||
measured_full_test_time_s = round(time.perf_counter() - start_time, 1)
|
||||
standalone_key = _normalize_standalone_key(standalone_rel)
|
||||
measurement = {
|
||||
"suite": suite,
|
||||
"standalone_file": standalone_rel,
|
||||
"measured_full_test_time_s": measured_full_test_time_s,
|
||||
"used_fallback_estimate": used_fallback_estimate,
|
||||
"fallback_estimate_s": DEFAULT_STANDALONE_EST_TIME_SECONDS,
|
||||
"had_configured_estimate": not used_fallback_estimate,
|
||||
"configured_or_fallback_estimate_s": est_time,
|
||||
}
|
||||
if used_fallback_estimate:
|
||||
_print_missing_standalone_estimate_message(
|
||||
suite, standalone_rel, measured_full_test_time_s
|
||||
)
|
||||
return (
|
||||
exit_code,
|
||||
[standalone_key],
|
||||
{standalone_key: "pass" if exit_code == 0 else "fail"},
|
||||
measurement,
|
||||
)
|
||||
|
||||
|
||||
def parse_args():
|
||||
suite_choices = sorted(set(FILE_SUITES) | set(PARAMETRIZED_CASE_GROUPS))
|
||||
parser = argparse.ArgumentParser(description="Run multimodal_gen test suite")
|
||||
parser.add_argument(
|
||||
"--suite",
|
||||
type=str,
|
||||
required=True,
|
||||
choices=suite_choices,
|
||||
help="The test suite to run.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--partition-id",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Index of the current partition (for parallel execution)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--total-partitions",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Total number of partitions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-dir",
|
||||
type=str,
|
||||
default="server",
|
||||
help="Base directory for tests relative to this script's parent",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
"--filter",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Pytest filter expression (passed to pytest -k)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--continue-on-error",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Continue running remaining tests even if one fails.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--partition-plan-json",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Full partition plan JSON for the current suite.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def write_execution_report(
|
||||
suite: str,
|
||||
partition_id: int,
|
||||
total_partitions: int,
|
||||
executed_cases: list[str],
|
||||
is_standalone: bool = False,
|
||||
standalone_file: str | None = None,
|
||||
case_results: dict[str, str] | None = None,
|
||||
missing_standalone_estimates: list[str] | None = None,
|
||||
standalone_measurements: list[dict] | None = None,
|
||||
) -> str:
|
||||
report = {
|
||||
"suite": suite,
|
||||
"partition_id": partition_id,
|
||||
"total_partitions": total_partitions,
|
||||
"is_standalone": is_standalone,
|
||||
"standalone_file": standalone_file,
|
||||
"executed_cases": executed_cases,
|
||||
"case_results": case_results or {},
|
||||
"missing_standalone_estimates": missing_standalone_estimates or [],
|
||||
"standalone_measurements": standalone_measurements or [],
|
||||
}
|
||||
|
||||
report_filename = f"execution_report_{suite}_{partition_id}.json"
|
||||
report_path = Path(__file__).resolve().parents[1] / report_filename
|
||||
with open(report_path, "w", encoding="utf-8") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
logger.info("Execution report written to: %s", report_path)
|
||||
return str(report_path)
|
||||
|
||||
|
||||
def run_component_accuracy_files(files, filter_expr=None, continue_on_error=False):
|
||||
exit_code = 0
|
||||
for file_path in files:
|
||||
file_name = Path(file_path).name
|
||||
num_gpus = COMPONENT_ACCURACY_FILE_NUM_GPUS.get(file_name, 1)
|
||||
if num_gpus > 1:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"torch.distributed.run",
|
||||
f"--nproc_per_node={num_gpus}",
|
||||
"-m",
|
||||
"pytest",
|
||||
"-s",
|
||||
"-v",
|
||||
]
|
||||
else:
|
||||
cmd = [sys.executable, "-m", "pytest", "-s", "-v"]
|
||||
|
||||
if filter_expr:
|
||||
cmd.extend(["-k", filter_expr])
|
||||
cmd.append(file_path)
|
||||
|
||||
print(f"Running command: {' '.join(cmd)}")
|
||||
file_exit_code = subprocess.call(cmd)
|
||||
if file_exit_code == 5:
|
||||
print(
|
||||
"No tests collected (exit code 5). This is expected when filters "
|
||||
"deselect all tests in a file. Treating as success."
|
||||
)
|
||||
file_exit_code = 0
|
||||
if file_exit_code != 0 and exit_code == 0:
|
||||
exit_code = file_exit_code
|
||||
if file_exit_code != 0 and not continue_on_error:
|
||||
return file_exit_code
|
||||
return exit_code
|
||||
|
||||
|
||||
def _is_in_ci() -> bool:
|
||||
return os.environ.get("SGLANG_IS_IN_CI", "").lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def _maybe_pin_update_weights_model_pair(suite_files_rel: list[str]) -> None:
|
||||
if not _is_in_ci():
|
||||
return
|
||||
if _UPDATE_WEIGHTS_FROM_DISK_TEST_FILE not in suite_files_rel:
|
||||
return
|
||||
if os.environ.get(_UPDATE_WEIGHTS_MODEL_PAIR_ENV):
|
||||
print(
|
||||
f"Using preset {_UPDATE_WEIGHTS_MODEL_PAIR_ENV}="
|
||||
f"{os.environ[_UPDATE_WEIGHTS_MODEL_PAIR_ENV]}"
|
||||
)
|
||||
return
|
||||
|
||||
selected_pair = random.choice(_UPDATE_WEIGHTS_MODEL_PAIR_IDS)
|
||||
os.environ[_UPDATE_WEIGHTS_MODEL_PAIR_ENV] = selected_pair
|
||||
print(f"Selected {_UPDATE_WEIGHTS_MODEL_PAIR_ENV}={selected_pair} for this CI run")
|
||||
|
||||
|
||||
def _resolve_suite_files(
|
||||
target_dir: Path, suite_files_rel: list[str], strict: bool
|
||||
) -> list[str]:
|
||||
suite_files_abs = []
|
||||
for f_rel in suite_files_rel:
|
||||
f_abs = target_dir / f_rel
|
||||
if not f_abs.exists():
|
||||
msg = f"Test file {f_rel} not found in {target_dir}."
|
||||
if strict:
|
||||
print(f"Error: {msg}")
|
||||
sys.exit(1)
|
||||
print(f"Warning: {msg} Skipping.")
|
||||
continue
|
||||
suite_files_abs.append(str(f_abs))
|
||||
return suite_files_abs
|
||||
|
||||
|
||||
def _run_file_suite(args, target_dir: Path) -> int:
|
||||
suite_files_rel = FILE_SUITES[args.suite]
|
||||
_maybe_pin_update_weights_model_pair(suite_files_rel)
|
||||
suite_files_abs = _resolve_suite_files(
|
||||
target_dir, suite_files_rel, args.suite in STRICT_SUITES
|
||||
)
|
||||
|
||||
if not suite_files_abs:
|
||||
print(f"No valid test files found for suite '{args.suite}'.")
|
||||
return 1 if args.suite in STRICT_SUITES else 0
|
||||
|
||||
exit_code, _, _ = run_pytest(
|
||||
suite_files_abs,
|
||||
filter_expr=args.filter,
|
||||
junit_xml_path=None,
|
||||
)
|
||||
return exit_code
|
||||
|
||||
|
||||
def _get_dynamic_suite_cases(suite: str) -> list[DiffusionTestCase]:
|
||||
cases = []
|
||||
for _, case_group in PARAMETRIZED_CASE_GROUPS[suite]:
|
||||
cases.extend(case_group)
|
||||
return cases
|
||||
|
||||
|
||||
def _get_parametrized_files_for_case_ids(
|
||||
suite: str, case_ids: set[str], target_dir: Path
|
||||
) -> list[str]:
|
||||
files = []
|
||||
for filename, case_group in PARAMETRIZED_CASE_GROUPS[suite]:
|
||||
if any(case.id in case_ids for case in case_group):
|
||||
file_path = target_dir / filename
|
||||
if file_path.exists():
|
||||
files.append(str(file_path))
|
||||
else:
|
||||
logger.warning("Test file %s not found in %s", filename, target_dir)
|
||||
return files
|
||||
|
||||
|
||||
def _run_dynamic_suite(args, target_dir: Path) -> int:
|
||||
if args.partition_plan_json:
|
||||
assignment = parse_partition_plan(
|
||||
suite=args.suite,
|
||||
partition_id=args.partition_id,
|
||||
total_partitions=args.total_partitions,
|
||||
plan_json=args.partition_plan_json,
|
||||
)
|
||||
else:
|
||||
assignment = build_local_partition_assignment(
|
||||
suite=args.suite,
|
||||
partition_id=args.partition_id,
|
||||
total_partitions=args.total_partitions,
|
||||
)
|
||||
return _run_partition_assignment(args, target_dir, assignment)
|
||||
|
||||
|
||||
def _run_partition_assignment(
|
||||
args, target_dir: Path, assignment: PartitionAssignment
|
||||
) -> int:
|
||||
rows = [[args.suite, f"{args.partition_id + 1}/{args.total_partitions}"]]
|
||||
print(tabulate.tabulate(rows, headers=["Suite", "Partition"], tablefmt="psql"))
|
||||
|
||||
total_est_time = 0.0
|
||||
executed_cases: list[str] = []
|
||||
case_results: dict[str, str] = {}
|
||||
missing_standalone_estimates: list[str] = []
|
||||
standalone_measurements: list[dict] = []
|
||||
overall_exit_code = 0
|
||||
|
||||
if assignment.case_ids:
|
||||
case_id_set = set(assignment.case_ids)
|
||||
total_est_time += sum(
|
||||
get_case_est_time(case_id) for case_id in assignment.case_ids
|
||||
)
|
||||
suite_files = _get_parametrized_files_for_case_ids(
|
||||
args.suite, case_id_set, target_dir
|
||||
)
|
||||
if not suite_files:
|
||||
print(f"No valid parametrized test files found for suite '{args.suite}'.")
|
||||
return 0
|
||||
|
||||
partition_filter = " or ".join(
|
||||
f"[{case_id}]" for case_id in assignment.case_ids
|
||||
)
|
||||
filter_expr = (
|
||||
f"({partition_filter}) and ({args.filter})"
|
||||
if args.filter
|
||||
else partition_filter
|
||||
)
|
||||
|
||||
print(
|
||||
f"Running {len(assignment.case_ids)} parametrized cases with estimated total "
|
||||
f"{sum(get_case_est_time(case_id) for case_id in assignment.case_ids):.1f}s:"
|
||||
)
|
||||
for case_id in assignment.case_ids:
|
||||
print(f" - case: {case_id} ({get_case_est_time(case_id):.1f}s)")
|
||||
print(f"Test files: {[Path(f).name for f in suite_files]}")
|
||||
print(f"Filter expression: {filter_expr}")
|
||||
|
||||
junit_xml_path = str(
|
||||
target_dir / f"junit_results_{args.suite}_{args.partition_id}.xml"
|
||||
)
|
||||
exit_code, new_executed_cases, new_case_results = run_pytest(
|
||||
suite_files,
|
||||
filter_expr=filter_expr,
|
||||
junit_xml_path=junit_xml_path,
|
||||
)
|
||||
_merge_execution_results(
|
||||
executed_cases, case_results, new_executed_cases, new_case_results
|
||||
)
|
||||
# A failing case must not swallow this shard's standalone files: they
|
||||
# are separate pytest runs, and they only share a shard because the
|
||||
# shard count is fixed. --continue-on-error still decides whether a
|
||||
# failing standalone file stops the ones queued behind it.
|
||||
if exit_code != 0 and overall_exit_code == 0:
|
||||
overall_exit_code = exit_code
|
||||
|
||||
if assignment.standalone_files:
|
||||
standalone_estimate = sum(
|
||||
get_standalone_file_est_time(args.suite, standalone_file)[0]
|
||||
for standalone_file in assignment.standalone_files
|
||||
)
|
||||
total_est_time += standalone_estimate
|
||||
print(
|
||||
f"Running {len(assignment.standalone_files)} standalone file(s) with estimated total "
|
||||
f"{standalone_estimate:.1f}s:"
|
||||
)
|
||||
for standalone_file in assignment.standalone_files:
|
||||
est_time, used_fallback_estimate = get_standalone_file_est_time(
|
||||
args.suite, standalone_file
|
||||
)
|
||||
fallback_suffix = (
|
||||
f", fallback estimate {DEFAULT_STANDALONE_EST_TIME_SECONDS:.1f}s"
|
||||
if used_fallback_estimate
|
||||
else ""
|
||||
)
|
||||
print(
|
||||
f" - standalone: {standalone_file} ({est_time:.1f}s{fallback_suffix})"
|
||||
)
|
||||
|
||||
for standalone_file in assignment.standalone_files:
|
||||
exit_code, new_executed_cases, new_case_results, measurement = (
|
||||
_run_standalone_file(
|
||||
args.suite,
|
||||
standalone_file,
|
||||
target_dir,
|
||||
extra_filter=args.filter,
|
||||
)
|
||||
)
|
||||
if measurement["used_fallback_estimate"]:
|
||||
missing_standalone_estimates.append(standalone_file)
|
||||
standalone_measurements.append(measurement)
|
||||
_merge_execution_results(
|
||||
executed_cases,
|
||||
case_results,
|
||||
new_executed_cases,
|
||||
new_case_results,
|
||||
)
|
||||
if exit_code != 0 and overall_exit_code == 0:
|
||||
overall_exit_code = exit_code
|
||||
if exit_code != 0 and not args.continue_on_error:
|
||||
break
|
||||
|
||||
if not assignment.case_ids and not assignment.standalone_files:
|
||||
print(f"No work assigned to partition {args.partition_id}. Exiting success.")
|
||||
|
||||
print(f"Partition estimated total time: {total_est_time:.1f}s")
|
||||
write_execution_report(
|
||||
suite=args.suite,
|
||||
partition_id=args.partition_id,
|
||||
total_partitions=args.total_partitions,
|
||||
executed_cases=executed_cases,
|
||||
is_standalone=False,
|
||||
standalone_file=None,
|
||||
case_results=case_results,
|
||||
missing_standalone_estimates=missing_standalone_estimates,
|
||||
standalone_measurements=standalone_measurements,
|
||||
)
|
||||
return overall_exit_code
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
validate_standalone_file_est_times()
|
||||
test_root_dir = Path(__file__).resolve().parents[1]
|
||||
target_dir = test_root_dir / args.base_dir
|
||||
|
||||
if not target_dir.exists():
|
||||
print(f"Error: Target directory {target_dir} does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
if args.suite in COMPONENT_ACCURACY_SUITES:
|
||||
suite_files_rel = FILE_SUITES[args.suite]
|
||||
suite_files_abs = _resolve_suite_files(
|
||||
target_dir, suite_files_rel, args.suite in STRICT_SUITES
|
||||
)
|
||||
|
||||
if not suite_files_abs:
|
||||
print(f"No valid test files found for suite '{args.suite}'.")
|
||||
sys.exit(1 if args.suite in STRICT_SUITES else 0)
|
||||
|
||||
my_files = partition_items_by_index(
|
||||
suite_files_abs, args.partition_id, args.total_partitions
|
||||
)
|
||||
partition_info = (
|
||||
f"{args.partition_id + 1}/{args.total_partitions} "
|
||||
f"(0-based id={args.partition_id})"
|
||||
)
|
||||
headers = ["Suite", "Partition"]
|
||||
rows = [[args.suite, partition_info]]
|
||||
msg = tabulate.tabulate(rows, headers=headers, tablefmt="psql") + "\n"
|
||||
msg += f"Enabled {len(my_files)} file(s):\n"
|
||||
for file_path in my_files:
|
||||
msg += f" - {file_path}\n"
|
||||
print(msg, flush=True)
|
||||
print(
|
||||
f"Suite: {args.suite} | Partition: {args.partition_id}/{args.total_partitions}"
|
||||
)
|
||||
print(f"Selected {len(suite_files_abs)} files:")
|
||||
for f in suite_files_abs:
|
||||
print(f" - {os.path.basename(f)}")
|
||||
|
||||
if not my_files:
|
||||
print("No files assigned to this partition. Exiting success.")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Running {len(my_files)} files in this shard: {', '.join(my_files)}")
|
||||
|
||||
exit_code = run_component_accuracy_files(
|
||||
my_files,
|
||||
filter_expr=args.filter,
|
||||
continue_on_error=args.continue_on_error,
|
||||
)
|
||||
|
||||
msg = "\n" + tabulate.tabulate(rows, headers=headers, tablefmt="psql") + "\n"
|
||||
msg += f"Executed {len(my_files)} file(s):\n"
|
||||
for file_path in my_files:
|
||||
msg += f" - {file_path}\n"
|
||||
print(msg, flush=True)
|
||||
elif args.suite in PARAMETRIZED_CASE_GROUPS:
|
||||
exit_code = _run_dynamic_suite(args, target_dir)
|
||||
else:
|
||||
exit_code = _run_file_suite(args, target_dir)
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,7 +2,7 @@
|
||||
"""
|
||||
Generate diffusion CI outputs for consistency testing.
|
||||
|
||||
This script reuses the CI test code by calling run_suite.py with SGLANG_GEN_GT=1,
|
||||
This script reuses the CI test adapter with SGLANG_GEN_GT=1,
|
||||
ensuring that GT generation uses exactly the same code path as CI tests.
|
||||
|
||||
Usage:
|
||||
@@ -21,7 +21,7 @@ from sglang.multimodal_gen.test.partitioning import (
|
||||
PartitionItem,
|
||||
partition_items_by_lpt,
|
||||
)
|
||||
from sglang.multimodal_gen.test.run_suite import (
|
||||
from sglang.multimodal_gen.test.runner.diffusion_suite_runner import (
|
||||
SUITES,
|
||||
_maybe_pin_update_weights_model_pair,
|
||||
get_case_est_time,
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_realtime_webui_presets_do_not_emit_camera_scripts():
|
||||
repo_root = Path(__file__).resolve().parents[6]
|
||||
app_js = (
|
||||
repo_root / "python/sglang/multimodal_gen/apps/realtime_webui/app.js"
|
||||
).read_text()
|
||||
index_html = (
|
||||
repo_root / "python/sglang/multimodal_gen/apps/realtime_webui/index.html"
|
||||
).read_text()
|
||||
styles_css = (
|
||||
repo_root / "python/sglang/multimodal_gen/apps/realtime_webui/styles.css"
|
||||
).read_text()
|
||||
playback_js = (
|
||||
repo_root
|
||||
/ "python/sglang/multimodal_gen/apps/realtime_webui/playback_controller.js"
|
||||
).read_text()
|
||||
|
||||
assert "preset.actions" not in app_js
|
||||
assert "repeatActions" not in app_js
|
||||
assert 'id="eventFrames"' not in index_html
|
||||
assert "ControlStateController" in app_js
|
||||
assert 'const DEFAULT_PREVIEW_OUTPUT_FORMAT = "webp";' in app_js
|
||||
assert 'id="transportFormat"' in index_html
|
||||
assert 'id="fps" type="number" value="25"' in index_html
|
||||
assert 'id="superResolution" type="checkbox"' in index_html
|
||||
assert 'id="upscalingScale"' in index_html
|
||||
assert 'class="workspace"' in index_html
|
||||
assert 'class="preview-frame"' in index_html
|
||||
assert 'id="previewOverlay" class="preview-overlay"' in index_html
|
||||
assert 'id="previewScale" type="range" min="80" max="170" value="120"' in index_html
|
||||
assert 'id="previewScaleText"' in index_html
|
||||
assert 'id="outputSizeText"' in index_html
|
||||
assert 'id="frameInterpolation" type="checkbox" />' in index_html
|
||||
assert (
|
||||
'id="serverUrl" value="ws://127.0.0.1:30000/v1/realtime_video/generate"'
|
||||
in index_html
|
||||
)
|
||||
assert '<option value="webp" selected>WebP preview</option>' in index_html
|
||||
assert 'id="serverSendText"' in index_html
|
||||
assert 'id="theoreticalFpsText"' in index_html
|
||||
assert 'id="renderFps"' in index_html
|
||||
assert 'id="stageRenderFps"' not in index_html
|
||||
assert "sglang-diffusion Realtime Studio" in index_html
|
||||
assert "SGLD" not in index_html
|
||||
assert 'class="tabs"' not in index_html
|
||||
assert "Recordings" not in index_html
|
||||
assert "API" not in index_html
|
||||
assert "Info" not in index_html
|
||||
assert 'id="steps" type="number" value="4"' in index_html
|
||||
assert 'id="guidance" type="number" value="1"' in index_html
|
||||
assert "styles.css?v=realtime-record-v49" in index_html
|
||||
assert "app.js?v=realtime-record-v75" in index_html
|
||||
assert (
|
||||
'const DECODER_WORKER_URL = "./decoder_worker.js?v=rgb-worker-v10";' in app_js
|
||||
)
|
||||
assert "const DEFAULT_TARGET_FPS = 25;" in app_js
|
||||
assert "const DEFAULT_FRAME_INTERPOLATION_EXP = 1;" in app_js
|
||||
assert "const DEFAULT_FRAME_INTERPOLATION_SCALE = 1.0;" in app_js
|
||||
assert "const DEFAULT_UPSCALING_SCALE = 2;" in app_js
|
||||
assert "const DEFAULT_PREVIEW_SCALE = 120;" in app_js
|
||||
assert 'setPreviewState("waiting")' in app_js
|
||||
assert "stage.dataset.previewState = state" in app_js
|
||||
assert "previewProgressSpin" in styles_css
|
||||
assert "previewDotPulse" not in styles_css
|
||||
assert 'document.querySelector(".preview-frame")' in app_js
|
||||
assert 'previewFrame.style.setProperty("--preview-scale"' in app_js
|
||||
assert "cancelAnimationFrame(previewScaleFrame)" in app_js
|
||||
assert "enable_frame_interpolation: true" in app_js
|
||||
assert "frame_interpolation_exp: DEFAULT_FRAME_INTERPOLATION_EXP" in app_js
|
||||
assert "frame_interpolation_scale: DEFAULT_FRAME_INTERPOLATION_SCALE" in app_js
|
||||
assert "readSuperResolutionParams()" in app_js
|
||||
assert "enable_upscaling: true" in app_js
|
||||
assert "upscaling_scale: readUpscalingScale()" in app_js
|
||||
assert "updateOutputSizeFromHeader(header)" in app_js
|
||||
assert "setPreviewScale(DEFAULT_PREVIEW_SCALE)" in app_js
|
||||
assert "preview_scale" in app_js
|
||||
assert "sr_scale" in app_js
|
||||
assert "elapsedMs < targetMs" in playback_js
|
||||
assert "queuedDecodeFrames > maxQueuedFrames" in app_js
|
||||
assert (
|
||||
'const REACTOR_PRESET_BASE_URL = "https://www.reactor.inc/lingbot-world-fast-v1";'
|
||||
in app_js
|
||||
)
|
||||
assert "Dragon Dolly" in app_js
|
||||
assert "no creature morphing" in app_js
|
||||
assert "the Plastic Beach island stays centered" in app_js
|
||||
assert "no camera descent, no push-in, no orbit" in app_js
|
||||
assert "Ziggy Stardust" in app_js
|
||||
assert "blue K. West sign" in app_js
|
||||
assert "wet pavement reflecting a yellow streetlamp" in app_js
|
||||
assert "ZiggyStardust.jpg" in app_js
|
||||
assert "A slow aerial orbit around a pastel floating island hotel" not in app_js
|
||||
assert app_js.index("Dragon Ride") < app_js.index("Dragon Dolly")
|
||||
assert app_js.index("Ziggy Stardust") < app_js.index("Plastic Beach")
|
||||
assert app_js.index("Dragon Dolly") < app_js.index("Kid A")
|
||||
assert "dragon-ride.jpg" in app_js
|
||||
assert "stageRenderFps" not in app_js
|
||||
assert 'setStatus("Receiving", "live")' in app_js
|
||||
assert "decodeQueue.push(" in app_js
|
||||
assert "receiveChain" not in app_js
|
||||
assert 'message.type === "chunk_stats"' in app_js
|
||||
assert "chunkTotal > 0 ? numFrames / chunkTotal" in app_js
|
||||
assert ".stage-stat" in styles_css
|
||||
assert ".workspace" in styles_css
|
||||
assert ".preview-frame" in styles_css
|
||||
assert ".preview-overlay" in styles_css
|
||||
assert ".preview-scale-control" in styles_css
|
||||
assert "--preview-scale" in styles_css
|
||||
@@ -47,18 +47,6 @@ def test_consistency_gt_urls_are_pinned_to_ci_data_revision():
|
||||
assert pinned_revision_path in test_utils.SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE
|
||||
|
||||
|
||||
def test_remote_file_exists_returns_false_for_definitive_404(monkeypatch):
|
||||
class Response:
|
||||
status_code = 404
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(test_utils.requests, "head", lambda *args, **kwargs: Response())
|
||||
|
||||
assert test_utils._remote_file_exists("https://example.com/missing.png") is False
|
||||
|
||||
|
||||
def test_remote_video_gt_candidates_survive_inconclusive_probe(monkeypatch):
|
||||
monkeypatch.setenv(test_utils.CONSISTENCY_PLATFORM_ENV, "h100")
|
||||
monkeypatch.setattr(test_utils, "_remote_file_exists", lambda url: None)
|
||||
|
||||
@@ -1907,18 +1907,6 @@ class TestCosmos3ModalitySamplingParams(unittest.TestCase):
|
||||
self.assertEqual(sp.condition_frame_indexes, [0, 1])
|
||||
self.assertEqual(sp.condition_video_keep, "first")
|
||||
|
||||
def test_action_fields_default_none(self):
|
||||
sp = Cosmos3SamplingParams(prompt="t")
|
||||
for field in (
|
||||
"action_mode",
|
||||
"domain_id",
|
||||
"domain_name",
|
||||
"raw_action_dim",
|
||||
"action_fps",
|
||||
"action",
|
||||
):
|
||||
self.assertIsNone(getattr(sp, field))
|
||||
|
||||
|
||||
class TestCosmos3CaptionMetadata(unittest.TestCase):
|
||||
"""Structured captions get generation metadata; prose prompts opt out."""
|
||||
|
||||
@@ -1,658 +0,0 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def _load_benchmark_module(temp_root: Path):
|
||||
multimodal_gen_root = Path(__file__).resolve().parents[2]
|
||||
script_path = (
|
||||
multimodal_gen_root
|
||||
/ ".claude"
|
||||
/ "skills"
|
||||
/ "sglang-diffusion-benchmark-profile"
|
||||
/ "scripts"
|
||||
/ "bench_diffusion_denoise.py"
|
||||
)
|
||||
fake_env = types.ModuleType("diffusion_skill_env")
|
||||
fake_env.ensure_dir = lambda path: (
|
||||
Path(path).mkdir(parents=True, exist_ok=True) or Path(path)
|
||||
)
|
||||
fake_env.get_assets_dir = lambda _root: temp_root / "assets"
|
||||
fake_env.get_output_dir = lambda _kind, _root: temp_root / "outputs"
|
||||
fake_env.get_repo_root = lambda: temp_root / "repo"
|
||||
fake_env.pick_idle_gpus = lambda count: list(range(count))
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"test_bench_diffusion_denoise", script_path
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, {"diffusion_skill_env": fake_env}):
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _load_skill_env_module():
|
||||
script_path = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ ".claude"
|
||||
/ "skills"
|
||||
/ "sglang-diffusion-benchmark-profile"
|
||||
/ "scripts"
|
||||
/ "diffusion_skill_env.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"test_diffusion_skill_env", script_path
|
||||
)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class TestDiffusionBenchmarkSkill(unittest.TestCase):
|
||||
def test_skill_env_prefers_own_worktree_over_installed_package(self):
|
||||
module = _load_skill_env_module()
|
||||
installed = types.ModuleType("sglang")
|
||||
installed.__file__ = "/sgl-workspace/sglang/python/sglang/__init__.py"
|
||||
|
||||
with patch.dict(sys.modules, {"sglang": installed}):
|
||||
repo_root = module.get_repo_root()
|
||||
|
||||
self.assertEqual(repo_root, Path(__file__).resolve().parents[5])
|
||||
|
||||
def test_nightly_presets_remain_aligned(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
module = _load_benchmark_module(Path(tmpdir))
|
||||
repo_root = Path(__file__).resolve().parents[5]
|
||||
module.NIGHTLY_CONFIG_PATH = (
|
||||
repo_root
|
||||
/ "scripts"
|
||||
/ "ci"
|
||||
/ "utils"
|
||||
/ "diffusion"
|
||||
/ "comparison_configs.json"
|
||||
)
|
||||
|
||||
self.assertEqual(module.validate_nightly_alignment(), 0)
|
||||
|
||||
def test_recent_model_presets_are_eager_by_default(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
module = _load_benchmark_module(Path(tmpdir))
|
||||
|
||||
expected = {
|
||||
"longcat-image",
|
||||
"longcat-image-edit",
|
||||
"longcat-image-edit-turbo",
|
||||
"qwen-edit-base",
|
||||
"qwen-image-layered",
|
||||
"stable-diffusion-3.5-medium",
|
||||
"sana-video",
|
||||
"sana-wm-bidirectional",
|
||||
"sana-wm-streaming",
|
||||
"lingbot-video-moe",
|
||||
"lingbot-world",
|
||||
"lingbot-world-v2",
|
||||
"fastwan21-t2v-1.3b",
|
||||
"fasth3-t2va-vsa",
|
||||
"wan22-t2v-nvfp4",
|
||||
"krea2-turbo",
|
||||
"krea2-raw",
|
||||
"ideogram4-fast",
|
||||
"ideogram4-instant",
|
||||
"longlive2-t2v",
|
||||
"longlive2-i2v",
|
||||
"fast-hunyuan",
|
||||
"turbowan21-t2v-1.3b",
|
||||
"helios-mid",
|
||||
"helios-distilled",
|
||||
"joy-echo",
|
||||
"cosmos3-edge-t2i",
|
||||
"cosmos3-super-t2v-cfg2tp2",
|
||||
"cosmos3-super-i2v",
|
||||
"cosmos3-super-t2i-distilled",
|
||||
"ltx25",
|
||||
"ltx25-diffusion-decoder",
|
||||
}
|
||||
self.assertTrue(expected.issubset(module.MODELS))
|
||||
|
||||
eager_cmd = module.build_sglang_cmd("longcat-image")
|
||||
self.assertNotIn("--enable-torch-compile", eager_cmd)
|
||||
self.assertIn("--enable-prompt-rewrite=false", eager_cmd)
|
||||
self.assertIn("--quality=lossless", eager_cmd)
|
||||
|
||||
compiled_cmd = module.build_sglang_cmd("longcat-image", torch_compile=True)
|
||||
self.assertIn("--enable-torch-compile", compiled_cmd)
|
||||
|
||||
longcat_edit_cmd = module.build_sglang_cmd("longcat-image-edit")
|
||||
self.assertIn(
|
||||
"--model-path=meituan-longcat/LongCat-Image-Edit",
|
||||
longcat_edit_cmd,
|
||||
)
|
||||
self.assertTrue(
|
||||
any(arg.startswith("--image-path=") for arg in longcat_edit_cmd)
|
||||
)
|
||||
self.assertIn("--enable-prompt-rewrite=false", longcat_edit_cmd)
|
||||
|
||||
longcat_edit_bcg_cmd = module.build_sglang_cmd(
|
||||
"longcat-image-edit", breakable_cuda_graph=True
|
||||
)
|
||||
resolution_index = longcat_edit_bcg_cmd.index("--warmup-resolutions")
|
||||
self.assertEqual(longcat_edit_bcg_cmd[resolution_index + 1], "1264x848")
|
||||
|
||||
longcat_edit_turbo_cmd = module.build_sglang_cmd("longcat-image-edit-turbo")
|
||||
self.assertIn(
|
||||
"--model-path=meituan-longcat/LongCat-Image-Edit-Turbo",
|
||||
longcat_edit_turbo_cmd,
|
||||
)
|
||||
|
||||
layered_cmd = module.build_sglang_cmd("qwen-image-layered")
|
||||
self.assertIn("--model-path=Qwen/Qwen-Image-Layered", layered_cmd)
|
||||
self.assertIn("--num-frames=4", layered_cmd)
|
||||
|
||||
sd35_cmd = module.build_sglang_cmd("stable-diffusion-3.5-medium")
|
||||
self.assertIn(
|
||||
"--model-path=stabilityai/stable-diffusion-3.5-medium-diffusers",
|
||||
sd35_cmd,
|
||||
)
|
||||
self.assertIn("stable-diffusion-3.5-medium", module.GATED_MODELS)
|
||||
|
||||
h3_cmd = module.build_sglang_cmd("minimax-h3-t2va", torch_compile=True)
|
||||
self.assertNotIn("--enable-torch-compile", h3_cmd)
|
||||
|
||||
fastwan_cmd = module.build_sglang_cmd("fastwan21-t2v-1.3b")
|
||||
self.assertIn("--num-frames=61", fastwan_cmd)
|
||||
self.assertIn("--num-inference-steps=3", fastwan_cmd)
|
||||
self.assertIn("--dit-layerwise-offload=false", fastwan_cmd)
|
||||
|
||||
wan_nvfp4_cmd = module.build_sglang_cmd("wan22-t2v-nvfp4")
|
||||
self.assertIn(
|
||||
"--model-path=nvidia/Wan2.2-T2V-A14B-Diffusers-NVFP4",
|
||||
wan_nvfp4_cmd,
|
||||
)
|
||||
self.assertIn("--num-frames=81", wan_nvfp4_cmd)
|
||||
self.assertIn("--dit-layerwise-offload=false", wan_nvfp4_cmd)
|
||||
self.assertEqual(module.required_gpus_for_model("wan22-t2v-nvfp4"), 1)
|
||||
|
||||
krea_raw_cmd = module.build_sglang_cmd("krea2-raw")
|
||||
self.assertIn("--num-inference-steps=50", krea_raw_cmd)
|
||||
self.assertIn("--guidance-scale=4.5", krea_raw_cmd)
|
||||
|
||||
cosmos_i2v_cmd = module.build_sglang_cmd("cosmos3-super-i2v")
|
||||
self.assertIn(
|
||||
"--model-path=nvidia/Cosmos3-Super-Image2Video", cosmos_i2v_cmd
|
||||
)
|
||||
self.assertIn("--num-gpus=2", cosmos_i2v_cmd)
|
||||
self.assertIn("--tp-size=2", cosmos_i2v_cmd)
|
||||
self.assertIn("--num-frames=81", cosmos_i2v_cmd)
|
||||
|
||||
cosmos_cfg_cmd = module.build_sglang_cmd("cosmos3-super-t2v-cfg2tp2")
|
||||
self.assertIn("--model-path=nvidia/Cosmos3-Super", cosmos_cfg_cmd)
|
||||
self.assertIn("--num-gpus=4", cosmos_cfg_cmd)
|
||||
self.assertIn("--tp-size=2", cosmos_cfg_cmd)
|
||||
|
||||
sana_wm_dense_cmd = module.build_sglang_cmd("sana-wm-bidirectional")
|
||||
self.assertIn(
|
||||
"--model-path=Efficient-Large-Model/SANA-WM_bidirectional",
|
||||
sana_wm_dense_cmd,
|
||||
)
|
||||
self.assertIn("--num-inference-steps=20", sana_wm_dense_cmd)
|
||||
self.assertNotIn("--streaming", sana_wm_dense_cmd)
|
||||
|
||||
sana_wm_streaming_cmd = module.build_sglang_cmd("sana-wm-streaming")
|
||||
self.assertIn("--streaming", sana_wm_streaming_cmd)
|
||||
self.assertIn("--refiner-chunked", sana_wm_streaming_cmd)
|
||||
self.assertIn("--action=w-16,wl-16,l-16", sana_wm_streaming_cmd)
|
||||
|
||||
lingbot_world_cmd = module.build_sglang_cmd("lingbot-world")
|
||||
self.assertIn(
|
||||
"--model-path=robbyant/lingbot-world-fast-diffusers",
|
||||
lingbot_world_cmd,
|
||||
)
|
||||
self.assertIn("--num-frames=9", lingbot_world_cmd)
|
||||
self.assertIn("--warmup-mode=off", lingbot_world_cmd)
|
||||
self.assertIn("--config=", " ".join(lingbot_world_cmd))
|
||||
self.assertEqual(
|
||||
module.MODELS["lingbot-world"]["config_overrides"]["actions"],
|
||||
[["w"] for _ in range(9)],
|
||||
)
|
||||
|
||||
lingbot_world_v2_cmd = module.build_sglang_cmd("lingbot-world-v2")
|
||||
self.assertIn(
|
||||
"--model-path=robbyant/lingbot-world-v2-14b-causal-fast-diffusers",
|
||||
lingbot_world_v2_cmd,
|
||||
)
|
||||
self.assertIn("--num-frames=9", lingbot_world_v2_cmd)
|
||||
self.assertIn("--num-inference-steps=4", lingbot_world_v2_cmd)
|
||||
|
||||
ideogram_cmd = module.build_sglang_cmd("ideogram4-instant")
|
||||
self.assertFalse(
|
||||
any(arg.startswith("--num-inference-steps") for arg in ideogram_cmd)
|
||||
)
|
||||
|
||||
longlive_i2v_cmd = module.build_sglang_cmd("longlive2-i2v")
|
||||
self.assertIn("--num-frames=61", longlive_i2v_cmd)
|
||||
self.assertTrue(
|
||||
any(arg.startswith("--image-path=") for arg in longlive_i2v_cmd)
|
||||
)
|
||||
|
||||
joy_echo_cmd = module.build_sglang_cmd("joy-echo")
|
||||
self.assertIn("--num-gpus=2", joy_echo_cmd)
|
||||
self.assertIn("--ulysses-degree=2", joy_echo_cmd)
|
||||
config_arg = next(
|
||||
arg for arg in joy_echo_cmd if arg.startswith("--config=")
|
||||
)
|
||||
config = json.loads(Path(config_arg.removeprefix("--config=")).read_text())
|
||||
self.assertFalse(config["enable_memory_bank"])
|
||||
|
||||
def test_quality_and_bcg_comparators_are_explicit_and_exclusive(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
module = _load_benchmark_module(Path(tmpdir))
|
||||
|
||||
high_cmd = module.build_sglang_cmd("longcat-image", quality="high")
|
||||
self.assertIn("--quality=high", high_cmd)
|
||||
self.assertNotIn("--enable-breakable-cuda-graph", high_cmd)
|
||||
|
||||
extra_high_cmd = module.build_sglang_cmd(
|
||||
"longcat-image", quality="extra-high"
|
||||
)
|
||||
self.assertIn("--quality=extra-high", extra_high_cmd)
|
||||
self.assertNotIn("--enable-breakable-cuda-graph", extra_high_cmd)
|
||||
|
||||
bcg_cmd = module.build_sglang_cmd(
|
||||
"longcat-image",
|
||||
breakable_cuda_graph=True,
|
||||
bcg_text_buckets=[256, 512],
|
||||
)
|
||||
self.assertIn("--enable-breakable-cuda-graph", bcg_cmd)
|
||||
self.assertEqual(
|
||||
bcg_cmd[bcg_cmd.index("--warmup-resolutions") + 1], "1024x1024"
|
||||
)
|
||||
bucket_index = bcg_cmd.index("--bcg-text-buckets")
|
||||
self.assertEqual(
|
||||
bcg_cmd[bucket_index + 1 : bucket_index + 3], ["256", "512"]
|
||||
)
|
||||
|
||||
sana_video_bcg_cmd = module.build_sglang_cmd(
|
||||
"sana-video", breakable_cuda_graph=True
|
||||
)
|
||||
self.assertEqual(
|
||||
sana_video_bcg_cmd[sana_video_bcg_cmd.index("--warmup-num-frames") + 1],
|
||||
"17",
|
||||
)
|
||||
|
||||
for _, quality, breakable_cuda_graph in module.QUALITY_BCG_ABBA_MATRIX:
|
||||
module.build_sglang_cmd(
|
||||
"longcat-image",
|
||||
quality=quality,
|
||||
breakable_cuda_graph=breakable_cuda_graph,
|
||||
bcg_text_buckets=[256, 512] if breakable_cuda_graph else None,
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "comparators"):
|
||||
module.build_sglang_cmd(
|
||||
"longcat-image",
|
||||
torch_compile=True,
|
||||
breakable_cuda_graph=True,
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "requires"):
|
||||
module.build_sglang_cmd("longcat-image", bcg_text_buckets=[256])
|
||||
|
||||
def test_isolated_cache_cleanup_writes_zero_residual_ledger(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
cache_dir = module._prepare_model_cache(
|
||||
cache_root, "longcat-image", "baseline"
|
||||
)
|
||||
|
||||
weight_path = cache_dir / "huggingface" / "hub" / "model.safetensors"
|
||||
weight_path.parent.mkdir(parents=True)
|
||||
weight_path.write_bytes(b"weights")
|
||||
env = module._model_cache_env(cache_dir)
|
||||
self.assertTrue(env["HF_HOME"].startswith(str(cache_dir)))
|
||||
self.assertTrue(env["HF_XET_CACHE"].startswith(str(cache_dir)))
|
||||
self.assertTrue(env["TRANSFORMERS_CACHE"].startswith(str(cache_dir)))
|
||||
self.assertTrue(env["MODELSCOPE_CACHE"].startswith(str(cache_dir)))
|
||||
|
||||
ledger_path = temp_root / "artifacts" / "cleanup.jsonl"
|
||||
record = module._cleanup_model_cache(
|
||||
cache_root,
|
||||
cache_dir,
|
||||
ledger_path,
|
||||
"longcat-image",
|
||||
"baseline",
|
||||
"success",
|
||||
)
|
||||
|
||||
self.assertFalse(cache_dir.exists())
|
||||
self.assertEqual(record["before"]["weight_file_count"], 1)
|
||||
self.assertEqual(record["after"]["file_count"], 0)
|
||||
ledger = json.loads(ledger_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(ledger["exit_reason"], "success")
|
||||
self.assertEqual(ledger["after"]["weight_file_count"], 0)
|
||||
|
||||
def test_isolated_cache_refuses_to_reuse_existing_run_directory(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
module._prepare_model_cache(cache_root, "sana-video", "baseline")
|
||||
|
||||
with self.assertRaises(FileExistsError):
|
||||
module._prepare_model_cache(cache_root, "sana-video", "baseline")
|
||||
|
||||
def test_isolated_cache_seeds_read_only_hf_cache_with_writable_overlay(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
seed_root = temp_root / "shared-hf"
|
||||
source_model = seed_root / "hub" / "models--org--model"
|
||||
source_weight = source_model / "snapshots" / "abc" / "model.safetensors"
|
||||
source_weight.parent.mkdir(parents=True)
|
||||
source_weight.write_bytes(b"shared weights")
|
||||
source_ref = source_model / "refs" / "main"
|
||||
source_ref.parent.mkdir()
|
||||
source_ref.write_text("abc")
|
||||
|
||||
cache_root = temp_root / "model-caches"
|
||||
cache_dir = module._prepare_model_cache(
|
||||
cache_root,
|
||||
"sana-video",
|
||||
"baseline",
|
||||
seed_model_cache_roots=[seed_root],
|
||||
)
|
||||
seeded_model = cache_dir / "huggingface" / "hub" / "models--org--model"
|
||||
self.assertTrue(seeded_model.is_dir())
|
||||
self.assertFalse(seeded_model.is_symlink())
|
||||
seeded_weight = seeded_model / "snapshots" / "abc" / "model.safetensors"
|
||||
self.assertTrue(seeded_weight.is_symlink())
|
||||
self.assertEqual(
|
||||
seeded_weight.read_bytes(),
|
||||
b"shared weights",
|
||||
)
|
||||
|
||||
new_blob = seeded_model / "blobs" / "downloaded"
|
||||
new_blob.parent.mkdir(exist_ok=True)
|
||||
new_blob.write_bytes(b"new download")
|
||||
(seeded_model / "refs" / "main").write_text("new-revision")
|
||||
self.assertEqual(new_blob.read_bytes(), b"new download")
|
||||
self.assertEqual(source_ref.read_text(), "abc")
|
||||
|
||||
module._cleanup_model_cache(
|
||||
cache_root,
|
||||
cache_dir,
|
||||
temp_root / "cleanup.jsonl",
|
||||
"sana-video",
|
||||
"baseline",
|
||||
"success",
|
||||
)
|
||||
self.assertFalse(cache_dir.exists())
|
||||
self.assertEqual(source_weight.read_bytes(), b"shared weights")
|
||||
|
||||
def test_interrupted_run_cleans_isolated_cache_in_finally(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
module, "_run_benchmark_once_impl", side_effect=KeyboardInterrupt
|
||||
),
|
||||
self.assertRaises(KeyboardInterrupt),
|
||||
):
|
||||
module.run_benchmark_once(
|
||||
"sana-video",
|
||||
"baseline",
|
||||
output_dir,
|
||||
model_cache_root=cache_root,
|
||||
cleanup_model_cache=True,
|
||||
)
|
||||
|
||||
self.assertFalse((cache_root / "sana-video-baseline").exists())
|
||||
ledger = json.loads(
|
||||
(output_dir / "cleanup.jsonl").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(ledger["exit_reason"], "interrupted")
|
||||
self.assertEqual(ledger["after"]["weight_file_count"], 0)
|
||||
|
||||
def test_failed_run_is_recorded_as_error_and_cleaned(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
module, "_run_benchmark_once_impl", side_effect=RuntimeError("boom")
|
||||
),
|
||||
self.assertRaisesRegex(RuntimeError, "boom"),
|
||||
):
|
||||
module.run_benchmark_once(
|
||||
"sana-video",
|
||||
"baseline",
|
||||
output_dir,
|
||||
model_cache_root=cache_root,
|
||||
cleanup_model_cache=True,
|
||||
)
|
||||
|
||||
self.assertFalse((cache_root / "sana-video-baseline").exists())
|
||||
ledger = json.loads(
|
||||
(output_dir / "cleanup.jsonl").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(ledger["exit_reason"], "error")
|
||||
|
||||
def test_zero_exit_without_artifacts_is_invalid(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
|
||||
with patch.object(module.subprocess, "Popen") as popen:
|
||||
popen.return_value.stdout = iter(())
|
||||
popen.return_value.wait.return_value = 0
|
||||
result = module._run_benchmark_once_impl(
|
||||
"sana-video",
|
||||
"missing-artifacts",
|
||||
output_dir,
|
||||
warmup=False,
|
||||
cuda_visible_devices="0",
|
||||
)
|
||||
|
||||
command = popen.call_args.args[0]
|
||||
self.assertIn("--output-path", command)
|
||||
self.assertIn("--output-file-name", command)
|
||||
self.assertTrue(result["error"])
|
||||
self.assertEqual(
|
||||
result["missing_artifacts"], ["perf dump", "generated output"]
|
||||
)
|
||||
|
||||
def test_mesh_artifacts_are_accepted_and_hashed(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
|
||||
def finish_run():
|
||||
(output_dir / "hunyuan3d-shape_mesh-output.json").write_text(
|
||||
json.dumps({"total_duration_ms": 1000, "steps": []}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(output_dir / "hunyuan3d-shape-mesh-output.obj").write_bytes(
|
||||
b"v 0 0 0\n"
|
||||
)
|
||||
return 0
|
||||
|
||||
with patch.object(module.subprocess, "Popen") as popen:
|
||||
popen.return_value.stdout = iter(())
|
||||
popen.return_value.wait.side_effect = finish_run
|
||||
result = module._run_benchmark_once_impl(
|
||||
"hunyuan3d-shape",
|
||||
"mesh-output",
|
||||
output_dir,
|
||||
warmup=False,
|
||||
cuda_visible_devices="0",
|
||||
)
|
||||
|
||||
self.assertFalse(result["error"])
|
||||
self.assertEqual(
|
||||
result["output_artifacts"],
|
||||
[str(output_dir / "hunyuan3d-shape-mesh-output.obj")],
|
||||
)
|
||||
self.assertEqual(len(result["output_sha256"]), 1)
|
||||
|
||||
def test_high_bcg_rejects_quality_fusion_mounted_after_capture(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
|
||||
with patch.object(module.subprocess, "Popen") as popen:
|
||||
popen.return_value.stdout = iter(
|
||||
(
|
||||
"[Diffusion BCG] captured 3 segment(s)\n",
|
||||
"Mounted LTX-2 fused RMSNorm+modulate for quality=high\n",
|
||||
)
|
||||
)
|
||||
popen.return_value.wait.return_value = 0
|
||||
result = module._run_benchmark_once_impl(
|
||||
"longcat-image",
|
||||
"bcg-high",
|
||||
output_dir,
|
||||
warmup=False,
|
||||
quality="high",
|
||||
breakable_cuda_graph=True,
|
||||
cuda_visible_devices="0",
|
||||
)
|
||||
|
||||
self.assertTrue(result["error"])
|
||||
self.assertEqual(
|
||||
result["bcg_invalid_signals"],
|
||||
[module.BCG_LATE_QUALITY_FUSION_SIGNAL],
|
||||
)
|
||||
|
||||
def test_extra_high_bcg_rejects_quality_fusion_mounted_after_capture(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
|
||||
with patch.object(module.subprocess, "Popen") as popen:
|
||||
popen.return_value.stdout = iter(
|
||||
(
|
||||
"[Diffusion BCG] captured 3 segment(s)\n",
|
||||
"Mounted Qwen fused added-QKV for quality=extra-high\n",
|
||||
)
|
||||
)
|
||||
popen.return_value.wait.return_value = 0
|
||||
result = module._run_benchmark_once_impl(
|
||||
"longcat-image",
|
||||
"bcg-extra-high",
|
||||
output_dir,
|
||||
warmup=False,
|
||||
quality="extra-high",
|
||||
breakable_cuda_graph=True,
|
||||
cuda_visible_devices="0",
|
||||
)
|
||||
|
||||
self.assertTrue(result["error"])
|
||||
self.assertEqual(
|
||||
result["bcg_invalid_signals"],
|
||||
[module.BCG_LATE_QUALITY_FUSION_SIGNAL],
|
||||
)
|
||||
|
||||
def test_quality_bcg_matrix_reuses_one_gpu_set_and_cleans_once(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
temp_root = Path(tmpdir)
|
||||
module = _load_benchmark_module(temp_root)
|
||||
cache_root = temp_root / "model-caches"
|
||||
output_dir = temp_root / "outputs"
|
||||
output_dir.mkdir()
|
||||
calls = []
|
||||
|
||||
def fake_run(model_key, label, _output_dir, **kwargs):
|
||||
calls.append((model_key, label, kwargs))
|
||||
cache_dir = kwargs["model_cache_dir"]
|
||||
weight_path = cache_dir / "hub" / "model.safetensors"
|
||||
weight_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
weight_path.write_bytes(b"weights")
|
||||
return {"model": model_key, "label": label, "error": False}
|
||||
|
||||
with patch.object(module, "_run_benchmark_once_impl", side_effect=fake_run):
|
||||
results = module.run_quality_bcg_matrix(
|
||||
"sana-video",
|
||||
"h200",
|
||||
output_dir,
|
||||
model_cache_root=cache_root,
|
||||
cleanup_model_cache=True,
|
||||
)
|
||||
|
||||
self.assertEqual(len(results), 12)
|
||||
self.assertEqual(
|
||||
[
|
||||
(call[2]["quality"], call[2]["breakable_cuda_graph"])
|
||||
for call in calls
|
||||
],
|
||||
[
|
||||
(quality, breakable_cuda_graph)
|
||||
for _, quality, breakable_cuda_graph in module.QUALITY_BCG_ABBA_MATRIX
|
||||
],
|
||||
)
|
||||
self.assertEqual({call[2]["cuda_visible_devices"] for call in calls}, {"0"})
|
||||
self.assertEqual(
|
||||
{call[2]["model_cache_dir"] for call in calls},
|
||||
{calls[0][2]["model_cache_dir"]},
|
||||
)
|
||||
self.assertFalse(calls[0][2]["model_cache_dir"].exists())
|
||||
ledger = json.loads(
|
||||
(output_dir / "cleanup.jsonl").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(ledger["exit_reason"], "success")
|
||||
self.assertEqual(ledger["before"]["weight_file_count"], 1)
|
||||
self.assertEqual(ledger["after"]["weight_file_count"], 0)
|
||||
|
||||
def test_quality_bcg_matrix_rejects_output_hash_mismatch(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
module = _load_benchmark_module(Path(tmpdir))
|
||||
results = [
|
||||
{
|
||||
"quality": "lossless",
|
||||
"breakable_cuda_graph": False,
|
||||
"output_sha256": ["eager"],
|
||||
"error": False,
|
||||
},
|
||||
{
|
||||
"quality": "lossless",
|
||||
"breakable_cuda_graph": True,
|
||||
"output_sha256": ["bcg"],
|
||||
"error": False,
|
||||
},
|
||||
]
|
||||
|
||||
module._validate_quality_bcg_output_hashes(results)
|
||||
|
||||
self.assertFalse(results[0]["error"])
|
||||
self.assertTrue(results[1]["error"])
|
||||
self.assertEqual(
|
||||
results[1]["output_hash_error"],
|
||||
"BCG lossless output hash differs from eager",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -617,13 +617,6 @@ class TestStageAffinityAndValidation(_GlobalStageArgsMixin, unittest.TestCase):
|
||||
pipeline.create_pipeline_stages(pipeline.server_args)
|
||||
self.assertEqual(list(pipeline._stage_name_mapping.keys()), stage_names)
|
||||
|
||||
def test_hunyuan3d_shape_stage_no_longer_stores_model_dtype(self):
|
||||
pipeline = self._make_hunyuan_pipeline(RoleType.ENCODER, paint_enable=False)
|
||||
pipeline.create_pipeline_stages(pipeline.server_args)
|
||||
stage = pipeline._stage_name_mapping["shape_before_denoising"]
|
||||
self.assertIsInstance(stage, Hunyuan3DShapeBeforeDenoisingStage)
|
||||
self.assertFalse(hasattr(stage, "model_dtype"))
|
||||
|
||||
def test_ltx2_refinement_stage_keeps_class_name_stage_key(self):
|
||||
stage = object.__new__(LTX2RefinementStage)
|
||||
self.assertEqual(
|
||||
|
||||
@@ -29,17 +29,6 @@ from sglang.multimodal_gen.runtime.utils.nvtx_pytorch_hooks import (
|
||||
|
||||
|
||||
class TestMaybeNvtxRange(unittest.TestCase):
|
||||
def test_disabled_returns_noop_context_manager(self) -> None:
|
||||
ran = False
|
||||
with maybe_nvtx_range("never", enabled=False):
|
||||
ran = True
|
||||
self.assertTrue(ran)
|
||||
|
||||
def test_disabled_propagates_exception(self) -> None:
|
||||
with self.assertRaises(RuntimeError):
|
||||
with maybe_nvtx_range("never", enabled=False):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
def test_disabled_does_not_call_nvtx(self) -> None:
|
||||
with (
|
||||
patch.object(nvtx_pytorch_hooks.nvtx, "range_push") as push,
|
||||
|
||||
@@ -15,8 +15,6 @@ from sglang.multimodal_gen.runtime.models.encoders.qwen2_5vl import (
|
||||
Qwen2_5_VLAttention,
|
||||
Qwen2_5_VLForConditionalGeneration,
|
||||
_apply_repetition_penalty,
|
||||
_make_column_linear,
|
||||
_make_row_linear,
|
||||
_select_next_token,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.encoders.qwen2_5vl_vision import (
|
||||
@@ -29,14 +27,7 @@ from sglang.multimodal_gen.runtime.pipelines.longcat_image import LongCatImagePi
|
||||
from sglang.srt.layers.linear import (
|
||||
ColumnParallelLinear,
|
||||
ReplicatedLinear,
|
||||
RowParallelLinear,
|
||||
)
|
||||
from sglang.srt.models.qwen2_5_vl import (
|
||||
Qwen2_5_VisionPatchEmbed,
|
||||
Qwen2_5_VisionPatchMerger,
|
||||
Qwen2_5_VLMLP,
|
||||
)
|
||||
from sglang.srt.runtime_context import get_parallel
|
||||
|
||||
|
||||
class _StubQwen2_5VL(Qwen2_5_VLForConditionalGeneration):
|
||||
@@ -79,49 +70,6 @@ class _AttentionRecorder(nn.Module):
|
||||
return query
|
||||
|
||||
|
||||
def test_native_vision_reuses_srt_modules():
|
||||
config = SimpleNamespace(
|
||||
hidden_size=16,
|
||||
intermediate_size=24,
|
||||
hidden_act="silu",
|
||||
num_heads=2,
|
||||
depth=0,
|
||||
patch_size=2,
|
||||
temporal_patch_size=1,
|
||||
in_channels=3,
|
||||
spatial_merge_size=2,
|
||||
out_hidden_size=12,
|
||||
fullatt_block_indexes=[],
|
||||
window_size=8,
|
||||
)
|
||||
with get_parallel().override(tp_size=1, tp_rank=0):
|
||||
model = Qwen2_5VLVisionTransformer(config)
|
||||
mlp = Qwen2_5_VLMLP(
|
||||
16,
|
||||
24,
|
||||
fuse_gate_up=False,
|
||||
)
|
||||
fused_mlp = Qwen2_5_VLMLP(16, 24)
|
||||
|
||||
assert isinstance(model.patch_embed, Qwen2_5_VisionPatchEmbed)
|
||||
assert isinstance(model.merger, Qwen2_5_VisionPatchMerger)
|
||||
assert not mlp.fuse_gate_up
|
||||
assert isinstance(mlp.gate_proj, ColumnParallelLinear)
|
||||
assert isinstance(mlp.up_proj, ColumnParallelLinear)
|
||||
assert mlp.gate_proj.tp_size == mlp.up_proj.tp_size == 1
|
||||
assert isinstance(mlp.down_proj, ReplicatedLinear)
|
||||
assert isinstance(fused_mlp.down_proj, RowParallelLinear)
|
||||
assert mlp.act is not None
|
||||
assert isinstance(
|
||||
_make_column_linear(16, 24, bias=False, use_tensor_parallel=False),
|
||||
ReplicatedLinear,
|
||||
)
|
||||
assert isinstance(
|
||||
_make_row_linear(24, 16, bias=False, use_tensor_parallel=False),
|
||||
ReplicatedLinear,
|
||||
)
|
||||
|
||||
|
||||
def test_text_mlp_uses_single_rank_when_intermediate_size_is_not_tp_divisible(
|
||||
monkeypatch,
|
||||
):
|
||||
|
||||
@@ -12,7 +12,6 @@ from sglang.multimodal_gen.runtime.models.encoders.minimax_h3_qwen3vl import (
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl import (
|
||||
Qwen3VLForConditionalGeneration,
|
||||
_make_text_rms_norm,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl_vision import (
|
||||
Qwen3VLVisionRotaryEmbedding,
|
||||
@@ -20,7 +19,6 @@ from sglang.multimodal_gen.runtime.models.encoders.qwen3vl_vision import (
|
||||
_vision_cu_seqlens,
|
||||
_vision_position_ids,
|
||||
)
|
||||
from sglang.srt.layers.layernorm import RMSNorm
|
||||
from sglang.srt.models.qwen3_vl import (
|
||||
Qwen3VLMoeVisionPatchMerger,
|
||||
Qwen3VLVisionPatchEmbed,
|
||||
@@ -48,13 +46,6 @@ def test_native_vision_layout_matches_qwen3_merge_order():
|
||||
assert cu_seqlens.tolist() == [0, 24, 32, 40]
|
||||
|
||||
|
||||
def test_qwen3vl_text_reuses_srt_rms_norm():
|
||||
norm = _make_text_rms_norm(16, 1e-6)
|
||||
|
||||
assert isinstance(norm, RMSNorm)
|
||||
assert norm.cast_x_before_out_mul
|
||||
|
||||
|
||||
def test_native_vision_keeps_checkpoint_parameter_names():
|
||||
config = SimpleNamespace(
|
||||
hidden_size=16,
|
||||
|
||||
@@ -770,11 +770,6 @@ class TestWarmupModeNormalization(unittest.TestCase):
|
||||
sa = self._resolve(warmup_mode="server")
|
||||
self.assertEqual(sa.warmup_mode, "server")
|
||||
|
||||
def test_defaulted_mode_applies_without_legacy_flags(self):
|
||||
# Bare `sglang serve` defaults to server-based warmup.
|
||||
sa = self._resolve(warmup_mode="server")
|
||||
self.assertEqual(sa.warmup_mode, "server")
|
||||
|
||||
def test_resolutions_force_warmup_on(self):
|
||||
sa = self._resolve(
|
||||
warmup_mode="off",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""Unit tests for server warmup progress reporting."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
|
||||
from sglang.multimodal_gen.runtime.server_warmup import SchedulerWarmupMixin
|
||||
|
||||
|
||||
class TestServerWarmupProgress(unittest.TestCase):
|
||||
def test_ci_progress_uses_scheduler_counter_when_tqdm_is_disabled(self):
|
||||
scheduler = SchedulerWarmupMixin()
|
||||
scheduler._show_warmup_progress = True
|
||||
scheduler._warmup_total = 1
|
||||
scheduler._warmup_processed = 1
|
||||
progress_bar = MagicMock(total=1, n=0)
|
||||
scheduler._warmup_progress_bar = progress_bar
|
||||
|
||||
with (
|
||||
patch(
|
||||
"sglang.multimodal_gen.runtime.server_warmup._is_ci_log_env",
|
||||
return_value=True,
|
||||
),
|
||||
patch("sglang.multimodal_gen.runtime.server_warmup.logger") as logger,
|
||||
):
|
||||
scheduler._advance_warmup_progress_bar(object(), OutputBatch())
|
||||
|
||||
logger.info.assert_called_once_with(
|
||||
"Warmup requests: %s/%s %s", 1, 1, "warmup req"
|
||||
)
|
||||
progress_bar.close.assert_called_once_with()
|
||||
self.assertIsNone(scheduler._warmup_progress_bar)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -25,11 +25,6 @@ class TestSingleRankDeviceGroup(unittest.TestCase):
|
||||
new_device_group(ranks, requested)
|
||||
new_group.assert_called_once_with(ranks, backend=requested)
|
||||
|
||||
def test_backend_defaults_to_none_for_multi_rank(self):
|
||||
with patch(NEW_GROUP_PATH) as new_group:
|
||||
new_device_group([0, 1])
|
||||
new_group.assert_called_once_with([0, 1], backend=None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,46 +1,14 @@
|
||||
import importlib.util
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends.sol_attn import (
|
||||
SolAttnBackend,
|
||||
SolAttnImpl,
|
||||
_get_sol_attn_runtime_config,
|
||||
_parse_layer_ranges,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms.cuda import CudaPlatformBase
|
||||
from sglang.multimodal_gen.runtime.platforms.interface import AttentionBackendEnum
|
||||
|
||||
|
||||
class FakeCudaPlatform(CudaPlatformBase):
|
||||
is_sm120_device = False
|
||||
is_blackwell_device = False
|
||||
supports_flash_attention = True
|
||||
|
||||
@classmethod
|
||||
def is_sm120(cls):
|
||||
return cls.is_sm120_device
|
||||
|
||||
@classmethod
|
||||
def is_blackwell(cls):
|
||||
return cls.is_blackwell_device
|
||||
|
||||
@classmethod
|
||||
def has_device_capability(
|
||||
cls,
|
||||
capability: tuple[int, int] | int,
|
||||
device_id: int = 0,
|
||||
) -> bool:
|
||||
return cls.supports_flash_attention
|
||||
|
||||
|
||||
class TestSolAttnBackend(unittest.TestCase):
|
||||
def test_enum_name(self):
|
||||
self.assertEqual(str(AttentionBackendEnum.SOL_ATTN), "sol_attn")
|
||||
self.assertTrue(AttentionBackendEnum.SOL_ATTN.is_sparse)
|
||||
|
||||
def test_parse_layer_ranges(self):
|
||||
self.assertEqual(_parse_layer_ranges("0,1,3-5"), frozenset({0, 1, 3, 4, 5}))
|
||||
|
||||
@@ -70,9 +38,6 @@ class TestSolAttnBackend(unittest.TestCase):
|
||||
):
|
||||
_get_sol_attn_runtime_config()
|
||||
|
||||
def test_backend_head_size(self):
|
||||
self.assertEqual(SolAttnBackend.get_supported_head_sizes(), [128])
|
||||
|
||||
def test_dense_guard_uses_early_steps(self):
|
||||
impl = SolAttnImpl(
|
||||
num_heads=8,
|
||||
@@ -127,19 +92,6 @@ class TestSolAttnBackend(unittest.TestCase):
|
||||
):
|
||||
self.assertFalse(impl._should_use_dense())
|
||||
|
||||
def test_cuda_resolver(self):
|
||||
if importlib.util.find_spec("sol_attn") is None:
|
||||
self.skipTest("sol_attn package is not available")
|
||||
cls_str = FakeCudaPlatform.get_attn_backend_cls_str(
|
||||
selected_backend=AttentionBackendEnum.SOL_ATTN,
|
||||
head_size=128,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
self.assertTrue(cls_str.endswith("SolAttnBackend"))
|
||||
|
||||
def test_supports_packed_varlen(self):
|
||||
self.assertTrue(SolAttnBackend.supports_packed_varlen())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -156,12 +156,6 @@ def test_strategy_sp1_replicates(monkeypatch):
|
||||
assert sps.plan_text_strategy(100) == "replicate"
|
||||
|
||||
|
||||
def test_strategy_shard_when_legal(monkeypatch):
|
||||
_fake_sp(monkeypatch, 2)
|
||||
assert sps.plan_text_strategy(15) == "shard"
|
||||
assert sps.plan_text_strategy(16) == "shard"
|
||||
|
||||
|
||||
def test_strategy_replicates_when_padding_spans_multiple_shards(monkeypatch):
|
||||
_fake_sp(monkeypatch, 8)
|
||||
assert sps.plan_text_strategy(1) == "replicate"
|
||||
|
||||
@@ -34,12 +34,6 @@ class _FakeProjection(nn.Module):
|
||||
return hidden_states, None
|
||||
|
||||
|
||||
def test_mmgen_clip_reuses_srt_components():
|
||||
assert mmgen_clip.CLIPEncoder is srt_clip.CLIPEncoder
|
||||
assert mmgen_clip.CLIPTextEmbeddings is srt_clip.CLIPTextEmbeddings
|
||||
assert mmgen_clip.CLIPVisionEmbeddings is srt_clip.CLIPVisionEmbeddings
|
||||
|
||||
|
||||
def test_clip_encoder_propagates_causal_semantics():
|
||||
with (
|
||||
patch.object(srt_clip, "CLIPAttention", return_value=nn.Identity()) as attn,
|
||||
|
||||
@@ -24,7 +24,6 @@ from sglang.multimodal_gen.runtime.layers.attention.backends.subblock_sparse.rou
|
||||
_snap_up_to_8,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends.subblock_sparse_attn import (
|
||||
SubBlockSparseAttentionBackend,
|
||||
SubBlockSparseAttentionImpl,
|
||||
SubBlockSparseSchedule,
|
||||
_dit_layer_index,
|
||||
@@ -187,17 +186,6 @@ class TestBudgetGranularity(unittest.TestCase):
|
||||
|
||||
|
||||
class TestSubBlockSparseBackend(unittest.TestCase):
|
||||
def test_the_advertised_builder_can_be_built(self):
|
||||
"""`AttentionMetadataBuilder.__init__` is abstract; a builder that does
|
||||
not override it makes `get_builder_cls()()` a TypeError."""
|
||||
builder = SubBlockSparseAttentionBackend.get_builder_cls()()
|
||||
builder.prepare()
|
||||
metadata = builder.build(current_timestep=7)
|
||||
self.assertIsInstance(
|
||||
metadata, SubBlockSparseAttentionBackend.get_metadata_cls()
|
||||
)
|
||||
self.assertEqual(metadata.current_timestep, 7)
|
||||
|
||||
def test_sm90_adapter_uses_presorted_indices_and_64x64_blocks(self):
|
||||
captured = {}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from sglang.multimodal_gen.test import run_suite
|
||||
from sglang.multimodal_gen.test.partitioning import PartitionItem, assign_partition
|
||||
from sglang.multimodal_gen.test.run_suite import (
|
||||
from sglang.multimodal_gen.test.runner import diffusion_suite_runner as run_suite
|
||||
from sglang.multimodal_gen.test.runner.diffusion_suite_runner import (
|
||||
PartitionAssignment,
|
||||
build_local_partition_assignment,
|
||||
)
|
||||
|
||||
@@ -71,9 +71,6 @@ class _FakeServerArgs:
|
||||
def should_start_component_on_cpu(self, _component_name):
|
||||
return False
|
||||
|
||||
def should_use_fsdp_for_component(self, _component_name):
|
||||
return False
|
||||
|
||||
def should_configure_layerwise_offload_for_lazy_component(self, component_name):
|
||||
return component_name in self.layerwise_components
|
||||
|
||||
@@ -615,12 +612,6 @@ class TestVAELoader(unittest.TestCase):
|
||||
|
||||
native_load.assert_not_called()
|
||||
|
||||
def test_pipeline_config_declares_an_empty_native_only_default(self):
|
||||
loader = vae_loader.VAELoader()
|
||||
server_args = _FakeServerArgs(QwenImagePipelineConfig())
|
||||
|
||||
self.assertFalse(loader.should_raise_customized_load_error(server_args, "vae"))
|
||||
|
||||
def test_backfill_ltx2_audio_vae_latent_stats_maps_official_keys(self):
|
||||
loaded = {
|
||||
"per_channel_statistics.mean-of-means": torch.tensor([1.0, 2.0]),
|
||||
|
||||
@@ -77,12 +77,6 @@ class _DispatchProbeVAE(ParallelTiledVAE):
|
||||
|
||||
|
||||
class TestVAESpatialParallelDecode(unittest.TestCase):
|
||||
def test_base_vae_config_defaults_to_auto_parallel_decode(self):
|
||||
config = VAEConfig()
|
||||
|
||||
self.assertTrue(config.use_parallel_decode)
|
||||
self.assertEqual(config.parallel_decode_mode, "auto")
|
||||
|
||||
def test_image_video_vae_configs_default_to_auto_parallel_decode(self):
|
||||
configs = (
|
||||
ErnieImageVAEConfig(),
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from torch import nn
|
||||
|
||||
from sglang.multimodal_gen.runtime.models.dits.wanvideo import WanSelfAttention
|
||||
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
||||
|
||||
_WAN = "sglang.multimodal_gen.runtime.models.dits.wanvideo"
|
||||
|
||||
|
||||
class TestWanAttentionBackendRole(unittest.TestCase):
|
||||
def test_cross_attention_role_is_forwarded_to_usp(self):
|
||||
with (
|
||||
patch(f"{_WAN}.ColumnParallelLinear", return_value=nn.Identity()),
|
||||
patch(f"{_WAN}.RowParallelLinear", return_value=nn.Identity()),
|
||||
patch(f"{_WAN}.get_tp_world_size", return_value=1),
|
||||
patch(f"{_WAN}.USPAttention") as usp_attention,
|
||||
):
|
||||
WanSelfAttention(
|
||||
dim=128,
|
||||
num_heads=1,
|
||||
qk_norm=False,
|
||||
is_cross_attention=True,
|
||||
supported_attention_backends={
|
||||
AttentionBackendEnum.FA,
|
||||
AttentionBackendEnum.TORCH_SDPA,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertTrue(usp_attention.call_args.kwargs["is_cross_attention"])
|
||||
self.assertTrue(usp_attention.call_args.kwargs["skip_sequence_parallel"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5,8 +5,7 @@ Importing this package is what registers them. An architecture may be claimed
|
||||
by more than one module here -- one supplies its attention shape, another its
|
||||
MoE runner -- but two of them must never declare the *same* field for it:
|
||||
nobody would own that value, and which module supplied it would come down to
|
||||
the order of the imports below. ``test_model_override_split.py`` forbids the
|
||||
overlap, which is why this list needs no particular order.
|
||||
the order of the imports below. Keep each field owned by one family module.
|
||||
"""
|
||||
|
||||
from sglang.srt.arg_groups.model_overrides import cohere2_moe # noqa: F401
|
||||
|
||||
@@ -33,8 +33,8 @@ def _minicpm_sala_overrides(server_args: Any, hf_config: Any) -> dict:
|
||||
"minicpm_flashattn": ("fa4" if get_platform().is_blackwell else "fa3"),
|
||||
"minicpm_flashinfer": "flashinfer",
|
||||
}
|
||||
# Literal keys keep the written-field set statically derivable; a loop
|
||||
# variable hides it from the census in test_chain_read_ratchet.py.
|
||||
# Keep the three backend decisions explicit so each resolved field is
|
||||
# easy to review independently.
|
||||
dense_attention = dense_backends.get(cfg.attention_backend)
|
||||
if dense_attention is not None:
|
||||
overrides["attention_backend"] = dense_attention
|
||||
|
||||
@@ -797,8 +797,7 @@ def m3_fp8_attn_gemm_enabled(args) -> bool:
|
||||
# NOTE: The process-wide ServerArgs is owned by the runtime context
|
||||
# (sglang.srt.runtime_context). The two functions below are LEGACY shims kept
|
||||
# for the existing call-sites; they publish/read the same live object by
|
||||
# reference. Do not add new call-sites — the counts are ratcheted
|
||||
# (decrease-only) by test/registered/unit/test_legacy_global_ratchet.py.
|
||||
# reference. Do not add new call-sites.
|
||||
# Imports are in-function so the two modules stay cycle-free at import time.
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _underscore_field_names() -> frozenset:
|
||||
|
||||
@@ -98,19 +98,6 @@ def register_amd_ci(
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Bridge registered diffusion suites to their case-aware pytest adapter."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _enabled(name: str) -> bool:
|
||||
return os.environ.get(name, "").lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def run_diffusion_suite(suite: str) -> None:
|
||||
"""Run one legacy-named diffusion suite without exposing a second CI CLI."""
|
||||
|
||||
from sglang.multimodal_gen.test.runner.diffusion_suite_runner import main
|
||||
|
||||
args = [sys.argv[0], "--suite", suite]
|
||||
optional_values = (
|
||||
("DIFFUSION_PARTITION_ID", "--partition-id"),
|
||||
("DIFFUSION_TOTAL_PARTITIONS", "--total-partitions"),
|
||||
("DIFFUSION_PARTITION_PLAN_JSON", "--partition-plan-json"),
|
||||
("DIFFUSION_PYTEST_FILTER", "--filter"),
|
||||
)
|
||||
for environment_name, option in optional_values:
|
||||
value = os.environ.get(environment_name)
|
||||
if value:
|
||||
args.extend([option, value])
|
||||
if _enabled("DIFFUSION_CONTINUE_ON_ERROR"):
|
||||
args.append("--continue-on-error")
|
||||
|
||||
# Preserve the historical cwd: several diffusion fixtures emit artifacts
|
||||
# relative to ``python/`` rather than to their source file.
|
||||
os.chdir(Path(__file__).resolve().parents[4] / "python")
|
||||
sys.argv = args
|
||||
main()
|
||||
Reference in New Issue
Block a user