import multiprocessing import multiprocessing.forkserver as forkserver import os from argparse import Namespace from collections.abc import AsyncIterator from contextlib import asynccontextmanager from http import HTTPStatus from typing import Any, Optional import vllm.envs as envs from fastapi import Depends, HTTPException, Request from fastapi.responses import JSONResponse, StreamingResponse from starlette.datastructures import State from vllm.config import VllmConfig from vllm.engine.protocol import EngineClient from vllm.entrypoints.chat_utils import load_chat_template, resolve_hf_chat_template, resolve_mistral_chat_template from vllm.entrypoints.launcher import serve_http from vllm.entrypoints.logger import RequestLogger from vllm.entrypoints.openai.api_server import ( base, build_app, load_log_config, maybe_register_tokenizer_info_endpoint, router, setup_server, validate_json_request, ) from vllm.entrypoints.openai.protocol import ChatCompletionRequest, ChatCompletionResponse, ErrorResponse from vllm.entrypoints.openai.serving_models import BaseModelPath, LoRAModulePath, OpenAIServingModels from vllm.entrypoints.openai.tool_parsers import ToolParserManager # yapf conflicts with isort for this block # yapf: disable # yapf: enable from vllm.entrypoints.tool_server import DemoToolServer, MCPToolServer, ToolServer from vllm.entrypoints.utils import load_aware_call, with_cancellation from vllm.logger import init_logger from vllm.transformers_utils.tokenizer import MistralTokenizer from vllm.utils import decorate_logs from vllm_omni.entrypoints.async_omni_llm import AsyncOmniLLM from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat logger = init_logger(__name__) async def omni_run_server(args, **uvicorn_kwargs) -> None: """Run a single-worker API server.""" # Add process-specific prefix to stdout and stderr. decorate_logs("APIServer") listen_address, sock = setup_server(args) await omni_run_server_worker(listen_address, sock, args, **uvicorn_kwargs) async def omni_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) # Load logging config for uvicorn if specified log_config = load_log_config(args.log_config_file) if log_config is not None: uvicorn_kwargs["log_config"] = log_config async with build_async_omni_llm( args, client_config=client_config, ) as engine_client: maybe_register_tokenizer_info_endpoint(args) app = build_app(args) vllm_config = await engine_client.get_vllm_config() await omni_init_app_state(engine_client, vllm_config, app.state, args) logger.info( "Starting vLLM API server %d on %s", vllm_config.parallel_config._api_process_rank, listen_address, ) shutdown_task = 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, h11_max_incomplete_event_size=args.h11_max_incomplete_event_size, h11_max_header_count=args.h11_max_header_count, **uvicorn_kwargs, ) # NB: Await server shutdown only after the backend context is exited try: await shutdown_task finally: sock.close() @asynccontextmanager async def build_async_omni_llm( args: Namespace, *, disable_frontend_multiprocessing: Optional[bool] = None, client_config: Optional[dict[str, Any]] = 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 async_omni_llm lifecycle # Ensures everything is shutdown and cleaned up on error/exit async with build_async_omni_llm_from_stage_config( args, disable_frontend_multiprocessing=disable_frontend_multiprocessing, ) as async_omni_llm: yield async_omni_llm @asynccontextmanager async def build_async_omni_llm_from_stage_config( args: Namespace, *, disable_frontend_multiprocessing: bool = False, client_config: Optional[dict[str, Any]] = None, ) -> AsyncIterator[EngineClient]: """ Create AsyncOmniLLM, either: - in-process using the AsyncOmniLLM Directly - multiprocess using AsyncOmniLLM RPC Returns the AsyncOmniLLM or None if the creation failed. """ # V1 AsyncLLM. assert envs.VLLM_USE_V1 if disable_frontend_multiprocessing: logger.warning( "V1 is enabled, but got --disable-frontend-multiprocessing. " "To disable frontend multiprocessing, set VLLM_USE_V1=0." ) async_omni_llm: Optional[EngineClient] = None try: if getattr(args, "stage_configs_path", None): async_omni_llm = AsyncOmniLLM(model=args.model, stage_configs_path=args.stage_configs_path) else: async_omni_llm = AsyncOmniLLM(model=args.model) # # Don't keep the dummy data in memory # await async_llm.reset_mm_cache() yield async_omni_llm finally: if async_omni_llm: async_omni_llm.shutdown() async def omni_init_app_state( engine_client: EngineClient, vllm_config: VllmConfig, state: State, args: Namespace, ) -> None: if args.served_model_name is not None: served_model_names = args.served_model_name else: served_model_names = [args.model] if args.enable_log_requests: request_logger = RequestLogger(max_log_len=args.max_log_len) else: request_logger = None base_model_paths = [BaseModelPath(name=name, model_path=args.model) for name in served_model_names] state.engine_client = engine_client state.log_stats = not args.disable_log_stats state.vllm_config = vllm_config model_config = vllm_config.model_config state.log_stats = not args.disable_log_stats # For omni models state.stage_configs = engine_client.stage_configs resolved_chat_template = load_chat_template(args.chat_template) if resolved_chat_template is not None: # Get the tokenizer to check official template tokenizer = await engine_client.get_tokenizer() if isinstance(tokenizer, MistralTokenizer): # The warning is logged in resolve_mistral_chat_template. resolved_chat_template = resolve_mistral_chat_template(chat_template=resolved_chat_template) else: hf_chat_template = resolve_hf_chat_template( tokenizer=tokenizer, chat_template=None, tools=None, model_config=vllm_config.model_config, ) if hf_chat_template != resolved_chat_template: logger.warning( "Using supplied chat template: %s\nIt is different from official chat template '%s'. This discrepancy may lead to performance degradation.", # noqa: E501 resolved_chat_template, args.model, ) if args.tool_server == "demo": tool_server: Optional[ToolServer] = DemoToolServer() assert isinstance(tool_server, DemoToolServer) await tool_server.init_and_validate() elif args.tool_server: tool_server = MCPToolServer() await tool_server.add_tool_server(args.tool_server) else: tool_server = None # Merge default_mm_loras into the static lora_modules default_mm_loras = vllm_config.lora_config.default_mm_loras if vllm_config.lora_config is not None else {} lora_modules = args.lora_modules if default_mm_loras: default_mm_lora_paths = [ LoRAModulePath( name=modality, path=lora_path, ) for modality, lora_path in default_mm_loras.items() ] if args.lora_modules is None: lora_modules = default_mm_lora_paths else: lora_modules += default_mm_lora_paths state.openai_serving_models = OpenAIServingModels( engine_client=engine_client, model_config=model_config, base_model_paths=base_model_paths, lora_modules=lora_modules, ) await state.openai_serving_models.init_static_loras() state.openai_serving_chat = OmniOpenAIServingChat( engine_client, model_config, state.openai_serving_models, args.response_role, request_logger=request_logger, chat_template=resolved_chat_template, chat_template_content_format=args.chat_template_content_format, trust_request_chat_template=args.trust_request_chat_template, return_tokens_as_token_ids=args.return_tokens_as_token_ids, enable_auto_tools=args.enable_auto_tool_choice, exclude_tools_when_tool_choice_none=args.exclude_tools_when_tool_choice_none, tool_parser=args.tool_call_parser, reasoning_parser=args.structured_outputs_config.reasoning_parser, enable_prompt_tokens_details=args.enable_prompt_tokens_details, enable_force_include_usage=args.enable_force_include_usage, enable_log_outputs=args.enable_log_outputs, log_error_stack=args.log_error_stack, ) state.enable_server_load_tracking = args.enable_server_load_tracking state.server_load_metrics = 0 def Omnichat(request: Request) -> Optional[OmniOpenAIServingChat]: return request.app.state.openai_serving_chat @router.post( "/v1/chat/completions", dependencies=[Depends(validate_json_request)], responses={ HTTPStatus.OK.value: {"content": {"text/event-stream": {}}}, HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse}, HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse}, HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse}, }, ) @with_cancellation @load_aware_call async def create_chat_completion(request: ChatCompletionRequest, raw_request: Request): handler = Omnichat(raw_request) if handler is None: return base(raw_request).create_error_response(message="The model does not support Chat Completions API") try: generator = await handler.create_chat_completion(request, raw_request) except Exception as e: raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value, detail=str(e)) from e if isinstance(generator, ErrorResponse): return JSONResponse(content=generator.model_dump(), status_code=generator.error.code) elif isinstance(generator, ChatCompletionResponse): return JSONResponse(content=generator.model_dump()) return StreamingResponse(content=generator, media_type="text/event-stream")