""" 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"