Files
enginex-ascend-910-vllm/tests/e2e/conftest.py

1955 lines
71 KiB
Python
Raw Permalink Normal View History

2025-09-09 09:40:35 +08:00
#
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
# Copyright 2023 The vLLM team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file is a part of the vllm-ascend project.
# Adapted from vllm-project/vllm/blob/main/tests/conftest.py
#
import contextlib
import copy
import functools
2025-09-09 09:40:35 +08:00
import gc
import hashlib
import json
import logging
import multiprocessing
2025-09-09 09:40:35 +08:00
import os
import shlex
import signal
import subprocess
import sys
import threading
import time
import traceback
from pathlib import Path
from typing import Any, TypeVar
import huggingface_hub
2025-09-09 09:40:35 +08:00
import numpy as np
import openai
import psutil
2025-09-09 09:40:35 +08:00
import pytest
import requests
2025-09-09 09:40:35 +08:00
import torch
from modelscope import snapshot_download # type: ignore[import-untyped]
from PIL import Image
from requests.exceptions import RequestException
2025-09-09 09:40:35 +08:00
from torch import nn
from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer, BatchEncoding, BatchFeature
2025-09-09 09:40:35 +08:00
from transformers.models.auto.auto_factory import _BaseAutoModelClass
from vllm import LLM, SamplingParams
from vllm.config.model import ConvertOption, RunnerOption, _get_and_verify_dtype
2025-09-09 09:40:35 +08:00
from vllm.inputs import TextPrompt
from vllm.outputs import RequestOutput
from vllm.platforms import current_platform
2025-09-09 09:40:35 +08:00
from vllm.transformers_utils.utils import maybe_model_redirect
from vllm.utils.network_utils import get_open_port
2025-09-09 09:40:35 +08:00
from tests.e2e.model_utils import TokensTextLogprobs, TokensTextLogprobsPromptLogprobs
from tests.e2e.nightly.multi_node.internal_dp.scripts.multi_node_config import DisaggregatedPrefillCfg, NodeInfo
2025-09-09 09:40:35 +08:00
from vllm_ascend.ascend_config import clear_ascend_config
2025-09-09 09:40:35 +08:00
# TODO: remove this part after the patch merged into vllm, if
# we not explicitly patch here, some of them might be effectiveless
# in pytest scenario
from vllm_ascend.utils import adapt_patch # noqa E402
adapt_patch(True)
adapt_patch(False)
from vllm.distributed.parallel_state import ( # noqa E402
destroy_distributed_environment,
destroy_model_parallel,
)
2025-09-09 09:40:35 +08:00
_T = TypeVar("_T", nn.Module, torch.Tensor, BatchEncoding, BatchFeature, dict)
_M = TypeVar("_M")
_PromptMultiModalInput = list[_M] | list[list[_M]]
2025-09-09 09:40:35 +08:00
PromptImageInput = _PromptMultiModalInput[Image.Image]
PromptAudioInput = _PromptMultiModalInput[tuple[np.ndarray, int]]
2025-09-09 09:40:35 +08:00
PromptVideoInput = _PromptMultiModalInput[np.ndarray]
logger = logging.getLogger(__name__)
2025-09-09 09:40:35 +08:00
_TEST_DIR = os.path.dirname(__file__)
_LONG_PROMPTS = [os.path.join(_TEST_DIR, "prompts", "long_prompt.txt")]
DISAGG_EPD_PROXY_SCRIPT = (
Path(__file__).parent.parent.parent / "examples" / "disaggregated_encoder" / "disagg_epd_proxy.py"
)
DISAGG_PD_PROXY_SCRIPT = (
Path(__file__).parent.parent.parent
/ "examples"
/ "disaggregated_prefill_v1"
/ "load_balance_proxy_server_example.py"
)
def _check_npu_memory_worker(target_free_percentage: float, max_wait_seconds: float):
# We can try to clean up memory in this subprocess, though it mostly affects this process.
# But if there are any lingering contexts in this process (unlikely for a fresh spawn), it helps.
gc.collect()
torch.npu.empty_cache()
_, total_npu_memory = torch.npu.mem_get_info()
start_time = time.time()
while True:
free_bytes, _ = torch.npu.mem_get_info()
if free_bytes / total_npu_memory >= target_free_percentage:
print("check_npu_memory_worker: npu free memory decreased target value.")
return # Success
elapsed = time.time() - start_time
if elapsed > max_wait_seconds:
# Print to stderr so it's visible in test logs even if captured
print(
f"Timeout: NPU memory free size did not reach "
f"{target_free_percentage} of total npu memory within {max_wait_seconds} seconds.",
file=sys.stderr,
)
sys.exit(1) # Failure
print(
f"Waiting for NPU memory to be free: "
f"{free_bytes / 1024**3:.2f} GB available, "
f"Elapsed time: {elapsed:.2f} s."
)
# Try to clean up
gc.collect()
torch.npu.empty_cache()
time.sleep(1)
def wait_until_npu_memory_free(target_free_percentage: float = 0.5, max_wait_seconds: float = 50):
"""Decorator to wait until the NPU memory free size is above target_free_percentage.
Args:
target_free_percentage (float): Target free memory percentage of total.
max_wait_seconds (float): Maximum wait time in seconds.
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Clean up non-NPU resources in the main process
cleanup_dist_env_and_memory()
# Use a spawned subprocess to check NPU memory to avoid initializing NPU in the main process
ctx = multiprocessing.get_context("spawn")
p = ctx.Process(target=_check_npu_memory_worker, args=(target_free_percentage, max_wait_seconds))
p.start()
p.join()
if p.exitcode != 0:
raise TimeoutError(
f"Timeout: NPU memory free size did not reach "
f"{target_free_percentage} of total npu memory within {max_wait_seconds} seconds."
)
return func(*args, **kwargs)
return wrapper
return decorator
2025-09-09 09:40:35 +08:00
def cleanup_dist_env_and_memory(shutdown_ray: bool = False):
destroy_model_parallel()
destroy_distributed_environment()
with contextlib.suppress(AssertionError):
torch.distributed.destroy_process_group()
if shutdown_ray:
import ray # Lazy import Ray
2025-09-09 09:40:35 +08:00
ray.shutdown()
gc.collect()
# Only clean NPU cache if NPU is already initialized/available in this process.
# This prevents accidental initialization of NPU context in the main process,
# which would break subsequent forks.
if hasattr(torch, "npu") and torch.npu.is_initialized():
torch.npu.empty_cache()
torch.npu.reset_peak_memory_stats()
2025-09-09 09:40:35 +08:00
class ModelName:
"""Global model name enumeration class."""
QWEN3_06B = "Qwen/Qwen3-0.6B"
QWEN3_8B = "Qwen/Qwen3-8B"
QWEN3_30B_A3B = "Qwen/Qwen3-30B-A3B"
DEEPSEEK = "vllm-ascend/DeepSeek-V2-Lite-W8A8"
class MooncakeLauncher:
def __init__(
self,
mooncake_port,
mooncake_metrics_port,
eviction_high_watermark_ratio=0.8,
eviction_ratio=0.05,
):
self.mooncake_port = mooncake_port
self.mooncake_metrics_port = mooncake_metrics_port
self.eviction_high_watermark_ratio = eviction_high_watermark_ratio
self.eviction_ratio = eviction_ratio
def __enter__(self):
cmd = [
"mooncake_master",
"--eviction_high_watermark_ratio",
str(self.eviction_high_watermark_ratio),
"--eviction_ratio",
str(self.eviction_ratio),
"--port",
str(self.mooncake_port),
"--metrics_port",
str(self.mooncake_metrics_port),
]
logger.info("Launching mooncake: %s", " ".join(cmd))
curr_ld_path = os.environ.get("LD_LIBRARY_PATH", "")
mooncake_ld_path = "/usr/local/Ascend/ascend-toolkit/latest/python/site-packages/mooncake:"
os.environ["LD_LIBRARY_PATH"] = mooncake_ld_path + curr_ld_path
env = os.environ.copy()
self.process = subprocess.Popen(cmd, env=env, start_new_session=True)
return self
def __exit__(self, exc_type, exc, tb):
if not self.process:
return
logger.info("Stopping mooncake server...")
try:
pgid = os.getpgid(self.process.pid)
os.killpg(pgid, signal.SIGTERM)
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
logger.warning("Mooncake server did not stop gracefully, force killing...")
os.killpg(pgid, signal.SIGKILL)
self.process.wait(timeout=5)
except ProcessLookupError:
pass
class RemoteOpenAIServer:
DUMMY_API_KEY = "token-abc123" # vLLM's OpenAI server does not need API key
def _start_server(self, model: str, server_cmd: list[str], env_dict: dict[str, str] | None) -> None:
"""Subclasses override this method to customize server process launch"""
env = os.environ.copy()
# the current process might initialize npu,
# to be safe, we should use spawn method
env["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
if env_dict is not None:
env.update(env_dict)
logger.info("Starting server with command: %s", " ".join(server_cmd))
self.proc: subprocess.Popen = subprocess.Popen(
server_cmd,
env=env,
stdout=sys.stdout,
stderr=sys.stderr,
)
def __init__(
self,
model: str,
vllm_serve_args: list[str] | str,
*,
server_host: str = "0.0.0.0",
server_port: int = 8080,
env_dict: dict[str, str] | None = None,
seed: int | None = None,
auto_port: bool = True,
nodes_info: list[NodeInfo] | None = None,
disaggregated_prefill: DisaggregatedPrefillCfg | None = None,
proxy_port: int | None = None,
max_wait_seconds: float | None = None,
override_hf_configs: dict[str, Any] | None = None,
) -> None:
if isinstance(vllm_serve_args, str):
vllm_serve_args = shlex.split(vllm_serve_args)
else:
vllm_serve_args = ["vllm", "serve", model, *vllm_serve_args]
if auto_port:
if "-p" in vllm_serve_args or "--port" in vllm_serve_args:
raise ValueError("You have manually specified the port when `auto_port=True`.")
# No need for a port if using unix sockets
if "--uds" not in vllm_serve_args:
# Don't mutate the input args
vllm_serve_args = vllm_serve_args + ["--port", str(get_open_port())]
if seed is not None:
if "--seed" in vllm_serve_args:
raise ValueError(f"You have manually specified the seed when `seed={seed}`.")
vllm_serve_args = vllm_serve_args + ["--seed", str(seed)]
if override_hf_configs is not None:
vllm_serve_args = vllm_serve_args + ["--hf-overrides", json.dumps(override_hf_configs)]
self.host = str(server_host)
self.port = int(server_port)
# for multi-nodes test
self.nodes_info = nodes_info
self.disaggregated_prefill = disaggregated_prefill
self.cur_index = os.getenv("LWS_WORKER_INDEX", 0)
self.proxy_port = proxy_port
self._start_server(model, vllm_serve_args, env_dict)
max_wait_seconds = max_wait_seconds or 2800
if self.disaggregated_prefill:
assert proxy_port is not None, "for disaggregated_prefill, proxy port must be provided"
self._wait_for_server_pd(timeout=max_wait_seconds)
else:
self._wait_for_multiple_servers([(self.host, self.url_for("health"))], timeout=max_wait_seconds)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self._terminate_server()
def _poll(self) -> int | None:
"""Subclasses override this method to customize process polling"""
return self.proc.poll()
def hang_until_terminated(self, url) -> None:
"""
Wait until the server process terminates.
This is for headless mode, where the api server
process only exists in the leader node.
"""
logger.info("Hanging until server process terminates...")
client = requests
try:
while True:
try:
resp = client.get(url, timeout=5)
if resp.status_code != 200:
break
time.sleep(5)
except Exception:
break
finally:
self._terminate_server()
def _wait_for_server_pd(self, timeout: float):
# Wait for all api_server nodes ready
assert self.nodes_info is not None, "cluster info must be provided"
proxy_port = self.proxy_port
def url_health(ip: str, port: int) -> str:
return f"http://{ip}:{port}/health"
targets = [
(node_info.ip, url_health(node_info.ip, self.port))
for node_info in self.nodes_info
if not node_info.headless
]
# Wait for proxy ready
master_node = self.nodes_info[0]
url_proxy = f"http://{master_node.ip}:{proxy_port}/healthcheck"
# Wait for master node proxy first
self._wait_for_multiple_servers([(master_node.ip, url_proxy)], timeout=timeout)
# Then wait for all api_server nodes
self._wait_for_multiple_servers(targets=targets, timeout=timeout)
def _wait_for_multiple_servers(
self, targets, timeout: float, log_interval: float = 30.0, always_check_nodes: bool = False
):
"""
targets: List[(node_ip, url)]
log_interval
"""
start = time.time()
client = requests
ready = {node_ip: False for node_ip, _ in targets}
last_log_time = 0.0
while True:
now = time.time()
all_ready = True
should_log = (now - last_log_time) >= log_interval
for node_ip, url in targets:
if ready[node_ip] and not always_check_nodes:
continue
try:
resp = client.get(url)
if resp.status_code == 200:
ready[node_ip] = True
logger.info("[READY] Node %s: %s is ready.", node_ip, url)
except RequestException:
all_ready = False
if should_log:
logger.debug("[WAIT] %s: connection failed", url)
# check unexpected exit
result = self._poll()
if result is not None and result != 0:
self._terminate_server()
raise RuntimeError(f"Server at {node_ip} exited unexpectedly.") from None
if should_log:
last_log_time = now
if all_ready:
break
if now - start > timeout:
not_ready_nodes = [n for n, ok in ready.items() if not ok]
self._terminate_server()
raise RuntimeError(
f"Timeout: these nodes did not become ready: {not_ready_nodes} in time: {timeout}s"
) from None
time.sleep(5)
@property
def url_root(self) -> str:
return f"http://{self.host}:{self.port}"
def _terminate_server(self) -> None:
"""Subclasses override this method to customize server process termination"""
self._terminate_process_tree(self.proc)
def _terminate_process_tree(self, proc: subprocess.Popen) -> None:
try:
parent = psutil.Process(proc.pid)
except psutil.NoSuchProcess:
return
children = parent.children(recursive=True)
try:
parent.terminate()
parent.wait(timeout=60)
except (psutil.NoSuchProcess, psutil.TimeoutExpired):
with contextlib.suppress(psutil.NoSuchProcess):
parent.kill()
for child in children:
with contextlib.suppress(psutil.NoSuchProcess):
child.terminate()
_, still_alive = psutil.wait_procs(children, timeout=10)
for child in still_alive:
with contextlib.suppress(psutil.NoSuchProcess):
child.kill()
def url_for(self, *parts: str) -> str:
return self.url_root + "/" + "/".join(parts)
def get_client(self, **kwargs):
if "timeout" not in kwargs:
kwargs["timeout"] = 600
return openai.OpenAI(
base_url=self.url_for("v1"),
api_key=self.DUMMY_API_KEY,
max_retries=0,
**kwargs,
)
def get_async_client(self, **kwargs):
if "timeout" not in kwargs:
kwargs["timeout"] = 600
return openai.AsyncOpenAI(base_url=self.url_for("v1"), api_key=self.DUMMY_API_KEY, max_retries=0, **kwargs)
def _get_pd_server_required_devices(vllm_serve_args: list[str]) -> int:
def get_size(arg_name: str) -> int:
value = 1
if arg_name in vllm_serve_args:
value = int(vllm_serve_args[vllm_serve_args.index(arg_name) + 1])
if value <= 0:
raise ValueError(f"{arg_name} must be positive, got {value}.")
return value
tensor_parallel_size = get_size("--tensor-parallel-size")
data_parallel_arg = (
"--data-parallel-size-local" if "--data-parallel-size-local" in vllm_serve_args else "--data-parallel-size"
)
data_parallel_size = get_size(data_parallel_arg)
return tensor_parallel_size * data_parallel_size
class RemotePDServer(RemoteOpenAIServer):
def __init__(
self,
vllm_serve_args: list[str] | list[list[str]],
server_host: str = "127.0.0.1",
env_dict: dict[str, str] | None = None,
max_wait_seconds: float | None = 600,
) -> None:
self._proc_list = []
self.env_dict: dict[str, str] = {}
if env_dict is not None:
self.env_dict.update(env_dict)
self.env_dict["VLLM_ALLOW_LONG_MAX_MODEL_LEN"] = "1"
self.env_dict["PYTORCH_NPU_ALLOC_CONF"] = "expandable_segments:True"
self.env_dict["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
self.vllm_serve_args_list = []
self.health_url_list = []
self.host = server_host
if isinstance(vllm_serve_args, list):
if not all(isinstance(item, list) for item in vllm_serve_args):
args_copy = copy.deepcopy(vllm_serve_args)
self.vllm_serve_args_list = [[str(arg) for arg in args_copy]]
else:
self.vllm_serve_args_list = [
[str(arg) for arg in sublist] for sublist in copy.deepcopy(vllm_serve_args)
]
else:
raise RuntimeError("vllm_serves_args must be a list")
serve_arg_cmd = ["vllm", "serve"]
start_device_id = 0
for i, vllm_serve_arg in enumerate(self.vllm_serve_args_list):
if "--port" not in vllm_serve_arg:
raise ValueError("You have to manually specify the port")
self.port = int(vllm_serve_arg[vllm_serve_arg.index("--port") + 1])
self.health_url_list.append(self.url_for("health"))
required_devices = _get_pd_server_required_devices(vllm_serve_arg)
server_env = copy.deepcopy(self.env_dict)
server_env["ASCEND_RT_VISIBLE_DEVICES"] = ",".join(
str(device_id) for device_id in range(start_device_id, start_device_id + required_devices)
)
start_device_id += required_devices
vllm_serve_arg = [*serve_arg_cmd, *vllm_serve_arg]
proc = self._start_server_with_prefix(vllm_serve_arg, server_env, f"[PD_{i}] ")
self._proc_list.append(proc)
timeout_value = float(max_wait_seconds) if max_wait_seconds is not None else 2800.0
self._wait_for_multiple_servers(
[(self.host, url) for url in self.health_url_list], timeout=timeout_value, always_check_nodes=True
)
def _poll(self) -> int | None:
for proc in self._proc_list:
result = proc.poll()
if result is not None and result != 0:
return result
return None
def _read_output(self, pipe, prefix):
try:
with pipe:
for line in iter(pipe.readline, ""):
if line:
print(f"{prefix}: {line}", end="")
except Exception as e:
print(f"error: {e}")
traceback.print_exc()
def _start_server_with_prefix(self, server_cmd: list[str], env_dict: dict[str, str] | None, log_prefix: str):
env = os.environ.copy()
if env_dict is not None:
env.update(env_dict)
proc = subprocess.Popen(
server_cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, bufsize=1
)
stdout_thread = threading.Thread(target=self._read_output, args=(proc.stdout, log_prefix), daemon=True)
stderr_thread = threading.Thread(target=self._read_output, args=(proc.stderr, log_prefix), daemon=True)
stdout_thread.start()
stderr_thread.start()
return proc
def _terminate_server(self) -> None:
print("pd instance is stopping")
for proc in self._proc_list:
self._terminate_process_tree(proc)
class DisaggPDProxy(RemotePDServer):
def __init__(
self,
port: int,
prefiller_ports: list[int],
decoder_ports: list[int],
host: str = "127.0.0.1",
env_dict: dict[str, str] | None = None,
max_wait_seconds: float | None = 600,
) -> None:
self.env_dict: dict[str, str] = {}
if env_dict is not None:
self.env_dict.update(env_dict)
self._proc_list = []
self.host = host
self.port = int(port)
self.proxy_args = [
"--host",
host,
"--port",
str(port),
"--prefiller-hosts",
*[host] * len(prefiller_ports),
"--prefiller-ports",
*[str(port) for port in prefiller_ports],
"--decoder-hosts",
*[host] * len(decoder_ports),
"--decoder-ports",
*[str(port) for port in decoder_ports],
]
print(f"proxy param is: {self.proxy_args}")
proxy_cmd = [sys.executable, str(DISAGG_PD_PROXY_SCRIPT), *self.proxy_args]
proc = self._start_server_with_prefix(proxy_cmd, self.env_dict, "[PD_PROXY] ")
self._proc_list.append(proc)
timeout_value = float(max_wait_seconds) if max_wait_seconds is not None else 600.0
self._wait_for_multiple_servers([(self.host, self.url_for("healthcheck"))], timeout=timeout_value)
class RemoteEPDServer(RemoteOpenAIServer):
def _start_server(self, model: str, server_cmd: list[str], env_dict: dict[str, str] | None) -> None:
"""Subclasses override this method to customize server process launch"""
raise NotImplementedError("RemoteEPDServer should use _start_server_with_prefix instead")
def __init__(
self,
vllm_serve_args: list[str] | list[list[str]],
server_host: str = "0.0.0.0",
env_dict: dict[str, str] | None = None,
max_wait_seconds: float | None = 2800,
) -> None:
self._proc_list = []
self.env_dict: dict[str, str] = {}
if env_dict is not None:
self.env_dict.update(env_dict)
self.env_dict["VLLM_ALLOW_LONG_MAX_MODEL_LEN"] = "1"
self.env_dict["PYTORCH_NPU_ALLOC_CONF"] = "expandable_segments:True"
self.env_dict["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
self.vllm_serve_args_list = []
self.health_url_list = []
self.host = server_host
if isinstance(vllm_serve_args, list):
if not all(isinstance(item, list) for item in vllm_serve_args):
args_copy = copy.deepcopy(vllm_serve_args)
self.vllm_serve_args_list.append([str(arg) for arg in args_copy])
else:
self.vllm_serve_args_list = [
[str(arg) for arg in sublist] for sublist in copy.deepcopy(vllm_serve_args)
]
else:
raise RuntimeError("vllm_serves_args must be a list")
serve_arg_cmd = ["vllm", "serve"]
for i, vllm_serve_arg in enumerate(self.vllm_serve_args_list):
self.env_dict["ASCEND_RT_VISIBLE_DEVICES"] = str(i)
if isinstance(vllm_serve_arg, list):
if "--port" not in vllm_serve_arg:
raise ValueError("You have to manually specify the port")
else:
port_arg = "--port"
try:
index = vllm_serve_arg.index(port_arg)
except ValueError:
raise ValueError(f"--port not found in args: {vllm_serve_arg}")
port_str = vllm_serve_arg[index + 1]
self.port = int(port_str)
else:
vllm_serve_arg_str = str(vllm_serve_arg)
if "--port" not in vllm_serve_arg_str:
raise ValueError("You have to manually specify the port")
else:
raise ValueError(f"Unexpected type for vllm_serve_arg: {type(vllm_serve_arg)}")
self.health_url_list.append(super().url_for("health"))
vllm_serve_arg = [*serve_arg_cmd, *vllm_serve_arg]
proc = self._start_server_with_prefix(vllm_serve_arg, self.env_dict, f"[VLLM_{i}] ")
self._proc_list.append(proc)
timeout_value = float(max_wait_seconds) if max_wait_seconds is not None else 2800.0
super()._wait_for_multiple_servers(
[(self.host, url) for url in self.health_url_list], timeout=timeout_value, always_check_nodes=True
)
def _poll(self) -> int | None:
return None
def _delete_shm(self) -> None:
for i, arg in enumerate(self.vllm_serve_args_list):
if "--ec-transfer-config" in arg:
index = arg.index("--ec-transfer-config")
config_str = arg[index + 1]
config_dict = json.loads(config_str)
ec_connector_extra_config = config_dict.get("ec_connector_extra_config", {})
shm_path = ec_connector_extra_config.get("shared_storage_path")
if shm_path:
args = ["rm", "-r", "-f", str(shm_path)]
print(f"delete shm_path is: {shm_path}")
self._start_server_with_prefix(args, None, "[DELETE] ")
def _read_output(self, pipe, prefix):
try:
with pipe:
for line in iter(pipe.readline, ""):
if line:
print(f"{prefix}: {line}", end="")
except Exception as e:
print(f"error: {e}")
traceback.print_exc()
def _start_server_with_prefix(self, server_cmd: list[str], env_dict: dict[str, str] | None, log_prefix: str):
env = os.environ.copy()
if env_dict is not None:
env.update(env_dict)
proc = subprocess.Popen(
server_cmd,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
bufsize=1,
)
stdout_thread = threading.Thread(target=self._read_output, args=(proc.stdout, log_prefix), daemon=True)
stderr_thread = threading.Thread(target=self._read_output, args=(proc.stderr, log_prefix), daemon=True)
stdout_thread.start()
stderr_thread.start()
return proc
def _terminate_server(self) -> None:
"""Kill server processes and their children."""
print("vllm instance is stopping")
for proc in self._proc_list:
self._terminate_process_tree(proc)
def __enter__(self):
"""Context manager entry point."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit point - clean up all processes."""
self._terminate_server()
class DisaggEpdProxy(RemoteEPDServer):
def __init__(
self,
proxy_args: list[str] | str | None = None,
env_dict: dict[str, str] | None = None,
server_host: str = "0.0.0.0",
max_wait_seconds: float | None = 2800,
) -> None:
if proxy_args is None:
proxy_args_list: list[str] = []
elif isinstance(proxy_args, str):
proxy_args_list = shlex.split(proxy_args)
else:
proxy_args_list = proxy_args
self.proxy_args = proxy_args_list
self.env_dict: dict[str, str] = {}
if env_dict is not None:
self.env_dict.update(env_dict)
self._proc_list = list()
self.host = server_host
print(f"proxy param is: {self.proxy_args}")
proxy_cmd = ["python", str(DISAGG_EPD_PROXY_SCRIPT), *self.proxy_args]
proc = self._start_server_with_prefix(proxy_cmd, self.env_dict, "[PROXY] ")
self._proc_list.append(proc)
if "--port" not in self.proxy_args:
raise ValueError("You have manually specified the port ")
else:
try:
index = self.proxy_args.index("--port")
except ValueError:
raise ValueError("--port not found in proxy args")
port_str = self.proxy_args[index + 1]
self.port = int(port_str)
timeout_value = float(max_wait_seconds) if max_wait_seconds is not None else 2800.0
super()._wait_for_multiple_servers([(self.host, super().url_for("health"))], timeout=timeout_value)
def __enter__(self):
"""Context manager entry point."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit point - clean up all processes."""
super()._terminate_server()
_DP_RUNNER_START_TIMEOUT_SECONDS = 900.0
_DP_RUNNER_REQUEST_TIMEOUT_SECONDS = 900.0
_DP_RUNNER_SHUTDOWN_TIMEOUT_SECONDS = 30.0
def _split_data_parallel_indices(num_items: int, dp_size: int) -> list[list[int]]:
if num_items < 0:
raise ValueError("num_items must be non-negative")
if dp_size <= 0:
raise ValueError("dp_size must be positive")
floor = num_items // dp_size
remainder = num_items % dp_size
def start(rank: int) -> int:
return rank * floor + min(rank, remainder)
return [list(range(start(rank), start(rank + 1))) for rank in range(dp_size)]
def _slice_optional_inputs(inputs: PromptImageInput | PromptAudioInput | PromptVideoInput | None, indices: list[int]):
if inputs is None:
return None
return [inputs[index] for index in indices]
def _slice_list_inputs(items: list[Any], indices: list[int]) -> list[Any]:
return [items[index] for index in indices]
def _merge_data_parallel_results(total_items: int, shard_results: list[tuple[list[int], list[Any]]]) -> list[Any]:
merged: list[Any] = [None] * total_items
for indices, results in shard_results:
if not indices:
continue
if len(indices) != len(results):
raise RuntimeError("Mismatched result count returned by data parallel worker")
for index, result in zip(indices, results):
merged[index] = result
if any(result is None for result in merged):
raise RuntimeError("Some data parallel results were not returned")
return merged
def _normalize_score_inputs(text_1: str | list[str], text_2: str | list[str]) -> tuple[list[str], list[str]]:
if isinstance(text_1, str) and isinstance(text_2, str):
return [text_1], [text_2]
if isinstance(text_1, str):
return [text_1] * len(text_2), list(text_2)
if isinstance(text_2, str):
return list(text_1), [text_2] * len(text_1)
if len(text_1) != len(text_2):
raise ValueError("`text_1` and `text_2` must have the same length")
return list(text_1), list(text_2)
def _run_vllm_runner_dp_worker(conn, llm_kwargs: dict[str, Any], dp_rank: int, dp_size: int, master_port: int) -> None:
llm = None
try:
os.environ["VLLM_DP_RANK"] = str(dp_rank)
os.environ["VLLM_DP_RANK_LOCAL"] = str(dp_rank)
os.environ["VLLM_DP_SIZE"] = str(dp_size)
os.environ["VLLM_DP_MASTER_IP"] = "127.0.0.1"
os.environ["VLLM_DP_MASTER_PORT"] = str(master_port)
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
from vllm_ascend.utils import vllm_version_is
if not vllm_version_is("0.23.0"):
import torch
visible = os.environ.get("ASCEND_RT_VISIBLE_DEVICES", "")
full_device_ids: list[str] = [d for d in visible.split(",") if d]
if not full_device_ids:
full_device_ids = [str(i) for i in range(torch.npu.device_count())]
if llm_kwargs.get("distributed_executor_backend") == "ray":
devs = full_device_ids
chunk = max(len(devs) // dp_size, 1)
start = dp_rank * chunk
os.environ["ASCEND_RT_VISIBLE_DEVICES"] = ",".join(devs[start : start + chunk])
else:
llm_kwargs["device_ids"] = full_device_ids
llm = LLM(**llm_kwargs)
conn.send({"status": "ready", "rank": dp_rank})
while True:
request = conn.recv()
command = request["command"]
if command == "shutdown":
break
result: Any
if command == "generate":
req_outputs = llm.generate(
request["inputs"], sampling_params=request["sampling_params"], **request["kwargs"]
)
result = VllmRunner._finalize_generate_outputs(req_outputs)
elif command == "generate_w_logprobs":
req_outputs = llm.generate(
request["inputs"], sampling_params=request["sampling_params"], **request["kwargs"]
)
result = VllmRunner._final_steps_generate_w_logprobs(req_outputs)
elif command == "classify":
req_outputs = llm.classify(request["prompts"])
result = [req_output.outputs.probs for req_output in req_outputs]
elif command == "embed":
req_outputs = llm.embed(request["inputs"], *request["args"], **request["kwargs"])
result = [req_output.outputs.embedding for req_output in req_outputs]
elif command == "encode":
req_outputs = llm.encode(request["prompts"])
result = [req_output.outputs.data for req_output in req_outputs]
elif command == "reward":
req_outputs = llm.reward(request["prompts"])
result = [req_output.outputs.data for req_output in req_outputs]
elif command == "score":
req_outputs = llm.score(request["text_1"], request["text_2"], *request["args"], **request["kwargs"])
result = [req_output.outputs.score for req_output in req_outputs]
else:
raise ValueError(f"Unsupported data parallel command: {command}")
conn.send({"status": "ok", "rank": dp_rank, "indices": request["indices"], "result": result})
except Exception:
with contextlib.suppress(Exception):
conn.send({"status": "error", "rank": dp_rank, "traceback": traceback.format_exc()})
raise
finally:
if llm is not None:
del llm
clear_ascend_config()
cleanup_dist_env_and_memory()
with contextlib.suppress(Exception):
conn.close()
class VllmRunner:
2025-09-09 09:40:35 +08:00
def __init__(
self,
model_name: str,
runner: RunnerOption = "auto",
convert: ConvertOption = "auto",
tokenizer_name: str | None = None,
2025-09-09 09:40:35 +08:00
tokenizer_mode: str = "auto",
max_model_len: int | None = 1024,
2025-09-09 09:40:35 +08:00
dtype: str = "auto",
disable_log_stats: bool = True,
tensor_parallel_size: int = 1,
block_size: int = 16,
enable_chunked_prefill: bool = True,
2025-09-09 09:40:35 +08:00
swap_space: int = 4,
enforce_eager: bool | None = False,
quantization: str | None = None,
2025-09-09 09:40:35 +08:00
**kwargs,
) -> None:
data_parallel_size = int(kwargs.get("data_parallel_size", 1))
if data_parallel_size > 1:
raise ValueError("VllmRunner does not support `data_parallel_size > 1`; use `DPVllmRunner` instead.")
2025-09-09 09:40:35 +08:00
self.model = LLM(
model=model_name,
runner=runner,
convert=convert,
2025-09-09 09:40:35 +08:00
tokenizer=tokenizer_name,
tokenizer_mode=tokenizer_mode,
trust_remote_code=True,
dtype=dtype,
swap_space=swap_space,
enforce_eager=enforce_eager,
disable_log_stats=disable_log_stats,
tensor_parallel_size=tensor_parallel_size,
max_model_len=max_model_len,
block_size=block_size,
enable_chunked_prefill=enable_chunked_prefill,
quantization=quantization,
**kwargs,
)
@staticmethod
def _finalize_generate_outputs(req_outputs: list[RequestOutput]) -> list[tuple[list[list[int]], list[str]]]:
outputs: list[tuple[list[list[int]], list[str]]] = []
2025-09-09 09:40:35 +08:00
for req_output in req_outputs:
prompt_str = req_output.prompt
prompt_ids = req_output.prompt_token_ids
req_sample_output_ids: list[list[int]] = []
req_sample_output_strs: list[str] = []
2025-09-09 09:40:35 +08:00
for sample in req_output.outputs:
output_str = sample.text
output_ids = list(sample.token_ids)
req_sample_output_ids.append(prompt_ids + output_ids)
req_sample_output_strs.append((prompt_str or "") + output_str)
2025-09-09 09:40:35 +08:00
outputs.append((req_sample_output_ids, req_sample_output_strs))
return outputs
def get_inputs(
self,
prompts: list[str] | list[torch.Tensor] | list[int],
images: PromptImageInput | None = None,
videos: PromptVideoInput | None = None,
audios: PromptAudioInput | None = None,
) -> list[TextPrompt]:
if any(x is not None and len(x) != len(prompts) for x in [images, videos, audios]):
raise ValueError("All non-None multimodal inputs must have the same length as prompts")
inputs = []
for i, prompt in enumerate(prompts):
multi_modal_data = {}
if images is not None and (image := images[i]) is not None:
multi_modal_data["image"] = image
if videos is not None and (video := videos[i]) is not None:
multi_modal_data["video"] = video # type: ignore
if audios is not None and (audio := audios[i]) is not None:
multi_modal_data["audio"] = audio # type: ignore
text_prompt_kwargs: dict[str, Any] = {"multi_modal_data": multi_modal_data or None}
if isinstance(prompt, str):
text_prompt_kwargs["prompt"] = prompt
elif isinstance(prompt, list):
text_prompt_kwargs["prompt_token_ids"] = prompt
else:
text_prompt_kwargs["prompt_embeds"] = prompt
inputs.append(TextPrompt(**text_prompt_kwargs))
return inputs
def generate(
self,
prompts: list[str] | list[torch.Tensor] | list[list[int]],
sampling_params: SamplingParams,
images: PromptImageInput | None = None,
videos: PromptVideoInput | None = None,
audios: PromptAudioInput | None = None,
**kwargs: Any,
) -> list[tuple[list[list[int]], list[str]]]:
inputs = self.get_inputs(prompts, images=images, videos=videos, audios=audios)
req_outputs = self.model.generate(inputs, sampling_params=sampling_params, **kwargs)
return self._finalize_generate_outputs(req_outputs)
2025-09-09 09:40:35 +08:00
@staticmethod
def _final_steps_generate_w_logprobs(
req_outputs: list[RequestOutput],
) -> list[TokensTextLogprobsPromptLogprobs]:
outputs: list[TokensTextLogprobsPromptLogprobs] = []
2025-09-09 09:40:35 +08:00
for req_output in req_outputs:
assert len(req_output.outputs) > 0
for sample in req_output.outputs:
output_str = sample.text
output_ids = list(sample.token_ids)
output_logprobs = sample.logprobs
outputs.append((output_ids, output_str, output_logprobs, req_output.prompt_logprobs))
2025-09-09 09:40:35 +08:00
return outputs
def generate_w_logprobs(
self,
prompts: list[str],
2025-09-09 09:40:35 +08:00
sampling_params: SamplingParams,
images: PromptImageInput | None = None,
audios: PromptAudioInput | None = None,
videos: PromptVideoInput | None = None,
**kwargs: Any,
) -> list[TokensTextLogprobs] | list[TokensTextLogprobsPromptLogprobs]:
inputs = self.get_inputs(prompts, images=images, videos=videos, audios=audios)
req_outputs = self.model.generate(inputs, sampling_params=sampling_params, **kwargs)
toks_str_logsprobs_prompt_logprobs = self._final_steps_generate_w_logprobs(req_outputs)
2025-09-09 09:40:35 +08:00
# Omit prompt logprobs if not required by sampling params
return (
[x[0:-1] for x in toks_str_logsprobs_prompt_logprobs]
if sampling_params.prompt_logprobs is None
else toks_str_logsprobs_prompt_logprobs
)
2025-09-09 09:40:35 +08:00
def generate_greedy(
self,
prompts: list[str] | list[torch.Tensor] | list[list[int]],
2025-09-09 09:40:35 +08:00
max_tokens: int,
images: PromptImageInput | None = None,
videos: PromptVideoInput | None = None,
audios: PromptAudioInput | None = None,
**kwargs: Any,
) -> list[tuple[list[int], str]]:
2025-09-09 09:40:35 +08:00
greedy_params = SamplingParams(temperature=0.0, max_tokens=max_tokens)
outputs = self.generate(prompts, greedy_params, images=images, videos=videos, audios=audios, **kwargs)
return [(output_ids[0], output_str[0]) for output_ids, output_str in outputs]
2025-09-09 09:40:35 +08:00
def generate_greedy_logprobs(
self,
prompts: list[str],
2025-09-09 09:40:35 +08:00
max_tokens: int,
num_logprobs: int | None,
num_prompt_logprobs: int | None = None,
images: PromptImageInput | None = None,
audios: PromptAudioInput | None = None,
videos: PromptVideoInput | None = None,
stop_token_ids: list[int] | None = None,
stop: list[str] | None = None,
**kwargs: Any,
) -> list[TokensTextLogprobs] | list[TokensTextLogprobsPromptLogprobs]:
2025-09-09 09:40:35 +08:00
greedy_logprobs_params = SamplingParams(
temperature=0.0,
max_tokens=max_tokens,
logprobs=num_logprobs,
prompt_logprobs=num_prompt_logprobs,
stop_token_ids=stop_token_ids,
stop=stop,
)
2025-09-09 09:40:35 +08:00
return self.generate_w_logprobs(
prompts, greedy_logprobs_params, images=images, audios=audios, videos=videos, **kwargs
)
2025-09-09 09:40:35 +08:00
def classify(self, prompts: list[str]) -> list[list[float]]:
req_outputs = self.model.classify(prompts)
return [req_output.outputs.probs for req_output in req_outputs]
def embed(
2025-09-09 09:40:35 +08:00
self,
prompts: list[str],
images: PromptImageInput | None = None,
videos: PromptVideoInput | None = None,
audios: PromptAudioInput | None = None,
*args,
**kwargs,
) -> list[list[float]]:
inputs = self.get_inputs(prompts, images=images, videos=videos, audios=audios)
req_outputs = self.model.embed(inputs, *args, **kwargs)
2025-09-09 09:40:35 +08:00
return [req_output.outputs.embedding for req_output in req_outputs]
def encode(self, prompts: list[str]) -> list[list[float]]:
req_outputs = self.model.encode(prompts)
return [req_output.outputs.data for req_output in req_outputs]
def reward(self, prompts: list[str]) -> list[list[float]]:
req_outputs = self.model.reward(prompts)
return [req_output.outputs.data for req_output in req_outputs]
def score(
self,
text_1: str | list[str],
text_2: str | list[str],
*args,
**kwargs,
) -> list[float]:
req_outputs = self.model.score(text_1, text_2, *args, **kwargs)
return [req_output.outputs.score for req_output in req_outputs]
2025-09-09 09:40:35 +08:00
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
del self.model
clear_ascend_config()
cleanup_dist_env_and_memory()
class ModelCache:
"""Model cache management class"""
def __init__(self):
self._cache: dict[str, VllmRunner] = {}
def close(self):
"""Properly closing all resources (including terminating child processes)"""
if hasattr(self, "model") and self.model is not None:
try:
if hasattr(self.model, "llm_engine"):
self.model.llm_engine.shutdown()
del self.model
import torch
torch.npu.empty_cache()
print("[INFO] VllmRunner closed successfully")
except Exception as e:
print(f"[WARNING] Error closing VllmRunner: {e}")
def _get_available_npu_memory(self) -> float:
"""Obtain NPU Available Memory (GiB)"""
import torch
free, _ = torch.npu.mem_get_info()
return free / (1024**3)
def _wait_for_memory(self, required_gib: float, timeout_seconds: int = 30) -> bool:
"""Wait until there is sufficient available memory."""
import time
start_time = time.time()
while time.time() - start_time < timeout_seconds:
available = self._get_available_npu_memory()
if available >= required_gib:
return True
print(f"[INFO] Waiting for NPU memory... Available: {available:.2f} GiB, Required: {required_gib:.2f} GiB")
time.sleep(2)
return False
def get_cache_key(self, model_config: dict[str, Any]) -> str:
"""Generating a unique cache key.
Args:
model_config: Model Configuration Dictionary
Returns:
str: The only cache key
"""
sorted_config = {k: v for k, v in sorted(model_config.items())}
config_str = json.dumps(sorted_config, sort_keys=True)
return hashlib.md5(config_str.encode()).hexdigest()
def get_or_create(self, model_config: dict[str, Any]) -> "VllmRunner":
"""Obtain or create a model instance
Args:
model_config: Model Configuration
Returns:
VllmRunner: Model Instance
"""
"""Obtain or create a model instance (with full memory management)"""
cache_key = self.get_cache_key(model_config)
if cache_key in self._cache:
print(f"[INFO] Reusing cached model instance for: {model_config['model_name']}")
return self._cache[cache_key]
gpu_memory_utilization = model_config.get("gpu_memory_utilization", 0.9)
free_memory_bytes, total_memory_bytes = torch.npu.mem_get_info()
gib = 1024**3
available_memory_gib = free_memory_bytes / gib
required_memory_gib = (total_memory_bytes / gib) * gpu_memory_utilization
print(
f"[DEBUG] Creating new model - Available: {available_memory_gib:.2f} GiB, "
f"Required: {required_memory_gib:.2f} GiB"
)
if available_memory_gib < required_memory_gib:
print("[WARNING] Insufficient memory! Cleaning oldest cache entries...")
self.clear()
wait_count = 0
max_wait = 10
while available_memory_gib < required_memory_gib and wait_count < max_wait:
time.sleep(3)
free_memory_bytes, _ = torch.npu.mem_get_info()
available_memory_gib = free_memory_bytes / (1024**3)
print(f"[INFO] Waiting for memory... Available: {available_memory_gib:.2f} GiB")
wait_count += 1
if available_memory_gib < required_memory_gib:
raise RuntimeError(
f"Failed to get enough NPU memory! "
f"Available: {available_memory_gib:.2f} GiB, "
f"Required: {required_memory_gib:.2f} GiB."
)
if cache_key not in self._cache:
runner = VllmRunner(
model_name=model_config["model_name"],
quantization=model_config.get("quantization"),
max_model_len=model_config.get("max_model_len", 1024),
dtype=model_config.get("dtype", "bfloat16"),
gpu_memory_utilization=model_config.get("gpu_memory_utilization", 0.9),
enable_prefix_caching=model_config.get("enable_prefix_caching", False),
max_num_seqs=model_config.get("max_num_seqs", 64),
tensor_parallel_size=model_config.get("tensor_parallel_size", 1),
distributed_executor_backend=model_config.get("distributed_executor_backend", "mp"),
compilation_config=model_config.get(
"compilation_config",
{"cudagraph_mode": "FULL_DECODE_ONLY", "cudagraph_capture_sizes": [1, 32, 64]},
),
**model_config.get("extra_kwargs", {}),
)
self._cache[cache_key] = runner
print(f"Created new model instance for: {model_config['model_name']}")
else:
print(f"Reusing existing model instance for: {model_config['model_name']}")
return self._cache[cache_key]
def clear(self):
"""Clearing All Cached Model Instances"""
import gc
for cache_key in list(self._cache.keys()):
runner = self._cache[cache_key]
try:
if hasattr(runner, "model"):
del runner.model
del self._cache[cache_key]
del runner
except Exception as e:
print(f"[WARNING] Error clearing runner {cache_key}: {e}")
gc.collect()
torch.npu.empty_cache()
time.sleep(3)
self._cache.clear()
print("[INFO] Model cache cleared")
model_cache = ModelCache()
2025-09-09 09:40:35 +08:00
@pytest.fixture(scope="session", autouse=True)
def cleanup_model_cache():
"""Clearing the Model Cache After the Test Session Ends"""
yield
model_cache.clear()
2025-09-09 09:40:35 +08:00
@pytest.fixture(scope="function")
def vllm_runner(request):
"""Obtain or create a model instance based on the model configuration.
Args:
request: pytest request object
Yields:
VllmRunner: Model Instance
"""
print(f"[DEBUG] Test name: {request.node.name}")
print(f"[DEBUG] All markers on test: {list(request.node.iter_markers())}")
model_marker = request.node.get_closest_marker("model")
if model_marker is None:
raise ValueError("Test must have @pytest.mark.model decorator")
model_config = model_marker.kwargs
print(f"[DEBUG] Final model_config: {model_config}")
if model_marker:
model_config = model_marker.kwargs
else:
model_config = {
"model_name": "Qwen/Qwen3-0.6B",
"quantization": None,
"max_model_len": 1024,
"dtype": "auto",
"gpu_memory_utilization": 0.9,
"enable_prefix_caching": False,
}
print(f"[DEBUG] vllm_runner fixture - model_config: {model_config}")
try:
runner = model_cache.get_or_create(model_config)
except Exception as e:
print(f"[ERROR] Failed to create model instance with config: {model_config}")
print(f"[ERROR] Exception: {type(e).__name__}: {e}")
raise
yield runner
class DPVllmRunner(VllmRunner):
def __init__(
self,
model_name: str,
runner: RunnerOption = "auto",
convert: ConvertOption = "auto",
tokenizer_name: str | None = None,
tokenizer_mode: str = "auto",
max_model_len: int | None = 1024,
dtype: str = "auto",
disable_log_stats: bool = True,
tensor_parallel_size: int = 1,
block_size: int = 16,
enable_chunked_prefill: bool = True,
swap_space: int = 4,
enforce_eager: bool | None = False,
quantization: str | None = None,
data_parallel_size: int = 2,
**kwargs,
) -> None:
if data_parallel_size < 2:
raise ValueError("DPVllmRunner requires `data_parallel_size >= 2`")
self._dp_size = data_parallel_size
self._dp_parent_conns: list[Any] = []
self._dp_processes: list[Any] = []
self._dp_start_timeout = float(kwargs.pop("dp_start_timeout", _DP_RUNNER_START_TIMEOUT_SECONDS))
self._dp_request_timeout = float(kwargs.pop("dp_request_timeout", _DP_RUNNER_REQUEST_TIMEOUT_SECONDS))
llm_kwargs = dict(
model=model_name,
runner=runner,
convert=convert,
tokenizer=tokenizer_name,
tokenizer_mode=tokenizer_mode,
trust_remote_code=True,
dtype=dtype,
swap_space=swap_space,
enforce_eager=enforce_eager,
disable_log_stats=disable_log_stats,
tensor_parallel_size=tensor_parallel_size,
max_model_len=max_model_len,
block_size=block_size,
enable_chunked_prefill=enable_chunked_prefill,
quantization=quantization,
**kwargs,
)
cleanup_dist_env_and_memory()
self._start_data_parallel_workers(llm_kwargs)
@property
def model(self) -> LLM:
raise RuntimeError("Direct access to `runner.model` is not supported by `DPVllmRunner`.")
def _start_data_parallel_workers(self, llm_kwargs: dict[str, Any]) -> None:
ctx = multiprocessing.get_context("spawn")
master_port = get_open_port()
try:
for dp_rank in range(self._dp_size):
parent_conn, child_conn = ctx.Pipe()
proc = ctx.Process(
target=_run_vllm_runner_dp_worker,
args=(child_conn, llm_kwargs, dp_rank, self._dp_size, master_port),
)
proc.start()
child_conn.close()
self._dp_parent_conns.append(parent_conn)
self._dp_processes.append(proc)
for rank, conn in enumerate(self._dp_parent_conns):
if not conn.poll(self._dp_start_timeout):
raise TimeoutError(f"Timed out waiting for data parallel worker {rank} to start")
message = conn.recv()
if message["status"] != "ready":
raise RuntimeError(
f"Failed to start data parallel worker {rank}:\n{message.get('traceback', 'unknown error')}"
)
except Exception:
self._stop_data_parallel_workers()
raise
def _stop_data_parallel_workers(self) -> None:
for conn in self._dp_parent_conns:
with contextlib.suppress(Exception):
conn.send({"command": "shutdown"})
for proc in self._dp_processes:
proc.join(timeout=_DP_RUNNER_SHUTDOWN_TIMEOUT_SECONDS)
if proc.is_alive():
proc.kill()
proc.join(timeout=5)
for conn in self._dp_parent_conns:
with contextlib.suppress(Exception):
conn.close()
self._dp_parent_conns.clear()
self._dp_processes.clear()
def _dispatch_prompt_command(
self,
command: str,
prompts: list[str] | list[torch.Tensor] | list[list[int]],
*,
images: PromptImageInput | None = None,
videos: PromptVideoInput | None = None,
audios: PromptAudioInput | None = None,
**payload: Any,
) -> list[Any]:
if not prompts:
return []
shard_results: list[tuple[list[int], list[Any]]] = []
shard_indices = _split_data_parallel_indices(len(prompts), self._dp_size)
for rank, conn in enumerate(self._dp_parent_conns):
indices = shard_indices[rank]
worker_indices = indices or [0]
worker_prompts = _slice_list_inputs(prompts, worker_indices)
conn.send(
{
"command": command,
"indices": indices,
"inputs": self.get_inputs(
worker_prompts,
images=_slice_optional_inputs(images, worker_indices),
videos=_slice_optional_inputs(videos, worker_indices),
audios=_slice_optional_inputs(audios, worker_indices),
),
"prompts": worker_prompts,
**payload,
}
)
try:
for rank, conn in enumerate(self._dp_parent_conns):
if not conn.poll(self._dp_request_timeout):
raise TimeoutError(f"Timed out waiting for data parallel worker {rank} to finish `{command}`")
message = conn.recv()
if message["status"] != "ok":
raise RuntimeError(
f"Data parallel worker {rank} failed during `{command}`:\n"
f"{message.get('traceback', 'unknown error')}"
)
shard_results.append((message["indices"], message["result"]))
except Exception:
self._stop_data_parallel_workers()
raise
return _merge_data_parallel_results(len(prompts), shard_results)
def _dispatch_text_command(self, command: str, prompts: list[str]) -> list[Any]:
if not prompts:
return []
shard_results: list[tuple[list[int], list[Any]]] = []
shard_indices = _split_data_parallel_indices(len(prompts), self._dp_size)
for rank, conn in enumerate(self._dp_parent_conns):
indices = shard_indices[rank]
worker_indices = indices or [0]
conn.send(
{
"command": command,
"indices": indices,
"prompts": _slice_list_inputs(prompts, worker_indices),
}
)
try:
for rank, conn in enumerate(self._dp_parent_conns):
if not conn.poll(self._dp_request_timeout):
raise TimeoutError(f"Timed out waiting for data parallel worker {rank} to finish `{command}`")
message = conn.recv()
if message["status"] != "ok":
raise RuntimeError(
f"Data parallel worker {rank} failed during `{command}`:\n"
f"{message.get('traceback', 'unknown error')}"
)
shard_results.append((message["indices"], message["result"]))
except Exception:
self._stop_data_parallel_workers()
raise
return _merge_data_parallel_results(len(prompts), shard_results)
def generate(
self,
prompts: list[str] | list[torch.Tensor] | list[list[int]],
sampling_params: SamplingParams,
images: PromptImageInput | None = None,
videos: PromptVideoInput | None = None,
audios: PromptAudioInput | None = None,
**kwargs: Any,
) -> list[tuple[list[list[int]], list[str]]]:
return self._dispatch_prompt_command(
"generate",
prompts,
images=images,
videos=videos,
audios=audios,
sampling_params=sampling_params,
kwargs=kwargs,
)
def generate_w_logprobs(
self,
prompts: list[str],
sampling_params: SamplingParams,
images: PromptImageInput | None = None,
audios: PromptAudioInput | None = None,
videos: PromptVideoInput | None = None,
**kwargs: Any,
) -> list[TokensTextLogprobs] | list[TokensTextLogprobsPromptLogprobs]:
toks_str_logsprobs_prompt_logprobs = self._dispatch_prompt_command(
"generate_w_logprobs",
prompts,
images=images,
videos=videos,
audios=audios,
sampling_params=sampling_params,
kwargs=kwargs,
)
return (
[x[0:-1] for x in toks_str_logsprobs_prompt_logprobs]
if sampling_params.prompt_logprobs is None
else toks_str_logsprobs_prompt_logprobs
)
def classify(self, prompts: list[str]) -> list[list[float]]:
return self._dispatch_text_command("classify", prompts)
def embed(
self,
prompts: list[str],
images: PromptImageInput | None = None,
videos: PromptVideoInput | None = None,
audios: PromptAudioInput | None = None,
*args,
**kwargs,
) -> list[list[float]]:
return self._dispatch_prompt_command(
"embed",
prompts,
images=images,
videos=videos,
audios=audios,
args=args,
kwargs=kwargs,
)
def encode(self, prompts: list[str]) -> list[list[float]]:
return self._dispatch_text_command("encode", prompts)
def reward(self, prompts: list[str]) -> list[list[float]]:
return self._dispatch_text_command("reward", prompts)
def score(
self,
text_1: str | list[str],
text_2: str | list[str],
*args,
**kwargs,
) -> list[float]:
normalized_text_1, normalized_text_2 = _normalize_score_inputs(text_1, text_2)
if not normalized_text_1:
return []
shard_results: list[tuple[list[int], list[Any]]] = []
shard_indices = _split_data_parallel_indices(len(normalized_text_1), self._dp_size)
for rank, conn in enumerate(self._dp_parent_conns):
indices = shard_indices[rank]
worker_indices = indices or [0]
conn.send(
{
"command": "score",
"indices": indices,
"text_1": _slice_list_inputs(normalized_text_1, worker_indices),
"text_2": _slice_list_inputs(normalized_text_2, worker_indices),
"args": args,
"kwargs": kwargs,
}
)
try:
for rank, conn in enumerate(self._dp_parent_conns):
if not conn.poll(self._dp_request_timeout):
raise TimeoutError(f"Timed out waiting for data parallel worker {rank} to finish `score`")
message = conn.recv()
if message["status"] != "ok":
raise RuntimeError(
f"Data parallel worker {rank} failed during `score`:\n"
f"{message.get('traceback', 'unknown error')}"
)
shard_results.append((message["indices"], message["result"]))
except Exception:
self._stop_data_parallel_workers()
raise
return _merge_data_parallel_results(len(normalized_text_1), shard_results)
def __exit__(self, exc_type, exc_value, traceback):
self._stop_data_parallel_workers()
clear_ascend_config()
cleanup_dist_env_and_memory()
DataParallelVllmRunner = DPVllmRunner
class HfRunner:
def get_default_device(self):
if current_platform.is_cpu():
return "cpu"
else:
torch.npu.set_compile_mode(jit_compile=False)
return current_platform.device_type
def wrap_device(self, x: _T, device: str | None = None) -> _T:
if x is None or isinstance(x, (bool,)):
2025-09-09 09:40:35 +08:00
return x
if device is None:
device = self.device
if isinstance(x, dict):
return {k: self.wrap_device(v, device) for k, v in x.items()}
if hasattr(x, "device") and x.device.type == device:
return x
return x.to(device)
def __init__(
self,
model_name: str,
dtype: str = "auto",
*,
model_kwargs: dict[str, Any] | None = None,
2025-09-09 09:40:35 +08:00
trust_remote_code: bool = True,
is_sentence_transformer: bool = False,
is_cross_encoder: bool = False,
skip_tokenizer_init: bool = False,
auto_cls: type[_BaseAutoModelClass] = AutoModelForCausalLM,
) -> None:
model_name = maybe_model_redirect(model_name)
self.model_name = model_name
self.config = AutoConfig.from_pretrained(
model_name,
trust_remote_code=trust_remote_code,
)
self.device = self.get_default_device()
self.dtype = torch_dtype = _get_and_verify_dtype(
self.model_name,
self.config,
dtype=dtype,
is_pooling_model=is_sentence_transformer or is_cross_encoder,
)
model_kwargs = model_kwargs if model_kwargs is not None else {}
model_kwargs.setdefault("torch_dtype", torch_dtype)
if is_sentence_transformer:
# Lazy init required for AMD CI
from sentence_transformers import SentenceTransformer
self.model = SentenceTransformer(
model_name,
device=self.device,
model_kwargs=model_kwargs,
trust_remote_code=trust_remote_code,
)
elif is_cross_encoder:
# Lazy init required for AMD CI
from sentence_transformers import CrossEncoder
self.model = CrossEncoder(
model_name,
device=self.device,
automodel_args=model_kwargs,
trust_remote_code=trust_remote_code,
)
else:
model = auto_cls.from_pretrained(
model_name,
trust_remote_code=trust_remote_code,
**model_kwargs,
)
# in case some unquantized custom models are not in same dtype
if getattr(model, "quantization_method", None) is None and any(
p.dtype != self.dtype for p in model.parameters()
):
2025-09-09 09:40:35 +08:00
model = model.to(dtype=self.dtype)
if (
getattr(model, "quantization_method", None) != "bitsandbytes"
and len({p.device for p in model.parameters()}) < 2
):
2025-09-09 09:40:35 +08:00
model = model.to(device=self.device)
self.model = model
if not skip_tokenizer_init:
self.tokenizer = AutoTokenizer.from_pretrained(
model_name,
torch_dtype=torch_dtype,
trust_remote_code=trust_remote_code,
)
# don't put this import at the top level
# it will call torch.cuda.device_count()
from transformers import AutoProcessor # noqa: F401
2025-09-09 09:40:35 +08:00
self.processor = AutoProcessor.from_pretrained(
model_name,
torch_dtype=torch_dtype,
trust_remote_code=trust_remote_code,
)
if skip_tokenizer_init:
self.tokenizer = self.processor.tokenizer
def get_inputs(
self,
prompts: list[str],
images: PromptImageInput | None = None,
videos: PromptVideoInput | None = None,
audios: PromptAudioInput | None = None,
) -> list[BatchFeature | BatchEncoding]:
if images is not None:
assert len(prompts) == len(images)
if videos is not None:
assert len(prompts) == len(videos)
if audios is not None:
assert len(prompts) == len(audios)
all_inputs: list[BatchFeature | BatchEncoding] = []
for i, prompt in enumerate(prompts):
processor_kwargs: dict[str, Any] = {
"text": prompt,
"return_tensors": "pt",
}
if images is not None and (image := images[i]) is not None:
processor_kwargs["images"] = image
if videos is not None and (video := videos[i]) is not None:
processor_kwargs["videos"] = video
if audios is not None and (audio_inputs := audios[i]) is not None:
# HACK - not all processors take sampling_rate; we should
# clean this up in the future.
if len(audio_inputs) == 2:
audio, sr = audio_inputs
processor_kwargs["audio"] = audio
processor_kwargs["sampling_rate"] = sr
else:
processor_kwargs["audio"] = audio_inputs
inputs = self.processor(**processor_kwargs)
if isinstance(inputs, BatchFeature):
inputs = inputs.to(dtype=self.dtype)
all_inputs.append(inputs)
return all_inputs
def classify(self, prompts: list[str]) -> list[str]:
# output is final logits
all_inputs = self.get_inputs(prompts)
outputs = []
problem_type = getattr(self.config, "problem_type", "")
for inputs in all_inputs:
output = self.model(**self.wrap_device(inputs))
if problem_type == "regression":
logits = output.logits[0].tolist()
elif problem_type == "multi_label_classification":
logits = output.logits.sigmoid()[0].tolist()
else:
logits = output.logits.softmax(dim=-1)[0].tolist()
outputs.append(logits)
return outputs
def encode(self, prompts: list[str], *args, **kwargs) -> list[list[torch.Tensor]]:
2025-09-09 09:40:35 +08:00
return self.model.encode(prompts, *args, **kwargs)
def predict(self, prompts: list[list[str]], *args, **kwargs) -> torch.Tensor:
return self.model.predict(prompts, *args, convert_to_tensor=True, **kwargs)
2025-09-09 09:40:35 +08:00
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
del self.model
cleanup_dist_env_and_memory()
@pytest.fixture(scope="session")
def ilama_lora_files():
return snapshot_download(
repo_id="vllm-ascend/ilama-text2sql-spider",
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
)
2025-09-09 09:40:35 +08:00
@pytest.fixture(scope="session")
def llama32_lora_files():
from huggingface_hub import snapshot_download as hf_snapshot_download
return hf_snapshot_download(repo_id="jeeejeee/llama32-3b-text2sql-spider", local_files_only=True)
@pytest.fixture(scope="session")
def qwen35_text_lora_files():
return snapshot_download(repo_id="vllm-ascend/qwen35-4b-text-only-sql-lora")
2025-09-09 09:40:35 +08:00
@pytest.fixture(scope="session")
def qwen3moe_lora_files():
return snapshot_download(repo_id="vllm-ascend/qwen3-moe-text2sql-spider")
2025-09-09 09:40:35 +08:00
@pytest.fixture(scope="session")
def olmoe_lora_files():
return snapshot_download(repo_id="vllm-ascend/olmoe-instruct-text2sql-spider")
def qwen_prompt(questions: list[str]) -> list[str]:
placeholder = "<|image_pad|>"
return [
(
"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n"
f"<|im_start|>user\n<|vision_start|>{placeholder}<|vision_end|>"
f"{q}<|im_end|>\n<|im_start|>assistant\n"
)
for q in questions
]
def hunyuan_prompt(questions: list[str]) -> list[str]:
placeholder = "<hy_place▁holder▁no▁100><hy_place▁holder▁no▁102><hy_place▁holder▁no▁101>" # noqa: E501
return [f"<hy_begin▁of▁sentence>{placeholder}{question}<hy_User>" for question in questions]
PROMPT_CONFIGS = {
"qwen-vl": {
"model": "Qwen/Qwen3-VL-8B-Instruct",
"prompt_fn": qwen_prompt,
"mm_processor_kwargs": {
"min_pixels": 28 * 28,
"max_pixels": 1280 * 28 * 28,
"fps": 1,
},
},
"hunyuan-vl": {
"model": "Tencent-Hunyuan/HunyuanOCR",
"prompt_fn": hunyuan_prompt,
"mm_processor_kwargs": {},
},
2025-09-09 09:40:35 +08:00
}
@pytest.fixture(params=PROMPT_CONFIGS.keys())
def vl_config(request):
config = PROMPT_CONFIGS[request.param]
if "skip" in config:
pytest.skip(config["skip"])
return config