diff --git a/docs/contributing/ci/CI_5levels.md b/docs/contributing/ci/CI_5levels.md index 7bad897e7..3baa7ff88 100644 --- a/docs/contributing/ci/CI_5levels.md +++ b/docs/contributing/ci/CI_5levels.md @@ -105,7 +105,8 @@ Through five levels (L1-L5) and common (Common) specifications, the system clari Depends on reality Stability:
- /tests/dfx/stability/tests/test.json
+ /tests/dfx/stability/tests/test_qwen3_omni.json
+ /tests/dfx/stability/tests/test_wan22.json
Reliability:
tests/e2e/reliability/test_{model_name}.py @@ -574,12 +575,12 @@ L5 level testing focuses on the performance of model services under ***long-runn ### 4.2 Testing Content and Scope -- ***Long-term Stability (Stability) Testing***: Uses the `tests/dfx/stability/tests/test.json` configuration to run the service under moderate load for an extended period (e.g., over 12 hours), monitoring whether metrics like memory/VRAM usage, response time, and throughput degrade over time, and whether the service process remains stable. +- ***Long-term Stability (Stability) Testing***: Uses JSON under `tests/dfx/stability/tests/` (for example `test_qwen3_omni.json` and `test_wan22.json`) to run the service under moderate load for an extended period (e.g., over 12 hours), monitoring whether metrics like memory/VRAM usage, response time, and throughput degrade over time, and whether the service process remains stable. - ***Reliability Testing***: Uses `tests/e2e/reliability/test_{model_name}.py` to actively simulate various fault and abnormal scenarios, such as: dependent service interruption, abnormal input data, network flicker, hardware resource preemption, etc., to verify the system's fault tolerance, self-healing, and graceful degradation capabilities. ### 4.3 Test Directory and Execution Files -- ***Stability Test Configuration***: `tests/dfx/stability/tests/test.json` +- ***Stability Test Configuration***: `tests/dfx/stability/tests/test_qwen3_omni.json`, `tests/dfx/stability/tests/test_wan22.json` (one JSON per model / runner family) - ***Reliability Test Suite***: `tests/e2e/reliability/test_{model_name}.py` ### 4.4 Execution Method and Example @@ -590,7 +591,7 @@ L5 level testing focuses on the performance of model services under ***long-runn
Test Examples -When you want to add L5-level stability test cases, you can refer to the following format for case addition in `tests/dfx/stability/tests/test.json`: +When you want to add L5-level stability test cases, add or extend the appropriate JSON file under `tests/dfx/stability/tests/` (for example `test_qwen3_omni.json` for Omni bench traffic, or `test_wan22.json` for diffusion `/v1/videos` workloads). The following illustrates the Qwen3-Omni shape: ```json { @@ -661,7 +662,7 @@ All other optional parameters follow the same rules as the in Chapter 3.4.
-- - ***Stability***: `pytest -s -v tests/dfx/stability/scripts/test_{model_name}.py` +- - ***Stability***: `pytest -s -v tests/dfx/stability/scripts/test_stability_qwen3_omni.py` or `pytest -s -v tests/dfx/stability/scripts/test_stability_wan22.py` (or add `test_stability_.py` alongside a matching JSON config) - ***Reliability***: `pytest -s -v tests/e2e/reliability/test_{model_name}.py` ## Summary diff --git a/docs/contributing/ci/test_guide.md b/docs/contributing/ci/test_guide.md index 0c9ec2fb3..018c47b05 100644 --- a/docs/contributing/ci/test_guide.md +++ b/docs/contributing/ci/test_guide.md @@ -83,6 +83,23 @@ Our test scripts use the pytest framework. First, please use `git clone https:// pytest -s -v -m "full_model and omni and H100" --run-level=full_model ``` +=== "L5 level" + + L5 includes stability and reliability testing. Typical commands: + + ```bash + cd tests + + # Stability: Qwen3-Omni + pytest -s -v dfx/stability/scripts/test_stability_qwen3_omni.py + + # Stability: Wan2.2 (v1/videos diffusion benchmark loop) + pytest -s -v dfx/stability/scripts/test_stability_wan22.py + + ``` + + The latest L5 commands for CI can be found in the [pipeline](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/test-ready.yml). + You can find more information about markers in the documentation: [marker doc](./tests_markers.md) ## Adding New Test Cases diff --git a/tests/dfx/conftest.py b/tests/dfx/conftest.py index c3f6d0a15..607570443 100644 --- a/tests/dfx/conftest.py +++ b/tests/dfx/conftest.py @@ -1,4 +1,7 @@ import json +import os +import subprocess +from datetime import datetime from pathlib import Path from typing import Any @@ -37,12 +40,45 @@ def modify_stage(default_path, updates, deletes): return path +def _build_serve_args(serve_args: Any) -> list[str]: + """Convert server_params.serve_args to a flat CLI args list.""" + if serve_args is None: + return [] + if isinstance(serve_args, list): + return [str(item) for item in serve_args] + if not isinstance(serve_args, dict): + raise TypeError(f"serve_args must be dict/list/None, got {type(serve_args).__name__}") + + args: list[str] = [] + for key, value in serve_args.items(): + flag = f"--{str(key).replace('_', '-')}" + if isinstance(value, bool): + if value: + args.append(flag) + continue + if value is None: + continue + if isinstance(value, (dict, list)): + args.extend([flag, json.dumps(value, ensure_ascii=False, separators=(",", ":"))]) + continue + args.extend([flag, str(value)]) + return args + + def create_unique_server_params( configs: list[dict[str, Any]], stage_configs_dir: Path, ) -> list[tuple[str, str, str | None, str | None, tuple[str, ...]]]: - unique_params = [] - seen = set() + """Return one row per unique server configuration (same 5-tuple shape as upstream). + + ``(test_name, model, deploy_yaml_path, stage_overrides_json, extra_cli_args)``. + + JSON ``server_params.serve_args`` (dict/list) is expanded via ``_build_serve_args`` + and **prepended** to ``extra_cli_args`` so perf / stability ``omni_server`` fixtures + stay identical to main while still honoring ``serve_args`` in benchmark JSON. + """ + unique_params: list[tuple[str, str, str | None, str | None, tuple[str, ...]]] = [] + seen: set[tuple[str, str, str | None, str | None, tuple[str, ...]]] = set() for config in configs: test_name = config["test_name"] server_params = config["server_params"] @@ -63,7 +99,9 @@ def create_unique_server_params( # ``vllm_omni.entrypoints.cli.main serve`` — used for flags that # don't map to stage-level overrides, e.g. ``--async-chunk`` / # ``--no-async-chunk`` toggling the deploy-level async_chunk bool. - extra_cli_args = tuple(server_params.get("extra_cli_args") or ()) + serve_flat = _build_serve_args(server_params.get("serve_args")) + raw_extra = tuple(server_params.get("extra_cli_args") or ()) + extra_cli_args = tuple(serve_flat) + raw_extra server_param = (test_name, model, stage_config_path, stage_overrides_json, extra_cli_args) if server_param not in seen: @@ -109,6 +147,139 @@ def create_benchmark_indices( return indices +def _safe_filename_token(value: Any | None, *, default: str = "na") -> str: + """Make a single path segment safe for result filenames on common filesystems.""" + if value is None: + return default + s = str(value).strip() + for bad in ("/", "\\", ":", "*", "?", '"', "<", ">", "|"): + s = s.replace(bad, "_") + return s if s else default + + +def _resolve_baseline_value( + baseline_raw: Any, + *, + sweep_index: int | None, + max_concurrency: Any = None, + request_rate: Any = None, +) -> Any: + """Pick the baseline threshold for this sweep step.""" + if baseline_raw is None: + return 100000 + if isinstance(baseline_raw, dict): + if max_concurrency is not None: + for key in (max_concurrency, str(max_concurrency)): + if key in baseline_raw: + return baseline_raw[key] + if request_rate is not None: + for key in (request_rate, str(request_rate)): + if key in baseline_raw: + return baseline_raw[key] + raise KeyError( + f"baseline dict has no key for max_concurrency={max_concurrency!r} " + f"or request_rate={request_rate!r}; keys={list(baseline_raw.keys())!r}" + ) + if isinstance(baseline_raw, (list, tuple)): + if sweep_index is None: + raise ValueError("list baseline requires sweep_index") + if not (0 <= sweep_index < len(baseline_raw)): + raise IndexError(f"baseline list len={len(baseline_raw)} has no index {sweep_index}") + return baseline_raw[sweep_index] + return baseline_raw + + +def _baseline_thresholds_for_step( + baseline_data: dict[str, Any], + *, + sweep_index: int | None = None, + max_concurrency: Any = None, + request_rate: Any = None, +) -> dict[str, Any]: + """Resolve baseline config to one threshold per metric for this iteration.""" + return { + metric_name: _resolve_baseline_value( + baseline_raw, + sweep_index=sweep_index, + max_concurrency=max_concurrency, + request_rate=request_rate, + ) + for metric_name, baseline_raw in baseline_data.items() + } + + +def run_benchmark( + args: list[str], + test_name: str, + flow: Any, + dataset_name: str, + num_prompt: int, + *, + baseline_config: dict[str, Any] | None = None, + sweep_index: int | None = None, + request_rate: Any | None = None, + max_concurrency: Any | None = None, + random_input_len: Any | None = None, + random_output_len: Any | None = None, +) -> dict[str, Any]: + """Run one ``vllm bench serve --omni`` iteration and return parsed metrics.""" + current_dt = datetime.now().strftime("%Y%m%d-%H%M%S") + ri = _safe_filename_token(random_input_len) + ro = _safe_filename_token(random_output_len) + result_filename = f"result_{test_name}_{dataset_name}_{flow}_{num_prompt}_in{ri}_out{ro}_{current_dt}.json" + if "--result-filename" in args: + print(f"The result file will be overwritten by {result_filename}") + command = ( + ["vllm", "bench", "serve", "--omni"] + + args + + [ + "--num-warmups", + "2", + "--save-result", + "--result-dir", + os.environ.get("BENCHMARK_DIR", "tests"), + "--result-filename", + result_filename, + ] + ) + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True + ) + + for line in iter(process.stdout.readline, ""): + print(line, end=" ") + + for line in iter(process.stderr.readline, ""): + print(line, end=" ") + + if "--result-dir" in command: + index = command.index("--result-dir") + result_dir = command[index + 1] + else: + result_dir = "./" + + result_path = os.path.join(result_dir, result_filename) + with open(result_path, encoding="utf-8") as f: + result = json.load(f) + + if baseline_config: + result["baseline"] = _baseline_thresholds_for_step( + baseline_config, + sweep_index=sweep_index, + request_rate=request_rate, + max_concurrency=max_concurrency, + ) + else: + result["baseline"] = {} + if random_input_len is not None: + result["random_input_len"] = random_input_len + if random_output_len is not None: + result["random_output_len"] = random_output_len + with open(result_path, "w", encoding="utf-8") as f: + json.dump(result, f, ensure_ascii=False, indent=2) + return result + + def pytest_addoption(parser: pytest.Parser) -> None: """Register shared CLI options for DFX benchmark suites.""" parser.addoption( diff --git a/tests/dfx/stability/conftest.py b/tests/dfx/stability/conftest.py index e36c88b9a..30718d4bf 100644 --- a/tests/dfx/stability/conftest.py +++ b/tests/dfx/stability/conftest.py @@ -1,4 +1,14 @@ -"""Stability pytest hooks and fixtures.""" +""" +Stability-specific conftest: when pytest is executed under this directory, +resource monitoring is started before each test and finalized after each test, +so each stability test case gets its own HTML report (one report per case). +No need to wrap pytest with `bash resource_monitor.sh run -- pytest ...`. + +Duration-based benchmark helper functions are hosted in ``helpers.py``, +while this file focuses on pytest fixtures and setup/teardown. +""" + +from __future__ import annotations import subprocess import sys @@ -6,12 +16,66 @@ import threading import pytest +from tests.dfx.conftest import get_benchmark_params_for_server from tests.dfx.stability.helpers import ( finalize_resource_monitor, report_latest_gpu_samples, start_resource_monitor, wait_for_run_dir, ) +from tests.helpers.runtime import OmniServer + +DEFAULT_STABILITY_SERVER_TIMEOUT_ARGS = ["--stage-init-timeout", "600", "--init-timeout", "900"] + +_omni_server_lock = threading.Lock() + + +@pytest.fixture(scope="module") +def omni_server(request: pytest.FixtureRequest): + """Start OmniServer for stability tests, with per-module timeout override.""" + timeout_args = getattr(request.module, "STABILITY_SERVER_TIMEOUT_ARGS", DEFAULT_STABILITY_SERVER_TIMEOUT_ARGS) + with _omni_server_lock: + # Same 5-tuple and CLI composition as ``tests/dfx/perf/scripts/run_benchmark.py`` on main; + # ``serve_args`` from JSON are folded into ``extra_cli_args`` inside + # ``create_unique_server_params``. + test_name, model, deploy_path, stage_overrides, extra_cli_args = request.param + + print(f"Starting OmniServer with test: {test_name}, model: {model}") + server_args = list(timeout_args) + if deploy_path: + server_args = ["--deploy-config", deploy_path] + server_args + if stage_overrides: + server_args = ["--stage-overrides", stage_overrides] + server_args + if extra_cli_args: + server_args = list(extra_cli_args) + server_args + with OmniServer(model, server_args) as server: + server.test_name = test_name + print("OmniServer started successfully") + yield server + print("OmniServer stopping...") + print("OmniServer stopped") + + +@pytest.fixture +def stability_benchmark_params(request: pytest.FixtureRequest, omni_server): + test_name, param_index = request.param + if test_name != omni_server.test_name: + pytest.skip(f"Skipping parameter for {test_name} - current server is {omni_server.test_name}") + + server_to_benchmark_mapping = getattr(request.module, "server_to_benchmark_mapping", None) + if server_to_benchmark_mapping is None: + raise ValueError("server_to_benchmark_mapping must be defined in the test module") + + all_params = get_benchmark_params_for_server(test_name, server_to_benchmark_mapping) + if not all_params: + raise ValueError(f"No benchmark parameters found for test: {test_name}") + if param_index >= len(all_params): + raise ValueError(f"No benchmark parameters found for index {param_index} in test: {test_name}") + + current = param_index + 1 + total = len(all_params) + print(f"\n Running benchmark {current}/{total} for {test_name}") + return {"test_name": test_name, "params": all_params[param_index]} @pytest.fixture(autouse=True) diff --git a/tests/dfx/stability/helpers.py b/tests/dfx/stability/helpers.py index 3956bd213..3a873f69c 100644 --- a/tests/dfx/stability/helpers.py +++ b/tests/dfx/stability/helpers.py @@ -1,17 +1,32 @@ -"""Stability resource monitor helpers.""" +"""Stability helpers for resource monitoring and benchmark execution.""" from __future__ import annotations +import json import os +import random +import re +import shlex import subprocess import sys +import tempfile import threading import time +from collections.abc import Callable from pathlib import Path +from typing import Any + +from tests.dfx.conftest import run_benchmark STABILITY_DIR = Path(__file__).resolve().parent RESOURCE_MONITOR_SCRIPT = STABILITY_DIR / "scripts" / "resource_monitor.sh" REPO_ROOT = STABILITY_DIR.parent.parent.parent +_BUCKET_KEY_PATTERN = re.compile(r"^\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^,]+)\s*\)$") + +RunOneBatchFn = Callable[ + [str, int, str, dict[str, Any], int, float | None, int | None, str, int], + dict[str, Any], +] def start_resource_monitor(): @@ -115,3 +130,375 @@ def finalize_resource_monitor() -> str | None: return None except (FileNotFoundError, OSError, subprocess.TimeoutExpired): return None + + +def _normalize_bench_metrics(raw: dict[str, Any]) -> dict[str, Any]: + completed = int(raw.get("completed", raw.get("completed_requests", 0) or 0)) + failed = int(raw.get("failed", raw.get("failed_requests", 0) or 0)) + duration = float(raw.get("duration", 0.0) or 0.0) + errors = list(raw.get("errors") or []) + if failed and not errors: + errors = [f"{failed} benchmark request(s) failed"] + return {"completed": completed, "failed": failed, "duration": duration, "errors": errors} + + +def _build_base_args(params: dict[str, Any], host: str, port: int) -> list[str]: + exclude = { + "request_rate", + "max_concurrency", + "num_prompts", + "baseline", + "duration_sec", + "num_prompts_per_batch", + } + args = ["--host", host, "--port", str(port)] + for key, value in params.items(): + if key in exclude or value is None: + continue + arg_name = f"--{key.replace('_', '-')}" + if isinstance(value, bool) and value: + args.append(arg_name) + elif isinstance(value, dict): + args.extend([arg_name, json.dumps(value, ensure_ascii=False, separators=(",", ":"))]) + elif not isinstance(value, bool): + args.extend([arg_name, str(value)]) + return args + + +def _build_diffusion_cmd( + host: str, + port: int, + model: str, + params: dict[str, Any], + num_prompts: int, + request_rate: float | None, + max_concurrency: int | None, + output_path: Path, + diffusion_benchmark_script: Path, +) -> list[str]: + skip_keys = { + "request_rate", + "max_concurrency", + "num_prompts", + "baseline", + "duration_sec", + "num_prompts_per_batch", + } + cmd: list[str] = [ + sys.executable, + "-u", + str(diffusion_benchmark_script), + "--host", + host, + "--port", + str(port), + "--model", + model, + "--output-file", + str(output_path), + ] + for key, value in params.items(): + if key in skip_keys or value is None: + continue + flag = f"--{str(key).replace('_', '-')}" + if isinstance(value, bool) and value: + cmd.append(flag) + elif isinstance(value, bool): + continue + elif isinstance(value, (dict, list)): + cmd.extend([flag, json.dumps(value, ensure_ascii=False, separators=(",", ":"))]) + else: + cmd.extend([flag, str(value)]) + + cmd.extend(["--num-prompts", str(num_prompts)]) + if request_rate is not None: + cmd.extend(["--request-rate", str(request_rate)]) + else: + cmd.extend(["--max-concurrency", str(max_concurrency), "--request-rate", "inf"]) + return cmd + + +def _sample_int_from_range_spec(value: Any, rng: random.Random) -> Any: + """Resolve one value that may be scalar or range spec into an int.""" + if isinstance(value, int): + return value + + if isinstance(value, (list, tuple)) and len(value) == 2 and all(isinstance(v, int) for v in value): + low, high = int(value[0]), int(value[1]) + if low > high: + low, high = high, low + return rng.randint(low, high) + + if isinstance(value, dict) and {"min", "max"} <= set(value): + low, high = int(value["min"]), int(value["max"]) + if low > high: + low, high = high, low + return rng.randint(low, high) + + if isinstance(value, str): + raw = value.strip() + if raw.isdigit(): + return int(raw) + if "-" in raw: + parts = [p.strip() for p in raw.split("-", 1)] + if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit(): + low, high = int(parts[0]), int(parts[1]) + if low > high: + low, high = high, low + return rng.randint(low, high) + + return value + + +def _sample_bucket_key(raw_key: str, rng: random.Random) -> str: + """Sample bucket tuple keys that use range syntax, e.g. ``(128-512, 128-512, 1)``.""" + match = _BUCKET_KEY_PATTERN.match(raw_key.strip()) + if not match: + return raw_key + + sampled_parts: list[int] = [] + for token in match.groups(): + sampled = _sample_int_from_range_spec(token.strip(), rng) + if not isinstance(sampled, int): + return raw_key + sampled_parts.append(sampled) + + # For video buckets (height>0 and num_frames>1), enforce even H/W to avoid + # ffmpeg yuv420p encoding/decoding failures ("Could not open video stream"). + if sampled_parts[0] > 0 and sampled_parts[2] > 1: + sampled_parts[0] = max(2, sampled_parts[0] - (sampled_parts[0] % 2)) + sampled_parts[1] = max(2, sampled_parts[1] - (sampled_parts[1] % 2)) + + return f"({sampled_parts[0]}, {sampled_parts[1]}, {sampled_parts[2]})" + + +def _sample_stability_batch_params(params: dict[str, Any], batch_index: int) -> dict[str, Any]: + """Materialize per-batch random values for configured range fields.""" + sampled = dict(params) + rng = random.Random(time.time_ns() + batch_index) + + for field_name in ( + "random_input_len", + "random_output_len", + "random_mm_base_items_per_request", + "width", + "height", + ): + if field_name in sampled: + sampled[field_name] = _sample_int_from_range_spec(sampled[field_name], rng) + + bucket_config = sampled.get("random_mm_bucket_config") + if isinstance(bucket_config, dict): + sampled_bucket_config: dict[str, float] = {} + for raw_key, probability in bucket_config.items(): + sampled_key = _sample_bucket_key(str(raw_key), rng) + sampled_bucket_config[sampled_key] = sampled_bucket_config.get(sampled_key, 0.0) + float(probability) + sampled["random_mm_bucket_config"] = sampled_bucket_config + + return sampled + + +def _run_one_vllm_bench_batch( + host: str, + port: int, + _model: str, + params: dict[str, Any], + num_prompts: int, + request_rate: float | None, + max_concurrency: int | None, + result_dir: str, + batch_index: int, +) -> dict[str, Any]: + base = _build_base_args(params, host, port) + if request_rate is not None: + args = base + ["--request-rate", str(request_rate), "--num-prompts", str(num_prompts)] + flow = request_rate + else: + args = base + [ + "--max-concurrency", + str(max_concurrency), + "--num-prompts", + str(num_prompts), + "--request-rate", + "inf", + ] + flow = max_concurrency + + # Print the exact per-batch benchmark CLI (randomized params are already materialized). + preview_cmd = ["vllm", "bench", "serve", "--omni", *args] + print(f"\n[Stability][Batch {batch_index}] Benchmark command:") + print(shlex.join(preview_cmd)) + + dataset_name = params.get("dataset_name", "random") + old_benchmark_dir = os.environ.get("BENCHMARK_DIR") + try: + os.environ["BENCHMARK_DIR"] = result_dir + result = run_benchmark( + args=args, + test_name="stability", + flow=flow, + dataset_name=dataset_name, + num_prompt=num_prompts, + random_input_len=params.get("random_input_len"), + random_output_len=params.get("random_output_len"), + ) + return _normalize_bench_metrics(result) + except (FileNotFoundError, OSError) as exc: + return { + "completed": 0, + "failed": 1, + "duration": 0.0, + "errors": [f"Benchmark batch failed: {type(exc).__name__}: {exc}"], + } + finally: + if old_benchmark_dir is not None: + os.environ["BENCHMARK_DIR"] = old_benchmark_dir + elif "BENCHMARK_DIR" in os.environ: + os.environ.pop("BENCHMARK_DIR") + + +def _run_one_diffusion_batch( + host: str, + port: int, + model: str, + params: dict[str, Any], + num_prompts: int, + request_rate: float | None, + max_concurrency: int | None, + _result_dir: str, + _batch_index: int, +) -> dict[str, Any]: + diffusion_benchmark_script = Path(REPO_ROOT / "benchmarks" / "diffusion" / "diffusion_benchmark_serving.py") + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", prefix="stability_diffusion_", delete=False) as tmp: + out_path = Path(tmp.name) + try: + cmd = _build_diffusion_cmd( + host, + port, + model, + params, + num_prompts, + request_rate, + max_concurrency, + out_path, + diffusion_benchmark_script, + ) + proc = subprocess.run( + cmd, + cwd=str(REPO_ROOT), + capture_output=True, + text=True, + ) + if proc.stdout: + print(proc.stdout, end="" if proc.stdout.endswith("\n") else "\n") + if proc.stderr: + print(proc.stderr, end="" if proc.stderr.endswith("\n") else "\n") + if proc.returncode != 0: + return { + "completed": 0, + "failed": 1, + "duration": 0.0, + "errors": [f"diffusion_benchmark_serving.py exited {proc.returncode}"], + } + if not out_path.is_file(): + return { + "completed": 0, + "failed": 1, + "duration": 0.0, + "errors": [f"Missing benchmark output: {out_path}"], + } + with open(out_path, encoding="utf-8") as file: + metrics = json.load(file) + return _normalize_bench_metrics(metrics) + except (FileNotFoundError, OSError, json.JSONDecodeError) as exc: + return { + "completed": 0, + "failed": 1, + "duration": 0.0, + "errors": [f"Diffusion batch failed: {type(exc).__name__}: {exc}"], + } + finally: + out_path.unlink(missing_ok=True) + + +def merge_batch_results(batch_results: list[dict[str, Any]], total_duration_sec: float) -> dict[str, Any]: + if not batch_results: + return {"completed": 0, "failed": 0, "duration": total_duration_sec, "errors": []} + + completed = sum(result.get("completed", 0) for result in batch_results) + failed = sum(result.get("failed", 0) for result in batch_results) + merged: dict[str, Any] = { + "completed": completed, + "failed": failed, + "duration": total_duration_sec, + "errors": [], + } + for result in batch_results: + merged["errors"].extend(result.get("errors") or []) + return merged + + +def print_merged_report(result: dict[str, Any]) -> None: + fmt = "{:<40} {:<10}" + fmt_float = "{:<40} {:<10.2f}" + completed = result.get("completed", 0) + failed = result.get("failed", 0) + duration = float(result.get("duration", 0.0) or 0.0) + print("\n============ Stability Benchmark Summary ============") + print(fmt.format("Successful requests:", completed)) + print(fmt.format("Failed requests:", failed)) + print(fmt_float.format("Total duration (s):", duration)) + print("==================================================\n") + + +def run_stability_benchmark_loop( + host: str, + port: int, + model: str, + duration_sec: int | float, + params: dict[str, Any], + *, + request_rate: float | None, + max_concurrency: int | None, + result_dir: str, + num_prompts_per_batch: int, + run_one_batch: RunOneBatchFn, + result_filename: str | None = None, +) -> dict[str, Any]: + if (request_rate is None) == (max_concurrency is None): + raise ValueError("Exactly one of request_rate or max_concurrency must be specified") + + start_time = time.perf_counter() + batch_results: list[dict[str, Any]] = [] + batch_index = 0 + + while True: + if (time.perf_counter() - start_time) >= duration_sec: + break + sampled_params = _sample_stability_batch_params(params, batch_index) + result = run_one_batch( + host, + port, + model, + sampled_params, + num_prompts_per_batch, + request_rate, + max_concurrency, + result_dir, + batch_index, + ) + batch_results.append(result) + batch_index += 1 + if (time.perf_counter() - start_time) >= duration_sec: + break + + total_duration = time.perf_counter() - start_time + merged = merge_batch_results(batch_results, total_duration) + print_merged_report(merged) + + if result_filename and result_dir: + result_path = Path(result_dir) / result_filename + with open(result_path, "w", encoding="utf-8") as file: + json.dump(merged, file, indent=2, ensure_ascii=False) + + return merged diff --git a/tests/dfx/stability/scripts/test_benchmark_stability.py b/tests/dfx/stability/scripts/test_benchmark_stability.py deleted file mode 100644 index 620241762..000000000 --- a/tests/dfx/stability/scripts/test_benchmark_stability.py +++ /dev/null @@ -1,297 +0,0 @@ -""" -Stability test cases: start OmniServer first, then run benchmark traffic with either -`request-rate` or `max-concurrency` for a fixed duration. No new requests are sent -after the duration is reached, and the test asserts that there are no failed requests. - -The overall flow matches the perf logic: `load_configs`, `modify_stage`, -`create_unique_server_params`, `create_test_parameter_mapping`, -`get_benchmark_params_for_server`, `create_benchmark_indices`, and the -`omni_server` fixture are aligned with perf. Only the benchmark execution -(`run_stability_benchmark`, which is duration-based here) and the test cases differ. - -All test-specific parameters, such as `duration_sec`, `request_rate` / -`max_concurrency`, and `num_prompts_per_batch`, are configured in -`tests/dfx/stability/tests/test.json` and are no longer overridden -through environment variables. -""" - -import json -import os -import threading -import time -from pathlib import Path -from typing import Any - -import pytest - -from tests.dfx.conftest import ( - create_benchmark_indices, - create_test_parameter_mapping, - create_unique_server_params, - get_benchmark_params_for_server, - load_configs, -) -from tests.dfx.perf.scripts.run_benchmark import run_benchmark -from tests.helpers.runtime import OmniServer - -STABILITY_DIR = Path(__file__).resolve().parent.parent -DEPLOY_CONFIGS_DIR = STABILITY_DIR / "deploy" -CONFIG_FILE_PATH = str(STABILITY_DIR / "tests" / "test.json") -DEFAULT_NUM_PROMPTS_PER_BATCH = 20 - - -try: - BENCHMARK_CONFIGS = load_configs(CONFIG_FILE_PATH) -except FileNotFoundError: - BENCHMARK_CONFIGS = [] - -test_params = create_unique_server_params(BENCHMARK_CONFIGS, DEPLOY_CONFIGS_DIR) if BENCHMARK_CONFIGS else [] -server_to_benchmark_mapping = create_test_parameter_mapping(BENCHMARK_CONFIGS) if BENCHMARK_CONFIGS else {} - -_omni_server_lock = threading.Lock() - - -benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchmark_mapping) - - -def _build_base_args(params: dict[str, Any], host: str, port: int) -> list[str]: - exclude = { - "request_rate", - "max_concurrency", - "num_prompts", - "baseline", - "duration_sec", - "num_prompts_per_batch", - } - args = ["--host", host, "--port", str(port)] - for key, value in params.items(): - if key in exclude or value is None: - continue - arg_name = f"--{key.replace('_', '-')}" - if isinstance(value, bool) and value: - args.append(arg_name) - elif isinstance(value, dict): - args.extend([arg_name, json.dumps(value, ensure_ascii=False, separators=(",", ":"))]) - elif not isinstance(value, bool): - args.extend([arg_name, str(value)]) - return args - - -def _run_one_benchmark_batch( - host: str, - port: int, - params: dict[str, Any], - num_prompts: int, - request_rate: float | None, - max_concurrency: int | None, - result_dir: str, - batch_index: int, -) -> dict[str, Any]: - base = _build_base_args(params, host, port) - if request_rate is not None: - args = base + ["--request-rate", str(request_rate), "--num-prompts", str(num_prompts)] - flow = request_rate - else: - args = base + [ - "--max-concurrency", - str(max_concurrency), - "--num-prompts", - str(num_prompts), - "--request-rate", - "inf", - ] - flow = max_concurrency - - dataset_name = params.get("dataset_name", "random") - old_benchmark_dir = os.environ.get("BENCHMARK_DIR") - try: - os.environ["BENCHMARK_DIR"] = result_dir - result = run_benchmark( - args=args, - test_name="stability", - flow=flow, - dataset_name=dataset_name, - num_prompt=num_prompts, - random_input_len=params.get("random_input_len"), - random_output_len=params.get("random_output_len"), - ) - return result - except (FileNotFoundError, OSError) as e: - # Surface batch failure so the stability test does not false-pass when - # run_benchmark fails before writing JSON (e.g. command not found). - return { - "completed": 0, - "failed": 1, - "duration": 0.0, - "errors": [f"Benchmark batch failed: {type(e).__name__}: {e}"], - } - finally: - if old_benchmark_dir is not None: - os.environ["BENCHMARK_DIR"] = old_benchmark_dir - elif "BENCHMARK_DIR" in os.environ: - os.environ.pop("BENCHMARK_DIR") - - -def _merge_batch_results(batch_results: list[dict[str, Any]], total_duration_sec: float) -> dict[str, Any]: - if not batch_results: - return {"completed": 0, "failed": 0, "duration": total_duration_sec, "errors": []} - - completed = sum(r.get("completed", 0) for r in batch_results) - failed = sum(r.get("failed", 0) for r in batch_results) - merged: dict[str, Any] = { - "completed": completed, - "failed": failed, - "duration": total_duration_sec, - "errors": [], - } - for r in batch_results: - merged["errors"].extend(r.get("errors") or []) - return merged - - -def _print_merged_report(result: dict[str, Any]) -> None: - """Print the final summary: successful requests, failed requests, and total duration only.""" - fmt = "{:<40} {:<10}" - fmt_float = "{:<40} {:<10.2f}" - completed = result.get("completed", 0) - failed = result.get("failed", 0) - duration = float(result.get("duration", 0.0) or 0.0) - print("\n============ Stability Benchmark Summary ============") - print(fmt.format("Successful requests:", completed)) - print(fmt.format("Failed requests:", failed)) - print(fmt_float.format("Total duration (s):", duration)) - print("==================================================\n") - - -def run_stability_benchmark( - host: str, - port: int, - duration_sec: int | float, - params: dict[str, Any], - *, - request_rate: float | None = None, - max_concurrency: int | None = None, - result_filename: str | None = None, - result_dir: str = "./", - num_prompts_per_batch: int = DEFAULT_NUM_PROMPTS_PER_BATCH, -) -> dict[str, Any]: - if (request_rate is None) == (max_concurrency is None): - raise ValueError("Exactly one of request_rate or max_concurrency must be specified") - - start_time = time.perf_counter() - batch_results: list[dict[str, Any]] = [] - batch_index = 0 - - while True: - if (time.perf_counter() - start_time) >= duration_sec: - break - result = _run_one_benchmark_batch( - host=host, - port=port, - params=params, - num_prompts=num_prompts_per_batch, - request_rate=request_rate, - max_concurrency=max_concurrency, - result_dir=result_dir, - batch_index=batch_index, - ) - batch_results.append(result) - batch_index += 1 - if (time.perf_counter() - start_time) >= duration_sec: - break - - total_duration = time.perf_counter() - start_time - merged = _merge_batch_results(batch_results, total_duration) - _print_merged_report(merged) - - if result_filename and result_dir: - result_path = Path(result_dir) / result_filename - with open(result_path, "w", encoding="utf-8") as f: - json.dump(merged, f, indent=2, ensure_ascii=False) - - return merged - - -@pytest.fixture(scope="module") -def omni_server(request): - """Start vLLM-Omni server as a subprocess with actual model weights. - Uses session scope so the server starts only once for the entire test session. - Multi-stage initialization can take 10-20+ minutes. - """ - with _omni_server_lock: - test_name, model, stage_config_path, stage_overrides, extra_cli_args = request.param - - print(f"Starting OmniServer with test: {test_name}, model: {model}") - - server_args = ["--stage-init-timeout", "120"] - # --deploy-config and --stage-overrides compose at the CLI (see vllm_omni/entrypoints/utils.py): - # deploy-config sets the base; stage-overrides are applied on top. Both can be set. - if stage_config_path: - server_args = ["--deploy-config", stage_config_path] + server_args - if stage_overrides: - server_args = ["--stage-overrides", stage_overrides] + server_args - if extra_cli_args: - server_args = list(extra_cli_args) + server_args - with OmniServer(model, server_args) as server: - server.test_name = test_name - print("OmniServer started successfully") - yield server - print("OmniServer stopping...") - - print("OmniServer stopped") - - -@pytest.fixture(params=benchmark_indices) -def stability_benchmark_params(request, omni_server): - """Benchmark parameters fixture with proper parametrization (same as perf).""" - test_name, param_index = request.param - - if test_name != omni_server.test_name: - pytest.skip(f"Skipping parameter for {test_name} - current server is {omni_server.test_name}") - - all_params = get_benchmark_params_for_server(test_name, server_to_benchmark_mapping) - - if not all_params: - raise ValueError(f"No benchmark parameters found for test: {test_name}") - - if param_index >= len(all_params): - raise ValueError(f"No benchmark parameters found for index {param_index} in test: {test_name}") - - current = param_index + 1 - total = len(all_params) - print(f"\n Running benchmark {current}/{total} for {test_name}") - - return {"test_name": test_name, "params": all_params[param_index]} - - -@pytest.mark.slow -@pytest.mark.parametrize("omni_server", test_params, indirect=True) -@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True) -def test_benchmark_stability(omni_server, stability_benchmark_params): - """Run the benchmark for a fixed duration using request-rate or max-concurrency and assert zero failed requests.""" - test_name = stability_benchmark_params["test_name"] - params = stability_benchmark_params["params"] - duration_sec = params.get("duration_sec", 300) - num_prompts_per_batch = params.get("num_prompts_per_batch", DEFAULT_NUM_PROMPTS_PER_BATCH) - request_rate = params.get("request_rate") - max_concurrency = params.get("max_concurrency") - - bench_params = { - k: v - for k, v in params.items() - if k not in ("duration_sec", "request_rate", "max_concurrency", "num_prompts_per_batch") - } - - result = run_stability_benchmark( - host=omni_server.host, - port=omni_server.port, - duration_sec=duration_sec, - params=bench_params, - request_rate=request_rate, - max_concurrency=max_concurrency, - result_dir=str(STABILITY_DIR), - num_prompts_per_batch=num_prompts_per_batch, - ) - - assert result.get("failed", 0) == 0, f"[{test_name}] Failed requests detected: {result.get('errors', [])}" - assert result.get("completed", 0) > 0, f"[{test_name}] No requests completed" diff --git a/tests/dfx/stability/scripts/test_stability_qwen3_omni.py b/tests/dfx/stability/scripts/test_stability_qwen3_omni.py new file mode 100644 index 000000000..d1c2af8cf --- /dev/null +++ b/tests/dfx/stability/scripts/test_stability_qwen3_omni.py @@ -0,0 +1,68 @@ +""" +Qwen3-Omni stability: OmniServer + ``vllm bench serve --omni`` for a fixed duration. + +Configuration: ``tests/dfx/stability/tests/test_qwen3_omni.json``. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tests.dfx.conftest import ( + create_benchmark_indices, + create_test_parameter_mapping, + create_unique_server_params, + load_configs, +) +from tests.dfx.stability.helpers import _run_one_vllm_bench_batch, run_stability_benchmark_loop + +STABILITY_DIR = Path(__file__).resolve().parent.parent +DEPLOY_CONFIGS_DIR = STABILITY_DIR / "deploy" +CONFIG_FILE_PATH = str(STABILITY_DIR / "tests" / "test_qwen3_omni.json") +DEFAULT_NUM_PROMPTS_PER_BATCH = 20 +STABILITY_SERVER_TIMEOUT_ARGS = ["--stage-init-timeout", "600"] + +try: + BENCHMARK_CONFIGS = load_configs(CONFIG_FILE_PATH) +except FileNotFoundError: + BENCHMARK_CONFIGS = [] + +test_params = create_unique_server_params(BENCHMARK_CONFIGS, DEPLOY_CONFIGS_DIR) if BENCHMARK_CONFIGS else [] +server_to_benchmark_mapping = create_test_parameter_mapping(BENCHMARK_CONFIGS) if BENCHMARK_CONFIGS else {} +benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchmark_mapping) + + +@pytest.mark.slow +@pytest.mark.parametrize("omni_server", test_params, indirect=True) +@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True) +def test_stability_qwen3_omni(omni_server, stability_benchmark_params): + test_name = stability_benchmark_params["test_name"] + params = stability_benchmark_params["params"] + duration_sec = params.get("duration_sec", 300) + num_prompts_per_batch = params.get("num_prompts_per_batch", DEFAULT_NUM_PROMPTS_PER_BATCH) + request_rate = params.get("request_rate") + max_concurrency = params.get("max_concurrency") + + bench_params = { + k: v + for k, v in params.items() + if k not in ("duration_sec", "request_rate", "max_concurrency", "num_prompts_per_batch") + } + + result = run_stability_benchmark_loop( + host=omni_server.host, + port=omni_server.port, + model=omni_server.model, + duration_sec=duration_sec, + params=bench_params, + request_rate=request_rate, + max_concurrency=max_concurrency, + result_dir=str(STABILITY_DIR), + num_prompts_per_batch=num_prompts_per_batch, + run_one_batch=_run_one_vllm_bench_batch, + ) + + assert result.get("failed", 0) == 0, f"[{test_name}] Failed requests detected: {result.get('errors', [])}" + assert result.get("completed", 0) > 0, f"[{test_name}] No requests completed" diff --git a/tests/dfx/stability/scripts/test_stability_qwen3_tts.py b/tests/dfx/stability/scripts/test_stability_qwen3_tts.py new file mode 100644 index 000000000..beccd67d9 --- /dev/null +++ b/tests/dfx/stability/scripts/test_stability_qwen3_tts.py @@ -0,0 +1,68 @@ +""" +Qwen3-TTS stability: OmniServer + ``vllm bench serve --omni`` for a fixed duration. + +Configuration: ``tests/dfx/stability/tests/test_qwen3_tts.json``. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tests.dfx.conftest import ( + create_benchmark_indices, + create_test_parameter_mapping, + create_unique_server_params, + load_configs, +) +from tests.dfx.stability.helpers import _run_one_vllm_bench_batch, run_stability_benchmark_loop + +STABILITY_DIR = Path(__file__).resolve().parent.parent +DEPLOY_CONFIGS_DIR = STABILITY_DIR / "deploy" +CONFIG_FILE_PATH = str(STABILITY_DIR / "tests" / "test_qwen3_tts.json") +DEFAULT_NUM_PROMPTS_PER_BATCH = 20 +STABILITY_SERVER_TIMEOUT_ARGS = ["--stage-init-timeout", "600"] + +try: + BENCHMARK_CONFIGS = load_configs(CONFIG_FILE_PATH) +except FileNotFoundError: + BENCHMARK_CONFIGS = [] + +test_params = create_unique_server_params(BENCHMARK_CONFIGS, DEPLOY_CONFIGS_DIR) if BENCHMARK_CONFIGS else [] +server_to_benchmark_mapping = create_test_parameter_mapping(BENCHMARK_CONFIGS) if BENCHMARK_CONFIGS else {} +benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchmark_mapping) + + +@pytest.mark.slow +@pytest.mark.parametrize("omni_server", test_params, indirect=True) +@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True) +def test_stability_qwen3_tts(omni_server, stability_benchmark_params): + test_name = stability_benchmark_params["test_name"] + params = stability_benchmark_params["params"] + duration_sec = params.get("duration_sec", 300) + num_prompts_per_batch = params.get("num_prompts_per_batch", DEFAULT_NUM_PROMPTS_PER_BATCH) + request_rate = params.get("request_rate") + max_concurrency = params.get("max_concurrency") + + bench_params = { + k: v + for k, v in params.items() + if k not in ("duration_sec", "request_rate", "max_concurrency", "num_prompts_per_batch") + } + + result = run_stability_benchmark_loop( + host=omni_server.host, + port=omni_server.port, + model=omni_server.model, + duration_sec=duration_sec, + params=bench_params, + request_rate=request_rate, + max_concurrency=max_concurrency, + result_dir=str(STABILITY_DIR), + num_prompts_per_batch=num_prompts_per_batch, + run_one_batch=_run_one_vllm_bench_batch, + ) + + assert result.get("failed", 0) == 0, f"[{test_name}] Failed requests detected: {result.get('errors', [])}" + assert result.get("completed", 0) > 0, f"[{test_name}] No requests completed" diff --git a/tests/dfx/stability/scripts/test_stability_qwen_image.py b/tests/dfx/stability/scripts/test_stability_qwen_image.py new file mode 100644 index 000000000..a90e2092f --- /dev/null +++ b/tests/dfx/stability/scripts/test_stability_qwen_image.py @@ -0,0 +1,68 @@ +""" +Qwen-Image stability: OmniServer (diffusion) + ``diffusion_benchmark_serving.py``. + +Configuration: ``tests/dfx/stability/tests/test_qwen_image.json``. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tests.dfx.conftest import ( + create_benchmark_indices, + create_test_parameter_mapping, + create_unique_server_params, + load_configs, +) +from tests.dfx.stability.helpers import _run_one_diffusion_batch, run_stability_benchmark_loop + +STABILITY_DIR = Path(__file__).resolve().parent.parent +DEPLOY_CONFIGS_DIR = STABILITY_DIR / "deploy" +CONFIG_FILE_PATH = str(STABILITY_DIR / "tests" / "test_qwen_image.json") +DEFAULT_NUM_PROMPTS_PER_BATCH = 20 +STABILITY_SERVER_TIMEOUT_ARGS = ["--stage-init-timeout", "600", "--init-timeout", "900"] + +try: + BENCHMARK_CONFIGS = load_configs(CONFIG_FILE_PATH) +except FileNotFoundError: + BENCHMARK_CONFIGS = [] + +test_params = create_unique_server_params(BENCHMARK_CONFIGS, DEPLOY_CONFIGS_DIR) if BENCHMARK_CONFIGS else [] +server_to_benchmark_mapping = create_test_parameter_mapping(BENCHMARK_CONFIGS) if BENCHMARK_CONFIGS else {} +benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchmark_mapping) + + +@pytest.mark.slow +@pytest.mark.parametrize("omni_server", test_params, indirect=True) +@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True) +def test_stability_qwen_image(omni_server, stability_benchmark_params): + test_name = stability_benchmark_params["test_name"] + params = stability_benchmark_params["params"] + duration_sec = params.get("duration_sec", 300) + num_prompts_per_batch = params.get("num_prompts_per_batch", DEFAULT_NUM_PROMPTS_PER_BATCH) + request_rate = params.get("request_rate") + max_concurrency = params.get("max_concurrency") + + bench_params = { + k: v + for k, v in params.items() + if k not in ("duration_sec", "request_rate", "max_concurrency", "num_prompts_per_batch") + } + + result = run_stability_benchmark_loop( + host=omni_server.host, + port=omni_server.port, + model=omni_server.model, + duration_sec=duration_sec, + params=bench_params, + request_rate=request_rate, + max_concurrency=max_concurrency, + result_dir=str(STABILITY_DIR), + num_prompts_per_batch=num_prompts_per_batch, + run_one_batch=_run_one_diffusion_batch, + ) + + assert result.get("failed", 0) == 0, f"[{test_name}] Failed requests detected: {result.get('errors', [])}" + assert result.get("completed", 0) > 0, f"[{test_name}] No requests completed" diff --git a/tests/dfx/stability/scripts/test_stability_wan22.py b/tests/dfx/stability/scripts/test_stability_wan22.py new file mode 100644 index 000000000..afe9c4d0c --- /dev/null +++ b/tests/dfx/stability/scripts/test_stability_wan22.py @@ -0,0 +1,68 @@ +""" +Wan2.2 T2V stability: OmniServer (diffusion) + ``diffusion_benchmark_serving.py`` / ``v1/videos``. + +Configuration: ``tests/dfx/stability/tests/test_wan22.json``. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tests.dfx.conftest import ( + create_benchmark_indices, + create_test_parameter_mapping, + create_unique_server_params, + load_configs, +) +from tests.dfx.stability.helpers import _run_one_diffusion_batch, run_stability_benchmark_loop + +STABILITY_DIR = Path(__file__).resolve().parent.parent +DEPLOY_CONFIGS_DIR = STABILITY_DIR / "deploy" +CONFIG_FILE_PATH = str(STABILITY_DIR / "tests" / "test_wan22.json") +DEFAULT_NUM_PROMPTS_PER_BATCH = 20 +STABILITY_SERVER_TIMEOUT_ARGS = ["--stage-init-timeout", "600", "--init-timeout", "900"] + +try: + BENCHMARK_CONFIGS = load_configs(CONFIG_FILE_PATH) +except FileNotFoundError: + BENCHMARK_CONFIGS = [] + +test_params = create_unique_server_params(BENCHMARK_CONFIGS, DEPLOY_CONFIGS_DIR) if BENCHMARK_CONFIGS else [] +server_to_benchmark_mapping = create_test_parameter_mapping(BENCHMARK_CONFIGS) if BENCHMARK_CONFIGS else {} +benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchmark_mapping) + + +@pytest.mark.slow +@pytest.mark.parametrize("omni_server", test_params, indirect=True) +@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True) +def test_stability_wan22(omni_server, stability_benchmark_params): + test_name = stability_benchmark_params["test_name"] + params = stability_benchmark_params["params"] + duration_sec = params.get("duration_sec", 300) + num_prompts_per_batch = params.get("num_prompts_per_batch", DEFAULT_NUM_PROMPTS_PER_BATCH) + request_rate = params.get("request_rate") + max_concurrency = params.get("max_concurrency") + + bench_params = { + k: v + for k, v in params.items() + if k not in ("duration_sec", "request_rate", "max_concurrency", "num_prompts_per_batch") + } + + result = run_stability_benchmark_loop( + host=omni_server.host, + port=omni_server.port, + model=omni_server.model, + duration_sec=duration_sec, + params=bench_params, + request_rate=request_rate, + max_concurrency=max_concurrency, + result_dir=str(STABILITY_DIR), + num_prompts_per_batch=num_prompts_per_batch, + run_one_batch=_run_one_diffusion_batch, + ) + + assert result.get("failed", 0) == 0, f"[{test_name}] Failed requests detected: {result.get('errors', [])}" + assert result.get("completed", 0) > 0, f"[{test_name}] No requests completed" diff --git a/tests/dfx/stability/tests/test.json b/tests/dfx/stability/tests/test.json deleted file mode 100644 index 255cd5b10..000000000 --- a/tests/dfx/stability/tests/test.json +++ /dev/null @@ -1,77 +0,0 @@ -[ - { - "test_name": "test_qwen3_omni_stability", - "server_params": { - "model": "Qwen/Qwen3-Omni-30B-A3B-Instruct", - "stage_overrides": { - "2": { - "max_num_batched_tokens": 1000000 - } - } - }, - "benchmark_params": [ - { - "dataset_name": "random", - "backend": "openai-chat-omni", - "endpoint": "/v1/chat/completions", - "duration_sec": 300, - "request_rate": 0.2, - "num_prompts_per_batch": 10, - "random_input_len": 2500, - "random_output_len": 900, - "ignore_eos": true, - "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration" - }, - { - "dataset_name": "random", - "backend": "openai-chat-omni", - "endpoint": "/v1/chat/completions", - "duration_sec": 300, - "max_concurrency": 2, - "num_prompts_per_batch": 10, - "random_input_len": 2500, - "random_output_len": 900, - "ignore_eos": true, - "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration" - } - ] - }, - { - "test_name": "test_qwen3_omni_stability_async_chunk", - "server_params": { - "model": "Qwen/Qwen3-Omni-30B-A3B-Instruct", - "stage_overrides": { - "2": { - "max_num_batched_tokens": 1000000 - } - }, - "extra_cli_args": ["--async-chunk"] - }, - "benchmark_params": [ - { - "dataset_name": "random", - "backend": "openai-chat-omni", - "endpoint": "/v1/chat/completions", - "duration_sec": 300, - "request_rate": 0.2, - "num_prompts_per_batch": 10, - "random_input_len": 2500, - "random_output_len": 900, - "ignore_eos": true, - "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration" - }, - { - "dataset_name": "random", - "backend": "openai-chat-omni", - "endpoint": "/v1/chat/completions", - "duration_sec": 300, - "max_concurrency": 2, - "num_prompts_per_batch": 10, - "random_input_len": 2500, - "random_output_len": 900, - "ignore_eos": true, - "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration" - } - ] - } -] diff --git a/tests/dfx/stability/tests/test_qwen3_omni.json b/tests/dfx/stability/tests/test_qwen3_omni.json new file mode 100644 index 000000000..a16ab805c --- /dev/null +++ b/tests/dfx/stability/tests/test_qwen3_omni.json @@ -0,0 +1,97 @@ +[ + { + "test_name": "test_qwen3_omni_stability", + "server_params": { + "model": "Qwen/Qwen3-Omni-30B-A3B-Instruct", + "stage_overrides": { + "2": { + "max_num_batched_tokens": 1000000 + } + } + }, + "benchmark_params": [ + { + "dataset_name": "random-mm", + "backend": "openai-chat-omni", + "endpoint": "/v1/chat/completions", + "duration_sec": 86400, + "request_rate": 0.3, + "num_prompts_per_batch": 10, + "random_input_len": { + "min": 0, + "max": 8000 + }, + "random_output_len": { + "min": 0, + "max": 1000 + }, + "random_range_ratio": 0.0, + "random_mm_base_items_per_request": { + "min": 0, + "max": 6 + }, + "random_mm_num_mm_items_range_ratio": 0.0, + "random_mm_limit_mm_per_prompt": { + "image": 2, + "video": 2, + "audio": 2 + }, + "random_mm_bucket_config": { + "(128-1024, 128-1024, 1)": 0.34, + "(256-1080, 256-1920, 2-16)": 0.33, + "(0, 1-60, 1-3)": 0.33 + }, + "ignore_eos": true, + "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration" + } + ] + }, + { + "test_name": "test_qwen3_omni_stability_async_chunk", + "server_params": { + "model": "Qwen/Qwen3-Omni-30B-A3B-Instruct", + "stage_overrides": { + "2": { + "max_num_batched_tokens": 1000000 + } + }, + "extra_cli_args": ["--async-chunk"] + }, + "benchmark_params": [ + { + "dataset_name": "random-mm", + "backend": "openai-chat-omni", + "endpoint": "/v1/chat/completions", + "duration_sec": 86400, + "max_concurrency": 2, + "num_prompts_per_batch": 10, + "random_input_len": { + "min": 0, + "max": 8000 + }, + "random_output_len": { + "min": 0, + "max": 1000 + }, + "random_range_ratio": 0.0, + "random_mm_base_items_per_request": { + "min": 0, + "max": 6 + }, + "random_mm_num_mm_items_range_ratio": 0.0, + "random_mm_limit_mm_per_prompt": { + "image": 2, + "video": 2, + "audio": 2 + }, + "random_mm_bucket_config": { + "(128-1024, 128-1024, 1)": 0.34, + "(256-1080, 256-1920, 2-16)": 0.33, + "(0, 1-60, 1-3)": 0.33 + }, + "ignore_eos": true, + "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration" + } + ] + } +] diff --git a/tests/dfx/stability/tests/test_qwen3_tts.json b/tests/dfx/stability/tests/test_qwen3_tts.json new file mode 100644 index 000000000..fbf30d88a --- /dev/null +++ b/tests/dfx/stability/tests/test_qwen3_tts.json @@ -0,0 +1,56 @@ +[ + { + "test_name": "test_qwen3_tts_stability", + "server_params": { + "model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice" + }, + "benchmark_params": [ + { + "dataset_name": "random", + "backend": "openai-audio-speech", + "endpoint": "/v1/audio/speech", + "duration_sec": 86400, + "request_rate": 0.3, + "num_prompts_per_batch": 10, + "random_input_len": { + "min": 0, + "max": 1000 + }, + "random_output_len": { + "min": 0, + "max": 1000 + }, + "random_range_ratio": 0.0, + "extra_body": { + "voice": "Vivian", + "language": "English" + }, + "ignore_eos": true, + "percentile-metrics": "ttft,e2el,audio_rtf,audio_ttfp,audio_duration" + }, + { + "dataset_name": "random", + "backend": "openai-audio-speech", + "endpoint": "/v1/audio/speech", + "duration_sec": 86400, + "max_concurrency": 2, + "num_prompts_per_batch": 10, + "random_input_len": { + "min": 0, + "max": 1000 + }, + "random_output_len": { + "min": 0, + "max": 1000 + }, + "random_range_ratio": 0.0, + "extra_body": { + "voice": "Vivian", + "language": "English" + }, + "ignore_eos": true, + "percentile-metrics": "ttft,e2el,audio_rtf,audio_ttfp,audio_duration" + } + ] + } +] diff --git a/tests/dfx/stability/tests/test_qwen_image.json b/tests/dfx/stability/tests/test_qwen_image.json new file mode 100644 index 000000000..f3dd93f6f --- /dev/null +++ b/tests/dfx/stability/tests/test_qwen_image.json @@ -0,0 +1,28 @@ +[ + { + "test_name": "test_qwen_image_stability", + "server_params": { + "model": "Qwen/Qwen-Image" + }, + "benchmark_params": [ + { + "dataset": "random", + "task": "t2i", + "backend": "vllm-omni", + "duration_sec": 86400, + "max_concurrency": 1, + "num_prompts_per_batch": 10, + "width": { + "min": 512, + "max": 2048 + }, + "height": { + "min": 512, + "max": 2048 + }, + "num_inference_steps": 50, + "enable_negative_prompt": true + } + ] + } +] diff --git a/tests/dfx/stability/tests/test_wan22.json b/tests/dfx/stability/tests/test_wan22.json new file mode 100644 index 000000000..c787ce96a --- /dev/null +++ b/tests/dfx/stability/tests/test_wan22.json @@ -0,0 +1,31 @@ +[ + { + "test_name": "test_wan22_i2v_stability_v1_videos", + "server_params": { + "model": "Wan-AI/Wan2.2-I2V-A14B-Diffusers", + "serve_args": { + "ulysses-degree": 2, + "vae-patch-parallel-size": 2, + "tensor-parallel-size": 1, + "use-hsdp": true, + "vae-use-slicing": true, + "vae-use-tiling": true + } + }, + "benchmark_params": [ + { + "dataset": "random", + "task": "i2v", + "backend": "v1/videos", + "duration_sec": 86400, + "max_concurrency": 1, + "num_prompts_per_batch": 20, + "enable_negative_prompt": true, + "random_request_config": [ + {"width": 832, "height": 480, "num_inference_steps": 4, "num_frames": 81, "fps": 16, "weight": 0.5}, + {"width": 1280, "height": 720, "num_inference_steps": 4, "num_frames": 121, "fps": 16, "weight": 0.5} + ] + } + ] + } +]