# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project import json import math import os import threading from collections.abc import Callable from contextlib import AbstractContextManager, ExitStack, contextmanager from pathlib import Path from typing import Any import pytest from tests.dfx.conftest import ( create_paired_omni_benchmark_pytest_params, create_test_parameter_mapping, get_benchmark_params_for_server, get_runtime_resource_label, is_diffusion_perf_config, load_benchmark_configs, run_benchmark, ) from tests.helpers.runtime import OmniServer # Optional JSON field ``mark`` is applied as pytest marks via # ``create_paired_omni_benchmark_pytest_params`` (e.g. ``"mark": [{"hardware_marks": # {"res": {"cuda": "H100"}, "num_cards": 2}}, "full_model", "omni"]``). os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" def _get_config_file_from_argv() -> str | None: """Read ``--test-config-file`` from ``sys.argv`` at import time so parametrization can use it.""" import sys for i, arg in enumerate(sys.argv): if arg == "--test-config-file" and i + 1 < len(sys.argv): return sys.argv[i + 1] if arg.startswith("--test-config-file="): return arg.split("=", 1)[1] return None _PERF_TESTS_DIR = Path(__file__).resolve().parent.parent / "tests" CONFIG_FILE_PATH = _get_config_file_from_argv() if CONFIG_FILE_PATH is None: _all_configs = load_benchmark_configs(config_dir=_PERF_TESTS_DIR) BENCHMARK_CONFIGS = [cfg for cfg in _all_configs if not is_diffusion_perf_config(cfg)] print( f"No --test-config-file: loaded {len(BENCHMARK_CONFIGS)} omni/tts/generation case(s) from " f"{_PERF_TESTS_DIR}/*.json (skipped {len(_all_configs) - len(BENCHMARK_CONFIGS)} diffusion; " f"use -m to filter, e.g. -m tts)" ) else: _loaded = load_benchmark_configs(CONFIG_FILE_PATH) BENCHMARK_CONFIGS = [cfg for cfg in _loaded if not is_diffusion_perf_config(cfg)] skipped = len(_loaded) - len(BENCHMARK_CONFIGS) if skipped: print( f"--test-config-file: loaded {len(BENCHMARK_CONFIGS)} omni/tts/generation case(s); " f"skipped {skipped} remaining diffusion case(s) (chat completions / custom jsonl)" ) DEPLOY_CONFIGS_DIR = Path(__file__).parent.parent / "deploy" server_to_benchmark_mapping = create_test_parameter_mapping(BENCHMARK_CONFIGS) paired_benchmark_params = create_paired_omni_benchmark_pytest_params(BENCHMARK_CONFIGS, DEPLOY_CONFIGS_DIR) _omni_server_lock = threading.Lock() class _SingleActiveContext: """Reuse one active context while its configuration key is unchanged.""" def __init__(self) -> None: self._key: Any = None self._stack: ExitStack | None = None self._value: Any = None def acquire(self, key: Any, factory: Callable[[], AbstractContextManager[Any]]) -> Any: if self._stack is not None and key == self._key: return self._value self.close() stack = ExitStack() value = stack.enter_context(factory()) self._key = key self._stack = stack self._value = value return value def close(self) -> None: stack = self._stack self._key = None self._stack = None self._value = None if stack is not None: stack.close() # OmniServer defaults for flags not already present in JSON ``serve_args`` / # ``extra_cli_args``. Add new (flag, value) pairs here rather than special-casing. _OMNI_DEFAULT_SERVER_ARGS: tuple[tuple[str, str], ...] = ( ("--stage-init-timeout", "600"), ("--init-timeout", "900"), ) def _cli_flag_names(cli_args: tuple[str, ...] | list[str]) -> set[str]: """Return long-option names present in a flat CLI argv list.""" names: set[str] = set() for item in cli_args: token = str(item) if not token.startswith("--"): continue names.add(token.split("=", 1)[0]) return names def _merge_omni_default_server_args( extra_cli_args: tuple[str, ...] | list[str], *, use_omni: bool, defaults: tuple[tuple[str, str], ...] = _OMNI_DEFAULT_SERVER_ARGS, ) -> list[str]: """Fill Omni defaults for flags not already set in JSON-derived CLI args. JSON ``serve_args`` / ``extra_cli_args`` win; only missing flags are appended. """ if not use_omni: return [] present = _cli_flag_names(extra_cli_args) args: list[str] = [] for flag, value in defaults: if flag not in present: args += [flag, value] return args def _omni_server_env() -> dict[str, str]: """Writable video/image storage for ``/v1/videos`` and related generation APIs.""" result_dir = Path(os.environ.get("BENCHMARK_DIR", "tests/dfx/perf/results")) storage_path = Path(os.environ.get("VLLM_OMNI_STORAGE_PATH", str(result_dir / "storage"))) storage_path.mkdir(parents=True, exist_ok=True) return {"VLLM_OMNI_STORAGE_PATH": str(storage_path)} def _resolve_offline_model(model: str) -> str: """Resolve HF ids / MiniMax env overrides the same way as the diffusion runner.""" import huggingface_hub from vllm_omni.transformers_utils.repo_utils import hf_api if not model or os.path.isdir(model): return model model_env_overrides = { "MiniMaxAI/MiniMax-H3": "VLLM_TEST_MINIMAX_H3_MODEL", "MiniMaxAI/MiniMax-H3/FL2VA": "VLLM_TEST_MINIMAX_H3_FL2VA_MODEL", "MiniMaxAI/MiniMax-H3/Ref2VA": "VLLM_TEST_MINIMAX_H3_REF2VA_MODEL", } env_name = model_env_overrides.get(model) if env_name: env_model = os.environ.get(env_name) if env_model: return env_model parts = model.split("/") if len(parts) >= 3: repo_id = "/".join(parts[:2]) subfolder = "/".join(parts[2:]) snapshot_root = hf_api().snapshot_download( repo_id, allow_patterns=[f"{subfolder}/**"], local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE, ) return str(Path(snapshot_root) / subfolder) if not huggingface_hub.constants.HF_HUB_OFFLINE: return model return hf_api().snapshot_download(model, local_files_only=True) @contextmanager def _start_omni_server(server_param): test_name, model, stage_config_path, stage_overrides, extra_cli_args, use_omni = server_param extra = tuple(extra_cli_args or ()) model = _resolve_offline_model(model) print(f"Starting OmniServer with test: {test_name}, model: {model}") server_args: list[str] = _merge_omni_default_server_args(extra, use_omni=use_omni) # --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: server_args = list(extra) + server_args with OmniServer(model, server_args, use_omni=use_omni, env_dict=_omni_server_env()) as server: server.test_name = test_name print("OmniServer started successfully") yield server print("OmniServer stopping...") print("OmniServer stopped") @pytest.fixture(scope="module") def omni_server_context(): """Start vLLM-Omni server as a subprocess with actual model weights. Reuse it for adjacent benchmark cases with the same server configuration. Multi-stage initialization can take 10-20+ minutes. """ with _omni_server_lock: active_context = _SingleActiveContext() try: yield active_context finally: active_context.close() @pytest.fixture def omni_server(request, omni_server_context): return omni_server_context.acquire(request.param, lambda: _start_omni_server(request.param)) @pytest.fixture def benchmark_params(request): """Benchmark parameters fixture; paired with ``omni_server`` via parametrization.""" test_name, param_index = request.param 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], } def _resolve_num_warmups(params: dict[str, Any], *, default: int) -> int: value = params.get("num_warmups") if value is None: return default if not isinstance(value, int) or isinstance(value, bool) or value < 0: raise ValueError("num_warmups must be a non-negative integer") return value def assert_result(result, params, num_prompt) -> None: assert result["completed"] == num_prompt, "Request failures exist" if params.get("dataset_name") == "omniinteract": summary = result.get("omniinteract") assert isinstance(summary, dict), "OmniInteract summary is missing" assert (summary.get("total"), summary.get("success"), summary.get("failed")) == ( num_prompt, num_prompt, 0, ), "OmniInteract requests did not all succeed" assert summary.get("artifacts_complete") is True, "OmniInteract artifacts are incomplete" baseline = params.get("baseline") hardware = result.get("Hardware") hardware_baseline = baseline.get(hardware) if isinstance(baseline, dict) and isinstance(hardware, str) else None if isinstance(hardware_baseline, dict) and "mean_tpot_ms" in hardware_baseline: num_tpot_samples = result.get("num_tpot_samples") mean_tpot_ms = result.get("mean_tpot_ms") assert isinstance(num_tpot_samples, int) and not isinstance(num_tpot_samples, bool) and num_tpot_samples > 0, ( "TPOT baseline is configured, but no measurable TPOT samples were produced" ) assert ( isinstance(mean_tpot_ms, int | float) and not isinstance(mean_tpot_ms, bool) and math.isfinite(mean_tpot_ms) ), "TPOT baseline is configured, but mean_tpot_ms is not finite" expected_audio_turns = params.get("expected_duplex_audio_turns_per_session") if expected_audio_turns is not None: session_metrics = result.get("duplex_session_metrics") assert isinstance(session_metrics, list), "Duplex session metrics are missing" assert len(session_metrics) == num_prompt, ( f"Expected {num_prompt} duplex session metric rows, got {len(session_metrics)}" ) assert all( isinstance(metric, dict) and metric.get("audio_turn_count") == expected_audio_turns for metric in session_metrics ), f"Not every duplex session emitted {expected_audio_turns} audio turns" @pytest.mark.benchmark @pytest.mark.parametrize( "omni_server,benchmark_params", paired_benchmark_params, indirect=["omni_server", "benchmark_params"], ) def test_performance_benchmark(omni_server, benchmark_params): test_name = benchmark_params["test_name"] params = benchmark_params["params"] dataset_name = params.get("dataset_name", "") host = omni_server.host port = omni_server.port model = omni_server.model print(f"Running benchmark for model: {model}") print(f"Benchmark parameters: {benchmark_params}") resource_label = get_runtime_resource_label() def to_list(value, default=None): if value is None: return [] if default is None else [default] return [value] if not isinstance(value, (list, tuple)) else list(value) qps_list = to_list(params.get("request_rate")) num_prompt_list = to_list(params.get("num_prompts")) max_concurrency_list = to_list(params.get("max_concurrency")) max_len = max(len(qps_list), len(max_concurrency_list)) if len(num_prompt_list) == 1 and max_len > 1: num_prompt_list = num_prompt_list * max_len elif max_len == 1 and len(num_prompt_list) > 1: if len(qps_list) == 1: qps_list = qps_list * len(num_prompt_list) if len(max_concurrency_list) == 1: max_concurrency_list = max_concurrency_list * len(num_prompt_list) max_len = max(len(qps_list), len(max_concurrency_list)) elif len(num_prompt_list) != max_len and max_len > 0: raise ValueError("The number of prompts does not match the QPS or max_concurrency") args = ["--host", host, "--port", str(port)] exclude_keys = { "request_rate", "baseline", "num_prompts", "max_concurrency", "num_warmups", "task", "enabled", "eval_phase", "trust_remote_code", "expected_duplex_audio_turns_per_session", "name", "enable_negative_prompt", "random_request_config", "num_input_images", "warmup_requests", "warmup_concurrency", "warmup_num_inference_steps", } param_keys = {str(key).replace("-", "_") for key in params} if "model" not in param_keys: args.extend(["--model", str(model)]) for key, value in params.items(): if key in exclude_keys or value is None: continue if key in {"extra_body", "extra-body"} and value == {}: continue arg_name = f"--{key.replace('_', '-')}" if isinstance(value, bool) and value: args.append(arg_name) elif isinstance(value, dict): json_str = json.dumps(value, ensure_ascii=False, separators=(",", ":")) args.extend([arg_name, json_str]) elif not isinstance(value, bool): args.extend([arg_name, str(value)]) for config in BENCHMARK_CONFIGS: if config.get("test_name") != test_name: continue server_params = config.get("server_params") or {} if server_params.get("trust_remote_code") or params.get("trust_remote_code"): args.append("--trust-remote-code") break # QPS / request-rate sweep for sweep_index, (qps, num_prompt) in enumerate(zip(qps_list, num_prompt_list)): args = args + ["--request-rate", str(qps), "--num-prompts", str(num_prompt)] result = run_benchmark( args=args, test_name=test_name, flow=qps, dataset_name=dataset_name, num_prompt=num_prompt, baseline_config=params.get("baseline"), sweep_index=sweep_index, random_input_len=params.get("random_input_len"), random_output_len=params.get("random_output_len"), resource_label=resource_label, num_warmups=_resolve_num_warmups(params, default=2), benchmark_params_name=params.get("name") if isinstance(params.get("name"), str) else None, ) assert_result(result, params, num_prompt) # concurrency test for sweep_index, (concurrency, num_prompt) in enumerate(zip(max_concurrency_list, num_prompt_list)): args = args + ["--max-concurrency", str(concurrency), "--num-prompts", str(num_prompt), "--request-rate", "inf"] result = run_benchmark( args=args, test_name=test_name, flow=concurrency, dataset_name=dataset_name, num_prompt=num_prompt, baseline_config=params.get("baseline"), sweep_index=sweep_index, random_input_len=params.get("random_input_len"), random_output_len=params.get("random_output_len"), resource_label=resource_label, num_warmups=_resolve_num_warmups(params, default=max(2, int(concurrency))), benchmark_params_name=params.get("name") if isinstance(params.get("name"), str) else None, ) assert_result(result, params, num_prompt)