231 lines
7.6 KiB
Python
231 lines
7.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
import asyncio
|
|
import multiprocessing
|
|
import multiprocessing.forkserver as forkserver
|
|
import os
|
|
import signal
|
|
import socket
|
|
import tempfile
|
|
from argparse import Namespace
|
|
from collections.abc import AsyncIterator
|
|
from contextlib import asynccontextmanager
|
|
from typing import Any
|
|
|
|
import vllm.envs as envs
|
|
from vllm.engine.arg_utils import AsyncEngineArgs
|
|
from vllm.engine.protocol import EngineClient
|
|
from vllm.logger import init_logger
|
|
from vllm.reasoning import ReasoningParserManager
|
|
from vllm.tool_parsers import ToolParserManager
|
|
from vllm.usage.usage_lib import UsageContext
|
|
from vllm.utils.system_utils import decorate_logs
|
|
|
|
from ..app import build_app
|
|
from ..launcher import serve_http, setup_server
|
|
from ..utils.server_utils import get_uvicorn_log_config
|
|
from .app_state import init_app_state
|
|
|
|
prometheus_multiproc_dir: tempfile.TemporaryDirectory
|
|
|
|
# Cannot use __name__ (https://github.com/vllm-project/vllm/pull/4765)
|
|
logger = init_logger("vllm.entrypoints.launchers.api_server.entry")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def build_async_engine_client(
|
|
args: Namespace,
|
|
*,
|
|
usage_context: UsageContext = UsageContext.OPENAI_API_SERVER,
|
|
client_config: dict[str, Any] | None = None,
|
|
) -> AsyncIterator[EngineClient]:
|
|
if os.getenv("VLLM_WORKER_MULTIPROC_METHOD") == "forkserver":
|
|
# The executor is expected to be mp.
|
|
# Pre-import heavy modules in the forkserver process
|
|
logger.debug("Setup forkserver with pre-imports")
|
|
multiprocessing.set_start_method("forkserver")
|
|
multiprocessing.set_forkserver_preload(["vllm.v1.engine.async_llm"])
|
|
forkserver.ensure_running()
|
|
logger.debug("Forkserver setup complete!")
|
|
|
|
# Context manager to handle engine_client lifecycle
|
|
# Ensures everything is shutdown and cleaned up on error/exit
|
|
engine_args = AsyncEngineArgs.from_cli_args(args)
|
|
if client_config:
|
|
engine_args._api_process_count = client_config.get("client_count", 1)
|
|
engine_args._api_process_rank = client_config.get("client_index", 0)
|
|
|
|
async with build_async_engine_client_from_engine_args(
|
|
engine_args,
|
|
usage_context=usage_context,
|
|
client_config=client_config,
|
|
) as engine:
|
|
yield engine
|
|
|
|
|
|
@asynccontextmanager
|
|
async def build_async_engine_client_from_engine_args(
|
|
engine_args: AsyncEngineArgs,
|
|
*,
|
|
usage_context: UsageContext = UsageContext.OPENAI_API_SERVER,
|
|
client_config: dict[str, Any] | None = None,
|
|
) -> AsyncIterator[EngineClient]:
|
|
"""
|
|
Create EngineClient, either:
|
|
- in-process using the AsyncLLMEngine Directly
|
|
- multiprocess using AsyncLLMEngine RPC
|
|
|
|
Returns the Client or None if the creation failed.
|
|
"""
|
|
|
|
# Create the EngineConfig (determines if we can use V1).
|
|
vllm_config = engine_args.create_engine_config(usage_context=usage_context)
|
|
|
|
from vllm.v1.engine.async_llm import AsyncLLM
|
|
|
|
async_llm: AsyncLLM | None = None
|
|
|
|
# Don't mutate the input client_config
|
|
client_config = dict(client_config) if client_config else {}
|
|
client_count = client_config.pop("client_count", 1)
|
|
client_index = client_config.pop("client_index", 0)
|
|
|
|
try:
|
|
async_llm = AsyncLLM.from_vllm_config(
|
|
vllm_config=vllm_config,
|
|
usage_context=usage_context,
|
|
enable_log_requests=engine_args.enable_log_requests,
|
|
aggregate_engine_logging=engine_args.aggregate_engine_logging,
|
|
disable_log_stats=engine_args.disable_log_stats,
|
|
client_addresses=client_config,
|
|
client_count=client_count,
|
|
client_index=client_index,
|
|
)
|
|
|
|
# Don't keep the dummy data in memory
|
|
assert async_llm is not None
|
|
await async_llm.reset_mm_cache()
|
|
|
|
yield async_llm
|
|
finally:
|
|
if async_llm:
|
|
async_llm.shutdown(timeout=vllm_config.shutdown_timeout)
|
|
|
|
|
|
async def build_and_serve(
|
|
engine_client: EngineClient,
|
|
listen_address: str,
|
|
sock: socket.socket,
|
|
args: Namespace,
|
|
**uvicorn_kwargs,
|
|
) -> asyncio.Task:
|
|
"""Build FastAPI app, initialize state, and start serving.
|
|
|
|
Returns the shutdown task for the caller to await.
|
|
"""
|
|
|
|
# Get uvicorn log config (from file or with endpoint filter)
|
|
log_config = get_uvicorn_log_config(args)
|
|
if log_config is not None:
|
|
uvicorn_kwargs["log_config"] = log_config
|
|
|
|
supported_tasks = await engine_client.get_supported_tasks()
|
|
model_config = engine_client.model_config
|
|
|
|
logger.info("Supported tasks: %s", supported_tasks)
|
|
app = build_app(args, supported_tasks, model_config)
|
|
await init_app_state(engine_client, app.state, args, supported_tasks)
|
|
|
|
logger.info("Starting vLLM server on %s", listen_address)
|
|
|
|
return await serve_http(
|
|
app,
|
|
sock=sock,
|
|
enable_ssl_refresh=args.enable_ssl_refresh,
|
|
host=args.host,
|
|
port=args.port,
|
|
log_level=args.uvicorn_log_level,
|
|
# NOTE: When the 'disable_uvicorn_access_log' value is True,
|
|
# no access log will be output.
|
|
access_log=not args.disable_uvicorn_access_log,
|
|
timeout_keep_alive=envs.VLLM_HTTP_TIMEOUT_KEEP_ALIVE,
|
|
ssl_keyfile=args.ssl_keyfile,
|
|
ssl_certfile=args.ssl_certfile,
|
|
ssl_ca_certs=args.ssl_ca_certs,
|
|
ssl_cert_reqs=args.ssl_cert_reqs,
|
|
ssl_ciphers=args.ssl_ciphers,
|
|
h11_max_incomplete_event_size=args.h11_max_incomplete_event_size,
|
|
h11_max_header_count=args.h11_max_header_count,
|
|
**uvicorn_kwargs,
|
|
)
|
|
|
|
|
|
async def run_server(args, **uvicorn_kwargs) -> None:
|
|
"""Run a single-worker API server."""
|
|
|
|
decorate_logs("APIServer", skip_if_decorated=True)
|
|
|
|
# Interrupt initialization if SIGTERM arrives before uvicorn installs its
|
|
# own signal handlers. Once uvicorn is running it replaces this.
|
|
def _interrupt_init(*_) -> None:
|
|
raise KeyboardInterrupt("terminated")
|
|
|
|
signal.signal(signal.SIGTERM, _interrupt_init)
|
|
|
|
listen_address, sock = setup_server(args, reuse_port=False)
|
|
await run_server_worker(listen_address, sock, args, **uvicorn_kwargs)
|
|
|
|
|
|
async def run_server_worker(
|
|
listen_address, sock, args, client_config=None, **uvicorn_kwargs
|
|
) -> None:
|
|
"""Run a single API server worker."""
|
|
|
|
if args.tool_parser_plugin and len(args.tool_parser_plugin) > 3:
|
|
ToolParserManager.import_tool_parser(args.tool_parser_plugin)
|
|
|
|
if args.reasoning_parser_plugin and len(args.reasoning_parser_plugin) > 3:
|
|
ReasoningParserManager.import_reasoning_parser(args.reasoning_parser_plugin)
|
|
|
|
async with build_async_engine_client(
|
|
args,
|
|
client_config=client_config,
|
|
) as engine_client:
|
|
shutdown_task = await build_and_serve(
|
|
engine_client, listen_address, sock, args, **uvicorn_kwargs
|
|
)
|
|
# NB: Await server shutdown only after the backend context is exited
|
|
try:
|
|
await shutdown_task
|
|
finally:
|
|
sock.close()
|
|
|
|
|
|
def main():
|
|
import uvloop
|
|
|
|
from vllm.entrypoints.serve.utils.api_utils import cli_env_setup
|
|
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
|
|
|
from ..cli_args import (
|
|
make_arg_parser,
|
|
validate_parsed_serve_args,
|
|
)
|
|
|
|
# NOTE(simon):
|
|
# This section should be in sync with vllm/entrypoints/cli/main.py for CLI
|
|
# entrypoints.
|
|
cli_env_setup()
|
|
parser = FlexibleArgumentParser(
|
|
description="vLLM OpenAI-Compatible RESTful API server."
|
|
)
|
|
parser = make_arg_parser(parser)
|
|
args = parser.parse_args()
|
|
validate_parsed_serve_args(args)
|
|
|
|
uvloop.run(run_server(args))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|