| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | import asyncio |
| 4 | import multiprocessing |
| 5 | import multiprocessing.forkserver as forkserver |
| 6 | import os |
| 7 | import signal |
| 8 | import socket |
| 9 | import tempfile |
| 10 | from argparse import Namespace |
| 11 | from collections.abc import AsyncIterator |
| 12 | from contextlib import asynccontextmanager |
| 13 | from typing import Any |
| 14 | |
| 15 | import vllm.envs as envs |
| 16 | from vllm.engine.arg_utils import AsyncEngineArgs |
| 17 | from vllm.engine.protocol import EngineClient |
| 18 | from vllm.logger import configure_logging_from_args, init_logger |
| 19 | from vllm.reasoning import ReasoningParserManager |
| 20 | from vllm.tool_parsers import ToolParserManager |
| 21 | from vllm.usage.usage_lib import UsageContext |
| 22 | from vllm.utils.system_utils import decorate_logs |
| 23 | |
| 24 | from ..app import build_app |
| 25 | from ..launcher import serve_http, setup_server |
| 26 | from ..utils.server_utils import get_uvicorn_log_config |
| 27 | from .app_state import init_app_state |
| 28 | |
| 29 | prometheus_multiproc_dir: tempfile.TemporaryDirectory |
| 30 | |
| 31 | # Cannot use __name__ (https://github.com/vllm-project/vllm/pull/4765) |
| 32 | logger = init_logger("vllm.entrypoints.launchers.api_server.entry") |
| 33 | |
| 34 | |
| 35 | @asynccontextmanager |
| 36 | async def build_async_engine_client( |
| 37 | args: Namespace, |
| 38 | *, |
| 39 | usage_context: UsageContext = UsageContext.OPENAI_API_SERVER, |
| 40 | client_config: dict[str, Any] | None = None, |
| 41 | ) -> AsyncIterator[EngineClient]: |
| 42 | if os.getenv("VLLM_WORKER_MULTIPROC_METHOD") == "forkserver": |
| 43 | # The executor is expected to be mp. |
| 44 | # Pre-import heavy modules in the forkserver process |
| 45 | logger.debug("Setup forkserver with pre-imports") |
| 46 | multiprocessing.set_start_method("forkserver") |
| 47 | multiprocessing.set_forkserver_preload(["vllm.v1.engine.async_llm"]) |
| 48 | forkserver.ensure_running() |
| 49 | logger.debug("Forkserver setup complete!") |
| 50 | |
| 51 | # Context manager to handle engine_client lifecycle |
| 52 | # Ensures everything is shutdown and cleaned up on error/exit |
| 53 | engine_args = AsyncEngineArgs.from_cli_args(args) |
| 54 | from ..cli_args import propagate_flash_late_interaction |
| 55 | |
| 56 | propagate_flash_late_interaction(args, engine_args) |
| 57 | if client_config: |
| 58 | engine_args._api_process_count = client_config.get("client_count", 1) |
| 59 | engine_args._api_process_rank = client_config.get("client_index", 0) |
| 60 | |
| 61 | async with build_async_engine_client_from_engine_args( |
| 62 | engine_args, |
| 63 | usage_context=usage_context, |
| 64 | client_config=client_config, |
| 65 | ) as engine: |
| 66 | yield engine |
| 67 | |
| 68 | |
| 69 | @asynccontextmanager |
| 70 | async def build_async_engine_client_from_engine_args( |
| 71 | engine_args: AsyncEngineArgs, |
| 72 | *, |
| 73 | usage_context: UsageContext = UsageContext.OPENAI_API_SERVER, |
| 74 | client_config: dict[str, Any] | None = None, |
| 75 | ) -> AsyncIterator[EngineClient]: |
| 76 | """Create EngineClient, either: |
| 77 | - in-process using the AsyncLLMEngine Directly |
| 78 | - multiprocess using AsyncLLMEngine RPC |
| 79 | |
| 80 | Returns the Client or None if the creation failed. |
| 81 | """ |
| 82 | # Create the EngineConfig (determines if we can use V1). |
| 83 | vllm_config = engine_args.create_engine_config(usage_context=usage_context) |
| 84 | |
| 85 | from vllm.v1.engine.async_llm import AsyncLLM |
| 86 | |
| 87 | async_llm: AsyncLLM | None = None |
| 88 | |
| 89 | # Don't mutate the input client_config |
| 90 | client_config = dict(client_config) if client_config else {} |
| 91 | client_count = client_config.pop("client_count", 1) |
| 92 | client_index = client_config.pop("client_index", 0) |
| 93 | |
| 94 | try: |
| 95 | async_llm = AsyncLLM.from_vllm_config( |
| 96 | vllm_config=vllm_config, |
| 97 | usage_context=usage_context, |
| 98 | enable_log_requests=engine_args.enable_log_requests, |
| 99 | aggregate_engine_logging=engine_args.aggregate_engine_logging, |
| 100 | disable_log_stats=engine_args.disable_log_stats, |
| 101 | client_addresses=client_config, |
| 102 | client_count=client_count, |
| 103 | client_index=client_index, |
| 104 | ) |
| 105 | |
| 106 | # Don't keep the dummy data in memory |
| 107 | assert async_llm is not None |
| 108 | await async_llm.reset_mm_cache() |
| 109 | |
| 110 | yield async_llm |
| 111 | finally: |
| 112 | if async_llm: |
| 113 | async_llm.shutdown(timeout=vllm_config.shutdown_timeout) |
| 114 | |
| 115 | |
| 116 | async def build_and_serve( |
| 117 | engine_client: EngineClient, |
| 118 | listen_address: str, |
| 119 | sock: socket.socket, |
| 120 | args: Namespace, |
| 121 | **uvicorn_kwargs, |
| 122 | ) -> asyncio.Task: |
| 123 | """Build FastAPI app, initialize state, and start serving. |
| 124 | |
| 125 | Returns the shutdown task for the caller to await. |
| 126 | """ |
| 127 | # Get uvicorn log config (from file or with endpoint filter) |
| 128 | log_config = get_uvicorn_log_config(args) |
| 129 | if log_config is not None: |
| 130 | uvicorn_kwargs["log_config"] = log_config |
| 131 | |
| 132 | supported_tasks = await engine_client.get_supported_tasks() |
| 133 | model_config = engine_client.model_config |
| 134 | |
| 135 | logger.info("Supported tasks: %s", supported_tasks) |
| 136 | app = build_app(args, supported_tasks, model_config) |
| 137 | await init_app_state(engine_client, app.state, args, supported_tasks) |
| 138 | |
| 139 | logger.info("Starting vLLM server on %s", listen_address) |
| 140 | |
| 141 | return await serve_http( |
| 142 | app, |
| 143 | sock=sock, |
| 144 | enable_ssl_refresh=args.enable_ssl_refresh, |
| 145 | host=args.host, |
| 146 | port=args.port, |
| 147 | log_level=args.uvicorn_log_level, |
| 148 | # NOTE: When the 'disable_uvicorn_access_log' value is True, |
| 149 | # no access log will be output. |
| 150 | access_log=not args.disable_uvicorn_access_log, |
| 151 | timeout_keep_alive=envs.VLLM_HTTP_TIMEOUT_KEEP_ALIVE, |
| 152 | ssl_keyfile=args.ssl_keyfile, |
| 153 | ssl_certfile=args.ssl_certfile, |
| 154 | ssl_ca_certs=args.ssl_ca_certs, |
| 155 | ssl_cert_reqs=args.ssl_cert_reqs, |
| 156 | ssl_ciphers=args.ssl_ciphers, |
| 157 | h11_max_incomplete_event_size=args.h11_max_incomplete_event_size, |
| 158 | h11_max_header_count=args.h11_max_header_count, |
| 159 | **uvicorn_kwargs, |
| 160 | ) |
| 161 | |
| 162 | |
| 163 | async def run_server(args, **uvicorn_kwargs) -> None: |
| 164 | """Run a single-worker API server.""" |
| 165 | decorate_logs("APIServer", skip_if_decorated=True) |
| 166 | |
| 167 | # Interrupt initialization if SIGTERM arrives before uvicorn installs its |
| 168 | # own signal handlers. Once uvicorn is running it replaces this. |
| 169 | def _interrupt_init(*_) -> None: |
| 170 | raise KeyboardInterrupt("terminated") |
| 171 | |
| 172 | signal.signal(signal.SIGTERM, _interrupt_init) |
| 173 | |
| 174 | listen_address, sock = setup_server(args, reuse_port=False) |
| 175 | await run_server_worker(listen_address, sock, args, **uvicorn_kwargs) |
| 176 | |
| 177 | |
| 178 | async def run_server_worker( |
| 179 | listen_address, sock, args, client_config=None, **uvicorn_kwargs |
| 180 | ) -> None: |
| 181 | """Run a single API server worker.""" |
| 182 | if args.tool_parser_plugin and len(args.tool_parser_plugin) > 3: |
| 183 | ToolParserManager.import_tool_parser(args.tool_parser_plugin) |
| 184 | |
| 185 | if args.reasoning_parser_plugin and len(args.reasoning_parser_plugin) > 3: |
| 186 | ReasoningParserManager.import_reasoning_parser(args.reasoning_parser_plugin) |
| 187 | |
| 188 | async with build_async_engine_client( |
| 189 | args, |
| 190 | client_config=client_config, |
| 191 | ) as engine_client: |
| 192 | shutdown_task = await build_and_serve( |
| 193 | engine_client, listen_address, sock, args, **uvicorn_kwargs |
| 194 | ) |
| 195 | # NB: Await server shutdown only after the backend context is exited |
| 196 | try: |
| 197 | await shutdown_task |
| 198 | finally: |
| 199 | sock.close() |
| 200 | |
| 201 | |
| 202 | def main(): |
| 203 | import uvloop |
| 204 | |
| 205 | from vllm.entrypoints.serve.utils.api_utils import cli_env_setup |
| 206 | from vllm.utils.argparse_utils import FlexibleArgumentParser |
| 207 | |
| 208 | from ..cli_args import ( |
| 209 | make_arg_parser, |
| 210 | validate_parsed_serve_args, |
| 211 | ) |
| 212 | |
| 213 | # NOTE(simon): |
| 214 | # This section should be in sync with vllm/entrypoints/cli/main.py for CLI |
| 215 | # entrypoints. |
| 216 | cli_env_setup() |
| 217 | parser = FlexibleArgumentParser( |
| 218 | description="vLLM OpenAI-Compatible RESTful API server." |
| 219 | ) |
| 220 | parser = make_arg_parser(parser) |
| 221 | args = parser.parse_args() |
| 222 | configure_logging_from_args(args) |
| 223 | validate_parsed_serve_args(args) |
| 224 | |
| 225 | uvloop.run(run_server(args)) |
| 226 | |
| 227 | |
| 228 | if __name__ == "__main__": |
| 229 | main() |