351 lines
13 KiB
Python
351 lines
13 KiB
Python
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]:
|
|
"""Build an AsyncOmniLLM instance from command-line arguments.
|
|
|
|
Creates an async context manager that yields an AsyncOmniLLM instance
|
|
configured from the provided arguments. Handles forkserver setup if
|
|
needed and ensures proper cleanup on exit.
|
|
|
|
Args:
|
|
args: Parsed command-line arguments containing model and configuration
|
|
disable_frontend_multiprocessing: Optional flag to disable frontend
|
|
multiprocessing (deprecated in V1)
|
|
client_config: Optional client configuration dictionary
|
|
|
|
Yields:
|
|
EngineClient instance (AsyncOmniLLM) ready for use
|
|
"""
|
|
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 from stage configuration.
|
|
|
|
Creates an AsyncOmniLLM instance either in-process or using multiprocess
|
|
RPC. Loads stage configurations from the model or from a specified path.
|
|
|
|
Args:
|
|
args: Parsed command-line arguments containing model and stage configs
|
|
disable_frontend_multiprocessing: Flag to disable frontend multiprocessing
|
|
(deprecated in V1)
|
|
client_config: Optional client configuration dictionary
|
|
|
|
Yields:
|
|
EngineClient instance (AsyncOmniLLM) ready for use
|
|
|
|
Note:
|
|
Stage configurations are loaded from args.stage_configs_path if provided,
|
|
otherwise from the model's default configuration.
|
|
"""
|
|
|
|
# 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:
|
|
"""Initialize the FastAPI application state for omni API server.
|
|
|
|
Sets up the application state with model information, request logger,
|
|
and other server configuration needed for handling API requests.
|
|
|
|
Args:
|
|
engine_client: Engine client instance (AsyncOmniLLM)
|
|
vllm_config: vLLM configuration object
|
|
state: FastAPI application state object to initialize
|
|
args: Parsed command-line arguments
|
|
"""
|
|
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")
|