Signed-off-by: lishunyang <lishunyang12@163.com> Signed-off-by: reidliu41 <reid201711@gmail.com> Signed-off-by: Alex Brooks <albrooks@redhat.com> Co-authored-by: reidliu41 <reid201711@gmail.com> Co-authored-by: xiaohajiayou <75477391+xiaohajiayou@users.noreply.github.com> Co-authored-by: Alex Brooks <albrooks@redhat.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
298 lines
11 KiB
Python
298 lines
11 KiB
Python
"""
|
|
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.conftest import OmniServer
|
|
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
|
|
|
|
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"
|