85742ab165
CPU Test / Lint - next (push) Waiting to run
Dashboard / Chromatic (push) Waiting to run
CPU Test / Lint - fast (push) Waiting to run
CPU Test / Build documentation (push) Waiting to run
CPU Test / Test (Store, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Weave, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Others, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Store, latest, Python 3.13) (push) Waiting to run
CPU Test / Lint - slow (push) Waiting to run
CPU Test / Lint - JavaScript (push) Waiting to run
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Others, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Weave, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (JavaScript) (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
182 lines
6.4 KiB
Python
182 lines
6.4 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from typing import Any, Optional
|
|
|
|
import httpx
|
|
import openai
|
|
|
|
VLLM_AVAILABLE = False
|
|
VLLM_UNAVAILABLE_REASON = ""
|
|
|
|
try:
|
|
import vllm
|
|
from vllm.engine.arg_utils import AsyncEngineArgs
|
|
from vllm.entrypoints.cli.serve import ServeSubcommand
|
|
from vllm.model_executor.model_loader import get_model_loader
|
|
from vllm.utils import FlexibleArgumentParser
|
|
|
|
VLLM_AVAILABLE = True # type: ignore
|
|
VLLM_VERSION = tuple(int(v) for v in vllm.__version__.split("."))
|
|
except ImportError as e:
|
|
AsyncEngineArgs = None
|
|
get_model_loader = None
|
|
FlexibleArgumentParser = None
|
|
ServeSubcommand = None
|
|
VLLM_VERSION = (0, 0, 0) # type: ignore
|
|
VLLM_UNAVAILABLE_REASON = str(e) # type: ignore
|
|
|
|
|
|
class RemoteOpenAIServer:
|
|
"""
|
|
A context manager for launching and interacting with a remote vLLM-based
|
|
OpenAI-compatible server instance.
|
|
|
|
This class handles:
|
|
- Preparing the environment and spawning the vLLM server process
|
|
- Ensuring that the requested model is downloaded before server startup
|
|
- Polling and health-checking the server until it is ready
|
|
- Providing helper methods to construct URLs for API calls
|
|
- Returning configured synchronous and asynchronous OpenAI clients
|
|
that can communicate with the launched server
|
|
|
|
Typical usage:
|
|
with RemoteOpenAIServer(vllm_serve_args, port, model) as server:
|
|
client = server.get_client()
|
|
response = client.chat.completions.create(...)
|
|
|
|
Attributes:
|
|
DUMMY_API_KEY (str): A placeholder API key for compatibility
|
|
(vLLM does not require authentication).
|
|
host (str): Host address of the server (default: "localhost").
|
|
port (int): TCP port number for the server.
|
|
proc (subprocess.Popen): Handle to the launched server process.
|
|
"""
|
|
|
|
DUMMY_API_KEY = "token-abc123" # vLLM's OpenAI server does not need API key
|
|
|
|
def _start_server(self, model: str, vllm_serve_args: list[str], env_dict: Optional[dict[str, str]]) -> None:
|
|
"""Subclasses override this method to customize server process launch"""
|
|
env = os.environ.copy()
|
|
env["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" # safer CUDA init
|
|
if env_dict is not None:
|
|
env.update(env_dict)
|
|
|
|
if VLLM_VERSION >= (0, 10, 2):
|
|
# Supports return_token_ids
|
|
self.proc: subprocess.Popen[bytes] = subprocess.Popen(
|
|
["vllm", "serve", model, *vllm_serve_args],
|
|
env=env,
|
|
stdout=sys.stdout,
|
|
stderr=sys.stderr,
|
|
)
|
|
else:
|
|
# Does not support return_token_ids
|
|
self.proc = subprocess.Popen(
|
|
["python", "-m", "agentlightning.cli.vllm", "serve", model, *vllm_serve_args],
|
|
env=env,
|
|
stdout=sys.stdout,
|
|
stderr=sys.stderr,
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
vllm_serve_args: list[str], # should not include the model name
|
|
env_dict: Optional[dict[str, str]] = None,
|
|
seed: Optional[int] = 0,
|
|
max_wait_seconds: Optional[float] = None,
|
|
) -> None:
|
|
if (
|
|
not VLLM_AVAILABLE
|
|
or AsyncEngineArgs is None
|
|
or get_model_loader is None
|
|
or FlexibleArgumentParser is None
|
|
or ServeSubcommand is None
|
|
):
|
|
raise ImportError("vLLM is not available: " + VLLM_UNAVAILABLE_REASON)
|
|
|
|
self.model = model
|
|
|
|
parser = FlexibleArgumentParser(description="vLLM's remote OpenAI server.")
|
|
subparsers = parser.add_subparsers(required=False, dest="subparser")
|
|
parser = ServeSubcommand().subparser_init(subparsers) # pyright: ignore[reportUnknownMemberType]
|
|
args = parser.parse_args(["--model", model, *vllm_serve_args])
|
|
assert args is not None
|
|
self.host = str(args.host or "localhost")
|
|
self.port = int(args.port)
|
|
|
|
# download the model before starting the server to avoid timeout
|
|
is_local = os.path.isdir(model)
|
|
if not is_local:
|
|
engine_args = AsyncEngineArgs.from_cli_args(args)
|
|
model_config = engine_args.create_model_config()
|
|
load_config = engine_args.create_load_config()
|
|
|
|
model_loader = get_model_loader(load_config)
|
|
model_loader.download_model(model_config)
|
|
|
|
self._start_server(model, vllm_serve_args, env_dict)
|
|
max_wait_seconds = max_wait_seconds or 240
|
|
self._wait_for_server(url=self.url_for("health"), timeout=max_wait_seconds)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any):
|
|
self.proc.terminate()
|
|
try:
|
|
self.proc.wait(8)
|
|
except subprocess.TimeoutExpired:
|
|
self.proc.kill()
|
|
|
|
def _poll(self) -> Optional[int]:
|
|
"""Subclasses override this method to customize process polling"""
|
|
return self.proc.poll()
|
|
|
|
def _wait_for_server(self, *, url: str, timeout: float):
|
|
start = time.time()
|
|
client = httpx.Client()
|
|
|
|
while True:
|
|
try:
|
|
if client.get(url).status_code == 200:
|
|
break
|
|
except Exception:
|
|
result = self._poll()
|
|
if result is not None and result != 0:
|
|
raise RuntimeError("Server exited unexpectedly.") from None
|
|
time.sleep(0.5)
|
|
if time.time() - start > timeout:
|
|
raise RuntimeError("Server failed to start in time.") from None
|
|
|
|
@property
|
|
def url_root(self) -> str:
|
|
return f"http://{self.host}:{self.port}"
|
|
|
|
def url_for(self, *parts: str) -> str:
|
|
return self.url_root + "/" + "/".join(parts)
|
|
|
|
def get_client(self, **kwargs: Any):
|
|
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: Any):
|
|
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,
|
|
)
|