Four changes, each measured on a Xeon 8480C with Spark-X2.5-1.7B at 24 threads. Numbers are best-of-interleaved-passes; this host is shared and a single sequential grid swung the llama.cpp control 30%, so anything under ~8% is not treated as a result. **vLLM's CPU backend does have a 4-bit kernel.** An earlier note in this tree said it did not, on the strength of `woq_int4_linear`, `int4_scaled_mm_with_quant` and `da8w4_linear` all being absent. Those are simply not the names it uses: `CPUWNA16LinearKernel` dispatches GPTQ/AWQ checkpoints to `int4_scaled_mm_cpu`, an AMX W4A8 GEMM, whenever activations are bf16 and the checkpoint carries no act-order. Four-bit weights times eight-bit activations is the arithmetic llama.cpp's Q4_K_M already does. `gptq_cpu_export` builds checkpoints for it; the kernel pins the zero point at 8, so the grid must be symmetric, and a per-group scale search recovers most of what that costs. **A per-device weight path** (`edge/weight_path.py`). The fast kernel on each device needs a different, mutually exclusive weight layout, so the choice has to come from a probe rather than a flag: AMX x86 -> W4A8/GPTQ; x86 without AMX, or priority="fidelity" -> W4A16/tinygemm (bf16 activations, the more faithful of the two); ARM -> W4A16 but flagged unverified, because PyTorch's 4-bit CPU GEMM dispatches a different kernel there than the AVX-512 one measured here; phone -> runs_in_vllm=False, the deliverable is an exported graph with calibrated w8a16. **The CPU compile cache never loaded.** `_try_load_aot_compiled_fn` finishes with `compiled_fn.finalize_loading(cfg)`, which the CUDA wrapper object has and the inductor CPU backend's plain function does not. The AttributeError was swallowed as a cache miss, so every engine start recompiled the model: warm start 49.8 s against 14.9 s with the missing no-op attached. Self- disabling if upstream gives the CPU path a real one. **The head gate stops being a GEMM.** A 2048x8 projection cannot amortize a thread fan-out: dispatched as `aten::mm` it costs 1.155 ms of a decode step, nearly all barrier. As an eager reduction behind an opaque op it costs 0.444 ms. It has to be opaque -- written inline the reduction is traced, and inductor's loop made the whole step 1.8x slower (78.8 -> 44.5 tok/s). Also included and *off by default*: `cpu_ir_ops` registers the fused `_C::rms_norm` / `_C::fused_add_rms_norm` kernels that vLLM's CPU build ships but gates on `GPGPU_DEVICE`. In isolation they are 3.6x and 4.5x faster than the native path; end to end they are slightly slower (72.7 vs 74.0 tok/s), because inductor was already fusing the native norm into its neighbours and an opaque call breaks that fusion. Kept with the measurement recorded so the next person does not repeat it. `gelu_and_mul` is deliberately not registered: its fused CPU kernel is 2.7x slower at Spark's GEGLU shape. Decode, W4A16 g64 -> W4A8 g128 with the gate fix: 69.1 -> 84.5 tok/s (+22%), against llama.cpp Q4_K_M's 90.0; prefill 3347 against 1121 (3.0x). The two 4-bit paths are not equally faithful: against each build's own bf16 over 12 greedy prompts, Q4_K_M reproduces 4/12, W4A16 2/12 and W4A8 1/12. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
655 lines
30 KiB
Python
655 lines
30 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field, fields
|
|
from typing import Any, cast
|
|
|
|
from vllm.engine.arg_utils import AsyncEngineArgs, EngineArgs
|
|
from vllm.logger import init_logger
|
|
|
|
from vllm_omni.config import OmniModelConfig
|
|
from vllm_omni.outputs.output_modality import OutputModality
|
|
from vllm_omni.platforms import current_omni_platform
|
|
from vllm_omni.plugins import load_omni_general_plugins
|
|
from vllm_omni.worker.omni_connector_validation import validate_worker_omni_connector
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
# Maps model architecture names to their HuggingFace model_type values.
|
|
# Used when auto-injecting hf_overrides for models with missing config.json.
|
|
_ARCH_TO_MODEL_TYPE: dict[str, str] = {
|
|
"CosyVoice3Model": "cosyvoice3",
|
|
"GLMTTSForConditionalGeneration": "glm_tts",
|
|
"IndexTTS2S2MelDecoder": "indextts2",
|
|
"IndexTTS2TalkerForConditionalGeneration": "indextts2",
|
|
"IndexTTS25S2MelDecoder": "indextts2_5",
|
|
"IndexTTS25TalkerForConditionalGeneration": "indextts2_5",
|
|
"OmniVoiceModel": "omnivoice",
|
|
# PersonaPlex ships an empty config.json, so create_model_config() must patch
|
|
# model_type=personaplex from the arch name or the staged pipeline can't load
|
|
# the HF config without manual overrides.
|
|
"PersonaPlexTalkerForConditionalGeneration": "personaplex",
|
|
"PersonaPlexCode2Wav": "personaplex",
|
|
# NemotronVoiceChat ships a NeMo-style config.json without model_type;
|
|
# create_model_config() patches model_type=nemotron_voicechat from the arch.
|
|
"NemotronVoiceChatThinkerForConditionalGeneration": "nemotron_voicechat",
|
|
"NemotronVoiceChatTalkerForConditionalGeneration": "nemotron_voicechat",
|
|
"NemotronVoiceChatCode2Wav": "nemotron_voicechat",
|
|
"VoxCPM2TalkerForConditionalGeneration": "voxcpm2",
|
|
}
|
|
|
|
# Maps model architecture names to tokenizer subfolder paths within HF repos.
|
|
_TOKENIZER_SUBFOLDER_MAP: dict[str, str] = {
|
|
"CosyVoice3Model": "CosyVoice-BlankEN",
|
|
"GLMTTSForConditionalGeneration": "vq32k-phoneme-tokenizer",
|
|
}
|
|
|
|
|
|
def _register_omni_hf_configs() -> None:
|
|
try:
|
|
from transformers import AutoConfig
|
|
|
|
from vllm_omni.model_executor.models.indextts2.configuration_indextts2 import (
|
|
IndexTTS2Config,
|
|
IndexTTS25Config,
|
|
)
|
|
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
|
MingDenseConfig,
|
|
MingMoeConfig,
|
|
)
|
|
from vllm_omni.model_executor.models.moss_tts.configuration_moss_tts import (
|
|
MossTTSLocalConfig,
|
|
MossTTSRealtimeConfig,
|
|
)
|
|
from vllm_omni.model_executor.models.nemotron_voicechat.configuration_nemotron_voicechat import (
|
|
NemotronVoiceChatConfig,
|
|
)
|
|
from vllm_omni.model_executor.models.personaplex.configuration_personaplex import (
|
|
PersonaPlexConfig,
|
|
)
|
|
from vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts import (
|
|
Qwen3TTSConfig,
|
|
)
|
|
from vllm_omni.model_executor.models.spark2_5.configuration_spark2_5 import (
|
|
Spark2_5Config,
|
|
)
|
|
from vllm_omni.transformers_utils.configs.cosyvoice3 import CosyVoice3Config
|
|
from vllm_omni.transformers_utils.configs.glm_tts import GLMTTSConfig
|
|
from vllm_omni.transformers_utils.configs.omnivoice import OmniVoiceConfig
|
|
from vllm_omni.transformers_utils.configs.voxcpm2 import VoxCPM2Config
|
|
except Exception as exc: # pragma: no cover - best-effort optional registration
|
|
logger.warning("Skipping omni HF config registration due to import error: %s", exc)
|
|
return
|
|
|
|
# Register with both transformers AutoConfig and vLLM's config registry
|
|
# so models with empty/missing config.json (e.g. CosyVoice3) can be
|
|
# resolved when model_type is injected via hf_overrides.
|
|
try:
|
|
from vllm.transformers_utils.config import _CONFIG_REGISTRY
|
|
except ImportError:
|
|
_CONFIG_REGISTRY = None
|
|
|
|
for model_type, config_cls in [
|
|
("dense", MingDenseConfig),
|
|
("bailingmm", MingMoeConfig),
|
|
("indextts2", IndexTTS2Config),
|
|
("indextts2_5", IndexTTS25Config),
|
|
("moss_tts_local", MossTTSLocalConfig),
|
|
("moss_tts_realtime", MossTTSRealtimeConfig),
|
|
("qwen3_tts", Qwen3TTSConfig),
|
|
("spark2_5", Spark2_5Config),
|
|
("personaplex", PersonaPlexConfig),
|
|
("nemotron_voicechat", NemotronVoiceChatConfig),
|
|
("cosyvoice3", CosyVoice3Config),
|
|
("glm_tts", GLMTTSConfig),
|
|
("omnivoice", OmniVoiceConfig),
|
|
("voxcpm2", VoxCPM2Config),
|
|
]:
|
|
try:
|
|
AutoConfig.register(model_type, config_cls)
|
|
except ValueError:
|
|
# Already registered elsewhere; ignore.
|
|
pass
|
|
if _CONFIG_REGISTRY is not None and model_type not in _CONFIG_REGISTRY:
|
|
_CONFIG_REGISTRY[model_type] = config_cls
|
|
|
|
|
|
def register_omni_models_to_vllm():
|
|
from vllm.model_executor.models import ModelRegistry
|
|
|
|
from vllm_omni.model_executor.models.registry import _OMNI_MODELS
|
|
|
|
_register_omni_hf_configs()
|
|
|
|
# Unconditionally (re)register every omni arch into the upstream global
|
|
# ModelRegistry: vLLM 0.27 added some omni archs (e.g.
|
|
# Qwen3OmniMoeForConditionalGeneration) to its own registry, but resolves
|
|
# them to upstream classes that lack omni features (e.g.
|
|
# buffer_realtime_audio for the realtime endpoint). Registering here
|
|
# overwrites those entries with the vllm-omni classes, which is the
|
|
# behavior entrypoints resolving through the global registry rely on
|
|
# (and matches OmniModelRegistry's omni-wins ordering).
|
|
for arch, (mod_folder, mod_relname, cls_name) in _OMNI_MODELS.items():
|
|
ModelRegistry.register_model(arch, f"vllm_omni.model_executor.models.{mod_folder}.{mod_relname}:{cls_name}")
|
|
|
|
# Register omni-specific reasoning parsers (e.g., step_audio).
|
|
import vllm_omni.reasoning # noqa: F401
|
|
|
|
# Register out-of-tree quantization methods (e.g. cpu_int4, which gives
|
|
# the CPU backend the 4-bit weight path vLLM 0.28 does not ship).
|
|
import vllm_omni.model_executor.layers.quantization # noqa: F401
|
|
|
|
# Let the CPU backend use the fused RMSNorm kernels it already ships but
|
|
# marks unsupported (see cpu_ir_ops): ~1.8 ms of a 12 ms decode step.
|
|
from vllm_omni.model_executor.layers import cpu_ir_ops
|
|
|
|
cpu_ir_ops.prefer_cpu_fused_norms()
|
|
|
|
|
|
@dataclass
|
|
class OmniEngineArgs(EngineArgs):
|
|
"""Engine arguments for omni models, extending base EngineArgs.
|
|
Adds omni-specific configuration fields for multi-stage pipeline
|
|
processing and output type specification.
|
|
Args:
|
|
stage_id: Identifier for the stage in a multi-stage pipeline.
|
|
Defaults to 0 for per-stage engine construction. The CLI-level
|
|
single-stage selector remains optional on the parsed argparse
|
|
namespace and should not be forwarded as a nullable per-stage
|
|
engine argument.
|
|
model_stage: Stage type identifier, e.g., "thinker" or "talker"
|
|
(default: "thinker")
|
|
model_arch: Model architecture name
|
|
(default: "Qwen2_5OmniForConditionalGeneration")
|
|
engine_output_type: Optional output type specification for the engine.
|
|
Used to route outputs to appropriate processors (e.g., "image",
|
|
"audio", "latents"). If None, output type is inferred.
|
|
hf_config_name: Optional key for HF config subkey to be extracted
|
|
for this stage, e.g., talker_config; If None, the default
|
|
HF config will be used.
|
|
custom_process_next_stage_input_func: Optional path to a custom function for processing
|
|
inputs from previous stages
|
|
If None, default processing is used.
|
|
stage_connector_spec: Extra configuration for stage connector
|
|
async_chunk: If set to True, perform async chunk
|
|
session_mode: Request lifecycle mode, either turn-based or duplex
|
|
worker_type: Model Type, e.g., "ar" or "generation"
|
|
task_type: Model-defined startup task type. Consumers validate the
|
|
supported values and decide whether it selects request behavior,
|
|
task-specific weights, or both.
|
|
omni_master_address: TCP address that the OmniMasterServer (running
|
|
inside AsyncOmniEngine) listens on for engine core registrations.
|
|
Required when single-stage mode is active.
|
|
omni_master_port: TCP port for the OmniMasterServer registration
|
|
socket. Required when single-stage mode is active.
|
|
output_modalities: Optional list of output modality names to enable
|
|
(e.g. ["text", "audio"]). If None, all modalities supported by
|
|
the model are used.
|
|
log_stats: Whether to log engine statistics. Defaults to False.
|
|
custom_pipeline_args: Dictionary of arguments for custom pipeline
|
|
initialization (e.g., ``{"pipeline_class": "my.Module"}``).
|
|
Passed through to the diffusion stage engine.
|
|
"""
|
|
|
|
stage_id: int = 0
|
|
model_stage: str = "thinker"
|
|
model_arch: str | None = None
|
|
engine_output_type: str | None = None
|
|
hf_config_name: str | None = None
|
|
custom_process_next_stage_input_func: str | None = None
|
|
requires_full_payload_input: bool = False
|
|
stage_connector_spec: dict[str, Any] = field(default_factory=dict)
|
|
subtalker_sampling_params: dict[str, Any] | None = None
|
|
silence_ban_frames: int = 0
|
|
async_chunk: bool = False
|
|
session_mode: str = "turn"
|
|
retains_state_across_chunks: bool = False
|
|
# WS-A: Stage-1 active stream slots. 0 = legacy preempt-everything.
|
|
# Must be declared here so engine_args dict propagation does not silently
|
|
# drop the value when constructing OmniEngineArgs from kwargs.
|
|
active_stream_window: int = 0
|
|
# Engine admission is the single source of truth for per-stage pools of
|
|
# model-owned streaming state (codec, decoder, and similar resources).
|
|
duplex_max_sessions: int = 1
|
|
omni_kv_config: dict | None = None
|
|
quantization_config: Any | None = None
|
|
force_cutlass_fp8: bool | None = None
|
|
worker_type: str | None = None
|
|
# Dotted path of a per-stage pooling-output decoder applied worker-side.
|
|
pooling_output_decoder: str | None = None
|
|
task_type: str | None = None
|
|
worker_cls: str = None # type: ignore[assignment] # Upstream default is "auto"; omni resolves
|
|
# in __post_init__ based on worker_type (ar/generation), so None is safe here.
|
|
enable_sleep_mode: bool = False
|
|
omni: bool = False
|
|
# Diffusion request-mode batch admission (forwarded to OmniDiffusionConfig).
|
|
request_batch_max_wait_ms: float = 0.0
|
|
fa_deterministic: bool = False
|
|
|
|
@classmethod
|
|
def _add_omni_specific_args(cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
|
try:
|
|
parser.add_argument("--omni", action="store_true", default=False, help="Enable Omni engine features.")
|
|
except argparse.ArgumentError:
|
|
pass
|
|
try:
|
|
parser.add_argument(
|
|
"--enable-sleep-mode", action="store_true", default=False, help="Enable GPU memory pool for sleep mode."
|
|
)
|
|
except argparse.ArgumentError:
|
|
pass
|
|
return parser
|
|
|
|
omni_master_address: str | None = None
|
|
omni_master_port: int | None = None
|
|
# OmniCoordinator integration knobs (process-local).
|
|
omni_dp_size_local: int = 1
|
|
omni_lb_policy: str = "random"
|
|
omni_heartbeat_timeout: float = 30.0
|
|
output_modalities: list[str] | None = None
|
|
log_stats: bool = False
|
|
custom_pipeline_args: dict[str, Any] | None = None
|
|
has_sampling_extra_args: bool = False
|
|
sampling_extra_args_keys: tuple[str, ...] = ()
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.worker_cls in (None, "auto"):
|
|
if self.worker_type == "ar":
|
|
self.worker_cls = current_omni_platform.get_omni_ar_worker_cls()
|
|
elif self.worker_type == "generation":
|
|
self.worker_cls = current_omni_platform.get_omni_generation_worker_cls()
|
|
load_omni_general_plugins()
|
|
|
|
connector_extra = self.stage_connector_spec.get("extra")
|
|
connector_role = connector_extra.get("role") if isinstance(connector_extra, dict) else None
|
|
needs_connector = bool(
|
|
self.requires_full_payload_input or self.custom_process_next_stage_input_func or connector_role is not None
|
|
)
|
|
validate_worker_omni_connector(self.worker_cls, needs_connector)
|
|
super().__post_init__()
|
|
|
|
def _ensure_omni_models_registered(self):
|
|
if hasattr(self, "_omni_models_registered"):
|
|
return True
|
|
register_omni_models_to_vllm()
|
|
self._omni_models_registered = True
|
|
return True
|
|
|
|
def _patch_empty_hf_config(self, model_type: str) -> None:
|
|
"""For models with empty config.json (e.g. CosyVoice3), create a
|
|
patched config in a temp directory with model_type set so that
|
|
transformers AutoConfig.from_pretrained can resolve the config class.
|
|
Sets self.hf_config_path to point to the patched directory."""
|
|
try:
|
|
from transformers import PretrainedConfig
|
|
|
|
config_dict, _ = PretrainedConfig.get_config_dict(self.model)
|
|
if config_dict.get("model_type"):
|
|
return # config.json already has model_type, no patching needed
|
|
except Exception:
|
|
# The official IndexTTS 2.5 bundle has no HuggingFace config.json.
|
|
# Keep this exception model-scoped so other loader failures retain
|
|
# vLLM's normal error path.
|
|
if model_type != "indextts2_5" or not os.path.isdir(self.model):
|
|
return
|
|
config_path = os.path.join(self.model, "config.json")
|
|
if os.path.lexists(config_path):
|
|
return
|
|
config_dict = {}
|
|
|
|
# Create a temp dir with a patched config.json
|
|
temp_dir = tempfile.mkdtemp(prefix="omni_hf_config_")
|
|
config_dict["model_type"] = model_type
|
|
config_dict.setdefault("architectures", [self.model_arch])
|
|
with open(os.path.join(temp_dir, "config.json"), "w") as f:
|
|
json.dump(config_dict, f)
|
|
self.hf_config_path = temp_dir
|
|
self._temp_config_dir = temp_dir
|
|
logger.info("Patched empty HF config with model_type=%s at %s", model_type, temp_dir)
|
|
|
|
def create_model_config(self) -> OmniModelConfig:
|
|
"""Create an OmniModelConfig from these engine arguments.
|
|
Returns:
|
|
OmniModelConfig instance with all configuration fields set
|
|
"""
|
|
# register omni models to avoid model not found error
|
|
self._ensure_omni_models_registered()
|
|
|
|
# Build stage_connector_config from stage_connector_spec
|
|
stage_connector_config = {
|
|
"name": self.stage_connector_spec.get("name", "SharedMemoryConnector"),
|
|
"extra": self.stage_connector_spec.get("extra", {}).copy(),
|
|
}
|
|
stage_connector_config["extra"]["stage_id"] = self.stage_id
|
|
|
|
hf_overrides = cast(dict[str, Any] | Callable[[Any], Any] | None, getattr(self, "hf_overrides", None))
|
|
# If model_arch is specified, inject it into hf_overrides so vLLM can
|
|
# resolve the architecture even when config.json lacks 'architectures'.
|
|
# Also inject model_type so AutoConfig can resolve the correct config
|
|
# class for models with empty or missing config.json (e.g. CosyVoice3).
|
|
if self.model_arch:
|
|
if hf_overrides is None:
|
|
hf_overrides = {}
|
|
self.hf_overrides = hf_overrides
|
|
if isinstance(hf_overrides, dict):
|
|
hf_overrides.setdefault("architectures", [self.model_arch])
|
|
if "model_type" not in hf_overrides:
|
|
model_type = _ARCH_TO_MODEL_TYPE.get(self.model_arch)
|
|
if model_type is not None:
|
|
hf_overrides.setdefault("model_type", model_type)
|
|
|
|
# Stage wrappers (e.g. Code2Wav) may need max_model_len larger
|
|
# than the base checkpoint's text max_position_embeddings.
|
|
if self.model_arch == "Qwen3TTSCode2Wav" and self.max_model_len is not None:
|
|
hf_overrides.setdefault("talker_config", {}).setdefault(
|
|
"max_position_embeddings", int(self.max_model_len)
|
|
)
|
|
|
|
# For models whose HF config.json is empty or lacks model_type
|
|
# (e.g. CosyVoice3), AutoConfig.from_pretrained fails because it
|
|
# cannot determine which config class to use from the empty dict.
|
|
# hf_overrides alone is not enough since transformers reads
|
|
# model_type from config_dict before applying overrides.
|
|
# Workaround: create a patched config.json in a temp directory
|
|
# and point hf_config_path to it so vLLM reads model_type from it.
|
|
if not self.hf_config_path:
|
|
model_type = _ARCH_TO_MODEL_TYPE.get(self.model_arch)
|
|
if model_type is not None:
|
|
self._patch_empty_hf_config(model_type)
|
|
|
|
tokenizer = cast(str | None, getattr(self, "tokenizer", None))
|
|
|
|
# NemotronVoiceChat: the checkpoint's only tokenizer subfolder is the
|
|
# auxiliary rnnt_tokenizer/ (ASR loss vocab), which the generic
|
|
# subfolder auto-detect below would wrongly pick up. The text tokenizer
|
|
# is the Nemotron backbone's, resolved from a local override or its
|
|
# HF id (cache-friendly).
|
|
if not tokenizer and self.model_arch in (
|
|
"NemotronVoiceChatThinkerForConditionalGeneration",
|
|
"NemotronVoiceChatTalkerForConditionalGeneration",
|
|
"NemotronVoiceChatCode2Wav",
|
|
):
|
|
self.tokenizer = os.environ.get("NEMOTRON_VOICECHAT_LLM_PATH") or "nvidia/NVIDIA-Nemotron-Nano-9B-v2"
|
|
tokenizer = self.tokenizer
|
|
logger.info("NemotronVoiceChat: using text tokenizer from %s", tokenizer)
|
|
|
|
# Auto-detect tokenizer for models that store it in a subdirectory
|
|
# rather than the root (e.g. CosyVoice3 uses CosyVoice-BlankEN/).
|
|
if not tokenizer and self.model:
|
|
model_path = self.model
|
|
if os.path.isdir(model_path) and not os.path.isfile(os.path.join(model_path, "tokenizer_config.json")):
|
|
for subfolder in sorted(os.listdir(model_path)):
|
|
candidate = os.path.join(model_path, subfolder)
|
|
if os.path.isdir(candidate) and os.path.isfile(os.path.join(candidate, "tokenizer_config.json")):
|
|
self.tokenizer = candidate
|
|
tokenizer = candidate
|
|
logger.info("Auto-detected tokenizer at %s", candidate)
|
|
break
|
|
elif not os.path.isdir(model_path):
|
|
tokenizer_subfolder = _TOKENIZER_SUBFOLDER_MAP.get(self.model_arch or "")
|
|
if tokenizer_subfolder:
|
|
# Download just the tokenizer files from the subfolder
|
|
try:
|
|
from huggingface_hub import snapshot_download
|
|
|
|
local_dir = snapshot_download(
|
|
model_path,
|
|
allow_patterns=[
|
|
f"{tokenizer_subfolder}/tokenizer*",
|
|
f"{tokenizer_subfolder}/tokenization_*",
|
|
f"{tokenizer_subfolder}/special_tokens*",
|
|
f"{tokenizer_subfolder}/vocab*",
|
|
f"{tokenizer_subfolder}/merges*",
|
|
f"{tokenizer_subfolder}/added_tokens*",
|
|
],
|
|
)
|
|
candidate = os.path.join(local_dir, tokenizer_subfolder)
|
|
if os.path.isdir(candidate):
|
|
self.tokenizer = candidate
|
|
logger.info("Downloaded tokenizer from %s/%s", model_path, tokenizer_subfolder)
|
|
except Exception as e:
|
|
logger.warning("Failed to download tokenizer subfolder: %s", e)
|
|
|
|
# Build the vLLM config first, then use it to create the Omni config.
|
|
try:
|
|
model_config = super().create_model_config()
|
|
finally:
|
|
# Clean up temp config dir if we created one
|
|
if hasattr(self, "_temp_config_dir"):
|
|
import shutil
|
|
|
|
shutil.rmtree(self._temp_config_dir, ignore_errors=True)
|
|
del self._temp_config_dir
|
|
|
|
omni_config = OmniModelConfig.from_vllm_model_config(
|
|
model_config=model_config,
|
|
# All kwargs below are Omni specific
|
|
stage_id=self.stage_id,
|
|
async_chunk=self.async_chunk,
|
|
session_mode=self.session_mode,
|
|
retains_state_across_chunks=self.retains_state_across_chunks,
|
|
active_stream_window=self.active_stream_window,
|
|
duplex_max_sessions=self.duplex_max_sessions,
|
|
model_stage=self.model_stage,
|
|
model_arch=self.model_arch,
|
|
worker_type=self.worker_type,
|
|
pooling_output_decoder=self.pooling_output_decoder,
|
|
engine_output_type=self.engine_output_type,
|
|
hf_config_name=self.hf_config_name,
|
|
custom_process_next_stage_input_func=self.custom_process_next_stage_input_func,
|
|
requires_full_payload_input=self.requires_full_payload_input,
|
|
stage_connector_config=stage_connector_config,
|
|
subtalker_sampling_params=self.subtalker_sampling_params,
|
|
silence_ban_frames=self.silence_ban_frames,
|
|
omni_kv_config=self.omni_kv_config,
|
|
task_type=self.task_type,
|
|
has_sampling_extra_args=self.has_sampling_extra_args,
|
|
sampling_extra_args_keys=tuple(self.sampling_extra_args_keys or ()),
|
|
)
|
|
return omni_config
|
|
|
|
|
|
@dataclass
|
|
class OmniAsyncEngineArgs(AsyncEngineArgs, OmniEngineArgs):
|
|
@classmethod
|
|
def add_cli_args(cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
|
parser = AsyncEngineArgs.add_cli_args(parser)
|
|
parser = OmniEngineArgs._add_omni_specific_args(parser)
|
|
return parser
|
|
|
|
@property
|
|
def output_modality(self) -> OutputModality:
|
|
"""Parse engine_output_type into a type-safe OutputModality flag."""
|
|
return OutputModality.from_string(self.engine_output_type)
|
|
|
|
|
|
# ============================================================================
|
|
# CLI argument routing
|
|
# ============================================================================
|
|
#
|
|
# vLLM-Omni's CLI flags live in three buckets:
|
|
#
|
|
# ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
|
# │ OrchestratorArgs │ │ OmniEngineArgs │ │ (upstream vllm) │
|
|
# │ │ │ │ │ server/api │
|
|
# │ stage_timeout │ │ max_num_seqs │ │ host, port │
|
|
# │ worker_backend │ │ gpu_mem_util │ │ ssl_keyfile │
|
|
# │ deploy_config │ │ dtype, quant │ │ api_key │
|
|
# │ ... │ │ ... │ │ ... │
|
|
# └──────────────────┘ └──────────────────┘ └──────────────────┘
|
|
# │ │ │
|
|
# ▼ ▼ ▼
|
|
# orchestrator each stage uvicorn /
|
|
# consumes engine FastAPI
|
|
#
|
|
# Fields in ``SHARED_FIELDS`` (e.g. ``model``, ``log_stats``) flow to BOTH
|
|
# orchestrator and engine by design.
|
|
#
|
|
# Invariants enforced by ``tests/engine/test_arg_utils_shared_fields.py``:
|
|
#
|
|
# 1. ``OrchestratorArgs`` ∩ ``OmniEngineArgs`` ⊆ ``SHARED_FIELDS``
|
|
# 2. Every CLI flag is classifiable into one of the three buckets
|
|
# 3. User-typed flags that match none of the above are logged as dropped
|
|
#
|
|
# Adding a new orchestrator-only flag → add a field to ``OrchestratorArgs``.
|
|
# Everything else is automatic.
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OrchestratorArgs:
|
|
"""CLI flags consumed by the orchestrator.
|
|
|
|
Contract: every field here is either
|
|
(a) orchestrator-only (never needed by a stage engine), OR
|
|
(b) orchestrator-read-then-redistributed (e.g. ``async_chunk`` is read
|
|
from CLI, written to ``DeployConfig``, then propagated to every
|
|
stage via ``merge_pipeline_deploy`` — not via direct kwargs
|
|
forwarding).
|
|
|
|
Fields that BOTH orchestrator and engine genuinely need (e.g. ``model``,
|
|
``log_stats``) should be listed in ``SHARED_FIELDS`` below.
|
|
"""
|
|
|
|
# === Lifecycle ===
|
|
stage_init_timeout: int = 300
|
|
init_timeout: int = 600
|
|
# Initialize stages sharing a physical GPU concurrently, guarded by
|
|
# pre-launch admission + engine-core SH/EX device locks. Off by default;
|
|
# enable only when the GPU is dedicated to this deployment.
|
|
parallel_stage_init: bool = False
|
|
|
|
# === Cross-stage Communication ===
|
|
batch_timeout: int = 10
|
|
|
|
# === Cluster / Backend ===
|
|
worker_backend: str = "multi_process"
|
|
ray_address: str | None = None
|
|
|
|
# === Config Files ===
|
|
deploy_config: str | None = None
|
|
# Named deploy profile (e.g. ``edge``) resolved to ``deploy/<profile>/<model_type>.yaml``
|
|
# by the orchestrator; mutually exclusive with ``deploy_config``.
|
|
deploy_profile: str | None = None
|
|
stage_overrides: str | None = None # raw JSON string; parsed downstream
|
|
# Optional composable-parallel strategy.yaml; orchestrator reads it, overlays
|
|
# derived sizing onto merged stages, then drops it before per-stage engine args.
|
|
strategy_config: str | None = None
|
|
|
|
# === Mode Switches (orchestrator reads, DeployConfig redistributes) ===
|
|
async_chunk: bool | None = None
|
|
|
|
# === Forced aligner (orchestrator injects a pooling stage; never a per-stage knob) ===
|
|
forced_aligner: str | None = None
|
|
forced_aligner_config: str | None = None
|
|
forced_aligner_device: str | None = None
|
|
|
|
# === Observability ===
|
|
log_stats: bool = False
|
|
enable_orch_monitor: bool = False
|
|
|
|
# === Headless Mode (also forwarded to engine — see SHARED_FIELDS) ===
|
|
stage_id: int | None = None
|
|
|
|
# === Pre-built Objects ===
|
|
parallel_config: Any = None
|
|
|
|
# === Diffusion model config ===
|
|
num_gpus: int | None = None
|
|
model_class_name: str | None = None
|
|
diffusion_load_format: str | None = None
|
|
lora_path: list[str] | None = None
|
|
lora_backend: str | None = None
|
|
lora_scale: float | None = None
|
|
diffusers_load_kwargs: str = "{}"
|
|
diffusers_call_kwargs: str = "{}"
|
|
ulysses_degree: int | None = None
|
|
ulysses_mode: str = "strict"
|
|
ulysses_a2a_permute: bool | None = None
|
|
ring_degree: int | None = None
|
|
allgather_degree: int | None = None
|
|
diffusion_quantization_config: str | None = None
|
|
use_hsdp: bool = False
|
|
hsdp_shard_size: int = -1
|
|
hsdp_replicate_size: int = 1
|
|
diffusion_attention_backend: str | None = None
|
|
fastvideo_vsa_topk: int | None = None
|
|
diffusion_attention_config: str | None = None
|
|
diffusion_compile_granularity: str | None = None
|
|
diffusion_compile_dynamic: bool | None = None
|
|
cache_backend: str = "none"
|
|
cache_config: str | None = None
|
|
enable_cache_dit_summary: bool = False
|
|
step_execution: bool = False
|
|
vae_use_slicing: bool = False
|
|
vae_use_tiling: bool = False
|
|
enable_multithread_weight_load: bool = True
|
|
num_weight_load_threads: int = 4
|
|
diffusion_offload_config: dict[str, Any] | None = None
|
|
# Compatibility aliases for existing callers and model-specific stage
|
|
# lifecycles that are broader than the compact dit/text_encoder selector.
|
|
enable_cpu_offload: bool = False
|
|
enable_layerwise_offload: bool = False
|
|
enable_distributed_layerwise_offload: bool = False
|
|
dlo_use_allgather: bool = True
|
|
dlo_resident_layers: int = 0
|
|
host_weight_runtime_mode: str = "disabled"
|
|
host_weight_runtime_root: str | None = None
|
|
dlo_host_registration_limit_gib: float = 0.0
|
|
boundary_ratio: float | None = None
|
|
flow_shift: float | None = None
|
|
diffusion_kv_cache_dtype: str | None = None
|
|
diffusion_kv_cache_skip_steps: str | None = None
|
|
diffusion_kv_cache_skip_layers: str | None = None
|
|
cfg_parallel_size: int = 1
|
|
vae_patch_parallel_size: int = 1
|
|
vae_parallel_mode: str = "tile"
|
|
default_sampling_params: str | None = None
|
|
max_generated_image_size: int | None = None
|
|
tts_max_instructions_length: int | None = None
|
|
enable_diffusion_pipeline_profiler: bool = False
|
|
enable_ar_profiler: bool = False
|
|
auxiliary_text_encoder: str | None = None
|
|
log_file: str | None = None
|
|
replica_id: int | None = None
|
|
omni_replica_address: str | None = None
|
|
|
|
# === Multi-stage guards ===
|
|
# --tokenizer is captured by the orchestrator and forwarded to stages
|
|
# only when the stage does not define tokenizer/tokenizer_subdir itself.
|
|
# Users wanting a per-stage tokenizer should set it in the deploy YAML.
|
|
tokenizer: str | None = None
|
|
|
|
|
|
# Fields that live in BOTH OrchestratorArgs and OmniEngineArgs by design.
|
|
# Changes to this set are a review red flag — revisit the contract.
|
|
SHARED_FIELDS: frozenset[str] = frozenset(
|
|
{
|
|
"model", # orch: detect model_type; engine: load weights
|
|
"stage_id", # orch: route (headless); engine: identity
|
|
"log_stats", # both want the flag
|
|
"async_chunk", # orch: read from CLI, redistribute; engine: per-stage flag
|
|
"tokenizer", # orch: detect model type; engine: tokenization
|
|
}
|
|
)
|
|
|
|
|
|
def orchestrator_field_names() -> frozenset[str]:
|
|
"""Return the names of every field on OrchestratorArgs."""
|
|
return frozenset(f.name for f in fields(OrchestratorArgs))
|
|
|
|
|
|
def internal_blacklist_keys() -> frozenset[str]:
|
|
"""Return the set of CLI keys that must never be forwarded as per-stage
|
|
engine overrides.
|
|
|
|
Derived from ``OrchestratorArgs`` fields minus ``SHARED_FIELDS``, so
|
|
adding a new orchestrator-owned flag is a one-line change to the
|
|
dataclass — this function updates automatically.
|
|
"""
|
|
return orchestrator_field_names() - SHARED_FIELDS
|