Merge branch 'main' into dev/vllm-align

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
This commit is contained in:
tzhouam committed 2026-01-30 02:53:03 +00:00
commit 4b895459e4
196 files changed
+9406 -4132

No files matched your search

+142 -24
View File
@@ -5,18 +5,20 @@ import multiprocessing.forkserver as forkserver
import os
# Image generation API imports
import random
import time
from argparse import Namespace
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from http import HTTPStatus
from typing import Any
from typing import Any, cast
import vllm.envs as envs
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from starlette.datastructures import State
from starlette.routing import Route
from vllm import SamplingParams
from vllm.engine.protocol import EngineClient
from vllm.entrypoints.anthropic.serving import AnthropicServingMessages
from vllm.entrypoints.chat_utils import load_chat_template
@@ -79,12 +81,30 @@ from vllm_omni.entrypoints.openai.protocol.images import (
)
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniSamplingParams, OmniTextPrompt
from vllm_omni.lora.request import LoRARequest
from vllm_omni.lora.utils import stable_lora_int_id
logger = init_logger(__name__)
router = APIRouter()
def _remove_route_from_router(
router: APIRouter,
path: str,
methods: set[str] | None = None,
) -> None:
methods_set = {method.upper() for method in methods} if methods else None
for route in list(router.routes):
if getattr(route, "path", None) != path:
continue
if methods_set is not None:
route_methods = {method.upper() for method in (getattr(route, "methods", None) or set())}
if not (route_methods & methods_set):
continue
router.routes.remove(route)
ENDPOINT_LOAD_METRICS_FORMAT_HEADER_LABEL = "endpoint-load-metrics-format"
@@ -741,6 +761,9 @@ async def create_chat_completion(request: ChatCompletionRequest, raw_request: Re
return StreamingResponse(content=generator, media_type="text/event-stream")
_remove_route_from_router(router, "/v1/audio/speech", {"POST"})
@router.post(
"/v1/audio/speech",
dependencies=[Depends(validate_json_request)],
@@ -769,6 +792,86 @@ async def create_speech(request: OpenAICreateSpeechRequest, raw_request: Request
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value, detail=str(e)) from e
# Health and Model endpoints for diffusion mode
# Remove existing health endpoint if present (from vllm imports)
# to ensure our handler takes precedence
_remove_route_from_router(router, "/health")
@router.get("/health")
async def health(raw_request: Request) -> JSONResponse:
"""Health check endpoint that works for both LLM and diffusion modes.
Returns 200 OK if the server is healthy.
For LLM mode: delegates to engine_client health check
For diffusion mode: checks if diffusion_engine is running
"""
# Check if we're in diffusion mode
diffusion_engine = getattr(raw_request.app.state, "diffusion_engine", None)
if diffusion_engine is not None:
# Diffusion mode health check
if hasattr(diffusion_engine, "is_running") and diffusion_engine.is_running:
return JSONResponse(content={"status": "healthy"})
return JSONResponse(
content={"status": "unhealthy", "reason": "Diffusion engine is not running"},
status_code=HTTPStatus.SERVICE_UNAVAILABLE.value,
)
# LLM mode - delegate to engine_client
engine_client = getattr(raw_request.app.state, "engine_client", None)
if engine_client is not None:
await engine_client.check_health()
return JSONResponse(content={"status": "healthy"})
return JSONResponse(
content={"status": "unhealthy", "reason": "No engine initialized"},
status_code=HTTPStatus.SERVICE_UNAVAILABLE.value,
)
# Remove existing models endpoint if present (from vllm imports)
# to ensure our handler takes precedence
_remove_route_from_router(router, "/v1/models")
@router.get("/v1/models")
async def show_available_models(raw_request: Request) -> JSONResponse:
"""Show available models endpoint that works for both LLM and diffusion modes.
Returns model information in OpenAI-compatible format.
"""
# Check if we're in diffusion mode
diffusion_model_name = getattr(raw_request.app.state, "diffusion_model_name", None)
if diffusion_model_name is not None:
# Diffusion mode - return the loaded model
return JSONResponse(
content={
"object": "list",
"data": [
{
"id": diffusion_model_name,
"object": "model",
"created": 0,
"owned_by": "vllm-omni",
"permission": [],
}
],
}
)
# LLM mode - delegate to openai_serving_models
openai_serving_models = getattr(raw_request.app.state, "openai_serving_models", None)
if openai_serving_models is not None:
models = await openai_serving_models.show_available_models()
return JSONResponse(content=models.model_dump())
return JSONResponse(
content={"object": "list", "data": []},
)
# Image generation API endpoints
@@ -799,7 +902,7 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
HTTPException: For validation errors, missing engine, or generation failures
"""
# Get engine client (AsyncOmni) from app state
engine_client: EngineClient | None = getattr(raw_request.app.state, "engine_client", None)
engine_client: EngineClient | AsyncOmni | None = getattr(raw_request.app.state, "engine_client", None)
if engine_client is None or not hasattr(engine_client, "stage_list"):
raise HTTPException(
status_code=HTTPStatus.SERVICE_UNAVAILABLE.value,
@@ -859,10 +962,8 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
try:
# Build params - pass through user values directly
gen_params = {
"prompt": request.prompt,
"num_outputs_per_prompt": request.n,
}
prompt: OmniTextPrompt = {"prompt": request.prompt}
gen_params = OmniDiffusionSamplingParams(num_outputs_per_prompt=request.n)
# Parse per-request LoRA (compatible with chat's extra_body.lora shape).
if request.lora is not None:
@@ -894,62 +995,79 @@ async def generate_images(request: ImageGenerationRequest, raw_request: Request)
detail="Invalid lora object: both name and path are required.",
)
gen_params["lora_request"] = LoRARequest(str(lora_name), int(lora_int_id), str(lora_path))
gen_params.lora_request = LoRARequest(str(lora_name), int(lora_int_id), str(lora_path))
if lora_scale is not None:
gen_params["lora_scale"] = float(lora_scale)
gen_params.lora_scale = float(lora_scale)
# Parse and add size if provided
if request.size:
width, height = parse_size(request.size)
gen_params["height"] = height
gen_params["width"] = width
gen_params.height = height
gen_params.width = width
size_str = f"{width}x{height}"
else:
size_str = "model default"
# Add optional parameters ONLY if provided
if request.num_inference_steps is not None:
gen_params["num_inference_steps"] = request.num_inference_steps
gen_params.num_inference_steps = request.num_inference_steps
if request.negative_prompt is not None:
gen_params["negative_prompt"] = request.negative_prompt
prompt["negative_prompt"] = request.negative_prompt
if request.guidance_scale is not None:
gen_params["guidance_scale"] = request.guidance_scale
gen_params.guidance_scale = request.guidance_scale
if request.true_cfg_scale is not None:
gen_params["true_cfg_scale"] = request.true_cfg_scale
gen_params.true_cfg_scale = request.true_cfg_scale
if request.seed is not None:
gen_params["seed"] = request.seed
gen_params["request_id"] = f"img_gen_{int(time.time())}"
gen_params.seed = request.seed
else:
# If seed is not provided, generate a random one to ensure
# a proper generator is initialized in the backend.
# This fixes issues where using the default global generator
# might produce blurry images in some environments.
gen_params.seed = random.randint(0, 2**32 - 1)
request_id = f"img_gen_{int(time.time())}"
logger.info(f"Generating {request.n} image(s) {size_str}")
# Generate images using AsyncOmni (multi-stage mode)
engine_client = cast(AsyncOmni, engine_client)
result = None
stage_list = getattr(engine_client, "stage_list", None)
if isinstance(stage_list, list):
default_params_list = getattr(engine_client, "default_sampling_params_list", None)
default_params_list: list[OmniSamplingParams] | None = getattr(
engine_client, "default_sampling_params_list", None
)
if not isinstance(default_params_list, list):
default_params_list = [{} for _ in stage_types]
default_params_list = [
OmniDiffusionSamplingParams() if st == "diffusion" else SamplingParams() for st in stage_types
]
else:
default_params_list = list(default_params_list)
if len(default_params_list) != len(stage_types):
default_params_list = (default_params_list + [{} for _ in stage_types])[: len(stage_types)]
default_params_list = (
default_params_list
+ [OmniDiffusionSamplingParams() if st == "diffusion" else SamplingParams() for st in stage_types]
)[: len(stage_types)]
sampling_params_list: list[dict[str, Any]] = []
sampling_params_list: list[OmniSamplingParams] = []
for idx, stage_type in enumerate(stage_types):
if stage_type == "diffusion":
sampling_params_list.append(gen_params)
else:
base_params = default_params_list[idx]
sampling_params_list.append(dict(base_params) if isinstance(base_params, dict) else base_params)
sampling_params_list.append(base_params)
async for output in engine_client.generate(
prompt=gen_params["prompt"],
request_id=gen_params["request_id"],
prompt=prompt,
request_id=request_id,
sampling_params_list=sampling_params_list,
):
result = output
else:
result = await engine_client.generate(**gen_params)
result = await engine_client.generate(
prompt=prompt, request_id=request_id, sampling_params_list=[gen_params]
)
if result is None:
raise HTTPException(