Files
Davids048andClaude Opus 5.5 6c2a250fd0 [refactor]: read typed config paths in the pipeline base, worker, and entry points
- ComposedPipelineBase owns a ComponentState: it records each component's
  path after loading and attaches the state to every stage it registers.
  from_pretrained(model_path, *, resolved_config, ...) takes a resolved
  config instead of building FastVideoArgs from keywords or argparse; the
  training_args alias and TrainingArgs checks are gone, and the training
  DiT offload is a recorded override. Compile settings come from
  engine.compile and torch_compile_kwargs(); LoRA settings from
  pipeline.components.lora_* and training.lora.*.
- The worker and executors read engine.num_gpus, engine.parallelism.*,
  engine.execution_backend, and pipeline.output_type; the Ray executor owns
  its placement group and runtime env.
- VideoGenerator builds from a resolved config only (_from_resolved_config);
  the prompt_txt fallback is gone (requests carry inputs.prompt_path). The
  OpenAI server state holds the resolved config (get_resolved_config()).
- Tests use resolved configs; the SSIM and performance helpers map their
  settings to dotted typed paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012M91wnVFmPEvJ9h39r5BH7
2026-10-03 06:35:19 +00:00

207 lines
7.1 KiB
Python

# Adapted from SGLang
# (https://github.com/sgl-project/sglang/blob/main/python/sglang/multimodal_gen/runtime/entrypoints/http_server.py)
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator, Callable
import os
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastvideo.api.presets import validate_preset_selection
from fastvideo.api.schema import GenerationRequest
from fastvideo.entrypoints.openai.state import (
DEFAULT_OUTPUT_DIR,
clear_state,
set_state,
)
from fastvideo.entrypoints.openai.serving_engine import OpenAIServingEngine, ServingGenerator
from fastvideo.entrypoints.openai.protocol import VideoGenerationRequest
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.api.resolution import ResolvedGeneratorConfig
from fastvideo.logger import init_logger
from fastvideo.registry import get_preset_selection
logger = init_logger(__name__)
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8000
def _validate_default_request_against_preset(
default_request: GenerationRequest,
model_path: str,
) -> None:
"""Validate ``default_request.stage_overrides`` against the model's preset.
Called once at server startup from :func:`run_server`. The
``default_request`` is static server config, so validation results are
invariant across requests — there's no reason to re-run per request.
"""
if not default_request.stage_overrides:
return
preset_name, model_family = get_preset_selection(model_path)
if preset_name is None or model_family is None:
return
validate_preset_selection(
preset_name,
model_family,
stage_overrides=default_request.stage_overrides,
)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Load model on startup, clean up on shutdown"""
resolved_config: ResolvedGeneratorConfig = app.state.resolved_config
output_dir: str = app.state.output_dir
served_model_name: str | None = app.state.served_model_name
default_request: GenerationRequest | None = getattr(app.state, "default_request", None)
logger.info("Initializing %s generation runtime for %s ...", app.state.runtime, resolved_config.model_path)
factory = app.state.generator_factory
generator = factory() if factory is not None else VideoGenerator._from_resolved_config(resolved_config)
serving_engine = OpenAIServingEngine(generator, app.state.video_request_validator)
logger.info("Generation runtime ready.")
set_state(
generator,
serving_engine,
resolved_config,
output_dir,
default_request=default_request,
served_model_name=served_model_name,
)
try:
yield # server is running
finally:
logger.info("Shutting down — releasing model resources ...")
from fastvideo.entrypoints.openai.video_api import shutdown_video_jobs
await shutdown_video_jobs()
await serving_engine.shutdown()
clear_state()
logger.info("Shutdown complete.")
def create_app(
resolved_config: ResolvedGeneratorConfig,
output_dir: str = DEFAULT_OUTPUT_DIR,
default_request: GenerationRequest | None = None,
served_model_name: str | None = None,
*,
generator_factory: Callable[[], ServingGenerator] | None = None,
video_request_validator: Callable[[VideoGenerationRequest], None] | None = None,
runtime: str = "cuda",
) -> FastAPI:
"""Build the FastAPI application with all routers mounted"""
app = FastAPI(
title="FastVideo OpenAI-Compatible API",
version="0.1.0",
lifespan=lifespan,
)
app.state.resolved_config = resolved_config
app.state.output_dir = output_dir
app.state.default_request = default_request
app.state.served_model_name = served_model_name
app.state.generator_factory = generator_factory
app.state.video_request_validator = video_request_validator
app.state.runtime = runtime
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(HTTPException)
async def openai_http_error(_request: Request, exc: HTTPException) -> JSONResponse:
"""Return the error envelope consumed by OpenAI-compatible clients."""
message = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
return JSONResponse(
status_code=exc.status_code,
headers=exc.headers,
content={
"error": {
"message": message,
"type": "invalid_request_error" if exc.status_code < 500 else "server_error",
"param": None,
"code": exc.status_code,
}
},
)
@app.exception_handler(RequestValidationError)
async def openai_validation_error(_request: Request, exc: RequestValidationError) -> JSONResponse:
return JSONResponse(
status_code=400,
content={"error": {
"message": str(exc),
"type": "invalid_request_error",
"param": None,
"code": 400,
}},
)
# Import and mount routers
from fastvideo.entrypoints.openai.common_api import router as common_router
from fastvideo.entrypoints.openai.image_api import router as image_router
from fastvideo.entrypoints.openai.playground import router as playground_router
from fastvideo.entrypoints.openai.video_api import router as video_router
app.include_router(common_router)
app.include_router(video_router)
if runtime != "mlx":
app.include_router(image_router)
app.include_router(playground_router)
@app.get("/health")
async def health():
from fastvideo.entrypoints.openai.state import get_serving_engine
engine = get_serving_engine()
if not engine.healthy:
raise HTTPException(status_code=503, detail=engine.unhealthy_reason or "generation engine is unhealthy")
return {"status": "ok"}
return app
def run_server(
resolved_config: ResolvedGeneratorConfig,
host: str = DEFAULT_HOST,
port: int = DEFAULT_PORT,
output_dir: str = DEFAULT_OUTPUT_DIR,
default_request: GenerationRequest | None = None,
served_model_name: str | None = None,
):
"""Create the app and run it with uvicorn"""
os.environ.setdefault("FASTVIDEO_STAGE_LOGGING", "1")
if default_request is not None:
_validate_default_request_against_preset(default_request, resolved_config.model_path)
app = create_app(
resolved_config,
output_dir=output_dir,
default_request=default_request,
served_model_name=served_model_name,
)
logger.info("Starting FastVideo server on %s:%d", host, port)
logger.info("Model: %s", resolved_config.model_path)
uvicorn.run(
app,
host=host,
port=port,
log_level="info",
timeout_keep_alive=300,
)