156 lines
4.9 KiB
Python
156 lines
4.9 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
||||
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|||
|
|
|
|||
|
|
"""Module-scoped judge server fixture for omni-duplex-eval CI guard."""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import subprocess
|
|||
|
|
import time
|
|||
|
|
from collections.abc import Generator
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
import requests
|
|||
|
|
import torch
|
|||
|
|
|
|||
|
|
_CONFIG_PATH = Path(__file__).parent / "omni_duplex_eval_ci_config.json"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _load_judge_model() -> str:
|
|||
|
|
"""Read judge model from CI config, fall back to default."""
|
|||
|
|
try:
|
|||
|
|
config = json.loads(_CONFIG_PATH.read_text())
|
|||
|
|
return config.get("judge", {}).get("model", "Qwen/Qwen2.5-VL-7B-Instruct")
|
|||
|
|
except Exception:
|
|||
|
|
return "Qwen/Qwen2.5-VL-7B-Instruct"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_visible_devices() -> tuple[str, list[str]]:
|
|||
|
|
"""Parse CUDA or Ascend NPU visible devices from environment."""
|
|||
|
|
if torch.cuda.is_available():
|
|||
|
|
env_key = "CUDA_VISIBLE_DEVICES"
|
|||
|
|
elif hasattr(torch, "npu") and torch.npu.is_available():
|
|||
|
|
env_key = "ASCEND_RT_VISIBLE_DEVICES"
|
|||
|
|
else:
|
|||
|
|
return "", []
|
|||
|
|
|
|||
|
|
raw = os.environ.get(env_key, "").strip()
|
|||
|
|
if not raw:
|
|||
|
|
# No env restriction — physical device IDs are 0, 1, 2...
|
|||
|
|
count = torch.accelerator.device_count() if hasattr(torch, "accelerator") else 0
|
|||
|
|
return env_key, [str(i) for i in range(count)]
|
|||
|
|
|
|||
|
|
devices = [d.strip() for d in raw.split(",") if d.strip()]
|
|||
|
|
return env_key, devices
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _omni_server_device_env() -> dict[str, str] | None:
|
|||
|
|
"""Env overrides pinning the Omni server to device[1] (split from judge).
|
|||
|
|
|
|||
|
|
Returns ``None`` when fewer than two devices are allocated (nothing to
|
|||
|
|
split, e.g. on CPU-only hosts where ``_parse_visible_devices`` yields an
|
|||
|
|
empty list). Pure function: never mutates the pytest process environment.
|
|||
|
|
"""
|
|||
|
|
env_key, devices = _parse_visible_devices()
|
|||
|
|
if len(devices) >= 2:
|
|||
|
|
return {env_key: devices[1]}
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="module")
|
|||
|
|
def judge_server(
|
|||
|
|
tmp_path_factory: pytest.TempPathFactory,
|
|||
|
|
) -> Generator[str, None, None]:
|
|||
|
|
"""Launch the Qwen2.5-VL-7B judge server, wait for health, yield base URL.
|
|||
|
|
|
|||
|
|
If ``VLLM_DUPLEX_EVAL_JUDGE_URL`` is already set *and* the endpoint
|
|||
|
|
responds to ``/health``, reuse it (developer convenience).
|
|||
|
|
|
|||
|
|
Otherwise, start the judge as a subprocess:
|
|||
|
|
- The judge is pinned to the first device of the job's allocation
|
|||
|
|
(``devices[0]``), so it never selects a device outside the job's set.
|
|||
|
|
- The omni server (managed by the ``omni_server`` fixture) is pinned to
|
|||
|
|
device[1] via ``OmniServerParams.env_dict`` (see
|
|||
|
|
``_omni_server_device_env``), leaving the pytest process environment
|
|||
|
|
untouched.
|
|||
|
|
|
|||
|
|
The fixture terminates the judge on teardown.
|
|||
|
|
"""
|
|||
|
|
port = int(os.environ.get("VLLM_DUPLEX_EVAL_JUDGE_PORT", "8001"))
|
|||
|
|
base_url = f"http://127.0.0.1:{port}"
|
|||
|
|
|
|||
|
|
# Reuse a running judge if URL is pre-set and healthy.
|
|||
|
|
preset_url = os.environ.get("VLLM_DUPLEX_EVAL_JUDGE_URL")
|
|||
|
|
if preset_url:
|
|||
|
|
try:
|
|||
|
|
resp = requests.get(f"{preset_url}/health", timeout=5)
|
|||
|
|
if resp.status_code == 200:
|
|||
|
|
yield preset_url
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Determine judge device from the job's device allocation.
|
|||
|
|
_, devices = _parse_visible_devices()
|
|||
|
|
judge_dev = devices[0] if devices else "0"
|
|||
|
|
|
|||
|
|
# Build device-isolated environment.
|
|||
|
|
env = os.environ.copy()
|
|||
|
|
if torch.cuda.is_available():
|
|||
|
|
env["CUDA_VISIBLE_DEVICES"] = judge_dev
|
|||
|
|
env["ASCEND_RT_VISIBLE_DEVICES"] = "" # clear NPU visibility
|
|||
|
|
elif hasattr(torch, "npu") and torch.npu.is_available():
|
|||
|
|
env["ASCEND_RT_VISIBLE_DEVICES"] = judge_dev
|
|||
|
|
env["CUDA_VISIBLE_DEVICES"] = "" # clear CUDA visibility
|
|||
|
|
|
|||
|
|
# Derive media allowlist from pytest's actual temp root.
|
|||
|
|
media_path = str(tmp_path_factory.getbasetemp())
|
|||
|
|
os.makedirs(media_path, exist_ok=True)
|
|||
|
|
|
|||
|
|
cmd = [
|
|||
|
|
"python",
|
|||
|
|
"-m",
|
|||
|
|
"vllm.entrypoints.openai.api_server",
|
|||
|
|
"--model",
|
|||
|
|
_load_judge_model(),
|
|||
|
|
"--port",
|
|||
|
|
str(port),
|
|||
|
|
"--max-model-len",
|
|||
|
|
"65536",
|
|||
|
|
"--trust-remote-code",
|
|||
|
|
"--dtype",
|
|||
|
|
"bfloat16",
|
|||
|
|
"--gpu-memory-utilization",
|
|||
|
|
"0.85",
|
|||
|
|
"--allowed-local-media-path",
|
|||
|
|
media_path,
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
proc = subprocess.Popen(cmd, env=env)
|
|||
|
|
|
|||
|
|
# Health-check loop: up to 60 iterations × 5 s = 5 min.
|
|||
|
|
for _ in range(60):
|
|||
|
|
try:
|
|||
|
|
resp = requests.get(f"{base_url}/health", timeout=2)
|
|||
|
|
if resp.status_code == 200:
|
|||
|
|
break
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
time.sleep(5)
|
|||
|
|
else:
|
|||
|
|
proc.kill()
|
|||
|
|
proc.wait()
|
|||
|
|
raise RuntimeError(f"Judge server failed to become healthy at {base_url} within 300 s")
|
|||
|
|
|
|||
|
|
yield base_url
|
|||
|
|
|
|||
|
|
# Teardown.
|
|||
|
|
proc.terminate()
|
|||
|
|
try:
|
|||
|
|
proc.wait(timeout=30)
|
|||
|
|
except subprocess.TimeoutExpired:
|
|||
|
|
proc.kill()
|
|||
|
|
proc.wait()
|