Merge branch 'main' into feat/diffusion-prefetch-protection

This commit is contained in:
Gao Han authored and GitHub committed 2026-06-02 23:22:08 +08:00
commit 69fd466fba
53 files changed
+1341 -1273

No files matched your search

@@ -411,6 +411,8 @@ Notes:
- `runtime.max_batch_size` controls stage-level batching. - `runtime.max_batch_size` controls stage-level batching.
- Thinker/Talker commonly use `enforce_eager: false` for CUDA Graph paths. - Thinker/Talker commonly use `enforce_eager: false` for CUDA Graph paths.
- Code2Wav often remains eager (`enforce_eager: true`) depending on runtime behavior. - Code2Wav often remains eager (`enforce_eager: true`) depending on runtime behavior.
- Qwen3-Omni defaults `VLLM_USE_FLASHINFER_MOE_FP16=0`. The Triton has been more stable & faster
than the FlashInfer CUTLASS unquantized MoE backend on recent vLLM rebases.
#### 2) Enable async chunk #### 2) Enable async chunk
+2 -1
View File
@@ -119,9 +119,10 @@ def extract_omni_serve_subparser_init():
exec_globals = { exec_globals = {
"_FlexibleArgumentParser": _FlexibleArgumentParser, "_FlexibleArgumentParser": _FlexibleArgumentParser,
"FlexibleArgumentParser": _FlexibleArgumentParser, "FlexibleArgumentParser": _FlexibleArgumentParser,
"TrackingArgumentParser": _FlexibleArgumentParser, # only needs add_argument, not tracking
"TrackingNamespace": None, # type annotation only
"make_arg_parser": lambda parser: parser, # no-op for doc "make_arg_parser": lambda parser: parser, # no-op for doc
"_ensure_vllm_platform": lambda: None, # no-op for doc "_ensure_vllm_platform": lambda: None, # no-op for doc
"nullify_stage_engine_defaults": lambda parser: None, # no-op for doc
"VLLM_SUBCMD_PARSER_EPILOG": "", "VLLM_SUBCMD_PARSER_EPILOG": "",
"logger": logger, "logger": logger,
"DummySubparsers": DummySubparsers, "DummySubparsers": DummySubparsers,
@@ -17,6 +17,9 @@ vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
The default deployment configuration situated at `vllm_omni/deploy/qwen3_omni_moe.yaml` is resolved and loaded The default deployment configuration situated at `vllm_omni/deploy/qwen3_omni_moe.yaml` is resolved and loaded
automatically via the model registry, obviating the necessity for the `--deploy-config` flag in standard deployment topologies. automatically via the model registry, obviating the necessity for the `--deploy-config` flag in standard deployment topologies.
The bundled Qwen3-Omni setup defaults `VLLM_USE_FLASHINFER_MOE_FP16=0`. This keeps the Thinker & Talker on vLLM's
Triton unquantized MoE path and avoids the performance regression observed with the FlashInfer CUTLASS unquantized MoE
backend.
Asynchronous chunk streaming is **enabled by default** within the bundled configuration. Asynchronous chunk streaming is **enabled by default** within the bundled configuration.
To explicitly utilize a custom deployment YAML, specify the configuration path: To explicitly utilize a custom deployment YAML, specify the configuration path:
@@ -72,6 +75,12 @@ vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \
--stage-overrides '{"1": {"gpu_memory_utilization": 0.5}}' --stage-overrides '{"1": {"gpu_memory_utilization": 0.5}}'
``` ```
To experiment with the FlashInfer FP16 MoE path, set `VLLM_USE_FLASHINFER_MOE_FP16=1` before launching the server:
```bash
VLLM_USE_FLASHINFER_MOE_FP16=1 \
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
For the stage-based CLI, you usually do **not** need `--stage-overrides` for For the stage-based CLI, you usually do **not** need `--stage-overrides` for
that kind of change. Since each command launches one stage, just pass the knob that kind of change. Since each command launches one stage, just pass the knob
directly on that stage command: directly on that stage command:
@@ -195,6 +195,101 @@ stages:
devices: "0,1" devices: "0,1"
``` ```
### VAE Patch Parallelism
[VAE Patch Parallelism](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/parallelism/vae_patch_parallel.html) splits Bagel VAE **decode/encode** tiles across multiple GPUs on the **DiT stage**, reducing **per-GPU peak memory during VAE decode**. Use it when high-resolution `text2img` or `img2img` hits VAE OOM or large decode spikes.
**Bagel-specific notes:**
- Implemented in `BagelPipeline` via `DistributedAutoEncoder` (DiT stage only).
- **Single-stage** is the simplest path: one DiT process with TP + VAE patch parallel.
- **Two-stage**: enable on **stage 1 (DiT)** only; stage 0 (Thinker) keeps encoder-only `VAEEncoder` and does not use VAE patch parallel.
- You need a DiT `world_size` ≥ `vae_patch_parallel_size` (typically `tensor_parallel_size=2` on that stage). VAE PP reuses the DiT process group; it is not a standalone second-GPU VAE worker.
**Single-stage via deploy YAML** (recommended for `end2end.py`):
```yaml
pipeline: bagel_single_stage
async_chunk: false
stages:
- stage_id: 0
max_num_batched_tokens: 32768
max_num_seqs: 1
enforce_eager: true
trust_remote_code: true
enable_prefix_caching: false
devices: "0,1"
vae_use_tiling: true
parallel_config:
tensor_parallel_size: 2
vae_patch_parallel_size: 2
default_sampling_params:
seed: 52
```
```bash
cd examples/offline_inference/bagel
CUDA_VISIBLE_DEVICES=0,1 python end2end.py \
--model /path/to/BAGEL-7B-MoT \
--deploy-config /path/to/bagel_single_stage_vae_pp.yaml \
--modality text2img \
--prompts "A cute cat" \
--steps 10 \
--output ./out_vae_pp
```
**Single-stage via `Omni` kwargs** (same flags as online serving):
```python
from vllm_omni.entrypoints.omni import Omni
omni = Omni(
model="ByteDance-Seed/BAGEL-7B-MoT",
deploy_config="vllm_omni/deploy/bagel_single_stage.yaml",
tensor_parallel_size=2,
vae_patch_parallel_size=2,
vae_use_tiling=True,
)
# Then call omni.generate(...) as in end2end.py
```
**Two-stage (VAE PP on DiT only):**
```yaml
stages:
- stage_id: 0
devices: "0"
# AR Thinker — no vae_patch_parallel here
- stage_id: 1
devices: "0,1"
vae_use_tiling: true
parallel_config:
tensor_parallel_size: 2
vae_patch_parallel_size: 2
```
```bash
python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
--deploy-config /path/to/bagel_vae_pp.yaml \
--modality text2img \
--prompts "A cute cat"
```
**Startup log checks:**
```text
INFO ... vae_patch_parallel_size=2 requires vae_use_tiling; automatically enabling it.
```
| Setting | Role |
| :------ | :--- |
| `parallel_config.tensor_parallel_size` | DiT world size / TP (must be ≥ `vae_patch_parallel_size`) |
| `parallel_config.vae_patch_parallel_size` | Number of ranks for distributed VAE tiles (`1` = off) |
| `vae_use_tiling` | Enable spatial tiling (auto-enabled when `vae_patch_parallel_size > 1`) |
#### Hybrid Sharded Data Parallel (HSDP) #### Hybrid Sharded Data Parallel (HSDP)
For larger Bagel deployments on multiple GPUs, you can enable HSDP (Hybrid Sharded Data Parallel) by modifying the stage configuration (for example, [`bagel.yaml`](../../../vllm_omni/deploy/bagel.yaml)). HSDP shards transformer weights across GPUs to reduce per-GPU memory usage. For larger Bagel deployments on multiple GPUs, you can enable HSDP (Hybrid Sharded Data Parallel) by modifying the stage configuration (for example, [`bagel.yaml`](../../../vllm_omni/deploy/bagel.yaml)). HSDP shards transformer weights across GPUs to reduce per-GPU memory usage.
+2 -6
View File
@@ -121,9 +121,6 @@ def parse_args():
help="Temperature for text generation sampling (default: 0.3).", help="Temperature for text generation sampling (default: 0.3).",
) )
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
nullify_stage_engine_defaults(parser)
args = parser.parse_args() args = parser.parse_args()
return args return args
@@ -154,6 +151,7 @@ def main():
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
omni_kwargs = vars(args).copy() omni_kwargs = vars(args).copy()
omni_kwargs.pop("model", None)
deploy_config = args.deploy_config deploy_config = args.deploy_config
if args.think and deploy_config is None: if args.think and deploy_config is None:
deploy_config = "vllm_omni/deploy/bagel_think.yaml" deploy_config = "vllm_omni/deploy/bagel_think.yaml"
@@ -164,9 +162,7 @@ def main():
if args.quantization: if args.quantization:
omni_kwargs["quantization_config"] = args.quantization omni_kwargs["quantization_config"] = args.quantization
# Override CLI --model with the derived model_name. omni = Omni(model=model_name, **omni_kwargs)
omni_kwargs["model"] = model_name
omni = Omni(**omni_kwargs)
formatted_prompts = [] formatted_prompts = []
for p in prompts: for p in prompts:
@@ -18,8 +18,6 @@ import numpy as np
import torch import torch
from PIL import Image from PIL import Image
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
TASK_CHOICES = ("t2t", "t2i", "t2s", "i2i", "i2t", "s2t", "v2t") TASK_CHOICES = ("t2t", "t2i", "t2s", "i2i", "i2t", "s2t", "v2t")
TASK_DEFAULT_RUNTIME = { TASK_DEFAULT_RUNTIME = {
@@ -972,8 +970,6 @@ def parse_args(repo_root: Path) -> argparse.Namespace:
parser.add_argument("--vq-model-audio-local-files-only", action=argparse.BooleanOptionalAction, default=None) parser.add_argument("--vq-model-audio-local-files-only", action=argparse.BooleanOptionalAction, default=None)
parser.add_argument("--disable-hf-xet", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("--disable-hf-xet", action=argparse.BooleanOptionalAction, default=True)
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -107,9 +107,6 @@ def parse_args():
), ),
) )
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -19,7 +19,6 @@ from vllm import SamplingParams
from vllm.multimodal.image import convert_image_mode from vllm.multimodal.image import convert_image_mode
from vllm_omni import Omni from vllm_omni import Omni
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
DEFAULT_SYSTEM = "You are a helpful assistant." DEFAULT_SYSTEM = "You are a helpful assistant."
DEFAULT_QUESTION = "Please summarize the content of this image." DEFAULT_QUESTION = "Please summarize the content of this image."
@@ -49,7 +48,6 @@ def parse_args() -> argparse.Namespace:
action="store_true", action="store_true",
help="Enable diffusion pipeline profiler to display stage durations.", help="Enable diffusion pipeline profiler to display stage durations.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -29,7 +29,6 @@ from PIL import Image
from vllm.sampling_params import SamplingParams from vllm.sampling_params import SamplingParams
from vllm_omni import Omni from vllm_omni import Omni
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -118,7 +117,6 @@ def parse_args() -> argparse.Namespace:
) )
p.add_argument("--out", type=str, default="output.png", help="Path to save the generated image.") p.add_argument("--out", type=str, default="output.png", help="Path to save the generated image.")
p.add_argument("--trust-remote-code", action="store_true", help="Trust remote code when loading the model.") p.add_argument("--trust-remote-code", action="store_true", help="Trust remote code when loading the model.")
nullify_stage_engine_defaults(p)
args = p.parse_args() args = p.parse_args()
if not args.prompt: if not args.prompt:
args.prompt = ["A stylish woman with sunglasses riding a motorcycle in NYC."] args.prompt = ["A stylish woman with sunglasses riding a motorcycle in NYC."]
@@ -21,11 +21,10 @@ from message_convert import (
to_prompt, to_prompt,
) )
from vllm import SamplingParams from vllm import SamplingParams
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniTokensPrompt from vllm_omni.inputs.data import OmniTokensPrompt
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
SEED = 42 SEED = 42
@@ -343,7 +342,7 @@ def main(args):
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models") parser = TrackingArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument( parser.add_argument(
"--model-name", "--model-name",
"-m", "-m",
@@ -439,7 +438,6 @@ def parse_args():
"vllm_omni/deploy/mimo_audio.yaml based on the HF model_type.", "vllm_omni/deploy/mimo_audio.yaml based on the HF model_type.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -17,14 +17,13 @@ from vllm.assets.image import ImageAsset
from vllm.assets.video import VideoAsset, video_to_ndarrays from vllm.assets.video import VideoAsset, video_to_ndarrays
from vllm.multimodal.image import convert_image_mode from vllm.multimodal.image import convert_image_mode
from vllm.multimodal.media.audio import load_audio from vllm.multimodal.media.audio import load_audio
from vllm.utils.argparse_utils import FlexibleArgumentParser
import vllm_omni import vllm_omni
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
# Imports the processor also registers itself # Imports the processor also registers itself
from vllm_omni.transformers_utils.processors.ming import MingFlashOmniProcessor # noqa: F401 from vllm_omni.transformers_utils.processors.ming import MingFlashOmniProcessor # noqa: F401
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
SEED = 42 SEED = 42
MODEL_NAME = "Jonathan1909/Ming-flash-omni-2.0" MODEL_NAME = "Jonathan1909/Ming-flash-omni-2.0"
@@ -397,7 +396,7 @@ def main(args):
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Ming-flash-omni 2.0 offline inference example") parser = TrackingArgumentParser(description="Ming-flash-omni 2.0 offline inference example")
parser.add_argument( parser.add_argument(
"--query-type", "--query-type",
"-q", "-q",
@@ -496,7 +495,6 @@ def parse_args():
help="Output directory for results.", help="Output directory for results.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -19,10 +19,9 @@ from vllm.assets.video import VideoAsset, video_to_ndarrays
from vllm.multimodal.image import convert_image_mode from vllm.multimodal.image import convert_image_mode
from vllm.multimodal.media.audio import load_audio from vllm.multimodal.media.audio import load_audio
from vllm.sampling_params import SamplingParams from vllm.sampling_params import SamplingParams
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
SEED = 42 SEED = 42
@@ -432,7 +431,7 @@ def main(args):
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models") parser = TrackingArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument( parser.add_argument(
"--model", "--model",
type=str, type=str,
@@ -554,7 +553,6 @@ def parse_args():
default=False, default=False,
help="Use py_generator mode. The returned type of Omni.generate() is a Python Generator object.", help="Use py_generator mode. The returned type of Omni.generate() is a Python Generator object.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -19,10 +19,9 @@ from vllm.assets.image import ImageAsset
from vllm.assets.video import VideoAsset, video_to_ndarrays from vllm.assets.video import VideoAsset, video_to_ndarrays
from vllm.multimodal.image import convert_image_mode from vllm.multimodal.image import convert_image_mode
from vllm.multimodal.media.audio import load_audio from vllm.multimodal.media.audio import load_audio
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
SEED = 42 SEED = 42
@@ -422,7 +421,7 @@ def main(args):
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models") parser = TrackingArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument( parser.add_argument(
"--model", "--model",
type=str, type=str,
@@ -561,7 +560,6 @@ def parse_args():
help="Model dtype (auto, half, float16, bfloat16, float, float32).", help="Model dtype (auto, half, float16, bfloat16, float, float32).",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -39,10 +39,9 @@ from vllm.assets.image import ImageAsset
from vllm.assets.video import VideoAsset, video_to_ndarrays from vllm.assets.video import VideoAsset, video_to_ndarrays
from vllm.multimodal.image import convert_image_mode from vllm.multimodal.image import convert_image_mode
from vllm.multimodal.media.audio import load_audio from vllm.multimodal.media.audio import load_audio
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.async_omni import AsyncOmni from vllm_omni.entrypoints.async_omni import AsyncOmni
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -383,7 +382,9 @@ async def run_all(args):
print(f"[Info] Creating AsyncOmni with deploy_config={args.deploy_config}") print(f"[Info] Creating AsyncOmni with deploy_config={args.deploy_config}")
async_omni = None async_omni = None
try: try:
async_omni = AsyncOmni(**vars(args)) # ``from_cli_args`` forwards only explicitly-passed CLI args so
# argparse defaults do not silently override deploy YAML values.
async_omni = AsyncOmni.from_cli_args(args, model=args.model)
# Use default sampling params from stage config (they are pre-configured # Use default sampling params from stage config (they are pre-configured
# in the YAML for each stage). # in the YAML for each stage).
@@ -453,7 +454,7 @@ async def run_all(args):
def parse_args(): def parse_args():
parser = FlexibleArgumentParser( parser = TrackingArgumentParser(
description=( description=(
"Offline inference with async_chunk enabled via AsyncOmni. " "Offline inference with async_chunk enabled via AsyncOmni. "
"Downstream stages start before upstream stages finish, " "Downstream stages start before upstream stages finish, "
@@ -589,7 +590,6 @@ def parse_args():
default=16000, default=16000,
help="Sampling rate for audio loading.", help="Sampling rate for audio loading.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -29,7 +29,6 @@ import os
from PIL import Image from PIL import Image
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.inputs.data import OmniDiffusionSamplingParams
@@ -168,7 +167,6 @@ def parse_args():
help="Enable cache-dit summary logging after diffusion forward passes.", help="Enable cache-dit summary logging after diffusion forward passes.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -307,9 +307,6 @@ def parse_args() -> argparse.Namespace:
help="Supplementary auxiliary text encoder parameters model name or path (especially for Hidream-l1-full).", help="Supplementary auxiliary text encoder parameters model name or path (especially for Hidream-l1-full).",
) )
current_omni_platform.pre_register_and_update(parser) current_omni_platform.pre_register_and_update(parser)
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -10,7 +10,6 @@ import soundfile as sf
from vllm import SamplingParams from vllm import SamplingParams
from vllm.multimodal.media.audio import load_audio from vllm.multimodal.media.audio import load_audio
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
from vllm_omni.model_executor.models.cosyvoice3.tokenizer import get_qwen_tokenizer from vllm_omni.model_executor.models.cosyvoice3.tokenizer import get_qwen_tokenizer
from vllm_omni.model_executor.models.cosyvoice3.utils import extract_text_token from vllm_omni.model_executor.models.cosyvoice3.utils import extract_text_token
@@ -65,7 +64,6 @@ def run_e2e():
required=True, required=True,
help="Path to tokenizer directory (e.g., <model_path>/CosyVoice-BlankEN).", help="Path to tokenizer directory (e.g., <model_path>/CosyVoice-BlankEN).",
) )
nullify_stage_engine_defaults(parser)
args = parser.parse_args() args = parser.parse_args()
# Ensure tokenizer directory exists # Ensure tokenizer directory exists
if not os.path.exists(args.tokenizer): if not os.path.exists(args.tokenizer):
@@ -26,16 +26,14 @@ import torch
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni import AsyncOmni, Omni from vllm_omni import AsyncOmni, Omni
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.model_executor.models.fish_speech.dac_utils import DAC_HOP_LENGTH, DAC_SAMPLE_RATE from vllm_omni.model_executor.models.fish_speech.dac_utils import DAC_HOP_LENGTH, DAC_SAMPLE_RATE
from vllm_omni.model_executor.models.fish_speech.prompt_utils import ( from vllm_omni.model_executor.models.fish_speech.prompt_utils import (
build_fish_text_only_prompt_ids, build_fish_text_only_prompt_ids,
estimate_fish_voice_clone_prompt_len_from_normalized, estimate_fish_voice_clone_prompt_len_from_normalized,
normalize_fish_voice_clone_texts, normalize_fish_voice_clone_texts,
) )
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -211,7 +209,7 @@ async def main_streaming(args):
def parse_args(): def parse_args():
parser = FlexibleArgumentParser( parser = TrackingArgumentParser(
description="Fish Speech S2 Pro offline inference with vLLM Omni", description="Fish Speech S2 Pro offline inference with vLLM Omni",
) )
parser.add_argument( parser.add_argument(
@@ -267,7 +265,6 @@ def parse_args():
default=False, default=False,
help="Stream audio chunks as they arrive via AsyncOmni.", help="Stream audio chunks as they arrive via AsyncOmni.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -30,10 +30,9 @@ from pathlib import Path
import numpy as np import numpy as np
import soundfile as sf import soundfile as sf
import torch import torch
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni import Omni from vllm_omni import Omni
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults from vllm_omni.utils.tracking_parser import TrackingArgumentParser
SAMPLE_RATE = 24_000 SAMPLE_RATE = 24_000
DEFAULT_TEXTS = ( DEFAULT_TEXTS = (
@@ -43,7 +42,7 @@ DEFAULT_TEXTS = (
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Offline higgs-audio v2 inference") parser = TrackingArgumentParser(description="Offline higgs-audio v2 inference")
parser.add_argument( parser.add_argument(
"--model", "--model",
type=str, type=str,
@@ -88,7 +87,6 @@ def parse_args():
default=None, default=None,
help="Transcript of the reference clip. Required when --ref-audio is set.", help="Transcript of the reference clip. Required when --ref-audio is set.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -5,11 +5,11 @@ from typing import Any
import soundfile as sf import soundfile as sf
import torch import torch
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniTokensPrompt from vllm_omni.inputs.data import OmniTokensPrompt
from vllm_omni.model_executor.models.ming_flash_omni.prompt_utils import ( from vllm_omni.model_executor.models.ming_flash_omni.prompt_utils import (
@@ -68,7 +68,7 @@ def save_audio(mm: dict[str, Any], output_path: str) -> None:
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Ming-flash-omni standalone talker offline e2e example") parser = TrackingArgumentParser(description="Ming-flash-omni standalone talker offline e2e example")
parser.add_argument("--model", type=str, default=MODEL_NAME, help="Model name or local path.") parser.add_argument("--model", type=str, default=MODEL_NAME, help="Model name or local path.")
parser.add_argument( parser.add_argument(
"--deploy-config", "--deploy-config",
@@ -89,7 +89,6 @@ def parse_args():
parser.add_argument("--init-timeout", type=int, default=600, help="Engine init timeout in seconds.") parser.add_argument("--init-timeout", type=int, default=600, help="Engine init timeout in seconds.")
parser.add_argument("--stage-init-timeout", type=int, default=300, help="Single stage init timeout in seconds.") parser.add_argument("--stage-init-timeout", type=int, default=300, help="Single stage init timeout in seconds.")
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -38,7 +38,8 @@ from pathlib import Path
import soundfile as sf import soundfile as sf
import torch import torch
from vllm import SamplingParams from vllm import SamplingParams
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
# Prevent multiprocessing from re-importing CUDA in the wrong context. # Prevent multiprocessing from re-importing CUDA in the wrong context.
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
@@ -155,7 +156,7 @@ def main(args) -> None:
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="MOSS-TTS-Nano offline inference") parser = TrackingArgumentParser(description="MOSS-TTS-Nano offline inference")
parser.add_argument("--text", default="Hello, this is MOSS-TTS-Nano speaking.", help="Text to synthesize.") parser.add_argument("--text", default="Hello, this is MOSS-TTS-Nano speaking.", help="Text to synthesize.")
parser.add_argument( parser.add_argument(
"--ref-audio", "--ref-audio",
@@ -21,7 +21,6 @@ import os
import numpy as np import numpy as np
import soundfile as sf import soundfile as sf
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.inputs.data import OmniDiffusionSamplingParams
@@ -86,7 +85,6 @@ def run_e2e():
default=None, default=None,
help="Random seed for generation", help="Random seed for generation",
) )
nullify_stage_engine_defaults(parser)
args = parser.parse_args() args = parser.parse_args()
if not os.path.exists(args.stage_config): if not os.path.exists(args.stage_config):
@@ -15,10 +15,8 @@ import torch
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni import AsyncOmni, Omni from vllm_omni import AsyncOmni, Omni
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults from vllm_omni.utils.tracking_parser import TrackingArgumentParser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -435,7 +433,7 @@ async def main_streaming(args):
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models") parser = TrackingArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument( parser.add_argument(
"--query-type", "--query-type",
"-q", "-q",
@@ -548,7 +546,6 @@ def parse_args():
help="Number of prompts per batch (default: 1, sequential).", help="Number of prompts per batch (default: 1, sequential).",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -12,17 +12,16 @@ from pathlib import Path
import soundfile as sf import soundfile as sf
import torch import torch
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni import Omni from vllm_omni import Omni
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults from vllm_omni.utils.tracking_parser import TrackingArgumentParser
REPO_ROOT = Path(__file__).resolve().parents[4] REPO_ROOT = Path(__file__).resolve().parents[4]
SAMPLE_RATE = 48_000 SAMPLE_RATE = 48_000
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Offline VoxCPM2 native AR inference") parser = TrackingArgumentParser(description="Offline VoxCPM2 native AR inference")
parser.add_argument( parser.add_argument(
"--model", "--model",
type=str, type=str,
@@ -60,7 +59,6 @@ def parse_args():
default=None, default=None,
help="Optional transcript of --ref-audio (enables continuation mode).", help="Optional transcript of --ref-audio (enables continuation mode).",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
@@ -27,11 +27,10 @@ except ImportError:
"pip install -e ." "pip install -e ."
) )
from vllm import SamplingParams from vllm import SamplingParams
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni import AsyncOmni from vllm_omni import AsyncOmni
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni from vllm_omni.entrypoints.omni import Omni
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -230,7 +229,7 @@ def run_non_streaming(inputs, sampling_params_list, model_name, args, output_dir
def parse_args() -> Namespace: def parse_args() -> Namespace:
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with Voxtral TTS") parser = TrackingArgumentParser(description="Demo on using vLLM for offline inference with Voxtral TTS")
parser.add_argument( parser.add_argument(
"--model", "--model",
type=str, type=str,
@@ -313,7 +312,6 @@ def parse_args() -> Namespace:
default=None, default=None,
help="Quantization method (e.g. 'fp8'). Applied to the language model only.", help="Quantization method (e.g. 'fp8'). Applied to the language model only.",
) )
nullify_stage_engine_defaults(parser)
return parser.parse_args() return parser.parse_args()
+63
View File
@@ -68,6 +68,67 @@ vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 --tensor-parallel-size
Or set `tensor_parallel_size` per stage in a custom deploy YAML. Or set `tensor_parallel_size` per stage in a custom deploy YAML.
### VAE Patch Parallelism
[VAE Patch Parallelism](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/parallelism/vae_patch_parallel.html) distributes Bagel VAE **decode/encode** across multiple GPUs by splitting latent tiles. It lowers **per-GPU peak memory during VAE decode**, which helps high-resolution `text2img` / `img2img` when VAE becomes a bottleneck.
**Scope for Bagel:**
| Topology | VAE patch parallel |
| :------- | :----------------- |
| **Single-stage** (DiT only) | Supported on stage 0 (`BagelPipeline` + `DistributedAutoEncoder`) |
| **Two-stage** | Supported on **stage 1 (DiT)** only; stage 0 (Thinker) uses encoder-only VAE and is unrelated |
**Requirements:**
- `vae_patch_parallel_size > 1` and a distributed VAE (`DistributedAutoEncoder` on the DiT pipeline).
- The DiT process group must have at least `vae_patch_parallel_size` ranks. In practice this means the diffusion stage `world_size` must be ≥ 2 (commonly `tensor_parallel_size=2` on that stage).
- `vae_use_tiling` must be enabled. If you set `vae_patch_parallel_size > 1` and omit tiling, the registry auto-enables `vae_use_tiling` at startup.
VAE patch parallel **reuses the DiT process group** (`dit_group`); it does not create a separate VAE-only worker pool. It is not a substitute for single-GPU VAE tiling (`vae_pp=1`).
**Online serving (single-stage, 2 GPUs):**
```bash
CUDA_VISIBLE_DEVICES=0,1 vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 \
--deploy-config vllm_omni/deploy/bagel_single_stage.yaml \
--tensor-parallel-size 2 \
--vae-patch-parallel-size 2 \
--vae-use-tiling
```
**Online serving (two-stage, VAE PP on DiT stage 1):** use a custom deploy YAML, for example:
```yaml
stages:
- stage_id: 0
devices: "0"
# Thinker (AR) — no VAE patch parallel here
- stage_id: 1
devices: "0,1"
vae_use_tiling: true
parallel_config:
tensor_parallel_size: 2
vae_patch_parallel_size: 2
```
```bash
vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 \
--deploy-config /path/to/bagel_vae_pp.yaml
```
**Verify it is active** (check server logs at startup):
```text
INFO ... vae_patch_parallel_size=2 requires vae_use_tiling; automatically enabling it.
```
| CLI flag | Default | Description |
| :------- | :------ | :---------- |
| `--vae-patch-parallel-size` | `1` | Number of DiT ranks used for VAE tile parallelism. Set to `2` or higher to enable. Should be ≤ DiT process group size (typically match `--tensor-parallel-size` on the diffusion stage). |
| `--vae-use-tiling` | off | Enable VAE spatial tiling. Required for VAE patch parallel (auto-enabled when `vae_patch_parallel_size > 1`). |
#### Hybrid Sharded Data Parallel (HSDP) #### Hybrid Sharded Data Parallel (HSDP)
For larger Bagel deployments on multiple GPUs, you can enable HSDP (Hybrid Sharded Data Parallel) by modifying the stage configuration (for example, [`bagel.yaml`](../../../vllm_omni/deploy/bagel.yaml)). HSDP shards transformer weights across GPUs to reduce per-GPU memory usage. For larger Bagel deployments on multiple GPUs, you can enable HSDP (Hybrid Sharded Data Parallel) by modifying the stage configuration (for example, [`bagel.yaml`](../../../vllm_omni/deploy/bagel.yaml)). HSDP shards transformer weights across GPUs to reduce per-GPU memory usage.
@@ -323,6 +384,8 @@ python openai_chat_client.py \
| `stages[].gpu_memory_utilization` | per-stage | Fraction of GPU memory to use | | `stages[].gpu_memory_utilization` | per-stage | Fraction of GPU memory to use |
| `stages[].enforce_eager` | per-stage | Disable CUDA graphs | | `stages[].enforce_eager` | per-stage | Disable CUDA graphs |
| `stages[].tensor_parallel_size` | per-stage | TP degree for this stage | | `stages[].tensor_parallel_size` | per-stage | TP degree for this stage |
| `stages[].parallel_config.vae_patch_parallel_size` | per-stage (DiT) | VAE tile parallelism degree (DiT stage only) |
| `stages[].vae_use_tiling` | per-stage (DiT) | Enable VAE tiling (required for VAE patch parallel) |
| `connectors` | top-level | Define available connector instances (SHM, Mooncake) | | `connectors` | top-level | Define available connector instances (SHM, Mooncake) |
| `platforms` | top-level | Platform-specific overrides (e.g. `xpu`) | | `platforms` | top-level | Platform-specific overrides (e.g. `xpu`) |
@@ -12,7 +12,8 @@ from typing import Any
import requests import requests
from openai import OpenAI from openai import OpenAI
from vllm.assets.audio import AudioAsset from vllm.assets.audio import AudioAsset
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
# Modify OpenAI's API key and API base to use vLLM's API server. # Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "EMPTY" openai_api_key = "EMPTY"
@@ -493,7 +494,7 @@ def run_multimodal_generation(args) -> None:
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models") parser = TrackingArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument( parser.add_argument(
"--query-type", "--query-type",
"-q", "-q",
@@ -6,7 +6,8 @@ from typing import NamedTuple
import requests import requests
from openai import OpenAI from openai import OpenAI
from vllm.assets.audio import AudioAsset from vllm.assets.audio import AudioAsset
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
SEED = 42 SEED = 42
@@ -478,7 +479,7 @@ def run_multimodal_generation(args, client: OpenAI) -> None:
def parse_args(): def parse_args():
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models") parser = TrackingArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument( parser.add_argument(
"--query-type", "--query-type",
"-q", "-q",
+2 -2
View File
@@ -30,12 +30,12 @@ from PIL import Image
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from vllm import SamplingParams from vllm import SamplingParams
from vllm.outputs import CompletionOutput, RequestOutput from vllm.outputs import CompletionOutput, RequestOutput
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.entrypoints.async_omni import AsyncOmni as RealAsyncOmni from vllm_omni.entrypoints.async_omni import AsyncOmni as RealAsyncOmni
from vllm_omni.entrypoints.cli.serve import OmniServeCommand from vllm_omni.entrypoints.cli.serve import OmniServeCommand
from vllm_omni.inputs.data import OmniSamplingParams from vllm_omni.inputs.data import OmniSamplingParams
from vllm_omni.outputs import OmniRequestOutput from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -444,7 +444,7 @@ def mock_async_omni(
@pytest.fixture @pytest.fixture
def api_server(unused_tcp_port_factory, server_case: ServerCase, mock_async_omni): def api_server(unused_tcp_port_factory, server_case: ServerCase, mock_async_omni):
"""Set up a API server in background process from command line with parametrized model name and mocked AsyncOmni.""" """Set up a API server in background process from command line with parametrized model name and mocked AsyncOmni."""
parser = FlexibleArgumentParser() parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="command") subparsers = parser.add_subparsers(dest="command")
cmd = OmniServeCommand() cmd = OmniServeCommand()
cmd.subparser_init(subparsers) cmd.subparser_init(subparsers)
@@ -9,6 +9,8 @@ Coverage:
- Ulysses-SP - Ulysses-SP
- Ring-Attention - Ring-Attention
- Layerwise Offloading - Layerwise Offloading
- Hybrid Sharded Data Parallel
- Tensor Parallelism + VAE Patch Parallelism
assert_diffusion_response validates successful generation and the expected assert_diffusion_response validates successful generation and the expected
512x512 resolution. 512x512 resolution.
@@ -38,12 +40,28 @@ BAGEL_HSDP_2_DEPLOY = modify_stage_config(
BAGEL_CI_DEPLOY, BAGEL_CI_DEPLOY,
updates={"stages": {0: {"devices": "0"}, 1: {"devices": "0,1"}}}, updates={"stages": {0: {"devices": "0"}, 1: {"devices": "0,1"}}},
) )
BAGEL_TP_VAE_PP_2_DEPLOY = modify_stage_config(
BAGEL_CI_DEPLOY,
updates={
"stages": {
0: {"devices": "0"},
1: {
"devices": "0,1",
"parallel_config": {
"tensor_parallel_size": 2,
"vae_patch_parallel_size": 2,
},
},
},
},
)
def _get_diffusion_feature_cases(model: str): def _get_diffusion_feature_cases(model: str):
"""Return L4 diffusion feature cases for Bagel. """Return L4 diffusion feature cases for Bagel.
TeaCache, Cache-DiT, CFG-Parallel, TeaCache, Cache-DiT, CFG-Parallel,
Ulysses-SP, Ring-Attention, Layerwise Offloading. Ulysses-SP, Ring-Attention, Layerwise Offloading,
Hybrid Sharded Data Parallel, Tensor Parallelism, VAE Patch Parallelism.
""" """
return [ return [
@@ -135,6 +153,15 @@ def _get_diffusion_feature_cases(model: str):
id="parallel_hsdp_2", id="parallel_hsdp_2",
marks=HSDP_2_FEATURE_MARKS, marks=HSDP_2_FEATURE_MARKS,
), ),
# Tensor Parallelism (TP) + VAE Patch Parallelism (size=2)
pytest.param(
OmniServerParams(
model=model,
stage_config_path=BAGEL_TP_VAE_PP_2_DEPLOY,
),
id="tp_vae_patch_parallel_2",
marks=PARALLEL_2_FEATURE_MARKS,
),
] ]
@@ -157,6 +184,7 @@ def test_bagel(
- Ring-Attention (degree=2) - Ring-Attention (degree=2)
- Layerwise Offloading - Layerwise Offloading
- Hybrid Sharded Data Parallel (size=2) - Hybrid Sharded Data Parallel (size=2)
- Tensor Parallelism (TP) + VAE Patch Parallelism (size=2)
Validation is delegated to assert_diffusion_response in tests/helpers/assertions.py, Validation is delegated to assert_diffusion_response in tests/helpers/assertions.py,
which checks output dimensions and basic correctness. which checks output dimensions and basic correctness.
@@ -640,38 +640,6 @@ def test_initialize_llm_replica_passes_stage_init_timeout_to_complete_stage_hand
assert captured_timeout == 302 assert captured_timeout == 302
def test_async_omni_engine_reads_tokenizer_from_engine_args(monkeypatch):
import vllm_omni.engine.async_omni_engine as engine_mod
class DummyThread:
def __init__(self, *args, **kwargs):
pass
def start(self):
pass
def is_alive(self):
return False
fake_engine_args = types.SimpleNamespace(
_explicit_fields=frozenset({"tokenizer"}),
explicit_kwargs=lambda: {"tokenizer": "/tokenizer/from-engine-args"},
)
monkeypatch.setattr(engine_mod.threading, "Thread", DummyThread)
monkeypatch.setattr(
AsyncOmniEngine,
"_resolve_stage_configs",
lambda self, model, kwargs: ("dummy-config", [types.SimpleNamespace(engine_args={})]),
)
monkeypatch.setattr(AsyncOmniEngine, "_wait_for_orchestrator_init", lambda *_, **__: None)
engine = AsyncOmniEngine("dummy-model", engine_args=fake_engine_args)
assert engine.tokenizer == "/tokenizer/from-engine-args"
engine.shutdown()
def test_build_engine_args_cli_tokenizer_overrides_inferred_base_tokenizer(tmp_path): def test_build_engine_args_cli_tokenizer_overrides_inferred_base_tokenizer(tmp_path):
from vllm_omni.engine.stage_init_utils import build_engine_args_dict from vllm_omni.engine.stage_init_utils import build_engine_args_dict
-33
View File
@@ -403,39 +403,6 @@ class TestSingleStageModeDetection:
assert engine.single_stage_mode is True assert engine.single_stage_mode is True
assert engine._single_stage_id_filter is None assert engine._single_stage_id_filter is None
def test_engine_args_create_only_forwards_explicit_fields(self, mocker: MockerFixture):
from vllm_omni.engine.arg_utils import OmniEngineArgs
captured: dict[str, Any] = {}
def fake_resolve(self, model: str, kwargs: dict[str, Any]):
captured.update(kwargs)
return "/fake/path", [_make_stage_cfg(0)]
mocker.patch.object(AsyncOmniEngine, "_resolve_stage_configs", fake_resolve)
mocker.patch.object(AsyncOmniEngine, "_bootstrap_orchestrator")
mock_thread_cls = mocker.patch("threading.Thread")
mock_future_cls = mocker.patch("concurrent.futures.Future")
mock_future = mocker.Mock()
mock_future.result.return_value = mocker.Mock()
mock_future_cls.return_value = mock_future
mock_thread = mocker.Mock()
mock_thread.is_alive.return_value = False
mock_thread_cls.return_value = mock_thread
ea = OmniEngineArgs.create(model="ignored", gpu_memory_utilization=0.5)
AsyncOmniEngine(model="fake-model", engine_args=ea)
assert captured["gpu_memory_utilization"] == 0.5
assert "model" not in captured
assert "max_num_seqs" not in captured
def test_bare_engine_args_rejected(self, mocker: MockerFixture):
from vllm_omni.engine.arg_utils import OmniEngineArgs
with pytest.raises(TypeError, match="OmniEngineArgs.create"):
self._make_engine_no_thread(mocker, engine_args=OmniEngineArgs(model="fake-model"))
def test_master_address_and_port_stored(self, mocker: MockerFixture): def test_master_address_and_port_stored(self, mocker: MockerFixture):
engine = self._make_engine_no_thread( engine = self._make_engine_no_thread(
mocker, mocker,
@@ -4,12 +4,11 @@
from types import SimpleNamespace from types import SimpleNamespace
import pytest import pytest
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.config.stage_config import deploy_override_field_names
from vllm_omni.diffusion.data import AttentionConfig from vllm_omni.diffusion.data import AttentionConfig
from vllm_omni.engine.async_omni_engine import AsyncOmniEngine from vllm_omni.engine.async_omni_engine import AsyncOmniEngine
from vllm_omni.entrypoints.cli.serve import OmniServeCommand, _create_default_diffusion_stage_cfg from vllm_omni.entrypoints.cli.serve import OmniServeCommand
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -34,15 +33,6 @@ def test_default_stage_config_includes_cache_backend():
assert engine_args["model_stage"] == "diffusion" assert engine_args["model_stage"] == "diffusion"
def test_default_stage_config_ignores_none_deploy_overrides():
"""Ensure nullified deploy override defaults do not alter diffusion defaults."""
baseline = AsyncOmniEngine._create_default_diffusion_stage_cfg({})[0]
nullified_overrides = {name: None for name in deploy_override_field_names()}
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(nullified_overrides)[0]
assert stage_cfg == baseline
def test_default_cache_config_used_when_missing(): def test_default_cache_config_used_when_missing():
"""Ensure default cache_config is synthesized when only backend is given.""" """Ensure default cache_config is synthesized when only backend is given."""
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg( stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(
@@ -221,7 +211,7 @@ def test_default_stage_config_whitelist_none_fallback():
def test_serve_cli_accepts_ulysses_mode(): def test_serve_cli_accepts_ulysses_mode():
"""Ensure diffusion serve CLI exposes ulysses_mode and wires it to parallel_config.""" """Ensure diffusion serve CLI exposes ulysses_mode and wires it to parallel_config."""
parser = FlexibleArgumentParser() parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="command") subparsers = parser.add_subparsers(dest="command")
OmniServeCommand().subparser_init(subparsers) OmniServeCommand().subparser_init(subparsers)
@@ -237,7 +227,8 @@ def test_serve_cli_accepts_ulysses_mode():
] ]
) )
stage_cfg = _create_default_diffusion_stage_cfg(args)[0] explicit_kwargs = args.get_explicit_kwargs_dict()
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(explicit_kwargs)[0]
parallel_config = stage_cfg["engine_args"]["parallel_config"] parallel_config = stage_cfg["engine_args"]["parallel_config"]
assert args.ulysses_mode == "advanced_uaa" assert args.ulysses_mode == "advanced_uaa"
@@ -247,7 +238,7 @@ def test_serve_cli_accepts_ulysses_mode():
def test_serve_cli_accepts_diffusion_pipeline_profiler_flag(): def test_serve_cli_accepts_diffusion_pipeline_profiler_flag():
"""Ensure diffusion serve CLI exposes the profiler switch.""" """Ensure diffusion serve CLI exposes the profiler switch."""
parser = FlexibleArgumentParser() parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="command") subparsers = parser.add_subparsers(dest="command")
OmniServeCommand().subparser_init(subparsers) OmniServeCommand().subparser_init(subparsers)
@@ -260,7 +251,8 @@ def test_serve_cli_accepts_diffusion_pipeline_profiler_flag():
] ]
) )
stage_cfg = _create_default_diffusion_stage_cfg(args)[0] explicit_kwargs = args.get_explicit_kwargs_dict()
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(explicit_kwargs)[0]
assert args.enable_diffusion_pipeline_profiler is True assert args.enable_diffusion_pipeline_profiler is True
assert stage_cfg["engine_args"]["enable_diffusion_pipeline_profiler"] is True assert stage_cfg["engine_args"]["enable_diffusion_pipeline_profiler"] is True
@@ -268,7 +260,7 @@ def test_serve_cli_accepts_diffusion_pipeline_profiler_flag():
def test_serve_cli_accepts_diffusion_attention_backend(): def test_serve_cli_accepts_diffusion_attention_backend():
"""Ensure diffusion serve CLI exposes the shorthand backend flag.""" """Ensure diffusion serve CLI exposes the shorthand backend flag."""
parser = FlexibleArgumentParser() parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="command") subparsers = parser.add_subparsers(dest="command")
OmniServeCommand().subparser_init(subparsers) OmniServeCommand().subparser_init(subparsers)
@@ -282,7 +274,8 @@ def test_serve_cli_accepts_diffusion_attention_backend():
] ]
) )
stage_cfg = _create_default_diffusion_stage_cfg(args)[0] explicit_kwargs = args.get_explicit_kwargs_dict()
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(explicit_kwargs)[0]
diffusion_attention_config = stage_cfg["engine_args"]["diffusion_attention_config"] diffusion_attention_config = stage_cfg["engine_args"]["diffusion_attention_config"]
assert args.diffusion_attention_backend == "FLASH_ATTN" assert args.diffusion_attention_backend == "FLASH_ATTN"
@@ -293,7 +286,7 @@ def test_serve_cli_accepts_diffusion_attention_backend():
def test_serve_cli_accepts_additional_config(): def test_serve_cli_accepts_additional_config():
"""Ensure diffusion serve CLI exposes additional_config and forwards it to stage config.""" """Ensure diffusion serve CLI exposes additional_config and forwards it to stage config."""
parser = FlexibleArgumentParser() parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="command") subparsers = parser.add_subparsers(dest="command")
OmniServeCommand().subparser_init(subparsers) OmniServeCommand().subparser_init(subparsers)
@@ -307,7 +300,8 @@ def test_serve_cli_accepts_additional_config():
] ]
) )
stage_cfg = _create_default_diffusion_stage_cfg(args)[0] stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(vars(args))[0]
engine_args = stage_cfg["engine_args"] engine_args = stage_cfg["engine_args"]
assert args.additional_config == {"torchair_graph_config": {"enabled": True}} assert args.additional_config == {"torchair_graph_config": {"enabled": True}}
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import argparse
import queue import queue
from collections.abc import Callable from collections.abc import Callable
from types import SimpleNamespace from types import SimpleNamespace
@@ -176,83 +175,6 @@ def _patch_engine(monkeypatch: pytest.MonkeyPatch, engine: FakeAsyncOmniEngine)
) )
def test_direct_omni_with_nullified_parser_only_nulls_untyped_override_fields(
monkeypatch: pytest.MonkeyPatch,
):
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.omni import Omni
captured: dict[str, Any] = {}
def fake_engine(*args: Any, **kwargs: Any) -> FakeAsyncOmniEngine:
captured.update(kwargs)
return FakeAsyncOmniEngine()
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.AsyncOmniEngine", fake_engine)
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.omni_snapshot_download", lambda model: model)
parser = argparse.ArgumentParser()
parser.add_argument("--gpu-memory-utilization", type=float, default=0.9)
parser.add_argument("--batch-timeout", type=int, default=10)
nullify_stage_engine_defaults(parser)
args = parser.parse_args([])
args.model = "fake-model"
Omni(**vars(args))
assert captured["gpu_memory_utilization"] is None
assert captured["batch_timeout"] == 10
assert "_cli_explicit_keys" not in captured
def test_from_cli_args_warns_and_forwards_without_internal_keys(
monkeypatch: pytest.MonkeyPatch,
):
captured: dict[str, Any] = {}
def fake_engine(*args: Any, **kwargs: Any) -> FakeAsyncOmniEngine:
captured.update(kwargs)
return FakeAsyncOmniEngine()
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.AsyncOmniEngine", fake_engine)
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.omni_snapshot_download", lambda model: model)
args = argparse.Namespace(model="fake-model", gpu_memory_utilization=0.9, _cli_explicit_keys={"model"})
with pytest.deprecated_call(match="from_cli_args"):
Omni.from_cli_args(args)
assert captured["gpu_memory_utilization"] == 0.9
assert "_cli_explicit_keys" not in captured
def test_deprecated_from_cli_args_preserves_legacy_parser_nulling(
monkeypatch: pytest.MonkeyPatch,
):
from vllm_omni.entrypoints.omni import Omni
captured: dict[str, Any] = {}
def fake_engine(*args: Any, **kwargs: Any) -> FakeAsyncOmniEngine:
captured.update(kwargs)
return FakeAsyncOmniEngine()
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.AsyncOmniEngine", fake_engine)
monkeypatch.setattr("vllm_omni.entrypoints.omni_base.omni_snapshot_download", lambda model: model)
monkeypatch.setattr("sys.argv", ["prog"])
parser = argparse.ArgumentParser()
parser.add_argument("--gpu-memory-utilization", type=float, default=0.9)
parser.add_argument("--batch-timeout", type=int, default=10)
args = parser.parse_args([])
args.model = "fake-model"
with pytest.deprecated_call(match="from_cli_args"):
Omni.from_cli_args(args, parser=parser)
assert captured["gpu_memory_utilization"] is None
assert captured["batch_timeout"] == 10
def _make_base(): def _make_base():
from vllm_omni.entrypoints.omni_base import OmniBase from vllm_omni.entrypoints.omni_base import OmniBase
+32 -47
View File
@@ -4,65 +4,56 @@ from __future__ import annotations
import argparse import argparse
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from vllm_omni.entrypoints.cli.serve import OmniServeCommand, run_headless from vllm_omni.entrypoints.cli.serve import OmniServeCommand, run_headless
from vllm_omni.utils.tracking_parser import TrackingArgumentParser, TrackingNamespace
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_serve_parser_accepts_no_async_chunk() -> None: def test_serve_parser_accepts_no_async_chunk_and_marks_it_explicit() -> None:
"""``--no-async-chunk`` should parse after deploy-overriding parser """``--no-async-chunk`` should parse to ``async_chunk=False`` and mark the
defaults are nullified.""" shared deploy-level dest as explicitly provided by the user."""
try: parser = TrackingArgumentParser()
from vllm.utils.argparse_utils import FlexibleArgumentParser subparsers = parser.add_subparsers(dest="subcommand")
except Exception as exc:
pytest.skip(f"Cannot build parser in this environment: {exc}")
root = FlexibleArgumentParser()
subparsers = root.add_subparsers(dest="subcommand")
cmd = OmniServeCommand() cmd = OmniServeCommand()
cmd.subparser_init(subparsers) cmd.subparser_init(subparsers)
argv = ["serve", "fake-model", "--omni", "--no-async-chunk"] argv = ["serve", "fake-model", "--omni", "--no-async-chunk"]
args = root.parse_args(argv) args = parser.parse_args(argv)
assert args.async_chunk is False assert args.async_chunk is False
explicit = args.get_explicit_kwargs_dict()
# --------------------------------------------------------------------------- assert args.get_explicit_kwargs_dict()
# run_headless validation assert not explicit["async_chunk"]
# ---------------------------------------------------------------------------
def _make_headless_args(**overrides: Any) -> argparse.Namespace: def _make_headless_args(**kwargs) -> TrackingNamespace:
"""Build an argparse.Namespace shaped like the headless CLI passes in. defaults = {
"model": "fake-model",
Defaults pass every validation gate so individual tests can mutate just "stage_id": 0,
the field they're exercising. "replica_id": 0,
""" "omni_master_address": "127.0.0.1",
defaults = dict( "omni_master_port": 26000,
model="fake-model", "omni_replica_address": None,
stage_id=0, "omni_dp_size_local": 1,
replica_id=0, "worker_backend": "multi_process",
omni_master_address="127.0.0.1", "stage_configs_path": None,
omni_master_port=26000, "deploy_config": None,
omni_replica_address=None, "log_stats": False,
omni_dp_size_local=1, "disable_log_stats": False,
api_server_count=0, "stage_init_timeout": 600,
worker_backend="multi_process", "tokenizer": None,
stage_configs_path=None, }
deploy_config=None, ns_kwargs = {**defaults, **kwargs}
log_stats=False, ns = argparse.Namespace(**ns_kwargs)
disable_log_stats=False, return TrackingNamespace(
stage_init_timeout=600, unfiltered_ns=ns,
tokenizer=None, explicit_keys=frozenset(ns.__dict__.keys()),
) )
defaults.update(overrides)
return argparse.Namespace(**defaults)
def test_run_headless_requires_stage_id() -> None: def test_run_headless_requires_stage_id() -> None:
@@ -83,12 +74,6 @@ def test_run_headless_requires_master_port() -> None:
run_headless(args) run_headless(args)
def test_run_headless_rejects_multi_api_server_count() -> None:
args = _make_headless_args(api_server_count=2)
with pytest.raises(ValueError, match="api_server_count can't be set"):
run_headless(args)
def test_run_headless_rejects_non_multiprocess_worker_backend() -> None: def test_run_headless_rejects_non_multiprocess_worker_backend() -> None:
args = _make_headless_args(worker_backend="ray") args = _make_headless_args(worker_backend="ray")
with pytest.raises(ValueError, match="worker_backend=multi_process"): with pytest.raises(ValueError, match="worker_backend=multi_process"):
+14 -487
View File
@@ -5,303 +5,41 @@ hold for the orchestrator/engine/server CLI flag partition."""
from __future__ import annotations from __future__ import annotations
import logging from dataclasses import fields
from dataclasses import dataclass, fields
import pytest import pytest
from vllm_omni.engine.arg_utils import ( from vllm_omni.engine.arg_utils import (
SHARED_FIELDS, SHARED_FIELDS,
derive_server_dests_from_vllm_parser, OmniEngineArgs,
internal_blacklist_keys, internal_blacklist_keys,
orchestrator_args_from_argparse,
orchestrator_field_names, orchestrator_field_names,
split_kwargs,
) )
# --------------------------------------------------------------------------- pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
# Fake engine class for unit testing — avoids pulling in the full vllm
# EngineArgs and its heavy __post_init__ at test time.
# ---------------------------------------------------------------------------
@dataclass
class _FakeEngineArgs:
"""Stand-in for OmniEngineArgs with a representative subset of fields."""
model: str = ""
stage_id: int = 0
max_num_seqs: int = 64
gpu_memory_utilization: float = 0.9
async_chunk: bool = False # also in OrchestratorArgs → shared
log_stats: bool = False # also in OrchestratorArgs → shared
stage_configs_path: str | None = None
# ============================================================================
# Invariant 1 — OrchestratorArgs and engine must not ambiguously overlap.
# ============================================================================
def test_no_ambiguous_overlap_with_fake_engine():
"""OrchestratorArgs ∩ engine fields must be ⊆ SHARED_FIELDS."""
orch = orchestrator_field_names()
engine = {f.name for f in fields(_FakeEngineArgs)}
overlap = orch & engine
unexpected = overlap - SHARED_FIELDS
assert not unexpected, (
f"Fields declared in both OrchestratorArgs and the engine class "
f"but not in SHARED_FIELDS: {sorted(unexpected)}. These cause "
f"double-routing — either remove the duplicate declaration or add "
f"to SHARED_FIELDS if sharing is intentional."
)
def test_no_ambiguous_overlap_with_real_engine(): def test_no_ambiguous_overlap_with_real_engine():
"""Same check, but against the real OmniEngineArgs.""" """OrchestratorArgs ∩ OmniEngineArgs must be ⊆ SHARED_FIELDS ∪ orchestrator-captured.
try:
from vllm_omni.engine.arg_utils import OmniEngineArgs Fields that the orchestrator intentionally captures to prevent uniform
except Exception as exc: propagation (async_chunk, tokenizer) are excluded — they exist in both
pytest.skip(f"OmniEngineArgs not importable: {exc}") classes by design but flow through DeployConfig, not kwargs forwarding.
"""
# Fields on both classes by design: orchestrator captures them to prevent
# uniform per-stage propagation; redistributed via DeployConfig.
orchestrator_captured = {"async_chunk", "tokenizer"}
orch = orchestrator_field_names() orch = orchestrator_field_names()
engine = {f.name for f in fields(OmniEngineArgs)} engine = {f.name for f in fields(OmniEngineArgs)}
overlap = orch & engine overlap = orch & engine
unexpected = overlap - SHARED_FIELDS unexpected = overlap - SHARED_FIELDS - orchestrator_captured
assert not unexpected, ( assert not unexpected, (
f"Real OmniEngineArgs has ambiguous overlap with OrchestratorArgs: " f"OmniEngineArgs has ambiguous overlap with OrchestratorArgs: "
f"{sorted(unexpected)}. Update SHARED_FIELDS or remove duplication." f"{sorted(unexpected)}. Update SHARED_FIELDS or remove duplication."
) )
# ============================================================================
# Invariant 2 — split_kwargs partitions correctly.
# ============================================================================
def test_split_orchestrator_only():
"""Pure orchestrator fields go to OrchestratorArgs, not engine_kwargs."""
raw = {"stage_init_timeout": 500, "worker_backend": "ray"}
orch, engine = split_kwargs(raw, engine_cls=_FakeEngineArgs)
assert orch.stage_init_timeout == 500
assert orch.worker_backend == "ray"
assert "stage_init_timeout" not in engine
assert "worker_backend" not in engine
def test_split_engine_only():
"""Pure engine fields go to engine_kwargs, not OrchestratorArgs."""
raw = {"max_num_seqs": 128, "gpu_memory_utilization": 0.85}
orch, engine = split_kwargs(raw, engine_cls=_FakeEngineArgs)
assert engine["max_num_seqs"] == 128
assert engine["gpu_memory_utilization"] == 0.85
# These fields don't exist on OrchestratorArgs at all.
def test_split_shared_fields_go_to_both():
"""Fields in SHARED_FIELDS are copied to both buckets."""
raw = {"model": "Qwen/Qwen2.5-Omni-7B", "log_stats": True}
orch, engine = split_kwargs(raw, engine_cls=_FakeEngineArgs)
assert orch.log_stats is True
assert engine["model"] == "Qwen/Qwen2.5-Omni-7B"
assert engine["log_stats"] is True
def test_split_drops_unclassified():
"""Unclassified fields (uvicorn/server) are dropped silently."""
raw = {
"max_num_seqs": 64, # engine
"host": "0.0.0.0", # unclassified (server)
"port": 8091, # unclassified (server)
"ssl_keyfile": "key.pem", # unclassified (server)
}
orch, engine = split_kwargs(raw, engine_cls=_FakeEngineArgs)
assert engine == {"max_num_seqs": 64}
assert "host" not in engine
assert "port" not in engine
assert "ssl_keyfile" not in engine
def test_split_mixed_real_world():
"""End-to-end: raw CLI kwargs with all three classes present."""
raw = {
# orchestrator
"stage_init_timeout": 400,
"deploy_config": "/tmp/deploy.yaml",
"worker_backend": "multi_process",
"async_chunk": True,
# engine
"max_num_seqs": 32,
"gpu_memory_utilization": 0.8,
# shared
"model": "Qwen/Qwen3-Omni",
"log_stats": False,
# server / unclassified
"host": "0.0.0.0",
"port": 8091,
"api_key": "secret",
# None values
"ray_address": None,
}
orch, engine = split_kwargs(raw, engine_cls=_FakeEngineArgs)
# Orchestrator side
assert orch.stage_init_timeout == 400
assert orch.deploy_config == "/tmp/deploy.yaml"
assert orch.worker_backend == "multi_process"
assert orch.async_chunk is True
assert orch.log_stats is False # shared, read from raw
assert orch.ray_address is None # default preserved
# Engine side
assert engine["max_num_seqs"] == 32
assert engine["gpu_memory_utilization"] == 0.8
assert engine["model"] == "Qwen/Qwen3-Omni"
assert engine["log_stats"] is False
assert "host" not in engine
assert "port" not in engine
assert "api_key" not in engine
# orchestrator-only keys never reach engine
assert "stage_init_timeout" not in engine
assert "deploy_config" not in engine
# async_chunk is in SHARED_FIELDS so it flows to both
assert "async_chunk" in engine
# ============================================================================
# Invariant 3 — user-typed unclassifiable flags warn (don't fail silently).
# ============================================================================
def test_user_typed_unclassified_warns(monkeypatch):
"""If the user types a flag we can't route, warn — don't silently drop."""
import io
buf = io.StringIO()
vllm_logger = logging.getLogger("vllm")
orig_handlers = list(vllm_logger.handlers)
vllm_logger.handlers.clear()
handler = logging.StreamHandler(buf)
handler.setLevel(logging.WARNING)
vllm_logger.addHandler(handler)
try:
raw = {"bogus_flag": "value", "max_num_seqs": 64}
split_kwargs(raw, engine_cls=_FakeEngineArgs, user_typed={"bogus_flag"})
assert "bogus_flag" in buf.getvalue(), f"Expected 'bogus_flag' in log output, got: {buf.getvalue()!r}"
finally:
vllm_logger.handlers.clear()
for h in orig_handlers:
vllm_logger.addHandler(h)
def test_unclassified_without_user_typed_silent():
"""Without user_typed, unclassified keys drop silently (argparse defaults
for server flags shouldn't spam logs on every launch)."""
import io
vllm_logger = logging.getLogger("vllm")
orig_handlers = list(vllm_logger.handlers)
vllm_logger.handlers.clear()
buf = io.StringIO()
handler = logging.StreamHandler(buf)
handler.setLevel(logging.WARNING)
vllm_logger.addHandler(handler)
try:
raw = {"host": "0.0.0.0", "port": 8091, "max_num_seqs": 64}
split_kwargs(raw, engine_cls=_FakeEngineArgs, user_typed=None)
assert "host" not in buf.getvalue()
finally:
vllm_logger.handlers.clear()
for h in orig_handlers:
vllm_logger.addHandler(h)
# ============================================================================
# Invariant 4 — CLI flag classification completeness.
# Catches new flags added without updating OrchestratorArgs or OmniEngineArgs.
# ============================================================================
def test_all_omni_cli_flags_classified():
"""Every vllm-omni-added CLI flag must be classifiable.
Runs ``OmniServeCommand.subparser_init`` and checks that every new
argument (compared to vllm's base parser) is either:
- a field on OrchestratorArgs, OR
- a field on OmniEngineArgs, OR
- in SHARED_FIELDS
"""
try:
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.engine.arg_utils import OmniEngineArgs
from vllm_omni.entrypoints.cli.serve import OmniServeCommand
except Exception as exc:
pytest.skip(f"Cannot build parser in this environment: {exc}")
# Build the serve parser
root = FlexibleArgumentParser()
subparsers = root.add_subparsers()
cmd = OmniServeCommand()
try:
parser = cmd.subparser_init(subparsers)
except Exception as exc:
pytest.skip(f"subparser_init failed (dev env issue): {exc}")
all_dests = {a.dest for a in parser._actions if a.dest and a.dest not in {"help", "model_tag"}}
orch = orchestrator_field_names()
engine = {f.name for f in fields(OmniEngineArgs)}
server_derived = derive_server_dests_from_vllm_parser()
unclassified = all_dests - orch - engine - SHARED_FIELDS - server_derived
# Some argparse-internal dests (suppressed, private) may not match —
# filter those out.
unclassified = {d for d in unclassified if not d.startswith("_")}
assert not unclassified, (
f"These CLI flags are not classified as "
f"orchestrator/engine/shared/server: {sorted(unclassified)}. "
f"Add them to OrchestratorArgs (if consumed by orchestrator), "
f"OmniEngineArgs (if consumed by per-stage engine), or the known-server "
f"allowlist (if they're vllm frontend flags). "
f"If intentional (e.g. a new CLI-only flag that doesn't map to either "
f"dataclass), add it to a KNOWN_UNROUTED allowlist."
)
# ============================================================================
# argparse interop (Phase 3).
# ============================================================================
def test_orchestrator_args_from_argparse():
"""Can build OrchestratorArgs from an argparse.Namespace."""
import argparse
ns = argparse.Namespace(
stage_init_timeout=500,
deploy_config="/tmp/x.yaml",
max_num_seqs=64, # engine field — ignored
host="0.0.0.0", # server field — ignored
)
orch = orchestrator_args_from_argparse(ns)
assert orch.stage_init_timeout == 500
assert orch.deploy_config == "/tmp/x.yaml"
assert orch.worker_backend == "multi_process" # default
def test_derive_server_dests_returns_frozenset():
"""Server-dest derivation returns a frozenset (possibly empty)."""
result = derive_server_dests_from_vllm_parser()
assert isinstance(result, frozenset)
# ============================================================================
# internal_blacklist_keys — single source of truth for per-stage forwarding.
# ============================================================================
def test_internal_blacklist_keys_derived_from_orchestrator(): def test_internal_blacklist_keys_derived_from_orchestrator():
"""Blacklist is exactly OrchestratorArgs fields minus SHARED_FIELDS. """Blacklist is exactly OrchestratorArgs fields minus SHARED_FIELDS.
@@ -311,214 +49,3 @@ def test_internal_blacklist_keys_derived_from_orchestrator():
""" """
blacklist = internal_blacklist_keys() blacklist = internal_blacklist_keys()
assert blacklist == orchestrator_field_names() - SHARED_FIELDS assert blacklist == orchestrator_field_names() - SHARED_FIELDS
# Spot-check expected entries
assert "stage_init_timeout" in blacklist
assert "deploy_config" in blacklist
assert "async_chunk" not in blacklist # now in SHARED_FIELDS
# Shared fields must NOT appear — they flow to both orchestrator and engine
assert "model" not in blacklist
assert "log_stats" not in blacklist
# ============================================================================
# Boundary value analysis — edge cases around split_kwargs.
# ============================================================================
def test_split_empty_kwargs():
"""Empty kwargs yields default OrchestratorArgs and empty engine dict."""
orch, engine = split_kwargs({}, engine_cls=_FakeEngineArgs)
assert orch.stage_init_timeout == 300 # dataclass default
assert orch.worker_backend == "multi_process" # dataclass default
assert engine == {}
def test_split_all_none_values_preserved_on_orchestrator():
"""None values for orchestrator fields are kept (represents 'not set')."""
raw = {"ray_address": None, "deploy_config": None, "max_num_seqs": None}
orch, engine = split_kwargs(raw, engine_cls=_FakeEngineArgs)
assert orch.ray_address is None
assert orch.deploy_config is None
# Engine-side None still passes through; caller decides semantics downstream.
assert engine.get("max_num_seqs") is None
def test_split_user_typed_with_empty_kwargs_no_warn(caplog):
"""user_typed non-empty but kwargs empty — no warnings emitted."""
with caplog.at_level(logging.WARNING, logger="vllm_omni.engine.arg_utils"):
split_kwargs({}, engine_cls=_FakeEngineArgs, user_typed={"nothing"})
assert not caplog.records
def test_ambiguous_field_strict_raises():
"""strict=True raises ValueError on overlap outside SHARED_FIELDS."""
# deploy_config is on OrchestratorArgs; declaring it on the engine class
# too (without adding to SHARED_FIELDS) creates an ambiguous route.
@dataclass
class _AmbiguousEngine:
deploy_config: str | None = None
with pytest.raises(ValueError, match="both OrchestratorArgs and"):
split_kwargs({"deploy_config": "x"}, engine_cls=_AmbiguousEngine, strict=True)
def test_ambiguous_field_non_strict_routes_to_orchestrator():
"""strict=False logs ERROR but routes the ambiguous field to orchestrator."""
@dataclass
class _AmbiguousEngine:
deploy_config: str | None = None
import io
vllm_logger = logging.getLogger("vllm")
orig_handlers = list(vllm_logger.handlers)
vllm_logger.handlers.clear()
buf = io.StringIO()
handler = logging.StreamHandler(buf)
handler.setLevel(logging.ERROR)
vllm_logger.addHandler(handler)
try:
orch, engine = split_kwargs({"deploy_config": "x"}, engine_cls=_AmbiguousEngine, strict=False)
assert orch.deploy_config == "x"
assert "deploy_config" not in engine
assert "both OrchestratorArgs" in buf.getvalue()
finally:
vllm_logger.handlers.clear()
for h in orig_handlers:
vllm_logger.addHandler(h)
# Sentinel-default precedence invariants (#3035)
def _build_full_serve_parser():
from vllm.utils.argparse_utils import FlexibleArgumentParser
try:
from vllm.entrypoints.openai.cli_args import make_arg_parser
except ImportError:
pytest.skip("vllm parser not importable")
return make_arg_parser(FlexibleArgumentParser())
def test_nullify_stage_engine_defaults_resets_inherited_defaults():
import argparse
from vllm_omni.config.stage_config import deploy_override_field_names
from vllm_omni.engine.arg_utils import (
nullify_stage_engine_defaults,
)
parser = _build_full_serve_parser()
nullify_stage_engine_defaults(parser)
override_dests = deploy_override_field_names()
offenders = [
(a.dest, a.default)
for a in parser._actions
if a.dest not in ("help", "version")
and a.option_strings
and a.dest in override_dests
and a.default is not None
and a.default is not argparse.SUPPRESS
]
assert not offenders, f"Stage flags with non-None defaults after nullify: {offenders}"
def test_non_override_flags_keep_real_defaults_after_nullify():
import argparse
from vllm_omni.config.stage_config import deploy_override_field_names
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
parser = argparse.ArgumentParser()
parser.add_argument("--batch-timeout", type=int, default=10, help="Batch timeout.")
parser.add_argument("--max-num-seqs", type=int, default=64, help="Max num seqs.")
nullify_stage_engine_defaults(parser)
assert "batch_timeout" not in deploy_override_field_names()
batch_timeout = next(a for a in parser._actions if a.dest == "batch_timeout")
max_num_seqs = next(a for a in parser._actions if a.dest == "max_num_seqs")
assert batch_timeout.default == 10
assert max_num_seqs.default is None
def test_help_text_preserves_default_after_nullify():
# Real defaults must stay visible in --help even though parser stores None.
import argparse
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
parser = argparse.ArgumentParser()
parser.add_argument("--max-num-seqs", type=int, default=42, help="Example knob.")
nullify_stage_engine_defaults(parser)
action = next(a for a in parser._actions if a.dest == "max_num_seqs")
assert action.default is None
assert "(default: 42)" in action.help
_OMNIENGINEARGS_USER_INPUT_FIELDS = frozenset(
{
"model_arch",
"engine_output_type",
"hf_config_name",
"custom_process_next_stage_input_func",
"subtalker_sampling_params",
"omni_kv_config",
"quantization_config",
"worker_type",
"task_type",
"worker_cls",
"omni_master_address",
"omni_master_port",
"stage_configs_path",
"output_modalities",
"custom_pipeline_args",
}
)
def test_omniengineargs_user_input_fields_default_to_none():
try:
from vllm_omni.engine.arg_utils import OmniEngineArgs
except Exception as exc:
pytest.skip(f"OmniEngineArgs not importable: {exc}")
offenders = [
(f.name, f.default)
for f in fields(OmniEngineArgs)
if f.name in _OMNIENGINEARGS_USER_INPUT_FIELDS
and f.default is not dataclasses.MISSING
and f.default is not None
]
assert not offenders, f"User-input fields with non-None defaults: {offenders}"
def test_omniengineargs_create_tracks_explicit_fields():
try:
from vllm_omni.engine.arg_utils import OmniEngineArgs
except Exception as exc:
pytest.skip(f"OmniEngineArgs not importable: {exc}")
ea = OmniEngineArgs.create(model="x", gpu_memory_utilization=0.5)
assert ea._explicit_fields == frozenset({"model", "gpu_memory_utilization"})
assert ea.explicit_kwargs() == {"model": "x", "gpu_memory_utilization": 0.5}
def test_omniengineargs_bare_constructor_has_no_explicit_tracking():
try:
from vllm_omni.engine.arg_utils import OmniEngineArgs
except Exception as exc:
pytest.skip(f"OmniEngineArgs not importable: {exc}")
ea = OmniEngineArgs(model="x")
assert not hasattr(ea, "_explicit_fields")
assert "model" in ea.explicit_kwargs()
# dataclasses already imported via ``from dataclasses import dataclass, fields``
import dataclasses # noqa: E402 -- needed for MISSING sentinel above
+2 -85
View File
@@ -29,7 +29,6 @@ from vllm_omni.config.stage_config import (
_deep_merge_stage, _deep_merge_stage,
_resolve_scheduler, _resolve_scheduler,
build_stage_runtime_overrides, build_stage_runtime_overrides,
deploy_override_field_names,
load_deploy_config, load_deploy_config,
merge_pipeline_deploy, merge_pipeline_deploy,
register_pipeline, register_pipeline,
@@ -39,6 +38,8 @@ from vllm_omni.engine.arg_utils import SHARED_FIELDS, EngineArgs, internal_black
pytestmark = [pytest.mark.core_model, pytest.mark.cpu] pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class TestStageType: class TestStageType:
"""Tests for StageType enum.""" """Tests for StageType enum."""
@@ -156,23 +157,6 @@ class TestStageConfig:
omega_config = config.to_omegaconf() omega_config = config.to_omegaconf()
assert omega_config.engine_args.max_num_seqs == 32 assert omega_config.engine_args.max_num_seqs == 32
def test_to_omegaconf_omits_none_deploy_overrides_for_engine_args(self):
"""None deploy overrides must fall through to EngineArgs defaults."""
config = StageConfig(
stage_id=0,
model_stage="thinker",
runtime_overrides={name: None for name in deploy_override_field_names()},
)
omega_config = config.to_omegaconf()
engine_args = dict(omega_config.engine_args)
assert "devices" not in engine_args
assert "max_batch_size" not in engine_args
for name in deploy_override_field_names() - {"devices"}:
assert name not in engine_args
def test_to_omegaconf_diffusion_parallel_overrides_replace_nested_values(self): def test_to_omegaconf_diffusion_parallel_overrides_replace_nested_values(self):
config = StageConfig( config = StageConfig(
stage_id=1, stage_id=1,
@@ -1018,64 +1002,7 @@ class TestPipelineRegistry:
class TestDeployConfigLoading: class TestDeployConfigLoading:
def test_deploy_override_fields_include_deploy_schema_fields(self):
expected_fields = {
"active_stream_window",
"async_chunk",
# StageDeployConfig: stage placement and runtime fields.
"devices",
# StageDeployConfig: vLLM EngineArgs fields.
"async_scheduling",
"compilation_config",
"config_format",
"custom_voice_dir",
"data_parallel_size",
"devices",
"disable_hybrid_kv_cache_manager",
"enable_flashinfer_autotune",
"enforce_eager",
"gpu_memory_utilization",
"load_format",
"max_model_len",
"max_num_batched_tokens",
"max_num_seqs",
"mm_processor_cache_gb",
"profiler_config",
"skip_mm_profiling",
"subtalker_sampling_params",
"tensor_parallel_size",
"tokenizer_mode",
# StageDeployConfig: diffusion parallel_config deploy override fields.
"cfg_parallel_size",
"enable_expert_parallel",
"hsdp_replicate_size",
"hsdp_shard_size",
"ring_degree",
"sequence_parallel_size",
"ulysses_degree",
"ulysses_mode",
"use_hsdp",
"vae_patch_parallel_size",
# DeployConfig: pipeline-wide engine settings.
"data_parallel_size",
"distributed_executor_backend",
"dtype",
"enable_chunked_prefill",
"enable_prefix_caching",
"pipeline_parallel_size",
"quantization",
"trust_remote_code",
}
actual_fields = deploy_override_field_names()
assert expected_fields == actual_fields, (
f"added={actual_fields - expected_fields}, removed={expected_fields - actual_fields}"
)
def test_custom_voice_dir_is_pipeline_wide_engine_arg(self, tmp_path): def test_custom_voice_dir_is_pipeline_wide_engine_arg(self, tmp_path):
import vllm_omni.model_executor.models.qwen3_tts.pipeline # noqa: F401
from vllm_omni.config.stage_config import load_deploy_config, merge_pipeline_deploy
deploy_path = tmp_path / "qwen3_tts_custom_voice.yaml" deploy_path = tmp_path / "qwen3_tts_custom_voice.yaml"
custom_voice_dir = tmp_path / "voices" custom_voice_dir = tmp_path / "voices"
deploy_path.write_text( deploy_path.write_text(
@@ -1913,16 +1840,6 @@ class TestSentinelDefaultPrecedence:
named = [p for p in sig.parameters.values() if p.kind != p.VAR_KEYWORD] named = [p for p in sig.parameters.values() if p.kind != p.VAR_KEYWORD]
assert "cli_explicit_keys" not in {p.name for p in named} assert "cli_explicit_keys" not in {p.name for p in named}
def test_cli_explicit_keys_kwarg_emits_deprecation(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
StageConfigFactory._create_from_registry(
"qwen3_omni_moe",
cli_overrides={},
cli_explicit_keys={"max_num_seqs"},
)
assert any(issubclass(x.category, DeprecationWarning) for x in w)
def test_async_chunk_dispatches_processors(self): def test_async_chunk_dispatches_processors(self):
"""A single ``qwen3_tts`` pipeline picks per-chunk vs end-to-end """A single ``qwen3_tts`` pipeline picks per-chunk vs end-to-end
processors based on ``deploy.async_chunk``, without needing a processors based on ``deploy.async_chunk``, without needing a
+545
View File
@@ -0,0 +1,545 @@
"""Tests for TrackingArgumentParser and related utilities."""
import argparse
import json
from unittest.mock import patch
import pytest
import yaml
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.config.stage_config import (
_PIPELINE_REGISTRY,
DeployConfig,
PipelineConfig,
StageConfigFactory,
StageDeployConfig,
StagePipelineConfig,
)
from vllm_omni.utils.tracking_parser import TrackingArgumentParser, TrackingNamespace
### Fake pipeline/deploy config for integration tests
_TEST_MODEL = "_test_tracking"
_TEST_PIPELINE = PipelineConfig(
model_type=_TEST_MODEL,
stages=(
StagePipelineConfig(stage_id=0, model_stage="stage0"),
StagePipelineConfig(stage_id=1, model_stage="stage1"),
StagePipelineConfig(stage_id=2, model_stage="stage2"),
),
)
_TEST_DEPLOY = DeployConfig(
async_chunk=False,
stages=[
StageDeployConfig(stage_id=0, gpu_memory_utilization=0.5, max_num_seqs=32),
StageDeployConfig(stage_id=1, gpu_memory_utilization=0.6, max_num_seqs=64),
StageDeployConfig(stage_id=2, gpu_memory_utilization=0.1, max_num_seqs=16),
],
)
@pytest.fixture()
def mock_stages(monkeypatch):
"""Register a fake pipeline and mock deploy YAML loading."""
monkeypatch.setitem(_PIPELINE_REGISTRY._loaded, _TEST_MODEL, _TEST_PIPELINE)
monkeypatch.setattr(
"vllm_omni.config.stage_config.load_deploy_config",
lambda _path: _TEST_DEPLOY,
)
return __file__
### Tests for TrackingNamespace
def test_tracking_namespaces_cant_be_nested():
"""Ensure tracking namespaces explode if we try to nest them."""
track_ns = TrackingNamespace(
unfiltered_ns=argparse.Namespace(foo="bar"),
explicit_keys=frozenset(),
)
with pytest.raises(ValueError):
TrackingNamespace(
unfiltered_ns=track_ns,
explicit_keys=frozenset(),
)
def test_tracking_namespaces_init():
"""Check simple initialization for tracking namespaces."""
unfiltered_ns = argparse.Namespace(foo="bar")
tracked_ns = TrackingNamespace(
unfiltered_ns=unfiltered_ns,
explicit_keys=frozenset({"foo"}),
)
assert tracked_ns.foo == "bar"
assert tracked_ns.explicit_keys == frozenset({"foo"})
def test_tracking_filtering():
"""Ensure tracking namespaces are filterable."""
unfiltered_ns = argparse.Namespace(foo="bar", baz="foobar")
tracked_ns = TrackingNamespace(
unfiltered_ns=unfiltered_ns,
explicit_keys=frozenset({"foo"}),
)
assert tracked_ns.foo == "bar"
assert tracked_ns.baz == "foobar"
assert tracked_ns.explicit_keys == frozenset({"foo"})
# baz gets dropped because it's not marked in explicit_keys
assert tracked_ns.get_explicit_kwargs_dict() == {"foo": "bar"}
def test_setattr_writes_through_to_unfiltered_ns():
"""Ensure mutating an attribute on TrackingNamespace forwards to
get_explicit_kwargs_dict() and vars()."""
unfiltered_ns = argparse.Namespace(model="original")
tracked_ns = TrackingNamespace(
unfiltered_ns=unfiltered_ns,
explicit_keys=frozenset({"model"}),
)
assert tracked_ns.model == "original"
assert tracked_ns.get_explicit_kwargs_dict() == {"model": "original"}
assert vars(tracked_ns) == {"model": "original"}
# Ensure if we update the namespace, it's forwarded correctly
tracked_ns.model = "updated"
assert tracked_ns.model == "updated"
assert tracked_ns.get_explicit_kwargs_dict() == {"model": "updated"}
assert vars(tracked_ns) == {"model": "updated"}
### Tests for simple cases (no nested parsers or groups)
def test_vars_does_not_expose_internals():
"""Ensure vars on a TrackingArgumentParser is identical to running on the real one."""
tracking = TrackingArgumentParser()
flexible = FlexibleArgumentParser()
namespaces = []
# For both parser, register two args, but only pass one
for parser in [tracking, flexible]:
parser.add_argument("--foo", type=int, default=42)
parser.add_argument("--bar", type=str, default="x")
namespaces.append(parser.parse_args(["--foo", "100"]))
# Ensure that vars are the same result, since it isn't filtered
tracked_ns, flexible_ns = namespaces
tracked_kwargs = vars(tracked_ns)
real_kwargs = vars(flexible_ns)
assert tracked_kwargs == real_kwargs == {"foo": 100, "bar": "x"}
assert tracked_ns.explicit_keys == {"foo"}
def test_default_not_detected():
"""Ensure omitted defaults aren't in explicit keys and take defaults."""
p = TrackingArgumentParser()
p.add_argument("--foo", type=int, default=42)
ns = p.parse_args([])
assert ns.explicit_keys == set()
assert isinstance(ns, TrackingNamespace)
assert ns.foo == 42
@pytest.mark.parametrize("val", ["42", "100"])
def test_explicit_value_equal_to_default(val):
"""Ensure explicit keys correctly handles passed values."""
p = TrackingArgumentParser()
p.add_argument("--foo", type=int, default=42)
ns = p.parse_args(["--foo", val])
assert ns.explicit_keys == {"foo"}
assert isinstance(ns, TrackingNamespace)
assert ns.foo == int(val)
def test_equals_syntax():
"""Ensure equals syntax is handled correctly wrt explicit keys."""
p = TrackingArgumentParser()
p.add_argument("--foo", type=int, default=42)
ns = p.parse_args(["--foo=100"])
assert ns.explicit_keys == {"foo"}
assert isinstance(ns, TrackingNamespace)
assert ns.foo == 100
def test_explicit_none_via_store_const():
"""Ensure passing `None` to the namespace, it isn't filtered from explicit_keys."""
parser = TrackingArgumentParser()
parser.add_argument("--foo", action="store_const", const=None, default="Something else")
ns = parser.parse_args(["--foo"])
# User explicitly passed --foo, so it should be in the explicit keys even though it's None
assert ns.explicit_keys == {"foo"}
assert isinstance(ns, TrackingNamespace)
assert ns.foo is None
def test_multiple_args_mixed():
"""Ensure that explicit keys are correct when some are passed and others aren't."""
p = TrackingArgumentParser()
p.add_argument("--foo", type=int, default=1)
p.add_argument("--bar", type=str, default="x")
p.add_argument("--baz", type=float, default=0.5)
ns = p.parse_args(["--foo", "10", "--baz", "0.9"])
assert ns.explicit_keys == {"foo", "baz"}
assert isinstance(ns, TrackingNamespace)
assert ns.bar == "x"
def test_store_true_default():
"""Ensure that store true is handled correctly when omitted."""
p = TrackingArgumentParser()
p.add_argument("--verbose", action="store_true")
ns = p.parse_args([])
assert ns.explicit_keys == set()
assert isinstance(ns, TrackingNamespace)
assert ns.verbose is False
def test_store_true_explicit():
"""Ensure that store true is handled correctly when passed."""
p = TrackingArgumentParser()
p.add_argument("--verbose", action="store_true")
ns = p.parse_args(["--verbose"])
assert ns.explicit_keys == {"verbose"}
assert isinstance(ns, TrackingNamespace)
assert ns.verbose is True
def test_store_false_default():
"""Ensure that store false is handled correctly when omitted."""
p = TrackingArgumentParser()
p.add_argument("--disable-x", action="store_false", dest="enable_x", default=True)
ns = p.parse_args([])
assert ns.explicit_keys == set()
assert isinstance(ns, TrackingNamespace)
assert ns.enable_x is True
def test_store_false_explicit():
"""Ensure that store false is handled correctly when passed."""
p = TrackingArgumentParser()
p.add_argument("--disable-x", action="store_false")
ns = p.parse_args(["--disable-x"])
assert ns.explicit_keys == {"disable_x"}
assert isinstance(ns, TrackingNamespace)
assert ns.disable_x is False
def test_dest_is_reflected_in_explicit_keys():
"""Ensure that explicit keys use dest correctly."""
p = TrackingArgumentParser()
p.add_argument("--foo", type=int, dest="bar")
ns = p.parse_args(["--foo", "100"])
assert ns.explicit_keys == {"bar"}
assert isinstance(ns, TrackingNamespace)
assert ns.bar == 100
def test_boolean_optional_action_default():
"""Check that boolean optional actions are handled correctly when omitted."""
p = TrackingArgumentParser()
p.add_argument("--flag", action=argparse.BooleanOptionalAction)
ns = p.parse_args([])
assert ns.explicit_keys == set()
assert isinstance(ns, TrackingNamespace)
assert ns.flag is None
def test_boolean_optional_action_positive():
"""Check that boolean optional actions are handled correctly."""
p = TrackingArgumentParser()
p.add_argument("--flag", action=argparse.BooleanOptionalAction, default=None)
ns = p.parse_args(["--flag"])
assert ns.explicit_keys == {"flag"}
assert isinstance(ns, TrackingNamespace)
assert ns.flag is True
def test_no_option_strings_are_handled():
"""Ensure --no-<feature> sets <feature> in the explicit keys correctly."""
p = TrackingArgumentParser()
p.add_argument("--flag", action=argparse.BooleanOptionalAction, default=None)
ns = p.parse_args(["--no-flag"])
assert ns.explicit_keys == {"flag"}
assert isinstance(ns, TrackingNamespace)
assert ns.flag is False
def test_json_type():
"""Ensure json type is handled correctly."""
p = TrackingArgumentParser()
p.add_argument("--cfg", type=json.loads, default="{}")
ns = p.parse_args(["--cfg", '{"a": 1}'])
assert "cfg" in ns.explicit_keys
assert isinstance(ns, TrackingNamespace)
assert ns.cfg == {"a": 1}
def test_choices():
"""Ensure choices are handled correctly."""
p = TrackingArgumentParser()
p.add_argument("--mode", choices=["fast", "slow"], default="fast")
ns = p.parse_args(["--mode", "slow"])
assert "mode" in ns.explicit_keys
assert ns is not None
assert ns.mode == "slow"
def test_explicit_positional_arg():
"""Ensure positional args are handled correctly when provided."""
p = TrackingArgumentParser()
p.add_argument("name", nargs="?", default=None)
ns = p.parse_args(["hello"])
assert "name" in ns.explicit_keys
assert isinstance(ns, TrackingNamespace)
assert ns.name == "hello"
def test_omitted_positional_arg():
"""Ensure positional args are handled correctly when omitted."""
p = TrackingArgumentParser()
p.add_argument("name", nargs="?", default=None)
ns = p.parse_args([])
assert "name" not in ns.explicit_keys
assert isinstance(ns, TrackingNamespace)
assert ns.name is None
def test_explicit_nargs():
"""Ensure that variable num args are handled correctly when provided."""
p = TrackingArgumentParser()
p.add_argument("--items", nargs="*", default=None)
ns = p.parse_args(["--items", "a", "b"])
assert "items" in ns.explicit_keys
assert isinstance(ns, TrackingNamespace)
assert ns.items == ["a", "b"]
def test_omitted_nargs():
"""Ensure that variable num args are handled correctly when omitted."""
p = TrackingArgumentParser()
p.add_argument("--items", nargs="*", default=None)
ns = p.parse_args([])
assert "items" not in ns.explicit_keys
assert isinstance(ns, TrackingNamespace)
assert ns.items is None
def test_parse_known_args_tracking():
"""Ensure parse_known_args is also trackable"""
p = TrackingArgumentParser()
p.add_argument("--foo", type=int, default=42)
ns, remaining = p.parse_known_args(["--foo", "10", "--unknown", "val"])
assert ns.explicit_keys == {"foo"}
assert isinstance(ns, TrackingNamespace)
assert ns.foo == 10
assert remaining == ["--unknown", "val"]
### Tests for group handling
def test_group_arg_default():
"""Ensure that that groups with defaults are handled correctly."""
p = TrackingArgumentParser()
g = p.add_argument_group("TestGroup")
g.add_argument("--bar", type=str, default="baz")
ns = p.parse_args([])
assert ns.explicit_keys == set()
assert isinstance(ns, TrackingNamespace)
assert ns.bar == "baz"
def test_group_arg_explicit():
"""Ensure that that groups are handled correctly."""
p = TrackingArgumentParser()
g = p.add_argument_group("TestGroup")
g.add_argument("--bar", type=str, default="baz")
ns = p.parse_args(["--bar", "qux"])
assert ns.explicit_keys == {"bar"}
assert isinstance(ns, TrackingNamespace)
assert ns.bar == "qux"
def test_multiple_groups():
"""Test multiple groups behave correctly."""
p = TrackingArgumentParser()
g1 = p.add_argument_group("Group1")
g2 = p.add_argument_group("Group2")
g1.add_argument("--a", type=int, default=1)
g2.add_argument("--b", type=int, default=2)
ns = p.parse_args(["--b", "20"])
assert ns.explicit_keys == {"b"}
assert isinstance(ns, TrackingNamespace)
assert ns.a == 1
assert ns.b == 20
def test_omitted_mutually_exclusive_group():
"""Ensure that that mutually exclusive groups with defaults are handled correctly."""
p = TrackingArgumentParser()
grp = p.add_mutually_exclusive_group()
grp.add_argument("--json", action="store_true")
grp.add_argument("--text", action="store_true")
ns = p.parse_args([])
assert ns.explicit_keys == set()
assert isinstance(ns, TrackingNamespace)
def test_mutually_exclusive_group():
"""Ensure that that mutually exclusive groups are handled correctly."""
p = TrackingArgumentParser()
grp = p.add_mutually_exclusive_group()
grp.add_argument("--json", action="store_true")
grp.add_argument("--text", action="store_true")
ns = p.parse_args(["--json"])
assert ns.explicit_keys == {"json"}
assert isinstance(ns, TrackingNamespace)
assert ns.json is True
assert ns.text is False
### Tests for subparser handling
def test_subparser_explicit_detection():
"""Ensure that subparsers are handled correctly."""
p = TrackingArgumentParser()
sub = p.add_subparsers()
child = sub.add_parser("foo")
child.add_argument("--bar", type=str)
ns = p.parse_args(["foo", "--bar", "baz"])
assert isinstance(child, TrackingArgumentParser)
assert isinstance(ns, TrackingNamespace)
assert ns.explicit_keys == {"bar"}
assert ns.bar == "baz"
def test_subparser_group_args():
"""Ensure that subparsers with groups are handled correctly."""
p = TrackingArgumentParser()
sub = p.add_subparsers()
child = sub.add_parser("foo")
g = child.add_argument_group("Config")
g.add_argument("--port", type=int, default=8000)
g.add_argument("--host", type=str, default="localhost")
ns = p.parse_args(["foo", "--port", "9000"])
assert ns.explicit_keys == {"port"}
assert isinstance(ns, TrackingNamespace)
assert ns.host == "localhost"
### Tests for specific behaviors against FlexibleArgumentParser
def test_config_file_args_detected(tmp_path):
"""Ensure config is handled correctly for vLLM's FlexibleArgumentParser."""
p = TrackingArgumentParser()
p.add_argument("--foo", type=int)
p.add_argument("--bar", type=int)
cfg = tmp_path / "test.yaml"
cfg.write_text(yaml.dump({"foo": 100}))
ns = p.parse_args(["--config", str(cfg)])
assert isinstance(ns, TrackingNamespace)
assert ns.explicit_keys == {"foo"}
assert ns.foo == 100
def test_cli_overrides_config(tmp_path):
"""Ensure tracking parser handles config vs cli overrides correctly."""
p = TrackingArgumentParser()
p.add_argument("--foo", type=int)
cfg = tmp_path / "test.yaml"
cfg.write_text(yaml.dump({"foo": 100}))
ns = p.parse_args(["--config", str(cfg), "--foo", "200"])
assert isinstance(ns, TrackingNamespace)
assert "foo" in ns.explicit_keys
assert ns.foo == 200
### Integration tests for arg resolution through StageConfigFactory
def test_explicit_cli_arg_reaches_runtime_overrides(mock_stages):
"""Explicitly passed CLI values reach runtime_overrides on all stages."""
p = TrackingArgumentParser()
p.add_argument("--max-num-seqs", type=int, default=64)
ns = p.parse_args(["--max-num-seqs", "999"])
explicit_kwargs = ns.get_explicit_kwargs_dict()
stages = StageConfigFactory._create_from_registry(
_TEST_MODEL,
explicit_kwargs,
deploy_config_path=mock_stages,
)
for stage in stages:
assert stage.runtime_overrides.get("max_num_seqs") == 999
def test_omitted_default_not_in_runtime_overrides(mock_stages):
"""Omitted defaults are overridden by deploy config values"""
p = TrackingArgumentParser()
p.add_argument("--max-num-seqs", type=int, default=64)
ns = p.parse_args([])
explicit_kwargs = ns.get_explicit_kwargs_dict()
stages = StageConfigFactory._create_from_registry(
_TEST_MODEL,
explicit_kwargs,
deploy_config_path=mock_stages,
)
for stage in stages:
assert stage.runtime_overrides == {}
def test_config_file_args_reach_runtime_overrides(mock_stages):
"""Args from --config YAML must be treated as explicitly passed and
flow through to runtime_overrides."""
p = TrackingArgumentParser()
p.add_argument("--max-num-seqs", type=int, default=64)
p.add_argument("--gpu-memory-utilization", type=float, default=0.9)
with patch(
"vllm.utils.argparse_utils.FlexibleArgumentParser.load_config_file", return_value=["--max-num-seqs", "999"]
):
ns = p.parse_args(["--config", "fake.yaml"])
explicit_kwargs = ns.get_explicit_kwargs_dict()
stages = StageConfigFactory._create_from_registry(
_TEST_MODEL,
explicit_kwargs,
deploy_config_path=mock_stages,
)
for stage in stages:
assert stage.runtime_overrides.get("max_num_seqs") == 999
assert "gpu_memory_utilization" not in stage.runtime_overrides
def test_per_stage_override_routes_correctly(mock_stages):
"""Ensure stage_<N>_<key> only affects the targeted stage."""
p = TrackingArgumentParser()
p.add_argument("--stage-0-gpu-memory-utilization", type=float)
ns = p.parse_args(["--stage-0-gpu-memory-utilization", "0.42"])
explicit_kwargs = ns.get_explicit_kwargs_dict()
stages = StageConfigFactory._create_from_registry(
_TEST_MODEL,
explicit_kwargs,
deploy_config_path=mock_stages,
)
assert stages[0].runtime_overrides == {"gpu_memory_utilization": 0.42}
assert stages[1].runtime_overrides == {}
assert stages[2].runtime_overrides == {}
def test_explicit_args_omitted_from_yaml(mock_stages):
"""Ensure only passed args end up in runtime overrides (regardless of whether
they are defined in the yaml config)."""
p = TrackingArgumentParser()
p.add_argument("--enforce-eager", action="store_true")
p.add_argument("--max-num-seqs", type=int, default=64)
p.add_argument("--gpu-memory-utilization", type=float, default=0.9)
# NOTE: enforce eager is not set in the mock config yaml.
ns = p.parse_args(["--enforce-eager"])
explicit_kwargs = ns.get_explicit_kwargs_dict()
stages = StageConfigFactory._create_from_registry(
_TEST_MODEL,
explicit_kwargs,
deploy_config_path=mock_stages,
)
for stage in stages:
assert stage.runtime_overrides == {"enforce_eager": True}
-24
View File
@@ -30,15 +30,6 @@ def get_pipeline_path(model_dir: str, filename: str) -> Path:
logger = init_logger(__name__) logger = init_logger(__name__)
def _warn_deprecated_kwargs(kwargs: dict[str, Any]) -> None:
if "cli_explicit_keys" in kwargs:
warnings.warn(
"cli_explicit_keys= is deprecated and ignored. Remove the kwarg.",
DeprecationWarning,
stacklevel=3,
)
_STAGE_OVERRIDE_PATTERN = re.compile(r"^stage_(\d+)_(.+)$") _STAGE_OVERRIDE_PATTERN = re.compile(r"^stage_(\d+)_(.+)$")
@@ -837,15 +828,6 @@ _PIPELINE_WIDE_ENGINE_FIELDS: tuple[str, ...] = (
) )
def deploy_override_field_names() -> frozenset[str]:
"""Return deploy-schema fields whose CLI defaults must not override YAML."""
return (
frozenset(_STAGE_DEPLOY_FIELDS)
| frozenset(_PIPELINE_WIDE_ENGINE_FIELDS)
| frozenset({"async_chunk", "devices"})
)
def _build_engine_args( def _build_engine_args(
ps: StagePipelineConfig, ps: StagePipelineConfig,
ds: StageDeployConfig | None, ds: StageDeployConfig | None,
@@ -1169,14 +1151,11 @@ class StageConfigFactory:
model: str, model: str,
cli_overrides: dict[str, Any] | None = None, cli_overrides: dict[str, Any] | None = None,
deploy_config_path: str | None = None, deploy_config_path: str | None = None,
**deprecated_kwargs: Any,
) -> list[StageConfig] | None: ) -> list[StageConfig] | None:
"""Load pipeline + deploy config, merge with CLI overrides. """Load pipeline + deploy config, merge with CLI overrides.
Checks _PIPELINE_REGISTRY first (new path), falls back to legacy YAML. Checks _PIPELINE_REGISTRY first (new path), falls back to legacy YAML.
""" """
_warn_deprecated_kwargs(deprecated_kwargs)
if cli_overrides is None: if cli_overrides is None:
cli_overrides = {} cli_overrides = {}
@@ -1256,15 +1235,12 @@ class StageConfigFactory:
model_type: str, model_type: str,
cli_overrides: dict[str, Any], cli_overrides: dict[str, Any],
deploy_config_path: str | None = None, deploy_config_path: str | None = None,
**deprecated_kwargs: Any,
) -> list[StageConfig]: ) -> list[StageConfig]:
"""Create StageConfigs from pipeline registry + deploy YAML. """Create StageConfigs from pipeline registry + deploy YAML.
Precedence: caller-typed (non-None) value > deploy YAML > Precedence: caller-typed (non-None) value > deploy YAML >
StageDeployConfig dataclass default. StageDeployConfig dataclass default.
""" """
_warn_deprecated_kwargs(deprecated_kwargs)
# Resolve deploy config path # Resolve deploy config path
if deploy_config_path is None: if deploy_config_path is None:
deploy_path = _DEPLOY_DIR / f"{model_type}.yaml" deploy_path = _DEPLOY_DIR / f"{model_type}.yaml"
@@ -14,6 +14,16 @@ from dataclasses import dataclass
import torch import torch
from einops import rearrange from einops import rearrange
from torch import Tensor, nn from torch import Tensor, nn
from vllm.logger import init_logger
from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor import (
DistributedOperator,
DistributedVaeMixin,
GridSpec,
TileTask,
)
logger = init_logger(__name__)
@dataclass @dataclass
@@ -322,3 +332,229 @@ class AutoEncoder(nn.Module):
def forward(self, x: Tensor) -> Tensor: def forward(self, x: Tensor) -> Tensor:
return self.decode(self.encode(x)) return self.decode(self.encode(x))
class DistributedAutoEncoder(AutoEncoder, DistributedVaeMixin):
def __init__(self, params: AutoEncoderParams):
super().__init__(params)
self.init_distributed()
self.use_tiling = False
self.tile_sample_min_height = 512
self.tile_sample_min_width = 512
self.tile_sample_stride_height = 448
self.tile_sample_stride_width = 448
self.spatial_compression_ratio = params.downsample
def blend_v(self, above: Tensor, current: Tensor, blend_extent: int) -> Tensor:
blend_extent = min(above.shape[-2], current.shape[-2], blend_extent)
if blend_extent <= 0:
return current
for y in range(blend_extent):
alpha = y / blend_extent
current[:, :, y, :] = above[:, :, -blend_extent + y, :] * (1 - alpha) + current[:, :, y, :] * alpha
return current
def blend_h(self, left: Tensor, current: Tensor, blend_extent: int) -> Tensor:
blend_extent = min(left.shape[-1], current.shape[-1], blend_extent)
if blend_extent <= 0:
return current
for x in range(blend_extent):
alpha = x / blend_extent
current[:, :, :, x] = left[:, :, :, -blend_extent + x] * (1 - alpha) + current[:, :, :, x] * alpha
return current
def decode_tile_split(self, z: Tensor) -> tuple[list[TileTask], GridSpec]:
_, _, latent_height, latent_width = z.shape
sample_height = latent_height * self.spatial_compression_ratio
sample_width = latent_width * self.spatial_compression_ratio
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_stride_height = self.tile_sample_stride_height // self.spatial_compression_ratio
tile_latent_stride_width = self.tile_sample_stride_width // self.spatial_compression_ratio
blend_height = self.tile_sample_min_height - self.tile_sample_stride_height
blend_width = self.tile_sample_min_width - self.tile_sample_stride_width
tiletask_list = []
for i in range(0, latent_height, tile_latent_stride_height):
for j in range(0, latent_width, tile_latent_stride_width):
tile = z[
:,
:,
i : i + tile_latent_min_height,
j : j + tile_latent_min_width,
]
tiletask_list.append(
TileTask(
tile_id=len(tiletask_list),
grid_coord=(i // tile_latent_stride_height, j // tile_latent_stride_width),
tensor=tile,
workload=tile.shape[-2] * tile.shape[-1],
)
)
grid_spec = GridSpec(
split_dims=(2, 3),
grid_shape=(tiletask_list[-1].grid_coord[0] + 1, tiletask_list[-1].grid_coord[1] + 1),
tile_spec={
"sample_height": sample_height,
"sample_width": sample_width,
"blend_height": blend_height,
"blend_width": blend_width,
},
output_dtype=z.dtype,
)
return tiletask_list, grid_spec
def decode_tile_exec(self, task: TileTask) -> Tensor:
tile = task.tensor
tile = tile / self.scale_factor + self.shift_factor
return self.decoder(tile)
def decode_tile_merge(self, coord_tensor_map: dict[tuple[int, ...], Tensor], grid_spec: GridSpec) -> Tensor:
grid_h, grid_w = grid_spec.grid_shape
result_rows = []
for i in range(grid_h):
result_row = []
for j in range(grid_w):
tile = coord_tensor_map[(i, j)]
if i > 0:
tile = self.blend_v(coord_tensor_map[(i - 1, j)], tile, grid_spec.tile_spec["blend_height"])
if j > 0:
tile = self.blend_h(coord_tensor_map[(i, j - 1)], tile, grid_spec.tile_spec["blend_width"])
result_row.append(
tile[
:,
:,
: self.tile_sample_stride_height,
: self.tile_sample_stride_width,
]
)
result_rows.append(torch.cat(result_row, dim=-1))
dec = torch.cat(result_rows, dim=-2)[
:,
:,
: grid_spec.tile_spec["sample_height"],
: grid_spec.tile_spec["sample_width"],
]
return dec
def decode(self, z: Tensor) -> Tensor:
if not self.is_distributed_enabled():
return super().decode(z)
logger.debug("Bagel VAE decode running with distributed executor")
return self.distributed_executor.execute(
z,
DistributedOperator(
split=self.decode_tile_split,
exec=self.decode_tile_exec,
merge=self.decode_tile_merge,
),
broadcast_result=True,
)
def encode_tile_split(self, x: Tensor) -> tuple[list[TileTask], GridSpec]:
_, _, height, width = x.shape
latent_height = height // self.spatial_compression_ratio
latent_width = width // self.spatial_compression_ratio
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_stride_height = self.tile_sample_stride_height // self.spatial_compression_ratio
tile_latent_stride_width = self.tile_sample_stride_width // self.spatial_compression_ratio
blend_height = tile_latent_min_height - tile_latent_stride_height
blend_width = tile_latent_min_width - tile_latent_stride_width
tiletask_list = []
for i in range(0, height, self.tile_sample_stride_height):
for j in range(0, width, self.tile_sample_stride_width):
tile = x[
:,
:,
i : i + self.tile_sample_min_height,
j : j + self.tile_sample_min_width,
]
tiletask_list.append(
TileTask(
tile_id=len(tiletask_list),
grid_coord=(i // self.tile_sample_stride_height, j // self.tile_sample_stride_width),
tensor=tile,
workload=tile.shape[-2] * tile.shape[-1],
)
)
grid_spec = GridSpec(
split_dims=(2, 3),
grid_shape=(tiletask_list[-1].grid_coord[0] + 1, tiletask_list[-1].grid_coord[1] + 1),
tile_spec={
"latent_height": latent_height,
"latent_width": latent_width,
"blend_height": blend_height,
"blend_width": blend_width,
"tile_latent_stride_height": tile_latent_stride_height,
"tile_latent_stride_width": tile_latent_stride_width,
},
output_dtype=next(self.parameters()).dtype,
)
return tiletask_list, grid_spec
def encode_tile_exec(self, task: TileTask) -> Tensor:
tile = task.tensor
z = self.reg(self.encoder(tile))
z = self.scale_factor * (z - self.shift_factor)
return z
def encode_tile_merge(self, coord_tensor_map: dict[tuple[int, ...], Tensor], grid_spec: GridSpec) -> Tensor:
grid_h, grid_w = grid_spec.grid_shape
result_rows = []
for i in range(grid_h):
result_row = []
for j in range(grid_w):
tile = coord_tensor_map[(i, j)]
if i > 0:
tile = self.blend_v(coord_tensor_map[(i - 1, j)], tile, grid_spec.tile_spec["blend_height"])
if j > 0:
tile = self.blend_h(coord_tensor_map[(i, j - 1)], tile, grid_spec.tile_spec["blend_width"])
result_row.append(
tile[
:,
:,
: grid_spec.tile_spec["tile_latent_stride_height"],
: grid_spec.tile_spec["tile_latent_stride_width"],
]
)
result_rows.append(torch.cat(result_row, dim=-1))
enc = torch.cat(result_rows, dim=-2)[
:,
:,
: grid_spec.tile_spec["latent_height"],
: grid_spec.tile_spec["latent_width"],
]
return enc
def encode(self, x: Tensor) -> Tensor:
if not self.is_distributed_enabled():
return super().encode(x)
logger.debug("Bagel VAE encode running with distributed executor")
return self.distributed_executor.execute(
x,
DistributedOperator(
split=self.encode_tile_split,
exec=self.encode_tile_exec,
merge=self.encode_tile_merge,
),
broadcast_result=True,
)
@@ -32,7 +32,7 @@ from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPi
from vllm_omni.diffusion.request import OmniDiffusionRequest from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
from .autoencoder import AutoEncoder, AutoEncoderParams from .autoencoder import AutoEncoder, AutoEncoderParams, DistributedAutoEncoder
from .bagel_transformer import Bagel, NaiveCache, Qwen2MoTConfig, Qwen2MoTForCausalLM from .bagel_transformer import Bagel, NaiveCache, Qwen2MoTConfig, Qwen2MoTForCausalLM
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -253,7 +253,7 @@ class BagelPipeline(nn.Module, SupportsComponentDiscovery, DiffusionPipelineProf
) )
self.transformer = self.language_model.model self.transformer = self.language_model.model
ae_params: AutoEncoderParams = default_ae_params() ae_params: AutoEncoderParams = default_ae_params()
self.vae = AutoEncoder(ae_params) self.vae = DistributedAutoEncoder(ae_params)
self.bagel = Bagel( self.bagel = Bagel(
language_model=self.language_model, language_model=self.language_model,
@@ -43,7 +43,6 @@ from vllm_omni.diffusion.distributed.sp_plan import (
) )
from vllm_omni.diffusion.forward_context import get_forward_context from vllm_omni.diffusion.forward_context import get_forward_context
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNorm from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNorm
from vllm_omni.diffusion.layers.norm import RMSNorm
from vllm_omni.diffusion.layers.rope import RotaryEmbedding from vllm_omni.diffusion.layers.rope import RotaryEmbedding
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -538,8 +537,8 @@ class QwenImageCrossAttention(nn.Module):
self.query_num_heads = self.to_qkv.num_heads self.query_num_heads = self.to_qkv.num_heads
self.kv_num_heads = self.to_qkv.num_kv_heads self.kv_num_heads = self.to_qkv.num_kv_heads
self.norm_q = RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity() self.norm_q = nn.RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity() self.norm_k = nn.RMSNorm(head_dim, eps=eps) if qk_norm else nn.Identity()
self.inner_dim = out_dim if out_dim is not None else head_dim * self.total_num_heads self.inner_dim = out_dim if out_dim is not None else head_dim * self.total_num_heads
@@ -576,8 +575,8 @@ class QwenImageCrossAttention(nn.Module):
prefix=_join_prefix(prefix, "to_out"), prefix=_join_prefix(prefix, "to_out"),
) )
self.norm_added_q = RMSNorm(head_dim, eps=eps) self.norm_added_q = nn.RMSNorm(head_dim, eps=eps)
self.norm_added_k = RMSNorm(head_dim, eps=eps) self.norm_added_k = nn.RMSNorm(head_dim, eps=eps)
self.attn = Attention( self.attn = Attention(
num_heads=self.query_num_heads, num_heads=self.query_num_heads,
@@ -992,7 +991,7 @@ class QwenImageTransformer2DModel(CachedTransformer):
quant_config=quant_config, quant_config=quant_config,
) )
self.txt_norm = RMSNorm(joint_attention_dim, eps=1e-6) self.txt_norm = nn.RMSNorm(joint_attention_dim, eps=1e-6)
# Entry projections (image/text) are kept full precision — # Entry projections (image/text) are kept full precision —
# small sensitive layers at the network boundary (see #2728). # small sensitive layers at the network boundary (see #2728).
@@ -402,12 +402,12 @@ class StableDiffusion3Pipeline(nn.Module, CFGParallelMixin, DiffusionPipelinePro
truncation=True, truncation=True,
add_special_tokens=True, add_special_tokens=True,
return_tensors="pt", return_tensors="pt",
).to(self.device) )
text_input_ids = text_inputs.input_ids text_input_ids = text_inputs.input_ids
untruncated_ids = self.tokenizer_3(prompt, padding="longest", return_tensors="pt").input_ids untruncated_ids = self.tokenizer_3(prompt, padding="longest", return_tensors="pt").input_ids
if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids): if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(text_input_ids, untruncated_ids):
removed_text = self.tokenizer_3.batch_decode(untruncated_ids[:, self.tokenizer_max_length - 1 : -1]) removed_text = self.tokenizer_3.batch_decode(untruncated_ids[:, max_sequence_length - 1 : -1])
logger.warning( logger.warning(
"The following part of your input was truncated because `max_sequence_length` is set to " "The following part of your input was truncated because `max_sequence_length` is set to "
f" {max_sequence_length} tokens: {removed_text}" f" {max_sequence_length} tokens: {removed_text}"
+1 -187
View File
@@ -1,5 +1,4 @@
import argparse import argparse
import dataclasses
import json import json
import os import os
import tempfile import tempfile
@@ -191,30 +190,6 @@ class OmniEngineArgs(EngineArgs):
load_omni_general_plugins() load_omni_general_plugins()
super().__post_init__() super().__post_init__()
@classmethod
def from_cli_args(cls, args: argparse.Namespace) -> "OmniEngineArgs":
attrs = [attr.name for attr in dataclasses.fields(cls)]
engine_args = cls(**{attr: getattr(args, attr) for attr in attrs if hasattr(args, attr)})
engine_args._explicit_fields = frozenset(
attr for attr in attrs if hasattr(args, attr) and getattr(args, attr) is not None
)
return engine_args
@classmethod
def create(cls, **explicit: Any) -> "OmniEngineArgs":
"""Tracks caller-set fields for ``Omni(..., engine_args=ea)``."""
ea = cls(**explicit)
ea._explicit_fields = frozenset(explicit.keys())
return ea
def explicit_kwargs(self) -> dict[str, Any]:
explicit = getattr(self, "_explicit_fields", None)
if explicit is None:
return {
f.name: getattr(self, f.name) for f in dataclasses.fields(self) if getattr(self, f.name) is not None
}
return {k: getattr(self, k) for k in explicit}
def _ensure_omni_models_registered(self): def _ensure_omni_models_registered(self):
if hasattr(self, "_omni_models_registered"): if hasattr(self, "_omni_models_registered"):
return True return True
@@ -420,8 +395,7 @@ class OrchestratorArgs:
forwarding). forwarding).
Fields that BOTH orchestrator and engine genuinely need (e.g. ``model``, Fields that BOTH orchestrator and engine genuinely need (e.g. ``model``,
``log_stats``) should be listed in ``SHARED_FIELDS`` below; ``split_kwargs`` ``log_stats``) should be listed in ``SHARED_FIELDS`` below.
will copy them to both buckets.
""" """
# === Lifecycle === # === Lifecycle ===
@@ -530,163 +504,3 @@ def internal_blacklist_keys() -> frozenset[str]:
dataclass — this function updates automatically. dataclass — this function updates automatically.
""" """
return orchestrator_field_names() - SHARED_FIELDS return orchestrator_field_names() - SHARED_FIELDS
def split_kwargs(
kwargs: dict[str, Any],
*,
engine_cls: type | None = None,
user_typed: set[str] | None = None,
strict: bool = False,
) -> tuple[OrchestratorArgs, dict[str, Any]]:
"""Partition CLI kwargs into (orchestrator, engine) buckets.
Args:
kwargs: Raw dict, typically ``vars(args)``.
engine_cls: Engine dataclass used to whitelist-filter the engine
bucket. Defaults to ``OmniEngineArgs``. Pass a custom class
for testing.
user_typed: Keys the user actually typed on the command line. Used
to warn when a user-typed flag is unclassifiable.
strict: If True, raise ``ValueError`` on ambiguous (double-classified
but not in ``SHARED_FIELDS``) fields. Default False to keep the
rollout non-breaking; flip to True in tests and CI.
Returns:
``(orchestrator_args, engine_kwargs)``. ``engine_kwargs`` has already
been whitelist-filtered against ``engine_cls`` — safe to pass directly
to ``engine_cls(**engine_kwargs)``.
"""
if engine_cls is None:
engine_cls = OmniEngineArgs
orch_fields = orchestrator_field_names()
engine_fields = {f.name for f in fields(engine_cls)}
orch_kwargs: dict[str, Any] = {}
engine_candidate: dict[str, Any] = {}
shared_values: dict[str, Any] = {}
unclassified: dict[str, Any] = {}
for key, value in kwargs.items():
in_orch = key in orch_fields
in_engine = key in engine_fields
is_shared = key in SHARED_FIELDS
if is_shared:
shared_values[key] = value
elif in_orch and in_engine:
# Declared in both but not marked shared → ambiguous.
msg = (
f"Field {key!r} is defined on both OrchestratorArgs and "
f"{engine_cls.__name__} but is not in SHARED_FIELDS. "
f"This causes double-routing. Either remove the duplicate or "
f"add {key!r} to SHARED_FIELDS if the sharing is intentional."
)
if strict:
raise ValueError(msg)
logger.error(msg)
# Default: treat as orchestrator-only to preserve existing behavior.
orch_kwargs[key] = value
elif in_orch:
orch_kwargs[key] = value
elif in_engine:
engine_candidate[key] = value
else:
unclassified[key] = value
# Warn on user-typed but unclassifiable flags so we don't silently drop
# something the user cared about (fixes the class of bug that spawned #873).
if unclassified and user_typed:
user_typed_unknown = sorted(k for k in unclassified if k in user_typed)
if user_typed_unknown:
logger.warning(
"CLI flags not consumed by vllm-omni and dropped before "
"per-stage engine construction: %s. If these are vllm "
"frontend/uvicorn flags (host, port, ssl_*, api_key, …) this "
"is expected; otherwise check your spelling.",
user_typed_unknown,
)
# Engine bucket: shared + engine-only. We do NOT pass through unclassified
# fields — that's exactly the server/uvicorn noise we want to shed.
engine_kwargs = {**shared_values, **engine_candidate}
# Construct the orchestrator dataclass. Shared fields that OrchestratorArgs
# also declares get copied into its constructor.
orch_init: dict[str, Any] = dict(orch_kwargs)
for key, value in shared_values.items():
if key in orch_fields:
orch_init[key] = value
orch_args = OrchestratorArgs(**orch_init)
return orch_args, engine_kwargs
def derive_server_dests_from_vllm_parser() -> frozenset[str]:
"""Derive the set of argparse dests that belong to vllm's frontend/server.
Returns every dest registered by ``make_arg_parser`` that is NOT a field
of ``OmniEngineArgs`` and NOT a field of ``OrchestratorArgs``. Useful for
CI tests to assert all CLI flags are classifiable without maintaining
a hardcoded server list.
Returns empty frozenset if vllm's parser cannot be built (e.g. in a
minimal test environment).
"""
try:
from vllm.entrypoints.openai.cli_args import make_arg_parser
from vllm.utils.argparse_utils import FlexibleArgumentParser
except ImportError:
logger.debug("Cannot import vllm parser — server-dest derivation skipped")
return frozenset()
try:
parser = make_arg_parser(FlexibleArgumentParser())
all_dests = {a.dest for a in parser._actions if a.dest and a.dest != "help"}
except Exception as exc:
logger.debug("Failed to build vllm parser: %s", exc)
return frozenset()
engine_fields = {f.name for f in fields(OmniEngineArgs)}
orch_fields = orchestrator_field_names()
return frozenset(all_dests - engine_fields - orch_fields - SHARED_FIELDS)
def orchestrator_args_from_argparse(args: Any) -> OrchestratorArgs:
"""Build an ``OrchestratorArgs`` from an ``argparse.Namespace``.
Only copies attributes that exist on the namespace — missing fields fall
back to the dataclass default. Useful when the full parser is already
built and ``vars(args)`` would include noise.
"""
kwargs: dict[str, Any] = {}
for f in fields(OrchestratorArgs):
if hasattr(args, f.name):
value = getattr(args, f.name)
if value is not None or f.default is None:
kwargs[f.name] = value
return OrchestratorArgs(**kwargs)
def nullify_stage_engine_defaults(parser: argparse.ArgumentParser) -> None:
"""Reset stage-level engine flag defaults to ``None``; preserve real
default in help text. Only deploy-YAML override fields are touched.
Idempotent."""
from vllm_omni.config.stage_config import deploy_override_field_names
override_dests = deploy_override_field_names()
for action in parser._actions:
if action.dest in ("help", "version") or not action.option_strings:
continue
if action.dest not in override_dests:
continue
if action.default is None or action.default is argparse.SUPPRESS:
continue
if action.help and "(default:" not in action.help and "%(default)" not in action.help:
action.help = f"{action.help} (default: {action.default})"
action.default = None
parser._omni_nullified = True # type: ignore[attr-defined]
+3 -22
View File
@@ -21,7 +21,7 @@ import weakref
from collections.abc import Awaitable, Callable, Mapping, Sequence from collections.abc import Awaitable, Callable, Mapping, Sequence
from contextlib import ExitStack from contextlib import ExitStack
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Any, Literal, cast from typing import Any, Literal, cast
import janus import janus
import torch import torch
@@ -113,9 +113,6 @@ from vllm_omni.inputs.data import OmniSamplingParams
from vllm_omni.metrics.prometheus import OmniRequestCounter from vllm_omni.metrics.prometheus import OmniRequestCounter
from vllm_omni.platforms import current_omni_platform from vllm_omni.platforms import current_omni_platform
if TYPE_CHECKING:
from vllm_omni.engine.arg_utils import OmniEngineArgs
logger = init_logger(__name__) logger = init_logger(__name__)
_STARTUP_POLL_INTERVAL_S = 1.0 _STARTUP_POLL_INTERVAL_S = 1.0
@@ -269,16 +266,17 @@ class AsyncOmniEngine:
def __init__( def __init__(
self, self,
model: str, model: str,
engine_args: OmniEngineArgs | None = None,
stage_init_timeout: int = 300, stage_init_timeout: int = 300,
init_timeout: int = 600, init_timeout: int = 600,
diffusion_batch_size: int = 1, diffusion_batch_size: int = 1,
single_stage_mode: bool = False, single_stage_mode: bool = False,
transfer_emitter: Any = None, transfer_emitter: Any = None,
log_stats: bool = False, log_stats: bool = False,
tokenizer: str | None = None,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
self.model = model self.model = model
self.tokenizer = tokenizer
self.diffusion_batch_size = diffusion_batch_size self.diffusion_batch_size = diffusion_batch_size
startup_timeout = int(init_timeout) startup_timeout = int(init_timeout)
# Forwarded into Orchestrator so its _forward_to_next_stage path can # Forwarded into Orchestrator so its _forward_to_next_stage path can
@@ -294,22 +292,6 @@ class AsyncOmniEngine:
logger.info(f"[AsyncOmniEngine] Initializing with model {model}") logger.info(f"[AsyncOmniEngine] Initializing with model {model}")
# Merge tracked engine_args fields into kwargs; explicit kwargs take priority.
if engine_args is not None:
if not hasattr(engine_args, "_explicit_fields"):
raise TypeError(
"engine_args=OmniEngineArgs(...) is ambiguous under "
"sentinel-default precedence. Use "
"OmniEngineArgs.create(**explicit) or pass explicit kwargs "
"directly."
)
ea_dict = engine_args.explicit_kwargs()
# Remove model since it is passed as a positional arg already.
ea_dict.pop("model", None)
kwargs = {**ea_dict, **kwargs}
self.tokenizer: str | None = kwargs.get("tokenizer")
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Single-stage mode detection # # Single-stage mode detection #
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@@ -2072,7 +2054,6 @@ class AsyncOmniEngine:
stage_configs_path = kwargs.get("stage_configs_path", None) stage_configs_path = kwargs.get("stage_configs_path", None)
deploy_config_path = kwargs.pop("deploy_config", None) deploy_config_path = kwargs.pop("deploy_config", None)
stage_overrides_json = kwargs.pop("stage_overrides", None) stage_overrides_json = kwargs.pop("stage_overrides", None)
kwargs.pop("_cli_explicit_keys", None)
explicit_stage_configs = kwargs.pop("stage_configs", None) explicit_stage_configs = kwargs.pop("stage_configs", None)
if explicit_stage_configs is not None: if explicit_stage_configs is not None:
logger.warning( logger.warning(
+13
View File
@@ -482,6 +482,15 @@ def prepare_engine_environment() -> None:
pass pass
def _maybe_set_qwen3_omni_moe_env(engine_args_dict: dict[str, Any]) -> None:
if (
engine_args_dict.get("model_arch") == "Qwen3OmniMoeForConditionalGeneration"
and "VLLM_USE_FLASHINFER_MOE_FP16" not in os.environ
):
os.environ["VLLM_USE_FLASHINFER_MOE_FP16"] = "0"
logger.info("[stage_init] Set VLLM_USE_FLASHINFER_MOE_FP16=0 for Qwen3-Omni stage")
def split_devices_for_replicas( def split_devices_for_replicas(
devices_str: str | None, devices_str: str | None,
num_replicas: int, num_replicas: int,
@@ -762,6 +771,10 @@ def build_engine_args_dict(
default_sp = _to_dict(getattr(stage_config, "default_sampling_params", {})) default_sp = _to_dict(getattr(stage_config, "default_sampling_params", {}))
engine_args_dict["has_sampling_extra_args"] = bool(default_sp.get("extra_args")) engine_args_dict["has_sampling_extra_args"] = bool(default_sp.get("extra_args"))
# TODO: Remove this after the performance regression is fixed
# Set VLLM_USE_FLASHINFER_MOE_FP16=0 for Qwen3-Omni to avoid performance regression
_maybe_set_qwen3_omni_moe_env(engine_args_dict)
return engine_args_dict return engine_args_dict
+2 -2
View File
@@ -9,7 +9,7 @@ from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
from vllm_omni.entrypoints.cli.benchmark.base import OmniBenchmarkSubcommandBase from vllm_omni.entrypoints.cli.benchmark.base import OmniBenchmarkSubcommandBase
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from vllm.utils.argparse_utils import FlexibleArgumentParser from vllm_omni.utils.tracking_parser import TrackingArgumentParser
class OmniBenchmarkSubcommand(CLISubcommand): class OmniBenchmarkSubcommand(CLISubcommand):
@@ -25,7 +25,7 @@ class OmniBenchmarkSubcommand(CLISubcommand):
def validate(self, args: argparse.Namespace) -> None: def validate(self, args: argparse.Namespace) -> None:
pass pass
def subparser_init(self, subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser: def subparser_init(self, subparsers: argparse._SubParsersAction) -> TrackingArgumentParser:
bench_parser = subparsers.add_parser( bench_parser = subparsers.add_parser(
self.name, description=self.help, usage=f"vllm {self.name} <bench_type> [options]" self.name, description=self.help, usage=f"vllm {self.name} <bench_type> [options]"
) )
+2 -2
View File
@@ -24,10 +24,10 @@ def main():
os.environ["VLLM_LOGGING_COLOR"] = "1" os.environ["VLLM_LOGGING_COLOR"] = "1"
from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG, cli_env_setup from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG, cli_env_setup
from vllm.utils.argparse_utils import FlexibleArgumentParser
import vllm_omni.entrypoints.cli.benchmark.main import vllm_omni.entrypoints.cli.benchmark.main
import vllm_omni.entrypoints.cli.serve import vllm_omni.entrypoints.cli.serve
from vllm_omni.utils.tracking_parser import TrackingArgumentParser
CMD_MODULES = [ CMD_MODULES = [
vllm_omni.entrypoints.cli.serve, vllm_omni.entrypoints.cli.serve,
@@ -40,7 +40,7 @@ def main():
_ensure_vllm_platform() _ensure_vllm_platform()
parser = FlexibleArgumentParser( parser = TrackingArgumentParser(
description="vLLM OMNI CLI", description="vLLM OMNI CLI",
epilog=VLLM_SUBCMD_PARSER_EPILOG.format(subcmd="[subcommand]"), epilog=VLLM_SUBCMD_PARSER_EPILOG.format(subcmd="[subcommand]"),
) )
+18 -35
View File
@@ -19,11 +19,10 @@ from vllm.entrypoints.cli.types import CLISubcommand
from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args 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.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
from vllm_omni.entrypoints.cli.logo import log_logo from vllm_omni.entrypoints.cli.logo import log_logo
from vllm_omni.entrypoints.openai.api_server import omni_run_server from vllm_omni.entrypoints.openai.api_server import omni_run_server
from vllm_omni.utils.tracking_parser import TrackingArgumentParser, TrackingNamespace
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -84,10 +83,10 @@ class OmniServeCommand(CLISubcommand):
name = "serve" name = "serve"
# Parser stashed at subparser_init so ``cmd`` can resolve each user-typed # Parser stashed at subparser_init so ``cmd`` can resolve each user-typed
# flag to its real ``dest`` via the parser's action table. # flag to its real ``dest`` via the parser's action table.
_parser: FlexibleArgumentParser | None = None _parser: TrackingArgumentParser
@staticmethod @staticmethod
def cmd(args: argparse.Namespace) -> None: def cmd(args: TrackingNamespace) -> None:
if not os.environ.get("VLLM_DISABLE_LOG_LOGO"): if not os.environ.get("VLLM_DISABLE_LOG_LOGO"):
os.environ["VLLM_DISABLE_LOG_LOGO"] = "1" os.environ["VLLM_DISABLE_LOG_LOGO"] = "1"
log_logo() log_logo()
@@ -176,7 +175,7 @@ class OmniServeCommand(CLISubcommand):
return return
validate_parsed_serve_args(args) validate_parsed_serve_args(args)
def subparser_init(self, subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser: def subparser_init(self, subparsers: argparse._SubParsersAction) -> TrackingArgumentParser:
serve_parser = subparsers.add_parser( serve_parser = subparsers.add_parser(
self.name, self.name,
description=DESCRIPTION, description=DESCRIPTION,
@@ -682,22 +681,10 @@ class OmniServeCommand(CLISubcommand):
# sandboxed globals dict via ``DummySelf``) doesn't fail on a NameError. # sandboxed globals dict via ``DummySelf``) doesn't fail on a NameError.
type(self)._parser = serve_parser type(self)._parser = serve_parser
nullify_stage_engine_defaults(serve_parser)
return serve_parser return serve_parser
def _create_default_diffusion_stage_cfg(args: argparse.Namespace) -> list[dict[str, Any]]: def run_headless(args: TrackingNamespace) -> None:
"""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. """Run a single stage in headless mode.
Honors ``--omni-dp-size-local``: launches that many replicas locally for Honors ``--omni-dp-size-local``: launches that many replicas locally for
@@ -738,43 +725,39 @@ def run_headless(args: argparse.Namespace) -> None:
stage_id: int | None = args.stage_id stage_id: int | None = args.stage_id
omni_master_address: str | None = args.omni_master_address omni_master_address: str | None = args.omni_master_address
omni_master_port: int | None = args.omni_master_port omni_master_port: int | None = args.omni_master_port
worker_backend: str | None = args.worker_backend
stage_configs_path: str | None = args.stage_configs_path
omni_replica_address: str | None = getattr(args, "omni_replica_address", None) omni_replica_address: str | None = getattr(args, "omni_replica_address", None)
omni_dp_size_local: int = max(1, int(getattr(args, "omni_dp_size_local", 1) or 1)) omni_dp_size_local: int = max(1, int(getattr(args, "omni_dp_size_local", 1) or 1))
if not model:
raise ValueError("Failed to pass model from kwargs")
if stage_id is None: if stage_id is None:
raise ValueError("--stage-id is required in headless mode") 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 worker_backend != "multi_process":
raise ValueError("headless mode requires worker_backend=multi_process")
# Filter down to a dict of things explicitly requested by the user
args_dict = args.get_explicit_kwargs_dict()
# ``--replica-id`` is deprecated and ignored — replica ids are # ``--replica-id`` is deprecated and ignored — replica ids are
# auto-assigned by ``OmniMasterServer`` so headless processes carry # auto-assigned by ``OmniMasterServer`` so headless processes carry
# no knowledge of their per-replica id at launch time. Warn (don't # no knowledge of their per-replica id at launch time. Warn (don't
# error) when the operator still supplies it so existing launchers # error) when the operator still supplies it so existing launchers
# keep working with a single log line. # keep working with a single log line.
explicit_cli_keys: set[str] = getattr(args, "_cli_explicit_keys", set()) or set() if "replica_id" in args_dict:
if "replica_id" in explicit_cli_keys:
logger.warning( logger.warning(
"[Headless] --replica-id is deprecated and ignored " "[Headless] --replica-id is deprecated and ignored "
"(supplied value: %s). Replica ids are auto-assigned by the " "(supplied value: %s). Replica ids are auto-assigned by the "
"master server.", "master server.",
args.replica_id, args.replica_id,
) )
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")
api_server_count = args.api_server_count or 0
if 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()
args_dict.pop("_cli_explicit_keys", None)
# Forward ``--deploy-config`` so the headless reads the same YAML the
# head was launched with — otherwise ``load_and_resolve_stage_configs``
# falls back to ``vllm_omni/deploy/<model>.yaml`` and the headless's
# view of ``stage.runtime.devices`` diverges from the head's, breaking
# the per-replica device split.
config_path, stage_configs = load_and_resolve_stage_configs( config_path, stage_configs = load_and_resolve_stage_configs(
model, model,
args_dict.get("stage_configs_path"), stage_configs_path,
args_dict, args_dict,
deploy_config_path=args_dict.get("deploy_config"), deploy_config_path=args_dict.get("deploy_config"),
) )
+29 -39
View File
@@ -1,13 +1,10 @@
from __future__ import annotations from __future__ import annotations
import argparse
import os import os
import sys
import time import time
import warnings
import weakref import weakref
from collections.abc import Sequence from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Literal from typing import Any, Literal
import huggingface_hub import huggingface_hub
from vllm.logger import init_logger from vllm.logger import init_logger
@@ -29,9 +26,7 @@ from vllm_omni.metrics.stats import OrchestratorAggregator as OrchestratorMetric
from vllm_omni.metrics.transfer import OmniTransferMetrics from vllm_omni.metrics.transfer import OmniTransferMetrics
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
from vllm_omni.outputs import OmniRequestOutput from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.utils.tracking_parser import TrackingNamespace
if TYPE_CHECKING:
from vllm_omni.engine.arg_utils import OmniEngineArgs
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -105,48 +100,44 @@ class OmniBase(PDDisaggregationMixin):
@classmethod @classmethod
def from_cli_args( def from_cli_args(
cls, cls,
args: argparse.Namespace, args: TrackingNamespace,
*, model: str | None = None,
parser: argparse.ArgumentParser | None = None,
**overrides: Any,
) -> OmniBase: ) -> OmniBase:
"""Deprecated argparse builder. """Build from a TrackingNamespace parsed by TrackingArgumentParser.
Only args that are explicitly passed to parse_args are forwarded.
Build from argparse. If ``parser`` is passed and not yet nullified,
un-typed engine fields are reset to ``None``. New callers should
nullify deploy-overriding parser defaults with
``nullify_stage_engine_defaults(parser)`` and construct Omni/AsyncOmni
directly.
""" """
warnings.warn( if not isinstance(args, TrackingNamespace):
"`from_cli_args()` is deprecated. Nullify deploy-overriding parser defaults " raise TypeError(
"with `nullify_stage_engine_defaults(parser)` and construct Omni/AsyncOmni " f"expected args to be of type TrackingNamespace, got {type(args)}. "
"directly from `vars(args)`.", "Hint: did you parse your args with TrackingArgumentParser?"
DeprecationWarning, )
stacklevel=2,
)
kwargs: dict[str, Any] = {k: v for k, v in vars(args).items() if not k.startswith("_")}
if parser is not None and not getattr(parser, "_omni_nullified", False): explicit_kwargs = args.get_explicit_kwargs_dict()
from vllm_omni.config.stage_config import deploy_override_field_names args_model = explicit_kwargs.pop("model", None) or args.model
from vllm_omni.entrypoints.utils import detect_explicit_cli_keys if model is not None and args_model is not None and model != args_model:
raise ValueError(
f"explicit model kwarg and args.model were both provided, but do not match [{model} != {args_model}]"
)
explicit = detect_explicit_cli_keys(sys.argv[1:], parser) or set() if model is None and args_model is None:
override_dests = deploy_override_field_names() raise ValueError(
for key in list(kwargs): "model must be explicitly passed as a parsed arg in the TrackingNamespace or directly provided."
if key in override_dests and key not in explicit: )
kwargs[key] = None
kwargs.update(overrides) resolved_model = model or args_model
return cls(**kwargs) return cls(model=resolved_model, **explicit_kwargs)
def __init__( def __init__(
self, self,
model: str, model: str,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
engine_args: OmniEngineArgs | None = kwargs.pop("engine_args", None) if "engine_args" in kwargs:
logger.warning(
"engine_args were passed as a kwarg to an Omni instance; this is not supported. "
"You should instead, pass the keyword arguments used to initialize the engine args "
"directly to this object's initializer."
)
stage_init_timeout = kwargs.pop("stage_init_timeout", 300) stage_init_timeout = kwargs.pop("stage_init_timeout", 300)
init_timeout = kwargs.pop("init_timeout", 600) init_timeout = kwargs.pop("init_timeout", 600)
log_stats = kwargs.pop("log_stats", False) log_stats = kwargs.pop("log_stats", False)
@@ -180,7 +171,6 @@ class OmniBase(PDDisaggregationMixin):
st = time.time() st = time.time()
self.engine = AsyncOmniEngine( self.engine = AsyncOmniEngine(
model=model, model=model,
engine_args=engine_args,
init_timeout=init_timeout, init_timeout=init_timeout,
stage_init_timeout=stage_init_timeout, stage_init_timeout=stage_init_timeout,
diffusion_batch_size=diffusion_batch_size, diffusion_batch_size=diffusion_batch_size,
+9 -10
View File
@@ -136,6 +136,7 @@ from vllm_omni.entrypoints.openai.utils import get_stage_type, parse_lora_reques
from vllm_omni.entrypoints.openai.video_api_utils import decode_input_reference from vllm_omni.entrypoints.openai.video_api_utils import decode_input_reference
from vllm_omni.entrypoints.openpi.serving import ServingRealtimeRobotOpenPI from vllm_omni.entrypoints.openpi.serving import ServingRealtimeRobotOpenPI
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
from vllm_omni.utils.tracking_parser import TrackingArgumentParser, TrackingNamespace
logger = init_logger(__name__) logger = init_logger(__name__)
router = APIRouter() router = APIRouter()
@@ -510,7 +511,7 @@ async def omni_run_server_worker(listen_address, sock, args, client_config=None,
@asynccontextmanager @asynccontextmanager
async def build_async_omni( async def build_async_omni(
args: Namespace, args: TrackingNamespace,
*, *,
disable_frontend_multiprocessing: bool | None = None, disable_frontend_multiprocessing: bool | None = None,
client_config: dict[str, Any] | None = None, client_config: dict[str, Any] | None = None,
@@ -550,7 +551,7 @@ async def build_async_omni(
@asynccontextmanager @asynccontextmanager
async def build_async_omni_from_stage_config( async def build_async_omni_from_stage_config(
args: Namespace, args: TrackingNamespace,
*, *,
disable_frontend_multiprocessing: bool = False, disable_frontend_multiprocessing: bool = False,
) -> AsyncIterator[EngineClient]: ) -> AsyncIterator[EngineClient]:
@@ -604,9 +605,9 @@ async def build_async_omni_from_stage_config(
logger.debug("Pre-loading transformers_modules failed: %s", e) logger.debug("Pre-loading transformers_modules failed: %s", e)
try: try:
kwargs = vars(args).copy() kwargs = args.get_explicit_kwargs_dict()
kwargs.pop("model", None) model = kwargs.pop("model", None) or args.model
async_omni = AsyncOmni(model=args.model, **kwargs) async_omni = AsyncOmni(model=model, **kwargs)
# # Don't keep the dummy data in memory # # Don't keep the dummy data in memory
# await async_llm.reset_mm_cache() # await async_llm.reset_mm_cache()
@@ -3040,25 +3041,23 @@ async def omni_wakeup(request: OmniWakeupRequest, raw_request: Request):
if __name__ == "__main__": if __name__ == "__main__":
import argparse
import asyncio import asyncio
from vllm.entrypoints.openai.cli_args import make_arg_parser from vllm.entrypoints.openai.cli_args import make_arg_parser
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults parser = TrackingArgumentParser(description="vLLM-Omni OpenAI-Compatible REST API server")
parser = argparse.ArgumentParser(description="vLLM-Omni OpenAI-Compatible REST API server")
parser = make_arg_parser(parser) parser = make_arg_parser(parser)
registered_flags = set() registered_flags = set()
for action in parser._actions: for action in parser._actions:
registered_flags.update(action.option_strings) registered_flags.update(action.option_strings)
# FIXME - this is broken on `main`. `make_arg_parser` does not handle omni engine args properly.
if "--omni" not in registered_flags: if "--omni" not in registered_flags:
parser.add_argument("--omni", action="store_true", default=False, help="Enable vLLM-Omni mode.") parser.add_argument("--omni", action="store_true", default=False, help="Enable vLLM-Omni mode.")
if "--enable-sleep-mode" not in registered_flags: if "--enable-sleep-mode" not in registered_flags:
parser.add_argument( parser.add_argument(
"--enable-sleep-mode", action="store_true", default=False, help="Enable GPU memory pool for sleep mode." "--enable-sleep-mode", action="store_true", default=False, help="Enable GPU memory pool for sleep mode."
) )
nullify_stage_engine_defaults(parser)
args = parser.parse_args() args = parser.parse_args()
if not hasattr(args, "model_tag"): if not hasattr(args, "model_tag"):
setattr(args, "model_tag", args.model) setattr(args, "model_tag", args.model)
-77
View File
@@ -1,4 +1,3 @@
import argparse
import os import os
import types import types
from collections import Counter from collections import Counter
@@ -24,81 +23,11 @@ PROJECT_ROOT = Path(__file__).parent.parent.parent
logger = init_logger(__name__) logger = init_logger(__name__)
def _warn_deprecated_explicit_keys(kwargs: dict[str, Any]) -> None:
if "cli_explicit_keys" in kwargs:
import warnings
warnings.warn(
"cli_explicit_keys= is deprecated and ignored. Remove the kwarg.",
DeprecationWarning,
stacklevel=3,
)
_DIFFUSERS_CLASS_TO_CONFIG: dict[str, str] = { _DIFFUSERS_CLASS_TO_CONFIG: dict[str, str] = {
"GlmImagePipeline": "glm_image", "GlmImagePipeline": "glm_image",
} }
def detect_explicit_cli_keys(
argv: list[str],
parser: argparse.ArgumentParser | None = None,
) -> set[str]:
"""Walk ``argv`` and return the set of ``dest`` attribute names the user
explicitly provided (e.g. ``--max-num-seqs 64`` → ``max_num_seqs``).
Used to distinguish user-typed CLI args from argparse default values so
deploy YAMLs are not silently overridden by parser defaults. Shared
across online (``vllm serve``) and offline (scripts, examples, tests,
CI) entry points — offline callers that parse CLI args via argparse
should invoke this on ``sys.argv[1:]`` and pass the result through to
``AsyncOmni`` / ``Omni`` via the ``_cli_explicit_keys`` kwarg.
When ``parser`` is provided, each token is looked up in the parser's
action table to find its real ``dest``. This correctly handles flags
with ``dest=`` overrides, alias flags (e.g. ``--usp`` /
``--ulysses-degree`` both mapping to ``ulysses_degree``), and
``--disable-foo`` / ``store_false`` patterns that map to a differently
named dest. Callers with access to an ``argparse.ArgumentParser`` should
always pass it.
When ``parser`` is ``None``, a name-based heuristic is used as a
fallback (hyphens → underscores, plus a ``no_`` prefix strip for
``argparse.BooleanOptionalAction``). This is correct for simple flags
but silently misidentifies ``--disable-X``-style flags and explicit
``dest=`` overrides, so prefer the parser-aware form.
"""
if parser is not None:
dest_map: dict[str, str] = {}
for action in parser._actions:
for opt in action.option_strings:
dest_map[opt] = action.dest
explicit: set[str] = set()
for tok in argv:
if not tok.startswith("--"):
continue
flag = tok.split("=", 1)[0]
dest = dest_map.get(flag)
if dest is not None:
explicit.add(dest)
return explicit
# Fallback: name-based heuristic (legacy path for callers without a parser).
explicit = set()
for tok in argv:
if not tok.startswith("--"):
continue
name = tok[2:].split("=", 1)[0]
if not name:
continue
attr = name.replace("-", "_")
explicit.add(attr)
# BooleanOptionalAction: --no-foo records as dest `foo`, not `no_foo`.
if attr.startswith("no_"):
explicit.add(attr[3:])
return explicit
def inject_omni_kv_config(stage: Any, omni_conn_cfg: dict[str, Any], omni_from: str, omni_to: str) -> None: def inject_omni_kv_config(stage: Any, omni_conn_cfg: dict[str, Any], omni_from: str, omni_to: str) -> None:
"""Inject connector configuration into stage engine arguments.""" """Inject connector configuration into stage engine arguments."""
# Prepare omni_kv_config dict # Prepare omni_kv_config dict
@@ -383,7 +312,6 @@ def load_stage_configs_from_model(
base_engine_args: dict | None = None, base_engine_args: dict | None = None,
deploy_config_path: str | None = None, deploy_config_path: str | None = None,
stage_overrides: dict[str, dict[str, Any]] | None = None, stage_overrides: dict[str, dict[str, Any]] | None = None,
**deprecated_kwargs: Any,
) -> list: ) -> list:
"""Load stage configurations from model's default config file. """Load stage configurations from model's default config file.
@@ -402,8 +330,6 @@ def load_stage_configs_from_model(
Returns: Returns:
List of stage configuration dictionaries List of stage configuration dictionaries
""" """
_warn_deprecated_explicit_keys(deprecated_kwargs)
if base_engine_args is None: if base_engine_args is None:
base_engine_args = {} base_engine_args = {}
@@ -577,7 +503,6 @@ def load_and_resolve_stage_configs(
default_stage_cfg_factory: Any = None, default_stage_cfg_factory: Any = None,
deploy_config_path: str | None = None, deploy_config_path: str | None = None,
stage_overrides: dict[str, dict[str, Any]] | None = None, stage_overrides: dict[str, dict[str, Any]] | None = None,
**deprecated_kwargs: Any,
) -> tuple[str, list]: ) -> tuple[str, list]:
"""Load stage configurations from model or YAML file with fallback to defaults. """Load stage configurations from model or YAML file with fallback to defaults.
@@ -621,8 +546,6 @@ def load_and_resolve_stage_configs(
stage_configs_path, stage_configs_path,
) )
_warn_deprecated_explicit_keys(deprecated_kwargs)
if deploy_config_path is not None: if deploy_config_path is not None:
config_path = deploy_config_path config_path = deploy_config_path
stage_configs = load_stage_configs_from_model( stage_configs = load_stage_configs_from_model(
+173
View File
@@ -0,0 +1,173 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import argparse
from typing import Any
from vllm.utils.argparse_utils import FlexibleArgumentParser
UNSET = object()
class TrackingNamespace(argparse.Namespace):
"""Proxy that wraps an argparse namespace with explicit keys, which
can be filtered down to a dict containing only explicitly passed values.
"""
def __init__(self, unfiltered_ns: argparse.Namespace, explicit_keys: frozenset[str]) -> None:
# We never have nested tracking namespaces, but explicitly guard
# against them to prevent bad behavior with nested __dict__ overrides.
if isinstance(unfiltered_ns, TrackingNamespace):
raise ValueError("Tracking namespaces cannot be nested")
self.unfiltered_ns = unfiltered_ns
self.explicit_keys = explicit_keys
def __setattr__(self, name: str, value: Any) -> None:
if name in ("unfiltered_ns", "explicit_keys"):
object.__setattr__(self, name, value)
else:
setattr(self.unfiltered_ns, name, value)
def get_explicit_kwargs_dict(self):
"""Return a dict containing only the explicitly passed key-value pairs."""
return {k: v for k, v in vars(self.unfiltered_ns).items() if k in self.explicit_keys}
def __getattr__(self, name: str) -> Any:
return getattr(self.unfiltered_ns, name)
@property
def __dict__(self):
# NOTE: We do this so that vars() etc forward directly into the encapsulated namespace,
# which makes this class a drop-in replacement for the original namespace, while also
# ensuring that updates to the encapsulated namespace are correctly reflected.
return self.unfiltered_ns.__dict__
class TrackingGroup:
"""Proxy that wraps an argument group and its corresponding shadow group."""
def __init__(
self,
real_group: argparse._ArgumentGroup,
shadow_group: argparse._ArgumentGroup,
):
self._real = real_group
self._shadow = shadow_group
def add_argument(self, *args: Any, **kwargs: Any) -> argparse.Action:
"""Add an argument to the real group and to the shadow group."""
action = self._real.add_argument(*args, **kwargs)
default_kwargs = {**kwargs, "default": UNSET}
self._shadow.add_argument(*args, **default_kwargs)
return action
def __getattr__(self, name: str) -> Any:
# Any attribute access is forwarded to the real argument group.
return getattr(self._real, name)
class TrackingSubparsers:
"""Proxy that wraps a subparser and its corresponding shadow subparser."""
def __init__(
self,
real_sub: argparse._SubParsersAction,
shadow_sub: argparse._SubParsersAction,
):
self._real = real_sub
self._shadow = shadow_sub
def add_parser(self, name, *args, **kwargs):
"""Add a parser to the encapsulated real parser and its shadow."""
real_parser = self._real.add_parser(name, *args, **kwargs)
# real_parser is a TrackingArgumentParser with its own _shadow.
# Reuse that shadow as the parent shadow's child — so when
# real_parser.add_argument() mirrors to real_parser._shadow,
# the parent's shadow sees it too.
self._shadow._name_parser_map[name] = real_parser._shadow
return real_parser
def __getattr__(self, name: str) -> Any:
# Any attribute access is forwarded to the real subparser.
return getattr(self._real, name)
class TrackingArgumentParser(FlexibleArgumentParser):
"""Drop-in replacement for FlexibleArgumentParser, which tracks keys that
were explicitly passed as args on the parser namespace.
Unfortunately, Argparse does not provide an easy way of doing this without
depending on a lot of internal attributes, so we implement it by instead
using a 'shadow' parser, which is essentially a clone of the parser, where
defaults are overridden to `None`. By comparing the parser against its
shadow, we can tell which values were passed in a non-destructive manner.
"""
def __init__(self, *args, **kwargs):
# NOTE: We have to define the shadow parser before calling init,
# with add_help=False, since otherwise init will call add_argument
# and delegate to the override on this class and cause problems.
shadow_kwargs = {**kwargs, "add_help": False}
self._shadow = FlexibleArgumentParser(*args, **shadow_kwargs)
super().__init__(*args, **kwargs)
def add_argument(self, *args: Any, **kwargs: Any) -> argparse.Action:
"""Add an arg to the parser & the shadow, where the latter has UNSET for the default."""
action = super().add_argument(*args, **kwargs)
shadow_kwargs = {**kwargs, "default": UNSET}
self._shadow.add_argument(*args, **shadow_kwargs)
return action
def add_argument_group(self, *args, **kwargs) -> TrackingGroup:
real_group = super().add_argument_group(*args, **kwargs)
shadow_group = self._shadow.add_argument_group(*args, **kwargs)
return TrackingGroup(real_group, shadow_group)
def add_mutually_exclusive_group(self, *args, **kwargs) -> TrackingGroup:
real_group = super().add_mutually_exclusive_group(*args, **kwargs)
shadow_group: argparse._MutuallyExclusiveGroup = self._shadow.add_mutually_exclusive_group(*args, **kwargs)
return TrackingGroup(real_group, shadow_group)
def add_subparsers(self, *args, **kwargs) -> TrackingSubparsers:
real_sub = super().add_subparsers(*args, **kwargs)
shadow_sub = self._shadow.add_subparsers(*args, **kwargs)
return TrackingSubparsers(real_sub, shadow_sub)
def build_tracking_namespace(self, real_ns: argparse.Namespace, shadow_ns: argparse.Namespace) -> TrackingNamespace:
"""Build a tracking namespace for the real / shadow namespaces."""
explicit_keys = frozenset(k for k, v in vars(shadow_ns).items() if v is not UNSET)
return TrackingNamespace(real_ns, explicit_keys)
def parse_args(
self,
args: list[str] | None = None,
namespace: argparse.Namespace | None = None,
) -> TrackingNamespace:
"""Parse the args on the real/shadow parser."""
# Only the real parser should use the namespace if one is,
# given since shadow parser will set its own defaults to None.
real_ns = super().parse_args(args, namespace)
shadow_ns = self._shadow.parse_args(args)
if real_ns is None or shadow_ns is None:
raise ValueError("Parse args created empty namespaces")
# If this is called through parse_known_args on self, we will already
# get a TrackingNamespace back, which will already have set the explicit
# keys through build_tracking_namespace, so no need to do it again.
if isinstance(real_ns, TrackingNamespace):
return real_ns
return self.build_tracking_namespace(real_ns, shadow_ns)
def parse_known_args(
self,
args: list[str] | None = None,
namespace: argparse.Namespace | None = None,
) -> tuple[TrackingNamespace, list[str]]:
"""Parse the known args on the real/shadow parser."""
real_ns, remaining = super().parse_known_args(args, namespace)
shadow_ns, _ = self._shadow.parse_known_args(args)
tracked_ns = self.build_tracking_namespace(real_ns, shadow_ns)
return tracked_ns, remaining