Merge branch 'main' into feat/diffusion-prefetch-protection
This commit is contained in:
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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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`) |
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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,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]
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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]"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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]"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user