Files
vllm-omni/vllm_omni/entrypoints/cli/serve.py
T
a3903810fe [Config Refactor][2/N] Pipeline + Deploy Config Schema (#2383)
Signed-off-by: lishunyang <lishunyang12@163.com>
Signed-off-by: reidliu41 <reid201711@gmail.com>
Signed-off-by: Alex Brooks <albrooks@redhat.com>
Co-authored-by: reidliu41 <reid201711@gmail.com>
Co-authored-by: xiaohajiayou <75477391+xiaohajiayou@users.noreply.github.com>
Co-authored-by: Alex Brooks <albrooks@redhat.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-04-19 14:36:00 +12:00

689 lines
26 KiB
Python

"""
Omni serve command for vLLM-Omni.
Supports both multi-stage LLM models (e.g., Qwen2.5-Omni) and
diffusion models (e.g., Qwen-Image) through the same CLI interface.
"""
import argparse
import json
import os
import signal
import sys
from types import FrameType
from typing import Any
import uvloop
from vllm.entrypoints.cli.types import CLISubcommand
from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args
from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
from vllm.logger import init_logger
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.entrypoints.cli.logo import log_logo
from vllm_omni.entrypoints.openai.api_server import omni_run_server
from vllm_omni.entrypoints.utils import detect_explicit_cli_keys
logger = init_logger(__name__)
DESCRIPTION = """Launch a local OpenAI-compatible API server to serve Omni models
via HTTP. Supports both multi-stage LLM models and diffusion models.
The server automatically detects the model type:
- LLM models: Served via /v1/chat/completions endpoint
- Diffusion models: Served via /v1/images/generations endpoint
Examples:
# Start an Omni LLM server
vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091
# Start a diffusion model server
vllm serve Qwen/Qwen-Image --omni --port 8091
Search by using: `--help=<ConfigGroup>` to explore options by section (e.g.,
--help=OmniConfig)
Use `--help=all` to show all available flags at once.
"""
def _ensure_vllm_platform():
"""Ensure vLLM's current_platform is valid before arg parsing.
Upstream vLLM's argument parser now instantiates DeviceConfig during
``make_arg_parser``, which requires a resolved platform with a non-empty
``device_type``. In some environments (e.g. editable installs with
broken package metadata), vLLM's own platform auto-detection may fail
and fall back to ``UnspecifiedPlatform``. When that happens, use the
Omni platform (which has its own detection logic) as a drop-in
replacement so that argument parsing succeeds.
"""
from vllm import platforms as vllm_platforms
if vllm_platforms.current_platform.is_unspecified():
from vllm_omni.platforms import current_omni_platform
if not current_omni_platform.is_unspecified():
vllm_platforms.current_platform = current_omni_platform
logger.debug(
"Replaced vLLM UnspecifiedPlatform with omni platform %s",
type(current_omni_platform).__name__,
)
else:
from vllm.platforms.cpu import CpuPlatform
vllm_platforms.current_platform = CpuPlatform()
logger.debug(
"Both vLLM and omni platforms are unspecified, falling back to CpuPlatform for arg parsing",
)
class OmniServeCommand(CLISubcommand):
"""The `serve` subcommand for the vLLM CLI."""
name = "serve"
# Parser stashed at subparser_init so ``cmd`` can resolve each user-typed
# flag to its real ``dest`` via the parser's action table.
_parser: FlexibleArgumentParser | None = None
@staticmethod
def cmd(args: argparse.Namespace) -> None:
if not os.environ.get("VLLM_DISABLE_LOG_LOGO"):
os.environ["VLLM_DISABLE_LOG_LOGO"] = "1"
log_logo()
# If model is specified in CLI (as positional arg), it takes precedence
if hasattr(args, "model_tag") and args.model_tag is not None:
args.model = args.model_tag
# Stash the set of long-option keys the user actually typed so the
# stage-config factory can give YAML precedence over argparse defaults.
args._cli_explicit_keys = detect_explicit_cli_keys(sys.argv[1:], OmniServeCommand._parser)
if args.headless:
run_headless(args)
else:
uvloop.run(omni_run_server(args))
def validate(self, args: argparse.Namespace) -> None:
if args.stage_id is not None and (args.omni_master_address is None or args.omni_master_port is None):
raise ValueError("--stage-id requires both --omni-master-address and --omni-master-port to be set")
# Skip validation for diffusion models as they have different requirements
from vllm_omni.diffusion.utils.hf_utils import is_diffusion_model
model = getattr(args, "model_tag", None) or getattr(args, "model", None)
if model and is_diffusion_model(model):
logger.info("Detected diffusion model: %s", model)
return
validate_parsed_serve_args(args)
def subparser_init(self, subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser:
serve_parser = subparsers.add_parser(
self.name,
description=DESCRIPTION,
usage="vllm serve [model_tag] --omni [options]",
)
_ensure_vllm_platform()
serve_parser = make_arg_parser(serve_parser)
serve_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(subcmd=self.name)
# Create OmniConfig argument group for omni-related parameters
# This ensures the parameters appear in --help output
omni_config_group = serve_parser.add_argument_group(
title="OmniConfig", description="Configuration for vLLM-Omni multi-stage and diffusion models."
)
omni_config_group.add_argument(
"--omni",
action="store_true",
help="Enable vLLM-Omni mode for multi-modal and diffusion models",
)
omni_config_group.add_argument(
"--task-type",
type=str,
default=None,
choices=["CustomVoice", "VoiceDesign", "Base"],
help="Default task type for TTS models (CustomVoice, VoiceDesign, or Base). "
"If not specified, will be inferred from model path.",
)
# TODO(@lishunyang12): deprecate once all models migrate to --deploy-config
omni_config_group.add_argument(
"--stage-configs-path",
type=str,
default=None,
help="[Deprecated — will be removed in a future release] Path to a legacy "
"stage configs YAML (stage_args format). Prefer --deploy-config for new-format deploy YAMLs.",
)
omni_config_group.add_argument(
"--deploy-config",
type=str,
default=None,
help="Path to a deploy config YAML (new format with stages/engine_args). "
"Mutually exclusive with --stage-configs-path.",
)
omni_config_group.add_argument(
"--stage-overrides",
type=str,
default=None,
help="Per-stage JSON overrides. Example: "
'\'{"0": {"gpu_memory_utilization": 0.8}, "2": {"enforce_eager": true}}\'',
)
omni_config_group.add_argument(
"--async-chunk",
action=argparse.BooleanOptionalAction,
default=None,
help="Override the deploy YAML's ``async_chunk:`` bool. Unset leaves the YAML value in force.",
)
omni_config_group.add_argument(
"--stage-id",
type=int,
default=None,
help="Select and launch a single stage by stage_id.",
)
omni_config_group.add_argument(
"--stage-init-timeout",
type=int,
default=300,
help="The timeout for initializing a single stage in seconds (default: 300)",
)
omni_config_group.add_argument(
"--init-timeout",
type=int,
default=600,
help="The timeout for initializing the stages.",
)
omni_config_group.add_argument(
"--shm-threshold-bytes",
type=int,
default=65536,
help="The threshold for the shared memory size.",
)
omni_config_group.add_argument(
"--log-stats",
action="store_true",
help="Enable logging the stats.",
)
omni_config_group.add_argument(
"--log-file",
type=str,
default=None,
help="The path to the log file.",
)
omni_config_group.add_argument(
"--batch-timeout",
type=int,
default=10,
help="The timeout for the batch.",
)
omni_config_group.add_argument(
"--worker-backend",
type=str,
default="multi_process",
choices=["multi_process", "ray"],
help="The backend to use for stage workers.",
)
omni_config_group.add_argument(
"--ray-address",
type=str,
default=None,
help="The address of the Ray cluster to connect to.",
)
omni_config_group.add_argument(
"--omni-master-address",
"-oma",
type=str,
help="Hostname or IP address of the Omni orchestrator (master).",
)
omni_config_group.add_argument(
"--omni-master-port",
"-omp",
type=int,
help="Port of the Omni orchestrator (master).",
)
# Diffusion model specific arguments
omni_config_group.add_argument(
"--num-gpus",
type=int,
default=None,
help="Number of GPUs to use for diffusion model inference.",
)
omni_config_group.add_argument(
"--model-class-name",
dest="model_class_name",
type=str,
default=None,
help="Override the diffusion pipeline class name (e.g. LTX2ImageToVideoPipeline).",
)
omni_config_group.add_argument(
"--usp",
"--ulysses-degree",
dest="ulysses_degree",
type=int,
default=None,
help="Ulysses Sequence Parallelism degree for diffusion models. "
"Equivalent to setting DiffusionParallelConfig.ulysses_degree.",
)
omni_config_group.add_argument(
"--ulysses-mode",
type=str,
default="strict",
choices=["strict", "advanced_uaa"],
help="Ulysses sequence-parallel mode for diffusion models. "
"'strict' keeps the original divisibility requirements; "
"'advanced_uaa' enables the experimental UAA path for uneven sequence/head shapes.",
)
omni_config_group.add_argument(
"--ring",
"--ring-degree",
dest="ring_degree",
type=int,
default=None,
help="Ring Sequence Parallelism degree for diffusion models. "
"Equivalent to setting DiffusionParallelConfig.ring_degree.",
)
omni_config_group.add_argument(
"--quantization-config",
type=json.loads,
default=None,
help=(
"JSON string for diffusion quantization_config. "
'Example: \'{"method":"gguf","gguf_model":"/path/to/model.gguf"}\'.'
),
)
# HSDP (Hybrid Sharded Data Parallel) parameters
omni_config_group.add_argument(
"--use-hsdp",
dest="use_hsdp",
action="store_true",
help="Enable HSDP (Hybrid Sharded Data Parallel) for diffusion models. "
"Shards model weights across GPUs to reduce per-GPU memory usage.",
)
omni_config_group.add_argument(
"--hsdp-shard-size",
type=int,
default=-1,
help="Number of GPUs to shard weights across. -1 = auto (world_size / replicate_size).",
)
omni_config_group.add_argument(
"--hsdp-replicate-size",
type=int,
default=1,
help="Number of replica groups for HSDP. Each group holds a full sharded copy.",
)
# Cache optimization parameters
omni_config_group.add_argument(
"--cache-backend",
type=str,
default="none",
help="Cache backend for diffusion models, options: 'tea_cache', 'cache_dit'",
)
omni_config_group.add_argument(
"--cache-config",
type=str,
default=None,
help="JSON string of cache configuration (e.g., '{\"rel_l1_thresh\": 0.2}').",
)
omni_config_group.add_argument(
"--enable-cache-dit-summary",
action="store_true",
help="Enable cache-dit summary logging after diffusion forward passes.",
)
omni_config_group.add_argument(
"--step-execution",
action="store_true",
help="Enable per-step diffusion execution so running requests can be aborted between denoise steps.",
)
# VAE memory optimization parameters
omni_config_group.add_argument(
"--vae-use-slicing",
action="store_true",
help="Enable VAE slicing for memory optimization (useful for mitigating OOM issues).",
)
omni_config_group.add_argument(
"--vae-use-tiling",
action="store_true",
help="Enable VAE tiling for memory optimization (useful for mitigating OOM issues).",
)
# Parallel weight loading (faster diffusion startup)
omni_config_group.add_argument(
"--disable-multithread-weight-load",
action="store_false",
dest="enable_multithread_weight_load",
default=True,
help="Disable multi-threaded safetensors loading (default: enabled with 4 threads).",
)
omni_config_group.add_argument(
"--num-weight-load-threads",
type=int,
default=4,
help="Number of threads for parallel weight loading (default: 4).",
)
# diffusion model offload parameters
omni_config_group.add_argument(
"--enable-cpu-offload",
action="store_true",
help="Enable CPU offloading for diffusion models.",
)
omni_config_group.add_argument(
"--enable-layerwise-offload",
action="store_true",
help="Enable layerwise (blockwise) offloading on DiT modules.",
)
# Video model parameters (e.g., Wan2.2) - engine-level
omni_config_group.add_argument(
"--boundary-ratio",
type=float,
default=None,
help="Boundary split ratio for low/high DiT in video models (e.g., 0.875 for Wan2.2).",
)
omni_config_group.add_argument(
"--flow-shift",
type=float,
default=None,
help="Scheduler flow_shift for video models (e.g., 5.0 for 720p, 12.0 for 480p).",
)
omni_config_group.add_argument(
"--cfg-parallel-size",
type=int,
default=1,
choices=[1, 2],
help="Number of devices for CFG parallel computation for diffusion models. "
"Equivalent to setting DiffusionParallelConfig.cfg_parallel_size.",
)
omni_config_group.add_argument(
"--vae-patch-parallel-size",
type=int,
default=1,
help="VAE Patch Parallelism degree for diffusion models. "
"Distributes VAE decode workload across multiple ranks by splitting the latent spatially. "
"Equivalent to setting DiffusionParallelConfig.vae_patch_parallel_size.",
)
# Default sampling parameters
omni_config_group.add_argument(
"--default-sampling-params",
type=str,
help="Json str for Default sampling parameters, \n"
'Structure: {"<stage_id>": {<sampling_param>: value, ...}, ...}\n'
'e.g., \'{"0": {"num_inference_steps":50, "guidance_scale":1}}\'. '
"Currently only supports diffusion models.",
)
# Diffusion model mixed precision
omni_config_group.add_argument(
"--max-generated-image-size",
type=int,
help="The max size of generate image (height * width).",
)
# TTS-specific parameters
omni_config_group.add_argument(
"--tts-max-instructions-length",
type=int,
default=None,
help="Maximum length for TTS voice style instructions (overrides stage config, default: 500).",
)
# Enable diffusion pipeline profiling
omni_config_group.add_argument(
"--enable-diffusion-pipeline-profiler",
action="store_true",
help="Enable diffusion pipeline profiler to display stage durations.",
)
# Stash via type(self) so the docs hook (which execs this function in a
# sandboxed globals dict via ``DummySelf``) doesn't fail on a NameError.
type(self)._parser = serve_parser
return serve_parser
def _create_default_diffusion_stage_cfg(args: argparse.Namespace) -> list[dict[str, Any]]:
"""Create default diffusion stage configuration.
Uses AsyncOmniEngine's implementation which doesn't have OmegaConf
compatibility issues.
"""
from vllm_omni.engine.async_omni_engine import AsyncOmniEngine
return AsyncOmniEngine._create_default_diffusion_stage_cfg(vars(args))
def run_headless(args: argparse.Namespace) -> None:
"""Run a single stage in headless mode."""
from vllm.v1.engine.coordinator import DPCoordinator
from vllm.v1.engine.utils import CoreEngineProcManager
from vllm.v1.executor.multiproc_executor import MultiprocExecutor
from vllm.version import __version__ as VLLM_VERSION
from vllm_omni.diffusion.stage_diffusion_proc import (
complete_diffusion_handshake,
spawn_diffusion_proc,
)
from vllm_omni.distributed.omni_connectors.utils.initialization import resolve_omni_kv_config_for_stage
from vllm_omni.engine.stage_engine_startup import register_stage_with_omni_master
from vllm_omni.engine.stage_init_utils import (
build_diffusion_config,
build_engine_args_dict,
build_vllm_config,
extract_stage_metadata,
get_stage_connector_spec,
inject_kv_stage_info,
load_omni_transfer_config_for_model,
prepare_engine_environment,
terminate_alive_proc,
)
from vllm_omni.entrypoints.utils import inject_omni_kv_config, load_and_resolve_stage_configs
model = args.model
stage_id: int | None = args.stage_id
omni_master_address: str | None = args.omni_master_address
omni_master_port: int | None = args.omni_master_port
if stage_id is None:
raise ValueError("--stage-id is required in headless mode")
if omni_master_address is None or omni_master_port is None:
raise ValueError("--omni-master-address and --omni-master-port are required in headless mode")
if getattr(args, "api_server_count", 0) and args.api_server_count > 1:
raise ValueError("api_server_count can't be set in headless mode")
if args.worker_backend != "multi_process":
raise ValueError("headless mode requires worker_backend=multi_process")
args_dict = vars(args).copy()
# Preserve the explicit-keys set captured at parse time so per-stage yaml
# values (e.g. stage 1's ``gpu_memory_utilization: 0.5``) are not
# overwritten by argparse defaults for flags the user didn't type.
cli_explicit_keys = args_dict.pop("_cli_explicit_keys", None)
config_path, stage_configs = load_and_resolve_stage_configs(
model,
args_dict.get("stage_configs_path"),
args_dict,
cli_explicit_keys=cli_explicit_keys,
)
# Locate the stage config that matches stage_id.
stage_cfg = None
for cfg in stage_configs:
if getattr(cfg, "stage_id", None) == stage_id:
stage_cfg = cfg
break
if stage_cfg is None:
raise ValueError(
f"No stage config found for stage_id={stage_id}. "
f"Available stage ids: {[getattr(c, 'stage_id', None) for c in stage_configs]}"
)
prepare_engine_environment()
omni_transfer_config = load_omni_transfer_config_for_model(model, config_path)
omni_conn_cfg, omni_from, omni_to = resolve_omni_kv_config_for_stage(omni_transfer_config, stage_id)
if getattr(stage_cfg, "stage_type", "llm") == "diffusion":
metadata = extract_stage_metadata(stage_cfg)
if omni_conn_cfg:
inject_omni_kv_config(stage_cfg, omni_conn_cfg, omni_from, omni_to)
inject_kv_stage_info(stage_cfg, stage_id)
od_config = build_diffusion_config(model, stage_cfg, metadata)
logger.info(
"[Headless] Launching diffusion stage %d via OmniMasterServer at %s:%d",
stage_id,
omni_master_address,
omni_master_port,
)
proc = None
try:
handshake_address, request_address, response_address = register_stage_with_omni_master(
omni_master_address=omni_master_address,
omni_master_port=omni_master_port,
omni_stage_id=stage_id,
omni_stage_config=stage_cfg,
return_addresses=True,
)
proc, _, _, _ = spawn_diffusion_proc(
model,
od_config,
handshake_address=handshake_address,
request_address=request_address,
response_address=response_address,
)
complete_diffusion_handshake(proc, handshake_address)
proc.join()
if proc.exitcode not in (None, 0):
raise RuntimeError(f"Diffusion stage {stage_id} exited with code {proc.exitcode}")
return
finally:
logger.info("[Headless] Shutting down stage %d.", stage_id)
if proc is not None and proc.is_alive():
terminate_alive_proc(proc)
stage_connector_spec = get_stage_connector_spec(
omni_transfer_config=omni_transfer_config,
stage_id=stage_id,
async_chunk=False,
)
# Device assignment is managed externally (e.g. CUDA_VISIBLE_DEVICES);
# runtime_cfg is intentionally ignored in headless mode.
engine_args_dict = build_engine_args_dict(
stage_cfg,
model,
stage_connector_spec=stage_connector_spec,
)
# Inject omni KV connector config so the engine runner can initialize the
# correct connector (sender/receiver role, type, addresses, etc.).
if omni_conn_cfg:
omni_kv = engine_args_dict.get("omni_kv_config") or {}
if not isinstance(omni_kv, dict):
omni_kv = dict(omni_kv)
omni_kv["connector_config"] = omni_conn_cfg
omni_kv["omni_from_stage"] = omni_from
omni_kv["omni_to_stage"] = omni_to
omni_kv.setdefault("stage_id", stage_id)
engine_args_dict["omni_kv_config"] = omni_kv
vllm_config, executor_class = build_vllm_config(
stage_cfg,
model,
stage_connector_spec=stage_connector_spec,
engine_args_dict=engine_args_dict,
headless=True,
)
parallel_config = vllm_config.parallel_config
local_engine_count = parallel_config.data_parallel_size_local
if local_engine_count <= 0:
raise ValueError("data_parallel_size_local must be > 0 in headless mode")
shutdown_requested = False
def signal_handler(signum: int, frame: FrameType | None) -> None:
nonlocal shutdown_requested
logger.debug("Received %d signal.", signum)
if not shutdown_requested:
shutdown_requested = True
raise SystemExit
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
if parallel_config.node_rank_within_dp > 0:
head_node_address = f"{parallel_config.master_addr}:{parallel_config.master_port}"
logger.info(
"Launching vLLM-Omni (v%s) headless multiproc executor, "
"with head node address %s for torch.distributed process group.",
VLLM_VERSION,
head_node_address,
)
executor = MultiprocExecutor(vllm_config, monitor_workers=False)
executor.start_worker_monitor(inline=True)
return
dp_rank = parallel_config.data_parallel_rank if parallel_config.data_parallel_rank is not None else 0
coordinator = None
if vllm_config.needs_dp_coordinator and dp_rank == 0:
coordinator = DPCoordinator(
parallel_config,
enable_wave_coordination=vllm_config.model_config.is_moe,
)
logger.info(
"[Headless] Started DP Coordinator process for stage %d (PID: %d)",
stage_id,
coordinator.proc.pid,
)
logger.info(
"[Headless] Launching %d engine core(s) for stage %d via OmniMasterServer at %s:%d",
local_engine_count,
stage_id,
omni_master_address,
omni_master_port,
)
# Headless mode launches all local engine cores for a single stage.
# The OmniMasterServer allocates one handshake endpoint per stage, so we
# register the stage once here and let every local engine core reuse the
# returned handshake address directly.
handshake_address = register_stage_with_omni_master(
omni_master_address=omni_master_address,
omni_master_port=omni_master_port,
omni_stage_id=stage_id,
omni_stage_config=stage_cfg,
coordinator=coordinator,
)
engine_manager = None
log_stats = bool(getattr(args, "log_stats", False))
if getattr(args, "disable_log_stats", False):
log_stats = False
try:
engine_manager = CoreEngineProcManager(
local_engine_count=local_engine_count,
start_index=dp_rank,
local_start_index=0,
vllm_config=vllm_config,
local_client=False,
handshake_address=handshake_address,
executor_class=executor_class,
log_stats=log_stats,
)
engine_manager.join_first()
finally:
logger.info("[Headless] Shutting down stage %d.", stage_id)
if engine_manager is not None:
engine_manager.shutdown()
if coordinator is not None:
coordinator.shutdown()
def cmd_init() -> list[CLISubcommand]:
return [OmniServeCommand()]