Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
380127049a | ||
|
|
743953b821 | ||
|
|
86166fd988 | ||
|
|
039808e0d9 | ||
|
|
e8526cc552 | ||
|
|
06541d1f52 | ||
|
|
1cd92553cf | ||
|
|
32370968d0 | ||
|
|
946b843e61 | ||
|
|
f26f9c3a7f | ||
|
|
ea3cf99894 | ||
|
|
44d3ae100a | ||
|
|
ae70479619 | ||
|
|
142151fef5 | ||
|
|
5d7fdf9350 | ||
|
|
d4096e01fe | ||
|
|
13c617a140 | ||
|
|
66bbebd9d3 | ||
|
|
1e74807c35 | ||
|
|
ae57e406fd | ||
|
|
b6b992ac7b | ||
|
|
66d938100b | ||
|
|
348fd0fc07 | ||
|
|
ea14b04e9f | ||
|
|
0f9ee6afd8 | ||
|
|
22cefe04b4 | ||
|
|
7493cbd3c2 | ||
|
|
41f4f9d8c9 | ||
|
|
10701e1838 | ||
|
|
0e903e9695 | ||
|
|
175dec11be | ||
|
|
8d98cc0cb7 | ||
|
|
4179b0cae1 | ||
|
|
e43c787544 | ||
|
|
fda3af8bbc |
No files matched your search
@@ -3,7 +3,6 @@
|
||||
|
||||
env:
|
||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||
HF_HUB_DOWNLOAD_TIMEOUT: 300
|
||||
HF_HUB_ETAG_TIMEOUT: 60
|
||||
|
||||
@@ -80,6 +79,7 @@ steps:
|
||||
steps:
|
||||
- label: "TTS · Qwen3-TTS CustomVoice Test"
|
||||
source_file_dependencies:
|
||||
- tests/helpers/media.py
|
||||
- tests/e2e/online_serving/test_qwen3_tts_customvoice.py
|
||||
- tests/e2e/offline_inference/test_qwen3_tts_customvoice.py
|
||||
- vllm_omni/model_executor/models/qwen3_tts/
|
||||
@@ -102,6 +102,7 @@ steps:
|
||||
|
||||
- label: "TTS · Qwen3-TTS Base Test"
|
||||
source_file_dependencies:
|
||||
- tests/helpers/media.py
|
||||
- tests/e2e/online_serving/test_qwen3_tts_base.py
|
||||
- tests/e2e/offline_inference/test_qwen3_tts_base.py
|
||||
- vllm_omni/model_executor/models/qwen3_tts/
|
||||
@@ -162,10 +163,11 @@ steps:
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5.py -m 'advanced_model and cuda' --run-level 'advanced_model'
|
||||
# h100_2, not h100_1: advanced_model asserts transcribe every audio
|
||||
# response with Whisper, and tests/helpers/media.py only uses an
|
||||
# accelerator for Whisper when a spare GPU exists. On a single card it
|
||||
# grades on CPU: ~8-10 min per clip vs ~30 s, which alone overruns the
|
||||
# 50-minute job timeout (build 2956).
|
||||
# response with Whisper, and tests/helpers/media.py requires at least
|
||||
# 16 GiB free VRAM on a device to run Whisper on GPU. When the model
|
||||
# server leaves insufficient memory, Whisper falls back to CPU:
|
||||
# ~8-10 min per clip vs ~30 s, which overruns the 50-minute job timeout
|
||||
# (build 2956). h100_2 guarantees a second GPU with ample free VRAM.
|
||||
mirror_hardwares: h100_2
|
||||
|
||||
- label: "Omni · MiniCPM-o 4.5 Duplex Test"
|
||||
@@ -181,6 +183,21 @@ steps:
|
||||
- tests/e2e/online_serving/run_minicpmo_realtime_duplex_soft_interrupt.py
|
||||
- vllm_omni/deploy/minicpmo_4_5.yaml
|
||||
- tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py
|
||||
- vllm_omni/engine/duplex/
|
||||
- vllm_omni/entrypoints/duplex/
|
||||
# Public duplex client library + its unit suites: client-side
|
||||
# changes must re-run the duplex e2e group. The live client probe
|
||||
# (test_duplex_client_live.py, single GPU, core tier) runs in the
|
||||
# ready pipeline's duplex group.
|
||||
- vllm_omni/clients/
|
||||
- tests/clients/
|
||||
- vllm_omni/entrypoints/duplex_request_client.py
|
||||
- vllm_omni/outputs/duplex.py
|
||||
- vllm_omni/model_executor/duplex_sampling.py
|
||||
- tests/engine/duplex/
|
||||
- tests/entrypoints/duplex/
|
||||
- tests/model_executor/models/minicpmo_4_5/duplex/
|
||||
# Legacy Realtime probe client + video stacking used by the e2e drivers.
|
||||
- vllm_omni/experimental/fullduplex/
|
||||
- vllm_omni/model_executor/models/minicpmo_4_5/
|
||||
- vllm_omni/model_executor/models/step_audio2/
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
env:
|
||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||
HF_HUB_DOWNLOAD_TIMEOUT: 300
|
||||
HF_HUB_ETAG_TIMEOUT: 60
|
||||
|
||||
@@ -344,6 +343,12 @@ steps:
|
||||
- pytest -s -v tests/e2e/online_serving/test_hunyuan_video_15_expansion.py -m "full_model and cuda" --run-level "full_model"
|
||||
mirror_hardwares: h100_2
|
||||
|
||||
- label: ":full_moon: Diffusion X2V · MAGI-2 Preview Serving Smoke"
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/online_serving/test_magi2.py -m "advanced_model and diffusion and cuda and H100 and cards_4" --run-level "advanced_model"
|
||||
mirror_hardwares: h100_4
|
||||
|
||||
- label: ":full_moon: Diffusion X2V · Doc Test"
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
env:
|
||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||
HF_HUB_DOWNLOAD_TIMEOUT: 300
|
||||
HF_HUB_ETAG_TIMEOUT: 60
|
||||
|
||||
@@ -215,6 +214,21 @@ steps:
|
||||
- tests/e2e/online_serving/run_minicpmo_realtime_duplex_soft_interrupt.py
|
||||
- vllm_omni/deploy/minicpmo_4_5.yaml
|
||||
- tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py
|
||||
- vllm_omni/engine/duplex/
|
||||
- vllm_omni/entrypoints/duplex/
|
||||
# Public duplex client library + its unit suites: client-side
|
||||
# changes must re-run the duplex e2e group, which includes the
|
||||
# live client probe (test_duplex_client_live.py, single GPU).
|
||||
- vllm_omni/clients/
|
||||
- tests/clients/
|
||||
- tests/e2e/online_serving/test_duplex_client_live.py
|
||||
- vllm_omni/entrypoints/duplex_request_client.py
|
||||
- vllm_omni/outputs/duplex.py
|
||||
- vllm_omni/model_executor/duplex_sampling.py
|
||||
- tests/engine/duplex/
|
||||
- tests/entrypoints/duplex/
|
||||
- tests/model_executor/models/minicpmo_4_5/duplex/
|
||||
# Legacy Realtime probe client + video stacking used by the e2e drivers.
|
||||
- vllm_omni/experimental/fullduplex/
|
||||
- vllm_omni/model_executor/models/minicpmo_4_5/
|
||||
- vllm_omni/model_executor/models/step_audio2/
|
||||
@@ -232,8 +246,8 @@ steps:
|
||||
- vllm_omni/config/stage_config.py
|
||||
commands:
|
||||
- |
|
||||
timeout 20m bash -c "
|
||||
pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py -m 'core_model and cuda' --run-level 'core_model'
|
||||
timeout 35m bash -c "
|
||||
pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py tests/e2e/online_serving/test_duplex_client_live.py -m 'core_model and cuda' --run-level 'core_model'
|
||||
"
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
@@ -251,6 +265,7 @@ steps:
|
||||
|
||||
- label: "TTS · Qwen3-TTS CustomVoice Test"
|
||||
source_file_dependencies:
|
||||
- tests/helpers/media.py
|
||||
- tests/e2e/online_serving/test_qwen3_tts_customvoice.py
|
||||
- vllm_omni/model_executor/models/qwen3_tts/
|
||||
- vllm_omni/model_executor/models/common/qwen3_code_predictor.py
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
env:
|
||||
VLLM_WORKER_MULTIPROC_METHOD: spawn
|
||||
VLLM_USE_FLASHINFER_MOE_FP16: 0
|
||||
HF_HUB_DOWNLOAD_TIMEOUT: 300
|
||||
HF_HUB_ETAG_TIMEOUT: 60
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: add-diffusion-model
|
||||
description: Add a new diffusion model (text-to-image, text-to-video, image-to-video, text-to-audio, image editing) to vLLM-Omni, including Cache-DiT acceleration and parallelism support (TP, SP/USP, CFG-Parallel, HSDP). Use when integrating a new diffusion model, porting a diffusers pipeline or a custom model repo to vllm-omni, creating a new DiT transformer adapter, adding diffusion model support, or enabling multi-GPU parallelism and cache acceleration for an existing model.
|
||||
description: Add a new diffusion model (text-to-image, text-to-video, image-to-video, text-to-audio, image editing) to vLLM-Omni, including native non-Diffusers ports, reference-parity validation, Cache-DiT, offload, and parallelism support (TP, SP/USP, CFG-Parallel, HSDP). Use when integrating or reviewing a new diffusion model, porting a Diffusers pipeline or custom model repository, creating a DiT adapter, reusing shared examples, or qualifying multi-GPU and memory optimizations.
|
||||
---
|
||||
|
||||
# Adding a Diffusion Model to vLLM-Omni
|
||||
@@ -26,9 +26,16 @@ Check the model's HF repo for `model_index.json`. This determines your path:
|
||||
|----------|----------------|----------------|
|
||||
| **Already supported** | `_class_name` in `model_index.json` matches a key in `_DIFFUSION_MODELS` in `registry.py` | Skip implementation, then validate model-specific examples, tests, and docs as needed |
|
||||
| **Diffusers-based** | Has standard `model_index.json` with `_diffusers_version`, subfolders for `transformer/`, `vae/`, etc. | Follow **Path A** below |
|
||||
| **Custom/private repo** | No diffusers `model_index.json`, weights in non-standard format, custom model code in a separate git repo | Follow **Path B** below |
|
||||
| **Native non-Diffusers model** | No Diffusers index, non-standard checkpoint hierarchy, or custom architecture in a separate repo | Follow **Path B** below; port the runtime natively unless an external adapter was explicitly requested |
|
||||
| **Hybrid** | Has some diffusers components (VAE) but custom transformer/fusion | Mix of Path A and Path B |
|
||||
|
||||
Before coding, write a short integration contract covering runtime ownership,
|
||||
checkpoint discovery, reference revision, I/O geometry, attention semantics,
|
||||
CFG behavior, auxiliary components, target hardware, and the default deployment.
|
||||
For Path B, hybrid models, or any optimization work, read
|
||||
[references/native-model-integration-checklist.md](references/native-model-integration-checklist.md)
|
||||
and use its phase gates.
|
||||
|
||||
## Path A: Diffusers-Based Model
|
||||
|
||||
For models with a standard diffusers layout. See [references/transformer-adaptation.md](references/transformer-adaptation.md) for detailed code patterns.
|
||||
@@ -103,9 +110,15 @@ For custom loop structures, follow `vllm_omni/diffusion/models/progress_bar.py`
|
||||
|
||||
---
|
||||
|
||||
## Path B: Custom/Private Repo Model
|
||||
## Path B: Native / Non-Diffusers Model
|
||||
|
||||
For models without a diffusers pipeline — weights in custom format, model code in a private repo. Real examples: BAGEL, HunyuanImage3. See [references/custom-model-patterns.md](references/custom-model-patterns.md) for concrete custom-model integration patterns.
|
||||
For models without a Diffusers pipeline—weights in custom formats and model
|
||||
code in another public or private repository. Treat that repository as a pinned
|
||||
correctness oracle. A request for native support means the vLLM-Omni runtime
|
||||
must not import the reference implementation; an external adapter is appropriate
|
||||
only when the requested scope explicitly permits that dependency. See
|
||||
[references/custom-model-patterns.md](references/custom-model-patterns.md) for
|
||||
concrete integration patterns.
|
||||
|
||||
### B1. Understand the reference repo
|
||||
|
||||
@@ -126,16 +139,25 @@ This is the key design decision for custom models. Follow these placement rules:
|
||||
| **Pipeline orchestration** (init, forward, denoise loop) | `vllm_omni/diffusion/models/<name>/pipeline_<name>.py` | Always required |
|
||||
| **Custom transformer/backbone** (ported and adapted to vllm-omni) | `vllm_omni/diffusion/models/<name>/<name>_transformer.py` or similar | `wan2_2.py`, `fusion.py`, `bagel_transformer.py` |
|
||||
| **Custom sub-models** (VAE, fusion, autoencoder) | `vllm_omni/diffusion/models/<name>/` as separate files | `autoencoder.py`, `fusion.py` |
|
||||
| **External dependency code** (original repo utilities) | **External repo**, installed via download script or pip | original-repo package via git clone |
|
||||
| **Reference-only code** | Keep outside the runtime; use a pinned revision for golden outputs and architecture analysis | Reference inference script |
|
||||
| **Explicit external adapter dependency** | External package, only when the requested scope and maintainer direction allow it | Compatibility adapter, not native support |
|
||||
| **Hardcoded model configs** | Module-level dicts in pipeline file | `VIDEO_CONFIG`, `AUDIO_CONFIG` dicts |
|
||||
| **Download/setup script** | `examples/offline_inference/<name>/download_<name>.py` | `download_<name>.py` |
|
||||
| **Custom `model_index.json`** | Generated by download script, placed at model root | Minimal: `{"_class_name": "YourPipeline", ...}` |
|
||||
|
||||
### B3. Handle external dependencies
|
||||
|
||||
If the model's code lives in a separate git repo:
|
||||
If the model's code lives in a separate git repo, first decide whether the
|
||||
requested deliverable is native support or an external adapter. Do not silently
|
||||
choose the adapter path.
|
||||
|
||||
**Option 1: Import with graceful fallback** (recommended for models with external utils)
|
||||
**Option 1: Port the code directly** (default for native support)
|
||||
|
||||
Copy the essential model files into `vllm_omni/diffusion/models/<name>/` and
|
||||
adapt them to shared vLLM-Omni contracts. Keep checkpoint loading strict and
|
||||
use the pinned reference only to generate parity evidence.
|
||||
|
||||
**Option 2: Import with graceful fallback** (adapter scope only)
|
||||
|
||||
```python
|
||||
try:
|
||||
@@ -147,11 +169,9 @@ except ImportError:
|
||||
)
|
||||
```
|
||||
|
||||
**Option 2: Port the code directly** (preferred when feasible)
|
||||
|
||||
Copy the essential model files into `vllm_omni/diffusion/models/<name>/` and adapt them. This avoids external dependencies. BAGEL does this — `autoencoder.py` and `bagel_transformer.py` are ported directly.
|
||||
|
||||
**Decision criteria**: Port if the code is self-contained and won't diverge. Use external deps if the model repo is actively maintained and the code is complex.
|
||||
Use an external runtime dependency only when the user explicitly requested an
|
||||
adapter or a maintainer approved the exception. Document the dependency,
|
||||
pinned revision, installation path, and unsupported native features.
|
||||
|
||||
### B4. Handle custom weight loading
|
||||
|
||||
@@ -221,7 +241,8 @@ class CustomPipeline(nn.Module):
|
||||
|
||||
### B5. Create the `model_index.json`
|
||||
|
||||
Custom models need a `model_index.json` at the model root for vllm-omni to discover them. For custom models, this is minimal:
|
||||
Prefer a `model_index.json` at the model root when vLLM-Omni owns an assembled
|
||||
checkpoint directory. For custom models, this is minimal:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -230,7 +251,14 @@ Custom models need a `model_index.json` at the model root for vllm-omni to disco
|
||||
}
|
||||
```
|
||||
|
||||
The `_class_name` must match a key in `_DIFFUSION_MODELS` in `registry.py`. Additional keys are model-specific (accessed via `od_config.model_config`).
|
||||
The `_class_name` must match a key in `_DIFFUSION_MODELS` in `registry.py`.
|
||||
Additional keys are model-specific (accessed via `od_config.model_config`).
|
||||
|
||||
If the released repository is immutable and has neither a root `config.json`
|
||||
nor a Diffusers index, add it to a generic native-checkpoint signature resolver:
|
||||
match the exact Hub ID or a distinctive set of local files and return the
|
||||
pipeline class. Do not use model-name substrings or add parallel one-off
|
||||
predicates in CLI, config, and serving consumers.
|
||||
|
||||
If the model's weights come from multiple HF repos, write a **download script** that:
|
||||
1. Downloads from each repo
|
||||
@@ -292,7 +320,9 @@ Use the appropriate existing example script:
|
||||
| Image-to-Image | `examples/offline_inference/image_to_image/image_edit.py` |
|
||||
| Text-to-Audio | `examples/offline_inference/text_to_audio/text_to_audio.py` |
|
||||
|
||||
For custom/Omni models that don't fit these categories, create a dedicated example script.
|
||||
Reuse these shared scripts even for custom models when their request and output
|
||||
contracts fit. Create a dedicated model script only when the shared category
|
||||
cannot represent the protocol, and document that gap in the PR.
|
||||
|
||||
**Validation**: No errors, output is meaningful, quality matches reference implementation.
|
||||
|
||||
@@ -300,13 +330,17 @@ See [references/troubleshooting.md](references/troubleshooting.md) for common er
|
||||
|
||||
### Step 6: Add Example Scripts
|
||||
|
||||
For Omni or custom models, create:
|
||||
Only when the shared category scripts cannot represent the model, create:
|
||||
- `examples/offline_inference/your_model_name/` — offline script + README
|
||||
- `examples/online_serving/your_model_name/` — server script + client
|
||||
- Download script if weights require assembly from multiple sources
|
||||
|
||||
### Step 7: Update Documentation
|
||||
|
||||
Follow the [`add-recipe` skill](../add-recipe/SKILL.md) to add or update the
|
||||
model-family recipe and its `recipes/README.md` row with verified specifications,
|
||||
hardware, commands, feature links, and qualification evidence.
|
||||
|
||||
Required updates:
|
||||
1. `docs/user_guide/diffusion/parallelism/overview.md` — parallelism support overview/table
|
||||
2. `docs/user_guide/diffusion/cpu_offload.md` — if CPU offload supported (add to supported models table)
|
||||
@@ -346,107 +380,20 @@ Classify the model's **CI priority** first:
|
||||
|
||||
**Keep model-specific code inside test modules — not `tests/helpers/{slug}.py`:** deploy constants, prompts, sampling dicts, and inline `request_config` / `form_data` belong in each `test_{slug}.py` and `test_{slug}_expansion.py`. Do not add per-model files under `tests/helpers/`; reuse only repo-wide harness (`mark`, `media`, `runtime`, `stage_config`, `assertions`). **L2+ online/offline e2e:** reuse or add `send_*_request` in `tests/helpers/runtime.py` — tests call the handler, not raw `omni.generate` / HTTP. See [vllm-omni-test skill](../vllm-omni-test/SKILL.md) § **Runtime send helpers**.
|
||||
|
||||
Keep the model suite proportional using the six distinct failure owners in the
|
||||
native integration checklist. Combine supported features into a few
|
||||
parametrized E2E rows instead of creating one test per optimization.
|
||||
|
||||
### Step 9: Add Cache-DiT Acceleration
|
||||
|
||||
Cache-DiT accelerates inference by caching intermediate computation results across denoising steps. After your model is working correctly on a single GPU, add cache-dit support.
|
||||
Add caching only after uncached single-device correctness. Read
|
||||
[references/cache-dit-patterns.md](references/cache-dit-patterns.md), use the
|
||||
automatic single-block-list path when possible, and add a registered
|
||||
`BlockAdapter` only for genuinely custom block topology.
|
||||
|
||||
See [references/cache-dit-patterns.md](references/cache-dit-patterns.md) for detailed code patterns.
|
||||
|
||||
#### 9a. Determine your model type
|
||||
|
||||
| Model Type | Description | Action |
|
||||
|------------|-------------|--------|
|
||||
| **Standard single-transformer** | One transformer with one `ModuleList` of blocks | No code needed — `CacheDiTBackend` auto-detects via `enable_cache_for_dit()` |
|
||||
| **Multi-block-list** | One transformer with multiple block lists (e.g., `transformer_blocks` + `single_transformer_blocks`) | Write custom enabler with `BlockAdapter` |
|
||||
| **Dual-transformer** | Two transformers (e.g., high-noise + low-noise) | Write custom enabler with `BlockAdapter` wrapping both |
|
||||
|
||||
#### 9b. Standard models — verify automatic support
|
||||
|
||||
For standard single-transformer models, test directly:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="your-model-name",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4,
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Check logs for "Cache-dit enabled successfully on xxx". If it works, skip to Step 9e.
|
||||
|
||||
#### 9c. Custom architectures — write a custom enabler
|
||||
|
||||
For multi-block-list or dual-transformer models, write a custom enabler function:
|
||||
|
||||
```python
|
||||
from cache_dit import BlockAdapter, ForwardPattern, ParamsModifier, DBCacheConfig
|
||||
|
||||
def enable_cache_for_your_model(pipeline, cache_config):
|
||||
db_cache_config = DBCacheConfig(
|
||||
num_inference_steps=None,
|
||||
Fn_compute_blocks=cache_config.Fn_compute_blocks,
|
||||
Bn_compute_blocks=cache_config.Bn_compute_blocks,
|
||||
max_warmup_steps=cache_config.max_warmup_steps,
|
||||
max_cached_steps=cache_config.max_cached_steps,
|
||||
max_continuous_cached_steps=cache_config.max_continuous_cached_steps,
|
||||
residual_diff_threshold=cache_config.residual_diff_threshold,
|
||||
)
|
||||
|
||||
cache_dit.enable_cache(
|
||||
BlockAdapter(
|
||||
transformer=pipeline.transformer,
|
||||
blocks=[
|
||||
pipeline.transformer.transformer_blocks,
|
||||
pipeline.transformer.single_transformer_blocks,
|
||||
],
|
||||
forward_pattern=[ForwardPattern.Pattern_1, ForwardPattern.Pattern_1],
|
||||
params_modifiers=[ParamsModifier(...)],
|
||||
),
|
||||
cache_config=db_cache_config,
|
||||
)
|
||||
|
||||
def refresh_cache_context(pipeline, num_inference_steps, verbose=True):
|
||||
cache_dit.refresh_context(
|
||||
pipeline.transformer, num_inference_steps=num_inference_steps, verbose=verbose
|
||||
)
|
||||
return refresh_cache_context
|
||||
```
|
||||
|
||||
#### 9d. Register the custom enabler
|
||||
|
||||
Add your enabler to `CUSTOM_DIT_ENABLERS` in
|
||||
`vllm_omni/diffusion/cache/cachedit/model_specific.py`:
|
||||
|
||||
```python
|
||||
CUSTOM_DIT_ENABLERS = {
|
||||
"Wan22Pipeline": enable_cache_for_wan22,
|
||||
"LongCatImagePipeline": enable_cache_for_longcat_image,
|
||||
"YourModelPipeline": enable_cache_for_your_model, # Add here
|
||||
}
|
||||
```
|
||||
|
||||
#### 9e. Test Cache-DiT
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="your-model-name",
|
||||
cache_backend="cache_dit",
|
||||
cache_config={
|
||||
"Fn_compute_blocks": 1, "Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 4, "residual_diff_threshold": 0.24,
|
||||
}
|
||||
)
|
||||
images = omni.generate("a beautiful landscape",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=50))
|
||||
```
|
||||
|
||||
**Verify**: 1) logs show cache enabled, 2) 1.5-2x speedup, 3) output quality acceptable vs baseline.
|
||||
|
||||
If quality degrades, lower `residual_diff_threshold` (try 0.12-0.18) or increase `max_warmup_steps` (try 6-8).
|
||||
Verify a real cache hit and compare quality with the uncached baseline. Make a
|
||||
speed claim only at a realistic step count where warmup permits hits; an
|
||||
all-warmup smoke proves integration, not acceleration.
|
||||
|
||||
---
|
||||
|
||||
@@ -460,107 +407,30 @@ See [references/parallelism-patterns.md](references/parallelism-patterns.md) for
|
||||
|
||||
#### 10a. Tensor Parallelism (TP)
|
||||
|
||||
Shards DiT linear layers across GPUs. Requires code changes in the transformer.
|
||||
|
||||
**What to change in the transformer**:
|
||||
1. Replace `nn.Linear` with `ColumnParallelLinear` / `RowParallelLinear` / `QKVParallelLinear`
|
||||
2. Update `load_weights()` to handle QKV fusion with `stacked_params_mapping`
|
||||
3. Use `self.to_qkv.num_heads` (local heads) instead of total heads for split sizes
|
||||
|
||||
```python
|
||||
from vllm.model_executor.layers.linear import (
|
||||
QKVParallelLinear, RowParallelLinear, ColumnParallelLinear,
|
||||
)
|
||||
|
||||
# Attention: QKV → RowParallel output
|
||||
self.to_qkv = QKVParallelLinear(dim, head_dim, num_heads, num_kv_heads)
|
||||
self.to_out = RowParallelLinear(dim, dim, input_is_parallel=True)
|
||||
|
||||
# FFN: ColumnParallel → RowParallel
|
||||
self.w1 = ColumnParallelLinear(dim, ffn_dim)
|
||||
self.w2 = RowParallelLinear(ffn_dim, dim, input_is_parallel=True)
|
||||
```
|
||||
|
||||
**Constraints**: `num_heads % tp_size == 0` and `num_kv_heads % tp_size == 0`.
|
||||
|
||||
**Test**: `--tensor-parallel-size 2`
|
||||
Replace compatible projections with vLLM parallel linears, preserve checkpoint
|
||||
fusion/loading, and use local head counts. Require query/KV head divisibility
|
||||
and compare a multi-rank forward with the one-rank oracle.
|
||||
|
||||
#### 10b. Sequence Parallelism (SP / USP)
|
||||
|
||||
Splits sequence tokens across GPUs. Non-intrusive via `_sp_plan` on the transformer class — no changes to `forward()`.
|
||||
|
||||
**What to change in the transformer**:
|
||||
|
||||
Add `_sp_plan` class attribute:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.sp_plan import (
|
||||
SequenceParallelInput, SequenceParallelOutput,
|
||||
)
|
||||
|
||||
class YourTransformer(nn.Module):
|
||||
_sp_plan = {
|
||||
"blocks.0": {
|
||||
"hidden_states": SequenceParallelInput(split_dim=1, expected_dims=3),
|
||||
},
|
||||
"proj_out": SequenceParallelOutput(gather_dim=1, expected_dims=3),
|
||||
}
|
||||
```
|
||||
|
||||
If inline tensor ops (e.g., `torch.cat`) exist between shard/gather points, extract them into `nn.Module` submodules so hooks can intercept them.
|
||||
|
||||
For RoPE that needs splitting, add an entry for the RoPE module with `split_output=True`.
|
||||
|
||||
**Test**: `--ulysses-degree 2` (offline) or `--usp 2` (online serving)
|
||||
Prefer the declarative `_sp_plan`. For packed variable-length attention or
|
||||
learned-sink LSE correction, keep model math explicit and reuse shared exchange
|
||||
utilities. Validate uneven sequence splits, RoPE coordinates, and outputs
|
||||
against the one-rank oracle.
|
||||
|
||||
#### 10c. CFG Parallel
|
||||
|
||||
Distributes positive/negative CFG branches across 2 GPUs. Requires the pipeline to inherit `CFGParallelMixin`.
|
||||
|
||||
**What to change in the pipeline**:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
|
||||
|
||||
class YourPipeline(nn.Module, CFGParallelMixin):
|
||||
def diffuse(self, ...) -> torch.Tensor:
|
||||
for i, t in enumerate(timesteps):
|
||||
positive_kwargs = {...}
|
||||
negative_kwargs = {...} if do_true_cfg else None
|
||||
noise_pred = self.predict_noise_maybe_with_cfg(
|
||||
do_true_cfg=do_true_cfg, true_cfg_scale=cfg_scale,
|
||||
positive_kwargs=positive_kwargs, negative_kwargs=negative_kwargs,
|
||||
)
|
||||
latents = self.scheduler_step_maybe_with_cfg(
|
||||
noise_pred, t, latents, do_true_cfg
|
||||
)
|
||||
return latents
|
||||
```
|
||||
|
||||
Override `predict_noise()` if your transformer call is non-standard. Override `combine_cfg_noise()` for multi-output models (e.g., video + audio).
|
||||
|
||||
**Constraint**: Exactly 2 GPUs. Only for models using classifier-free guidance.
|
||||
|
||||
**Test**: `--cfg-parallel-size 2`
|
||||
Confirm the model uses CFG. Then reuse `CFGParallelMixin`, overriding prediction
|
||||
or recombination only for non-standard or multi-output pipelines. Distinguish a
|
||||
packed positive/negative implementation from two independent branches and
|
||||
validate the two-rank result against the packed one-rank oracle.
|
||||
|
||||
#### 10d. HSDP (Hybrid Sharded Data Parallel)
|
||||
|
||||
Shards transformer weights via PyTorch FSDP2 to reduce per-GPU VRAM. No code changes to the forward pass — just add a class attribute.
|
||||
|
||||
**What to change in the transformer**:
|
||||
|
||||
```python
|
||||
class YourTransformer(nn.Module):
|
||||
@staticmethod
|
||||
def _is_transformer_block(name: str, module) -> bool:
|
||||
return "blocks" in name and name.split(".")[-1].isdigit()
|
||||
|
||||
_hsdp_shard_conditions = [_is_transformer_block]
|
||||
```
|
||||
|
||||
**Constraint**: Cannot combine with TP. For standalone HSDP, set `hsdp_shard_size` explicitly.
|
||||
|
||||
**Test**: `--use-hsdp` or `DiffusionParallelConfig(use_hsdp=True)`
|
||||
Declare layer shard conditions and ignored rank-local modules; preserve mixed
|
||||
checkpoint dtypes. HSDP cannot combine with TP. Measure parameter loading,
|
||||
FSDP materialization, warm HBM, and host PSS rather than assuming sharding saves
|
||||
peak memory. Keep the resident layout as default if HSDP is worse.
|
||||
|
||||
#### 10e. Update parallelism documentation
|
||||
|
||||
@@ -599,6 +469,12 @@ Pipelines without `SupportsComponentDiscovery` fall back to scanning
|
||||
well-known attribute names (`transformer`, `text_encoder`, `vae`,
|
||||
etc.), which fails for non-standard names.
|
||||
|
||||
Keep model-specific checkpoint paths, nested block aliases, layout transforms,
|
||||
and component lifecycles in the model package. Change a shared offloader only
|
||||
for a general contract, demonstrate another consumer or a framework-level bug,
|
||||
and add one focused shared regression. Avoid `if ModelName` branches in shared
|
||||
backends.
|
||||
|
||||
### Step 12: Performance Profiling
|
||||
|
||||
After verifying correctness and implementing parallelism/caching, profile the model's performance to identify bottlenecks and ensure optimal execution.
|
||||
@@ -608,6 +484,12 @@ See the [Profiling Single-Stage Diffusion](../../../docs/contributing/profiling.
|
||||
2. Using Nsight Systems (`nsys`) with `profiler: "cuda"` for low-overhead CUDA traces.
|
||||
3. Controlling profiling via `omni.start_profile()` and `omni.stop_profile()`.
|
||||
|
||||
Report cold E2E, warm user latency, steady-state wave time, peak allocated and
|
||||
reserved HBM, host PSS for the full process tree, and stage boundaries. State
|
||||
whether prompt encoding and VAE/audio decoding are included. For multi-device
|
||||
layouts, plot user latency against throughput per device and keep different
|
||||
denoising-step counts on separate Pareto frontiers.
|
||||
|
||||
---
|
||||
|
||||
## Pre-commit conventions
|
||||
@@ -635,7 +517,7 @@ shellcheck). In particular:
|
||||
3. **Use small models**: Test with smaller variants first
|
||||
4. **Check tensor shapes**: Most errors are reshape mismatches in attention
|
||||
5. **Add features incrementally**: Single GPU → TP → SP → CFG → HSDP → Cache-DiT
|
||||
6. **For custom models**: Get the model running with the original code first, then progressively replace components with vllm-omni equivalents
|
||||
6. **For custom models**: Run the pinned reference separately, then port the runtime natively; do not ship temporary imports from the reference implementation
|
||||
7. **Cache-DiT before parallelism tuning**: Cache-DiT is lossy — verify quality at baseline before combining with parallelism
|
||||
8. **Combine lossless + lossy**: e.g., TP + SP + Cache-DiT for maximum throughput
|
||||
|
||||
@@ -644,6 +526,7 @@ shellcheck). In particular:
|
||||
- [vllm-omni-test skill](../vllm-omni-test/SKILL.md) — L1–L4 markers, naming, Buildkite wiring, run commands
|
||||
- [Transformer Adaptation](references/transformer-adaptation.md) — porting transformers from diffusers
|
||||
- [Custom Model Patterns](references/custom-model-patterns.md) — patterns for non-diffusers models
|
||||
- [Native Model Integration Checklist](references/native-model-integration-checklist.md) — ownership boundaries, phase gates, qualification matrix, and review evidence
|
||||
- [Parallelism Patterns](references/parallelism-patterns.md) — TP, SP/USP, CFG parallel, HSDP implementation details
|
||||
- [Cache-DiT Patterns](references/cache-dit-patterns.md) — cache-dit acceleration for standard and custom architectures
|
||||
- [Troubleshooting](references/troubleshooting.md) — common errors and fixes
|
||||
@@ -9,7 +9,9 @@ Three caching strategies:
|
||||
- **TaylorSeer**: Calibration-based prediction using Taylor expansion to estimate block outputs
|
||||
- **SCM** (Step Computation Masking): Dynamic step skipping based on configurable policies
|
||||
|
||||
**Typical speedup**: 1.5-2.5x depending on model and configuration.
|
||||
Speedup is model-, step-, and policy-dependent. Treat a cache-enabled run as
|
||||
integration evidence until logs or a focused test show real hits and repeated
|
||||
timings show an improvement over the uncached baseline.
|
||||
|
||||
**Official docs**: https://docs.vllm.ai/projects/vllm-omni/en/latest/design/feature/cache_dit
|
||||
|
||||
@@ -243,8 +245,10 @@ vllm serve your-model --omni --port 8098 \
|
||||
|
||||
**Verification checklist**:
|
||||
1. Logs show "Cache-dit enabled successfully on xxx"
|
||||
2. Performance: 1.5-2x speedup vs no cache
|
||||
3. Quality: compare output with `cache_backend=None`
|
||||
2. A focused test or summary confirms at least one real cache hit
|
||||
3. Quality is compared with `cache_backend=None`
|
||||
4. Performance uses a realistic step count, repeated timings, and a policy
|
||||
whose warmup does not consume the whole run
|
||||
|
||||
## Excluded Models
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ vllm_omni/diffusion/models/wan2_2/
|
||||
|
||||
The transformer is loaded separately via `weights_sources` + `load_weights()`. Non-transformer components (VAE, text encoder) are loaded in `__init__` via `from_pretrained()`.
|
||||
|
||||
### Custom model with external deps
|
||||
### External adapter (not native support)
|
||||
|
||||
```
|
||||
vllm_omni/diffusion/models/<name>/
|
||||
@@ -28,7 +28,10 @@ examples/offline_inference/<task>/
|
||||
└── download_<name>.py # Downloads weights and/or clones code repo
|
||||
```
|
||||
|
||||
All weights loaded eagerly in `__init__`. `load_weights()` is a no-op. External dependency imported with try/except.
|
||||
All weights are loaded eagerly in `__init__`. `load_weights()` is a no-op. The
|
||||
external dependency is imported with try/except. Use this pattern only when the
|
||||
requested scope explicitly allows an adapter; it does not satisfy a request for
|
||||
a native vLLM-Omni implementation.
|
||||
|
||||
### Custom model with ported code (e.g., BAGEL)
|
||||
|
||||
@@ -86,7 +89,7 @@ init → load ALL weights eagerly via custom helpers → load_weights() = no-op
|
||||
- `load_weights()` is `pass`
|
||||
- Weights may come from multiple HF repos in different formats (`.pth`, `.safetensors`)
|
||||
|
||||
Use this when:
|
||||
Use this only for an explicitly approved external adapter when:
|
||||
- The original model has complex, well-tested loading code you don't want to rewrite
|
||||
- Weights span multiple HF repos
|
||||
- Weight format is non-standard (e.g., a single `.pth` file, not sharded safetensors)
|
||||
@@ -112,7 +115,14 @@ Custom model `model_index.json` (minimal):
|
||||
}
|
||||
```
|
||||
|
||||
The only **required** field is `_class_name` — it must match a key in `_DIFFUSION_MODELS` in `registry.py`. Other fields are model-specific and accessible via `od_config.model_config` dict.
|
||||
The only **required** field is `_class_name` — it must match a key in
|
||||
`_DIFFUSION_MODELS` in `registry.py`. Other fields are model-specific and
|
||||
accessible via `od_config.model_config`.
|
||||
|
||||
When the released repository cannot be modified and contains neither this file
|
||||
nor a root `config.json`, use the generic native-checkpoint signature resolver.
|
||||
Match the exact Hub ID or a distinctive local file set; never infer support from
|
||||
a model-name substring.
|
||||
|
||||
## External Dependency Management
|
||||
|
||||
@@ -141,6 +151,10 @@ def download_dependency():
|
||||
|
||||
Copy essential files from the original repo into `vllm_omni/diffusion/models/<name>/`. Adapt imports to use vllm-omni utilities. Benefits: no external dependency, no git clone step. Drawback: must maintain the ported code.
|
||||
|
||||
This is the default for native model support. Keep the reference repository at
|
||||
a pinned revision for architecture analysis and golden outputs, but do not
|
||||
import it from production code.
|
||||
|
||||
## Multi-Modal Input/Output Protocols
|
||||
|
||||
Custom models that handle images, audio, or video I/O should implement protocol classes:
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Native Diffusion Model Integration Checklist
|
||||
|
||||
Use this checklist for native non-Diffusers models, hybrid pipelines, and
|
||||
model-specific optimization work.
|
||||
|
||||
## 1. Freeze the contract before coding
|
||||
|
||||
Record:
|
||||
|
||||
- Reference repository and immutable revision.
|
||||
- Official checkpoint ID, local layout, license, and required auxiliary files.
|
||||
- Pipeline stages: prompt/image encoding, denoising, video/image/audio decoding,
|
||||
and muxing.
|
||||
- Token packing, RoPE coordinates, attention sinks/LSE behavior, MoE layout,
|
||||
scheduler equations, CFG formulation, dtypes, and checkpoint transforms.
|
||||
- Output geometry, frame count/FPS, audio rate/channels, supported tasks, and
|
||||
default generation steps.
|
||||
- Available local hardware versus the official qualification hardware.
|
||||
- Intended default deployment and optional optimization paths.
|
||||
|
||||
State whether the deliverable is a native implementation or an external
|
||||
adapter. For native support, use the reference as an oracle only.
|
||||
|
||||
## 2. Use phase gates
|
||||
|
||||
| Phase | Required exit evidence |
|
||||
|---|---|
|
||||
| Architecture | Component/weight inventory; unsupported reference dependencies identified; ownership decisions documented |
|
||||
| Native correctness | Strict checkpoint load; tiny forward or one-step golden vs pinned reference; output contract validated |
|
||||
| Framework integration | Registry/discovery, shared request types, existing category example, serving route, docs, and recipe work without model-name branching in shared scripts |
|
||||
| Lossless distribution | One-rank oracle vs each supported TP/SP/CFG/HSDP/VAE layout; released-checkpoint smoke for the recommended layout |
|
||||
| Memory optimization | Ordinary/layerwise/DLO behavior, peak HBM, full-tree host PSS, and stage timeline; shared backend unchanged unless a general contract is proven |
|
||||
| Lossy optimization | Cache hit demonstrated; quality delta measured; speed claim made only when warmup permits hits |
|
||||
| Full qualification | Default-resolution E2E at official step count for every supported task; media metadata and hashes recorded |
|
||||
| Review readiness | Focused tests, Ruff/format, strict docs, DCO, current-main rebase, issue/RFC for large changes, concise PR evidence and limitations |
|
||||
|
||||
Do not start performance tuning before native one-step parity. Do not claim a
|
||||
feature from topology validation alone when released weights can exercise it.
|
||||
|
||||
## 3. Keep ownership narrow
|
||||
|
||||
Default placement:
|
||||
|
||||
| Concern | Owner |
|
||||
|---|---|
|
||||
| Architecture, checkpoint remap, packed attention math, nested block alias, auxiliary lifecycle | Model package |
|
||||
| Kernel/version dispatch already useful to multiple models | Shared attention utility |
|
||||
| Request-dependent example defaults | Typed model-extra entry consumed through a registry |
|
||||
| Exact-ID/local-file detection for repositories without standard metadata | Generic native-checkpoint signature table |
|
||||
| General offload/cache/HSDP bug | Shared layer plus one focused shared regression |
|
||||
|
||||
Before changing shared code, answer:
|
||||
|
||||
1. Is this a framework contract or only this checkpoint's layout?
|
||||
2. Can the model expose an existing declarative hook instead?
|
||||
3. Is there a second consumer or a framework-level failing test?
|
||||
4. Can the shared change avoid importing or naming the model?
|
||||
|
||||
If the answers do not justify a shared change, keep it model-local.
|
||||
|
||||
## 4. Reuse examples without leaking model logic
|
||||
|
||||
- Reuse the existing `x_to_y.py` category script.
|
||||
- Resolve the pipeline class through standard metadata or native signature
|
||||
discovery, not path substrings.
|
||||
- Put dynamic model defaults and validation in `model_extras`; let shared
|
||||
examples consume only the generic typed API.
|
||||
- Do not create image/speech/default abstractions without a real consumer.
|
||||
- Add a dedicated example only when the shared request/output protocol cannot
|
||||
express the model.
|
||||
|
||||
## 5. Build a bounded evidence matrix
|
||||
|
||||
Start with the recommended resident layout. Add only supported, useful points:
|
||||
|
||||
- Single request: TP, SP, or hybrid TP/SP.
|
||||
- Throughput: DLO/DP layouts when concurrent requests are valid.
|
||||
- CFG: compare CFG-parallel output with the packed-CFG oracle only if the model
|
||||
actually uses CFG.
|
||||
- HSDP: record loading and materialization peaks; it can use more HBM than a
|
||||
resident SP layout.
|
||||
- VAE patch parallelism: compare decoded tensors or media exactly where the
|
||||
algorithm should be lossless.
|
||||
- Cache: separate cache integration from acceleration evidence.
|
||||
|
||||
With four devices, qualify four-device layouts such as DP4, DP2SP2, SP4, or
|
||||
CFG2xSP2. Do not present CFG2xSP4 or an official eight-device runtime as locally
|
||||
validated.
|
||||
|
||||
For each measured layout record:
|
||||
|
||||
- Cold E2E and warm user latency.
|
||||
- Steady-state wave time and throughput per device.
|
||||
- Prompt, denoise, video decode, audio decode, and mux times.
|
||||
- Peak allocated/reserved HBM and full-process-tree host PSS.
|
||||
- Step count, seed, output geometry, frame/FPS/audio metadata, and parity metric.
|
||||
|
||||
Plot latency versus throughput/device. Keep one-step, four-step, and full-step
|
||||
results separate because they are not quality-equivalent.
|
||||
|
||||
## 6. Keep tests proportional
|
||||
|
||||
Prefer a small set with distinct failure ownership:
|
||||
|
||||
1. Strict weight-key/dtype/load contract.
|
||||
2. Tiny native golden or one-step reference parity.
|
||||
3. One parametrized distributed-parity test covering supported layouts.
|
||||
4. Pipeline/request/output geometry and serving smoke.
|
||||
5. Shared-example/default test when the category script changes.
|
||||
6. One shared regression only for a genuine shared-layer fix.
|
||||
|
||||
Avoid duplicating the same shape or configuration assertion across
|
||||
model-specific files. Keep expensive released-checkpoint evidence in the PR or
|
||||
recipe unless the repository's CI tier explicitly requires it.
|
||||
|
||||
## 7. Write evidence, not marketing
|
||||
|
||||
In the PR description include:
|
||||
|
||||
- A compact architecture/data-flow explanation.
|
||||
- Default deployment and why it is recommended.
|
||||
- Code-change counts by component.
|
||||
- Accuracy and distributed-parity evidence tied to commands/scripts.
|
||||
- Separate Pareto figures for different step counts.
|
||||
- Supported versus validated versus planned optimizations.
|
||||
- Hardware gaps and untested official-runtime claims.
|
||||
|
||||
Call a result a bounded local qualification unless it includes repeated timing
|
||||
and variance. State whether latency includes text/image encoding and all
|
||||
decoders. Preserve the reference revision and exact commands needed to
|
||||
reproduce the evidence.
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: add-recipe
|
||||
description: Add or update an in-repository vLLM-Omni model recipe with verified task, input, output, hardware, command, feature, and validation contracts. Use when creating files under recipes/, restructuring a recipe after review, documenting a newly supported model, or synchronizing recipe claims with support tables and shared feature guides.
|
||||
---
|
||||
|
||||
# Add a vLLM-Omni Recipe
|
||||
|
||||
## Source the contract
|
||||
|
||||
Read [`recipes/TEMPLATE.md`](../../../recipes/TEMPLATE.md) and
|
||||
[`recipes/README.md`](../../../recipes/README.md) before editing. Inspect the
|
||||
canonical model card/repository, implementation, shared examples, tests, and
|
||||
available local qualification evidence.
|
||||
|
||||
Do not infer support from a model name, sibling recipe, registry entry, or
|
||||
topology validator. Mark unexecuted configurations as configuration-only.
|
||||
|
||||
## Route by model family
|
||||
|
||||
Read only the contributor and user documentation relevant to the model:
|
||||
|
||||
| Family | Contributor guide | User-facing documentation to synchronize |
|
||||
|---|---|---|
|
||||
| Diffusion | [`adding a diffusion model`](../add-diffusion-model/SKILL.md) | `docs/user_guide/diffusion_features.md`, applicable diffusion feature guides, and shared image/video/audio examples |
|
||||
| TTS | [`adding a TTS model`](../add-tts-model/SKILL.md) | `examples/offline_inference/text_to_speech/README.md`, `examples/online_serving/text_to_speech/README.md`, and `docs/serving/speech_api.md` |
|
||||
| Omni | [`adding an omni model`](../../../docs/contributing/model/adding_omni_model.md) | model-family offline/online example docs, `docs/serving/chat_completions_api.md`, and `docs/user_guide/feature_compatibility.md` |
|
||||
|
||||
All families must update `docs/models/supported_models.md` and the matching row
|
||||
in `recipes/README.md`. Do not add a modality-specific support document when
|
||||
the repository has no such table; update the closest shared user contract.
|
||||
|
||||
## Structure the recipe
|
||||
|
||||
Keep the recipe task-oriented and use one hardware-specific file named
|
||||
`recipes/<vendor>/<model>-<hardware>.md`. A recipe may contain multiple
|
||||
deployment topologies on that hardware, but each additional accelerator model
|
||||
or platform requires its own suffixed file.
|
||||
|
||||
1. **Summary:** vendor, exact model ID, runtime, modes, named hardware,
|
||||
recommended deployment, and maintainer.
|
||||
2. **Supported model contract:** put task, input, output, and provided-profile
|
||||
tables before setup or commands.
|
||||
3. **References:** link the canonical model source, shared examples, supported
|
||||
model table, and feature matrix.
|
||||
4. **Checkpoint/setup:** pin revisions when possible and state required assets.
|
||||
5. **Hardware:** document the single accelerator named by the file suffix,
|
||||
including every validated device-count/topology profile on it.
|
||||
6. **Commands:** reuse shared `examples/` entrypoints unless the model contract
|
||||
truly requires a dedicated script. Include offline and online paths that
|
||||
were validated.
|
||||
7. **Supported features:** use a compact model-specific topology/status table
|
||||
with links to shared feature guides.
|
||||
8. **Verification:** provide a quick command and exact expected output
|
||||
contract.
|
||||
9. **Qualification evidence:** report correctness, memory, and timing evidence
|
||||
with its measurement scope and caveats.
|
||||
|
||||
## Separate hardware from software
|
||||
|
||||
For every locally validated hardware profile, record:
|
||||
|
||||
- accelerator vendor/model and per-device memory;
|
||||
- number of devices;
|
||||
- interconnect (`NVLink`, `PCIe`, or the platform equivalent);
|
||||
- host memory when CPU staging/offload is material;
|
||||
- whether the profile is runtime-qualified or configuration-only.
|
||||
|
||||
Record OS, Python, driver/runtime, framework versions, and the vLLM-Omni
|
||||
revision in a separate software-environment table. Keep precision, worker
|
||||
count, and DP/TP/SP/PP sizes with the deployment profile or exact command.
|
||||
|
||||
Never generalize a result from one accelerator family to another or combine
|
||||
NVIDIA, AMD, NPU, or distinct accelerator models in one recipe. Create a
|
||||
separate hardware-suffixed recipe and `recipes/README.md` row. Distinguish
|
||||
upstream requirements from hardware exercised by the PR.
|
||||
|
||||
## Keep shared documentation concise
|
||||
|
||||
Link the applicable diffusion, TTS, omni, serving, or design guide for generic
|
||||
feature semantics and launch instructions. The recipe should contain only the
|
||||
model-specific status, valid topology, required flag difference, and evidence
|
||||
boundary.
|
||||
|
||||
Put unsupported combinations in the same feature table. Avoid repeating the
|
||||
same model-specific prose below the global feature matrix; the recipe is the
|
||||
detailed source of truth.
|
||||
|
||||
## Report evidence precisely
|
||||
|
||||
For every performance or memory value, state:
|
||||
|
||||
- checkpoint/revision and workload;
|
||||
- accelerator and device count;
|
||||
- precision and topology;
|
||||
- cold/warm scope, step count, and concurrency;
|
||||
- peak HBM and host-memory metric when measured;
|
||||
- output/quality guard and sample count;
|
||||
- monitoring overhead or other known variance.
|
||||
|
||||
Call a single bounded run qualification evidence, not a benchmark. Do not add
|
||||
a benchmark script unless the contribution explicitly requires one.
|
||||
|
||||
## Synchronize repository documentation
|
||||
|
||||
Update all applicable locations:
|
||||
|
||||
- the row in `recipes/README.md`;
|
||||
- `docs/models/supported_models.md`;
|
||||
- the family-specific documentation selected in **Route by model family**;
|
||||
- shared example documentation when commands or flags change.
|
||||
|
||||
Keep global support tables compact. Link to the recipe instead of duplicating
|
||||
its deployment explanation.
|
||||
|
||||
## Validate
|
||||
|
||||
Run, at minimum:
|
||||
|
||||
```bash
|
||||
pre-commit run --files <changed recipe/docs/skill files>
|
||||
mkdocs build --strict
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Remove generated documentation artifacts after validation. Confirm every
|
||||
recipe link resolves and every claimed profile has matching code or evidence.
|
||||
@@ -499,6 +499,13 @@ Common pre-commit failures, recovery commands for missing sign-off, and the
|
||||
full `pre-commit run` invocation for a TTS model:
|
||||
[references/precommit-dco.md](references/precommit-dco.md).
|
||||
|
||||
## Documentation and recipe
|
||||
|
||||
Follow the [`add-recipe` skill](../add-recipe/SKILL.md) to add or update the
|
||||
in-repository model-family recipe, the `recipes/README.md` index, the supported
|
||||
models table, TTS example documentation, and the speech API contract using
|
||||
validated evidence.
|
||||
|
||||
## Integration Checklist
|
||||
|
||||
Use this checklist when integrating a new TTS model:
|
||||
@@ -543,6 +550,7 @@ Use this checklist when integrating a new TTS model:
|
||||
- [ ] Buildkite entries match level: `test-ready.yml` / `test-merge.yml` or nightly TTS job / `test-nightly.yml`
|
||||
- [ ] Gradio demo working
|
||||
- [ ] Documentation added (offline + online docs, nav, supported models)
|
||||
- [ ] In-repository recipe and `recipes/README.md` row added via the `add-recipe` skill
|
||||
|
||||
### Phase 4: Async Chunk
|
||||
- [ ] Pipeline declares the async handoff processor
|
||||
|
||||
@@ -26,6 +26,9 @@ self-check.
|
||||
|
||||
- [`add-diffusion-model`](add-diffusion-model/SKILL.md): guides integration of
|
||||
a new diffusion model into `vllm-omni`
|
||||
- [`add-recipe`](add-recipe/SKILL.md): creates or updates in-repository model
|
||||
recipes with verified task/input/output contracts, concrete hardware,
|
||||
shared feature-guide links, exact commands, and qualification evidence
|
||||
- [`add-tts-model`](add-tts-model/SKILL.md): covers integration of new TTS
|
||||
models and related serving workflows
|
||||
- [`diffusion-perf-opt`](diffusion-perf-opt/SKILL.md): guides diffusion model
|
||||
|
||||
@@ -53,7 +53,7 @@ vLLM-Omni is flexible and easy to use with:
|
||||
- Tensor, pipeline, data and expert parallelism support for distributed inference
|
||||
- Streaming outputs
|
||||
- OpenAI-compatible API server
|
||||
- Full-duplex realtime serving with streaming audio input and output (experimental)
|
||||
- [Full-duplex realtime serving](docs/serving/realtime_duplex_api.md) with streaming audio input and output
|
||||
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Host weight dependency memory diagnostic
|
||||
|
||||
`safetensors_retention.py` isolates repeated CPU `get_tensor()` calls from
|
||||
access to one cached tensor view. It imports neither vLLM nor HWR and uses a
|
||||
synthetic float32 payload controlled by `--tensor-elements` (default: 64).
|
||||
Tensor size can affect retention: a negative result for 16 elements does not
|
||||
rule out growth for 64 elements. The selected size is recorded in the JSON
|
||||
arguments and used for both file creation and the correctness check.
|
||||
HWR itself calls `get_tensor()` when
|
||||
acquiring a lease, then exposes cached views; this probe does not measure HWR
|
||||
requests or repeated lease acquisition.
|
||||
|
||||
Run one mode per fresh process in the same Python environment and CPU affinity:
|
||||
|
||||
```bash
|
||||
git rev-parse HEAD > revision.txt
|
||||
git status --short > working-tree.txt
|
||||
git diff > working-tree.patch
|
||||
# Select an allowed CPU from your process affinity; 0 is only an example.
|
||||
CUDA_VISIBLE_DEVICES='' taskset -c 0 timeout 120s python \
|
||||
benchmarks/host_weight_runtime/safetensors_retention.py \
|
||||
--mode get_tensor --iterations 10000 --sample-every 5000 > feasibility.json
|
||||
```
|
||||
|
||||
For a size comparison, run 16 and 64 elements twice each in fresh processes,
|
||||
with cached reuse as a control at both sizes:
|
||||
|
||||
```bash
|
||||
for repetition in 1 2; do
|
||||
for elements in 16 64; do
|
||||
for mode in get_tensor reuse; do
|
||||
CUDA_VISIBLE_DEVICES='' taskset -c 0 timeout 120s python \
|
||||
benchmarks/host_weight_runtime/safetensors_retention.py \
|
||||
--mode "$mode" --tensor-elements "$elements" \
|
||||
> "$mode-$elements-$repetition.json"
|
||||
done
|
||||
done
|
||||
done
|
||||
```
|
||||
|
||||
The diagnostic sets Torch to one CPU thread, warms up before the baseline,
|
||||
and reports preparation separately from timed loop work. Checkpoints run GC
|
||||
and report Linux private/anonymous memory, RSS, and open descriptors. It checks
|
||||
that transient tensor objects and the final cached view are released. Linux
|
||||
`/proc/self/smaps_rollup` must be readable; invalid arguments and missing proc
|
||||
support fail explicitly. Keep raw JSON and the repository snapshot with results.
|
||||
|
||||
Compare the final loop sample with the warmed baseline, then inspect the closed
|
||||
sample. Private-memory growth after Python tensor objects disappear is evidence
|
||||
of retention in the dependency/allocator path, not proof of a leak's root cause.
|
||||
RSS can include file-backed pages; it is not a private-memory metric. Samples
|
||||
also include small diagnostic bookkeeping allocations. Do not assert a portable
|
||||
memory threshold or turn this synthetic loop into a per-request service estimate.
|
||||
|
||||
Do not drop shared caches, trim allocators, or add serving-time GC to make the
|
||||
numbers look smaller. A dependency replacement or version change requires its
|
||||
own reproducible comparison and correctness/lifecycle validation.
|
||||
@@ -0,0 +1,120 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Linux CPU diagnostic: repeated safetensors materialization versus view reuse."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import tempfile
|
||||
import time
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def positive_int(value: str) -> int:
|
||||
parsed = int(value)
|
||||
if parsed <= 0:
|
||||
raise argparse.ArgumentTypeError("must be positive")
|
||||
return parsed
|
||||
|
||||
|
||||
def sample(phase: str, iterations: int, loop_seconds: float) -> dict[str, object]:
|
||||
gc.collect()
|
||||
counters = {}
|
||||
for line in Path("/proc/self/smaps_rollup").read_text().splitlines():
|
||||
fields = line.split()
|
||||
if len(fields) == 3 and fields[2] == "kB":
|
||||
counters[fields[0].removesuffix(":")] = int(fields[1]) * 1024
|
||||
return {
|
||||
"phase": phase,
|
||||
"iterations": iterations,
|
||||
"loop_seconds": loop_seconds,
|
||||
"rss_bytes": counters["Rss"],
|
||||
"private_bytes": counters["Private_Clean"] + counters["Private_Dirty"],
|
||||
"anonymous_bytes": counters["Anonymous"],
|
||||
"fd_count": len(tuple(Path("/proc/self/fd").iterdir())),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--mode", choices=("get_tensor", "reuse"), required=True)
|
||||
parser.add_argument(
|
||||
"--tensor-elements", type=positive_int, default=64, help="Number of float32 elements in the synthetic tensor"
|
||||
)
|
||||
parser.add_argument("--iterations", type=positive_int, default=1_000_000)
|
||||
parser.add_argument("--sample-every", type=positive_int, default=100_000)
|
||||
parser.add_argument("--warmup", type=positive_int, default=1000)
|
||||
args = parser.parse_args()
|
||||
if not Path("/proc/self/smaps_rollup").is_file():
|
||||
parser.error("requires Linux /proc/self/smaps_rollup")
|
||||
|
||||
preparation_start = time.monotonic()
|
||||
import safetensors
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
from safetensors.torch import save_file
|
||||
|
||||
torch.set_num_threads(1)
|
||||
torch.set_num_interop_threads(1)
|
||||
samples = [sample("before_file", 0, 0.0)]
|
||||
with tempfile.TemporaryDirectory(prefix="safetensors-retention-") as directory:
|
||||
path = Path(directory) / "synthetic.safetensors"
|
||||
save_file({"weight": torch.arange(args.tensor_elements, dtype=torch.float32)}, str(path))
|
||||
with safe_open(path, framework="pt", device="cpu") as reader:
|
||||
cached = reader.get_tensor("weight")
|
||||
assert torch.equal(cached, torch.arange(args.tensor_elements, dtype=torch.float32))
|
||||
for _ in range(args.warmup):
|
||||
tensor = reader.get_tensor("weight") if args.mode == "get_tensor" else cached
|
||||
del tensor
|
||||
samples.append(sample("baseline", 0, 0.0))
|
||||
preparation_seconds = time.monotonic() - preparation_start
|
||||
completed = 0
|
||||
loop_seconds = 0.0
|
||||
while completed < args.iterations:
|
||||
count = min(args.sample_every, args.iterations - completed)
|
||||
start = time.monotonic()
|
||||
for _ in range(count):
|
||||
tensor = reader.get_tensor("weight") if args.mode == "get_tensor" else cached
|
||||
last_tensor = weakref.ref(tensor)
|
||||
del tensor
|
||||
loop_seconds += time.monotonic() - start
|
||||
completed += count
|
||||
snapshot = sample("loop", completed, loop_seconds)
|
||||
snapshot["last_tensor_alive"] = last_tensor() is not None
|
||||
if args.mode == "get_tensor":
|
||||
assert last_tensor() is None, "diagnostic retained a transient tensor"
|
||||
samples.append(snapshot)
|
||||
del cached
|
||||
del reader
|
||||
samples.append(sample("closed", completed, loop_seconds))
|
||||
assert last_tensor() is None, "last tensor survived reader/view teardown"
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"arguments": vars(args),
|
||||
"python": platform.python_version(),
|
||||
"torch": torch.__version__,
|
||||
"safetensors": safetensors.__version__,
|
||||
"kernel": platform.release(),
|
||||
"hostname": platform.node(),
|
||||
"pid": os.getpid(),
|
||||
"cpu_affinity": sorted(os.sched_getaffinity(0)),
|
||||
"torch_threads": torch.get_num_threads(),
|
||||
"pythonmalloc": os.environ.get("PYTHONMALLOC", "default"),
|
||||
"preparation_seconds": preparation_seconds,
|
||||
"samples": samples,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+34
-24
@@ -2,7 +2,7 @@
|
||||
|
||||
A model-agnostic serving benchmark for TTS models in vllm-omni. One CLI
|
||||
(`bench_tts.py`) + one YAML registry (`model_configs.yaml`) drive perf and
|
||||
quality runs for every registered checkpoint: **Qwen3-TTS** (Base / CustomVoice)
|
||||
quality runs for every registered checkpoint: **Qwen3-TTS** (Base / CustomVoice / VoiceDesign)
|
||||
and **VoxCPM2** today, more to come.
|
||||
|
||||
The same three task types — `voice_clone`, `default_voice`, `voice_design` —
|
||||
@@ -21,6 +21,11 @@ The server auto-loads its Deploy YAML from `vllm_omni/deploy/qwen3_tts.yaml`
|
||||
(Pipeline + Deploy schema introduced in #2383). No explicit `--deploy-config`
|
||||
flag is needed for a registered model.
|
||||
|
||||
Start the checkpoint that matches the benchmark task below: `-Base` for
|
||||
`voice_clone`, `-CustomVoice` for `default_voice`, or `-VoiceDesign` for
|
||||
`voice_design`. Restart the server with the matching checkpoint when switching
|
||||
tasks; changing only the benchmark's `--model` does not reload the server.
|
||||
|
||||
### 2. Run the benchmark (`vllm bench serve --omni`)
|
||||
|
||||
The primary, directly-controllable path. Copy-paste one of these and tweak
|
||||
@@ -62,12 +67,12 @@ vllm bench serve --omni \
|
||||
--save-result --result-dir ./results
|
||||
```
|
||||
|
||||
#### voice_design (Qwen3-TTS-CustomVoice, bundled seed_tts_design)
|
||||
#### voice_design (Qwen3-TTS-VoiceDesign, bundled seed_tts_design)
|
||||
|
||||
```bash
|
||||
vllm bench serve --omni \
|
||||
--host 127.0.0.1 --port 8000 \
|
||||
--model Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
|
||||
--model Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign \
|
||||
--backend openai-audio-speech \
|
||||
--endpoint /v1/audio/speech \
|
||||
--dataset-name seed-tts-design \
|
||||
@@ -180,11 +185,11 @@ Outputs TTFP / RTF / throughput curves (and a markdown table) for every
|
||||
|
||||
## Task types
|
||||
|
||||
| Task | Dataset | Request body | Checkpoints that support it |
|
||||
|-----------------|-------------------|-----------------------------------------------------|------------------------------------------|
|
||||
| `voice_clone` | `seed-tts` | `ref_audio` + `ref_text` + `task_type=Base` | `Qwen3-TTS-*-Base`, `VoxCPM2` |
|
||||
| `default_voice` | `seed-tts-text` | `voice=Vivian` + `task_type=CustomVoice` | `Qwen3-TTS-*-CustomVoice` |
|
||||
| `voice_design` | `seed-tts-design` | `instructions=<natural-language description>` + `task_type=VoiceDesign` | `Qwen3-TTS-*-CustomVoice` |
|
||||
| Task | Dataset | Request body | Checkpoints that support it |
|
||||
| --------------- | ----------------- | ----------------------------------------------------------------------- | ----------------------------- |
|
||||
| `voice_clone` | `seed-tts` | `ref_audio` + `ref_text` + `task_type=Base` | `Qwen3-TTS-*-Base`, `VoxCPM2` |
|
||||
| `default_voice` | `seed-tts-text` | `voice=Vivian` + `task_type=CustomVoice` | `Qwen3-TTS-*-CustomVoice` |
|
||||
| `voice_design` | `seed-tts-design` | `instructions=<natural-language description>` + `task_type=VoiceDesign` | `Qwen3-TTS-*-VoiceDesign` |
|
||||
|
||||
**`-CustomVoice` checkpoints do NOT ship `speaker_encoder` weights**, so
|
||||
voice_clone requests raise `ValueError` at model runtime. Use `-Base` for
|
||||
@@ -210,12 +215,12 @@ Then add the model's Deploy YAML under `vllm_omni/deploy/<model>.yaml`
|
||||
|
||||
## Datasets
|
||||
|
||||
| Dataset | Bundled? | Format | Source |
|
||||
|--------------------|----------|-------------------|----------------------------------------------------------------|
|
||||
| `seed-tts-design` | ✅ | 5-field meta.lst | `benchmarks/build_dataset/seed_tts_design/en/meta.lst` (20 prompts) |
|
||||
| `seed_tts_smoke` | ✅ | 4-field meta.lst | `benchmarks/build_dataset/seed_tts_smoke/en/meta.lst` (20 text-only) |
|
||||
| `seed-tts` | ❌ | 4-field meta.lst + WAVs | Google-Drive: [BytedanceSpeech/seed-tts-eval][seedtts] (~1.2 GB) |
|
||||
| `seed-tts-text` | ❌ | 4-field meta.lst | Same archive as `seed-tts` (wav column unused) |
|
||||
| Dataset | Bundled? | Format | Source |
|
||||
| ----------------- | -------- | ----------------------- | -------------------------------------------------------------------- |
|
||||
| `seed-tts-design` | ✅ | 5-field meta.lst | `benchmarks/build_dataset/seed_tts_design/en/meta.lst` (20 prompts) |
|
||||
| `seed_tts_smoke` | ✅ | 4-field meta.lst | `benchmarks/build_dataset/seed_tts_smoke/en/meta.lst` (20 text-only) |
|
||||
| `seed-tts` | ❌ | 4-field meta.lst + WAVs | Google-Drive: [BytedanceSpeech/seed-tts-eval][seedtts] (~1.2 GB) |
|
||||
| `seed-tts-text` | ❌ | 4-field meta.lst | Same archive as `seed-tts` (wav column unused) |
|
||||
|
||||
[seedtts]: https://github.com/BytedanceSpeech/seed-tts-eval
|
||||
|
||||
@@ -227,11 +232,11 @@ For manual voice_clone / default_voice runs against the full corpus, follow
|
||||
|
||||
`tests/dfx/perf/tests/test_tts.json` wires three perf regimes plus quality:
|
||||
|
||||
| eval_phase | concurrency | purpose | Baseline metrics |
|
||||
|---------------|-------------|---------------------------------------------------------|-----------------------------------------|
|
||||
| `latency` | 1 | Single-request TTFP / RTF SLO | `median_audio_ttfp_ms`, `median_audio_rtf` |
|
||||
| `throughput` | 8 | Codec-batching cliff sentinel (PDF #272 concurrency≥8) | `median_audio_ttfp_ms`, `median_audio_rtf` |
|
||||
| `quality` | 4 | WER / SIM / UTMOS regression (disabled in CI by default)| `mean_audio_rtf` |
|
||||
| eval_phase | concurrency | purpose | Baseline metrics |
|
||||
| ------------ | ----------- | -------------------------------------------------------- | ------------------------------------------ |
|
||||
| `latency` | 1 | Single-request TTFP / RTF SLO | `median_audio_ttfp_ms`, `median_audio_rtf` |
|
||||
| `throughput` | 8 | Codec-batching cliff sentinel (PDF #272 concurrency≥8) | `median_audio_ttfp_ms`, `median_audio_rtf` |
|
||||
| `quality` | 4 | WER / SIM / UTMOS regression (disabled in CI by default) | `mean_audio_rtf` |
|
||||
|
||||
Why `median_*` for latency/throughput and `mean_*` for quality: latency
|
||||
distributions have cold-start tails that drag the mean; quality aggregates
|
||||
@@ -245,10 +250,15 @@ PR #2558 — quality runs are manual / release-validation, not nightly).
|
||||
|
||||
Observed on H20-3e, Qwen3-TTS-1.7B (measured pre-merge on this branch):
|
||||
|
||||
| Task | Model | c=1 | c=4 | **c=8** | c=16 | c=32 |
|
||||
|---------------|---------------|--------|--------|------------|--------|--------|
|
||||
| voice_clone | 1.7B-Base | RTF 0.15 / TTFP 165ms | 0.28 / 412ms | **0.49 / 1701ms** | 0.72 / 3355ms | 0.77 / 3772ms |
|
||||
| voice_design | 1.7B-CustomVoice | RTF 0.08 / TTFP 53ms | 0.11 / 154ms | **0.21 / 872ms** | 0.33 / 1801ms | 0.38 / 1989ms |
|
||||
| Task | Model | c=1 | c=4 | **c=8** | c=16 | c=32 |
|
||||
| ------------ | ---------------- | --------------------- | ------------ | ----------------- | ------------- | ------------- |
|
||||
| voice_clone | 1.7B-Base | RTF 0.15 / TTFP 165ms | 0.28 / 412ms | **0.49 / 1701ms** | 0.72 / 3355ms | 0.77 / 3772ms |
|
||||
| voice_design | 1.7B-CustomVoice | RTF 0.08 / TTFP 53ms | 0.11 / 154ms | **0.21 / 872ms** | 0.33 / 1801ms | 0.38 / 1989ms |
|
||||
|
||||
The historical `voice_design` row above lists a CustomVoice checkpoint, which
|
||||
current task validation rejects for VoiceDesign requests. It does not establish
|
||||
a VoiceDesign checkpoint baseline; rerun with `Qwen3-TTS-12Hz-1.7B-VoiceDesign`
|
||||
before comparing performance.
|
||||
|
||||
Both models show a **4–6× TTFP jump from c=4 to c=8** while audio throughput
|
||||
saturates around c=4–8 — the codec-bs=1 bottleneck documented in
|
||||
@@ -257,7 +267,7 @@ sentinel for regressions in this area.
|
||||
|
||||
## File layout
|
||||
|
||||
```
|
||||
```text
|
||||
benchmarks/tts/
|
||||
├── README.md (this file)
|
||||
├── bench_tts.py CLI — serve-mode benchmark driver
|
||||
|
||||
@@ -22,7 +22,7 @@ models:
|
||||
# NOT supported (an attempt raises ValueError from _extract_speaker_embedding
|
||||
# at model runtime). Use -Base for voice_clone.
|
||||
Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice:
|
||||
supported_tasks: [default_voice, voice_design]
|
||||
supported_tasks: [default_voice]
|
||||
backend: openai-audio-speech
|
||||
endpoint: /v1/audio/speech
|
||||
trust_remote_code: true
|
||||
@@ -31,6 +31,13 @@ models:
|
||||
voice: Vivian
|
||||
language: English
|
||||
task_type: CustomVoice
|
||||
|
||||
Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign:
|
||||
supported_tasks: [voice_design]
|
||||
backend: openai-audio-speech
|
||||
endpoint: /v1/audio/speech
|
||||
trust_remote_code: true
|
||||
task_extra_body:
|
||||
voice_design:
|
||||
task_type: VoiceDesign
|
||||
language: English
|
||||
|
||||
+4
-2
@@ -17,6 +17,7 @@ nav:
|
||||
- Streaming and Realtime:
|
||||
- Realtime Audio: serving/realtime_api.md
|
||||
- Full Duplex: serving/full_duplex_api.md
|
||||
- Realtime Duplex Reference: serving/realtime_duplex_api.md
|
||||
- Streaming Video Input: serving/video_stream_api.md
|
||||
- Streaming Video Output: serving/streaming_video_output_api.md
|
||||
- OpenPI Robot Policy: serving/openpi_api.md
|
||||
@@ -65,13 +66,14 @@ nav:
|
||||
- ModelOpt: user_guide/quantization/modelopt.md
|
||||
- AutoRound: user_guide/quantization/autoround.md
|
||||
- msModelSlim: user_guide/quantization/msmodelslim.md
|
||||
- TorchAO: user_guide/quantization/torchao.md
|
||||
- Diffusion Acceleration:
|
||||
- Overview and Compatibility:
|
||||
- Overview: user_guide/diffusion_features.md
|
||||
- Feature Compatibility: user_guide/feature_compatibility.md
|
||||
- CPU Offloading:
|
||||
- Overview: user_guide/diffusion/cpu_offload.md
|
||||
- Model-Level: user_guide/diffusion/offloader/cpu_offload.md
|
||||
- Model-Level: user_guide/diffusion/offloader/module_offload.md
|
||||
- Layerwise: user_guide/diffusion/offloader/layerwise_offload.md
|
||||
- Distributed Layerwise: user_guide/diffusion/offloader/distributed_layerwise_offload.md
|
||||
- Cache Acceleration:
|
||||
@@ -150,7 +152,7 @@ nav:
|
||||
- Skip-Softmax: design/feature/skip_softmax.md
|
||||
- CPU Offloading:
|
||||
- Overview: design/feature/offloader/README.md
|
||||
- Model-Level: design/feature/offloader/cpu_offload.md
|
||||
- Model-Level: design/feature/offloader/module_offload.md
|
||||
- Layerwise: design/feature/offloader/layerwise_offload.md
|
||||
- Distributed Layerwise: design/feature/offloader/distributed_layerwise_offload.md
|
||||
- Cache-DiT: design/feature/cache_dit.md
|
||||
|
||||
+1
-2
@@ -23,7 +23,6 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
<a class="github-button" href="https://github.com/vllm-project/vllm-omni/fork" data-show-count="true" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork">Fork</a>
|
||||
</p>
|
||||
|
||||
|
||||
## About
|
||||
|
||||
[vLLM](https://github.com/vllm-project/vllm) was originally designed to support large language models for text-based autoregressive generation tasks. vLLM-Omni is a framework that extends its support for omni-modality model inference and serving:
|
||||
@@ -52,7 +51,7 @@ vLLM-Omni is flexible and easy to use with:
|
||||
- Tensor, pipeline, data and expert parallelism support for distributed inference
|
||||
- Streaming outputs
|
||||
- OpenAI-compatible API server
|
||||
- Full-duplex realtime serving with streaming audio input and output (experimental)
|
||||
- [Full-duplex realtime serving](serving/realtime_duplex_api.md) with streaming audio input and output
|
||||
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
|
||||
@@ -15,6 +15,36 @@ Main entry points for vLLM-Omni inference and serving.
|
||||
- [vllm_omni.entrypoints.omni_base.OmniBase][]
|
||||
- [vllm_omni.entrypoints.pd_utils.PDDisaggregationMixin][]
|
||||
|
||||
## Clients
|
||||
|
||||
Client libraries that talk to a running vLLM-Omni server (the full-duplex Realtime client and its per-model session presets).
|
||||
|
||||
- [vllm_omni.clients.duplex.AudioDelta][]
|
||||
- [vllm_omni.clients.duplex.AudioFormat][]
|
||||
- [vllm_omni.clients.duplex.ConnectionResumed][]
|
||||
- [vllm_omni.clients.duplex.DuplexClient][]
|
||||
- [vllm_omni.clients.duplex.DuplexClientError][]
|
||||
- [vllm_omni.clients.duplex.DuplexConnectionError][]
|
||||
- [vllm_omni.clients.duplex.DuplexEvent][]
|
||||
- [vllm_omni.clients.duplex.DuplexProtocolError][]
|
||||
- [vllm_omni.clients.duplex.DuplexSessionClosedError][]
|
||||
- [vllm_omni.clients.duplex.ErrorEvent][]
|
||||
- [vllm_omni.clients.duplex.EventCollector][]
|
||||
- [vllm_omni.clients.duplex.ListenDecision][]
|
||||
- [vllm_omni.clients.duplex.ReconnectPolicy][]
|
||||
- [vllm_omni.clients.duplex.ResponseCreated][]
|
||||
- [vllm_omni.clients.duplex.ResponseDone][]
|
||||
- [vllm_omni.clients.duplex.ResponseHandle][]
|
||||
- [vllm_omni.clients.duplex.SessionClosed][]
|
||||
- [vllm_omni.clients.duplex.SessionConfig][]
|
||||
- [vllm_omni.clients.duplex.SessionCreated][]
|
||||
- [vllm_omni.clients.duplex.SessionExpired][]
|
||||
- [vllm_omni.clients.duplex.SessionResumed][]
|
||||
- [vllm_omni.clients.duplex.SpeakDecision][]
|
||||
- [vllm_omni.clients.duplex.TextDelta][]
|
||||
- [vllm_omni.clients.duplex.TranscriptDelta][]
|
||||
- [vllm_omni.clients.duplex.WebSocketTransport][]
|
||||
|
||||
## Inputs
|
||||
|
||||
Input data structures for multi-modal inputs.
|
||||
|
||||
+45
-7
@@ -1,11 +1,15 @@
|
||||
# vLLM-Omni Benchmark CLI Guide
|
||||
|
||||
The vllm bench command launches the vLLM-Omni benchmark to evaluate the performance of multimodal models.
|
||||
|
||||
## Notes
|
||||
|
||||
vLLM-Omni registers the `openai-chat-omni`, `openai-audio-speech`, `openai-image-edits-omni`, `daily-omni`, and `openai-realtime-duplex` serving benchmark backends. It also adds the `omniinteract` dataset.
|
||||
|
||||
## Basic Parameter Description
|
||||
|
||||
You can use `vllm bench serve --omni --help=all` to get descriptions of all parameters. The commonly used parameters are described below:
|
||||
|
||||
- `--omni`
|
||||
Enable Omni (multimodal) mode, supporting multimodal inputs and outputs such as images, videos, and audio.
|
||||
|
||||
@@ -77,8 +81,8 @@ Specify to save benchmark results to a json file
|
||||
- `--random-prefix-len`
|
||||
Number of fixed prefix tokens before the random context in a request.
|
||||
The total input length is the sum of random-prefix-len and a random
|
||||
context length sampled from [input_len * (1 - range_ratio),
|
||||
input_len * (1 + range_ratio)].Only the random and random-mm modes
|
||||
context length sampled from [input_len *(1 - range_ratio),
|
||||
input_len* (1 + range_ratio)].Only the random and random-mm modes
|
||||
support this parameter.
|
||||
|
||||
- `--random-input-len`
|
||||
@@ -91,7 +95,7 @@ Specify to save benchmark results to a json file
|
||||
Range ratio for sampling input/output length,
|
||||
used only for random sampling. Must be in the range [0, 1) to define
|
||||
a symmetric sampling range
|
||||
[length * (1 - range_ratio), length * (1 + range_ratio)].
|
||||
[length *(1 - range_ratio), length* (1 + range_ratio)].
|
||||
Only the random and random-mm modes support this parameter.
|
||||
|
||||
- `--random-mm-base-items-per-request`
|
||||
@@ -137,6 +141,7 @@ Specify to save benchmark results to a json file
|
||||
## Usage Examples
|
||||
|
||||
### Online Benchmark
|
||||
|
||||
<details class="admonition abstract" markdown="1">
|
||||
<summary>Show more</summary>
|
||||
|
||||
@@ -162,7 +167,9 @@ vllm bench serve \
|
||||
--dataset-path ShareGPT_V3_unfiltered_cleaned_split.json \
|
||||
--percentile-metrics ttft,tpot,itl,e2el
|
||||
```
|
||||
|
||||
If successful, you will see the following output:
|
||||
|
||||
```text
|
||||
============ Serving Benchmark Result ============
|
||||
Successful requests: 2
|
||||
@@ -264,6 +271,7 @@ Median AUDIO_RTF: 0.47
|
||||
P99 AUDIO_RTF: 0.48
|
||||
==================================================
|
||||
```
|
||||
|
||||
Notes:
|
||||
We use audio generation time / audio duration to calculate RTF.
|
||||
|
||||
@@ -301,12 +309,36 @@ options apply. Use `--omniinteract-require-response` only for functional E2E cas
|
||||
Each completed case writes `output.wav`, `wav_transcript.json`, `events.json`, `result.json`, and a final `.done` marker under
|
||||
`--omniinteract-output-dir`. The root also contains `batch_summary.json` and `official_eval_manifest.jsonl`; failed cases write
|
||||
`.failed.json`. Runs sharing one output root are serialized. Completion validates transport, response lifecycle, and artifacts,
|
||||
not answer accuracy. Transcript timestamps are serialized playback-queue times. Clipped or cancelled outputs are ineligible and
|
||||
omitted from the official manifest; `audio_clipped_bytes` records output beyond the rounded video horizon.
|
||||
not answer accuracy. Transcript timestamps are serialized playback-queue times. Playback ACKs report cumulative progress
|
||||
incrementally along that serialized clock, like a live listener; the first ack for a response goes out as soon as its audio
|
||||
arrives, checkpointing the response's history position so a later committed user input updates it in place. A residual
|
||||
`playback_ack_too_late` rejection is recorded as an artifact warning rather than failing the case. Clipped or cancelled
|
||||
outputs are ineligible and omitted from the official manifest; `audio_clipped_bytes` records output beyond the rounded video
|
||||
horizon.
|
||||
|
||||
TTFT, TTFP, and RTF start at client receipt of `response.created`. TPOT/ITL use engine stage-0 timing; ITL is emitted only when
|
||||
every token interval is present.
|
||||
|
||||
The checked-in local performance configuration measures four deterministic cases from each OmniInteract subset (12 videos
|
||||
total), with no benchmark warmups and a maximum concurrency of two. Each subset also sends one readiness request before its
|
||||
measured cases. Run it from the repository root with:
|
||||
|
||||
```bash
|
||||
export HF_HOME=/path/to/persistent/huggingface-cache
|
||||
export BENCHMARK_DIR=tests/dfx/perf/results
|
||||
bash tools/nightly/run_nightly_jobs.sh \
|
||||
--test-type local \
|
||||
--model-type omni \
|
||||
--label-substr minicpmo_4_5_omniinteract
|
||||
```
|
||||
|
||||
The first run downloads the pinned OmniInteract archive into `HF_HOME`; later runs reuse that cache.
|
||||
|
||||
It requires every case to commit its input, complete any emitted response lifecycles, and publish the expected WAV,
|
||||
transcript, event, and result artifacts without errors. A valid LISTEN-only case may have no response audio or transcript
|
||||
chunks. Official-manifest eligibility is reported separately because clipped or cancelled output is a benchmark-quality
|
||||
signal, not a transport failure. This local performance test does not score answer accuracy.
|
||||
|
||||
### Multi-Modal Benchmark
|
||||
|
||||
<details class="admonition abstract" markdown="1">
|
||||
@@ -319,8 +351,8 @@ Generate synthetic image、video、audio inputs alongside random text prompts to
|
||||
Notes:
|
||||
|
||||
- Works only with online benchmark via:
|
||||
- the OpenAI chat backend (`--backend openai-chat-omni`) and endpoint `/v1/chat/completions`.
|
||||
- the OpenAI edit image backend (`--backend openai-image-edits-omni`) and endpoint `/v1/images/edits`.
|
||||
- the OpenAI chat backend (`--backend openai-chat-omni`) and endpoint `/v1/chat/completions`.
|
||||
- the OpenAI edit image backend (`--backend openai-image-edits-omni`) and endpoint `/v1/images/edits`.
|
||||
|
||||
Start the server (example):
|
||||
|
||||
@@ -331,6 +363,7 @@ vllm serve Qwen/Qwen2.5-Omni-7B --omni
|
||||
It is recommended to use the flag `--ignore-eos` to simulate real responses. You can set the size of the output via the arg `random-output-len`.
|
||||
|
||||
Then run the benchmarking script:
|
||||
|
||||
```bash
|
||||
vllm bench serve \
|
||||
--omni \
|
||||
@@ -400,6 +433,7 @@ How sampling works:
|
||||
- If a modality (e.g., image) reaches its limit from `--random-mm-limit-mm-per-prompt`, all buckets of that modality are excluded and the remaining bucket probabilities are renormalized before continuing.
|
||||
This should be seen as an edge case, and if this behavior can be avoided by setting `--random-mm-limit-mm-per-prompt` to a large number. Note that this might result in errors due to engine config `--limit-mm-per-prompt`.
|
||||
- The resulting request contains synthetic image data in `multi_modal_data` (OpenAI Chat format). When `random-mm` is used with the OpenAI Chat backend, prompts remain text and MM content is attached via `multi_modal_data`.
|
||||
|
||||
</details>
|
||||
|
||||
### Multi-Stage Benchmark
|
||||
@@ -414,6 +448,7 @@ vllm serve --omni --port 29999 --model ~/Qwen3-Omni-30B-A3B-Instruct
|
||||
```
|
||||
|
||||
Then run the benchmarking script (remember to add the flag `--print-stage`):
|
||||
|
||||
```bash
|
||||
vllm bench serve --omni \
|
||||
--dataset-name random \
|
||||
@@ -433,6 +468,7 @@ vllm bench serve --omni \
|
||||
```
|
||||
|
||||
Besides the "Serving Benchmark Result", there will be "Stage Benchmark Result" below:
|
||||
|
||||
```text
|
||||
============= Stage Benchmark Result =============
|
||||
=============== Stage 0 (thinker) ================
|
||||
@@ -489,7 +525,9 @@ Mean AUDIO_RTF: 0.24
|
||||
Median AUDIO_RTF: 0.26
|
||||
P99 AUDIO_RTF: 0.27
|
||||
```
|
||||
|
||||
Explanation:
|
||||
|
||||
- stage_gen_time: Time from submitting a request to a specific stage to that stage finishing generation.
|
||||
|
||||
- Serving TTFC (Time to First Chunk): Time from the HTTP request being accepted by the serving frontend to
|
||||
|
||||
@@ -29,7 +29,7 @@ Each stage is implemented as a separate model class that can be configured indep
|
||||
|
||||
When adding a new model, you'll need to create the following structure:
|
||||
|
||||
```
|
||||
```text
|
||||
vllm_omni/model_executor/models/
|
||||
└── your_model_name/ # Model directory (e.g., qwen3_omni)
|
||||
├── __init__.py # Exports main model class
|
||||
@@ -422,22 +422,25 @@ The stage transition process follows these steps:
|
||||
Understanding the data structures is crucial for implementing stage transitions:
|
||||
|
||||
**Input to your function:**
|
||||
|
||||
- `stage_list[source_stage_id].engine_outputs`: List of `EngineCoreOutput` objects
|
||||
- - Each contains `outputs`: List of `RequestOutput` objects
|
||||
- Each `RequestOutput` has:
|
||||
- - - `token_ids`: Generated token IDs
|
||||
- `multimodal_output`: Dict with keys like `"code_predictor_codes"`, etc.These are the hidden states or intermediate outputs from the model's forward pass
|
||||
- `prompt_token_ids`: Original prompt token IDs
|
||||
- Each contains `outputs`: List of `RequestOutput` objects
|
||||
- Each `RequestOutput` has:
|
||||
- `token_ids`: Generated token IDs
|
||||
- `multimodal_output`: Dict with keys like `"code_predictor_codes"`, etc. These are the hidden states or intermediate outputs from the model's forward pass
|
||||
- `prompt_token_ids`: Original prompt token IDs
|
||||
|
||||
**Output from your function:**
|
||||
|
||||
- Must return `list[OmniTokensPrompt]` where each `OmniTokensPrompt` contains:
|
||||
- - `prompt_token_ids`: List[int] - Token IDs for the next stage
|
||||
- `prompt_token_ids`: List[int] - Token IDs for the next stage
|
||||
- `additional_information`: Dict[str, Any] - Optional metadata (e.g., embeddings, hidden states)
|
||||
- `multi_modal_data`: Optional multimodal data if needed
|
||||
|
||||
### How Model Outputs Are Stored
|
||||
|
||||
The model's `forward()` method returns an `OmniOutput` object that contains:
|
||||
|
||||
- `text_hidden_states`: Final hidden states for text generation
|
||||
- `multimodal_outputs`: Dict containing intermediate outputs
|
||||
|
||||
@@ -464,6 +467,7 @@ def forward(self, ...):
|
||||
```
|
||||
|
||||
These keys are then accessible in your stage transition function:
|
||||
|
||||
```python
|
||||
# In stage_input_processors/qwen3_omni.py
|
||||
thinker_prefill_embeddings = output.multimodal_output["0"] # Access by key
|
||||
@@ -493,7 +497,7 @@ thinker_hidden_states = output.multimodal_output["24"]
|
||||
Create stage transition processors in `vllm_omni/model_executor/stage_input_processors/your_model_name.py`. Each inter-stage edge should provide a **coherent processor set** rather than a single monolithic function:
|
||||
|
||||
| Suffix | Role | Runs when |
|
||||
|--------|------|-----------|
|
||||
| -------- | ------ | ----------- |
|
||||
| `*_full_payload` | Worker-side payload producer | `async_chunk=false`; accumulates tensors and ships via connector |
|
||||
| `*_async_chunk` | Scheduler-side streaming producer | `async_chunk=true`; emits per-chunk payloads |
|
||||
| `*_token_only` | Orchestrator placeholder builder | `async_chunk=false`; allocates downstream prompt slots only |
|
||||
@@ -554,7 +558,16 @@ For comprehensive testing guidelines, please refer to the [Test Writing Guide](.
|
||||
|
||||
## Adding a Model Recipe
|
||||
|
||||
After implementing and testing your model, please add a model recipe to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository. This helps other users understand how to use your model with vLLM-Omni.
|
||||
After implementing and testing your model, add a recipe under this repository's
|
||||
`recipes/` directory. Follow the
|
||||
[`add-recipe` skill](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/add-recipe/SKILL.md)
|
||||
and [`recipes/TEMPLATE.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/TEMPLATE.md),
|
||||
then update the model row in
|
||||
[`recipes/README.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/README.md).
|
||||
The external
|
||||
[vllm-project/recipes](https://github.com/vllm-project/recipes) repository can
|
||||
be used as a structural reference, but it is not the destination for a
|
||||
vLLM-Omni model recipe.
|
||||
|
||||
### What to Include
|
||||
|
||||
@@ -568,15 +581,10 @@ Your recipe should include:
|
||||
3. **Usage Examples**: Command-line examples demonstrating how to run the model
|
||||
4. **Configuration Details**: Important configuration parameters and their meanings
|
||||
|
||||
### Example
|
||||
|
||||
For reference, see the [LongCat recipe example](https://github.com/vllm-project/recipes/pull/179) which demonstrates the expected format and structure.
|
||||
|
||||
### Recipe Location
|
||||
|
||||
Create your recipe file in the appropriate directory structure:
|
||||
- For organization-specific models: `OrganizationName/ModelName.md`
|
||||
- For general models: `ModelName.md`
|
||||
Create `recipes/<vendor>/<model-family>.md` and keep commands, feature status,
|
||||
hardware evidence, and limitations specific to the model family.
|
||||
|
||||
The recipe should be a Markdown file that provides clear, reproducible instructions for users to get started with your model.
|
||||
|
||||
@@ -590,7 +598,7 @@ Adding a new model to vLLM-Omni involves:
|
||||
4. **Define pipeline topology and deploy defaults** in `pipeline.py` and `vllm_omni/deploy/`
|
||||
5. **Implement stage input processors** for stage transitions
|
||||
6. **Write tests** to verify functionality
|
||||
7. **Add model recipe** to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository (see [Adding a Model Recipe](#adding-a-model-recipe) section)
|
||||
7. **Add an in-repository model recipe** under `recipes/` and update its index (see [Adding a Model Recipe](#adding-a-model-recipe))
|
||||
|
||||
### Qwen3-Omni Reference Files
|
||||
|
||||
@@ -607,6 +615,7 @@ For a complete reference implementation, see:
|
||||
- **Testing**: `vllm_omni/tests/e2e/offline_inference/test_qwen3_omni.py`
|
||||
|
||||
For more information, see:
|
||||
|
||||
- [Architecture Overview](../../design/architecture_overview.md)
|
||||
- [Supported Models](../../models/supported_models.md)
|
||||
- [Pipeline and Deploy Configuration Guide](../../configuration/stage_configs.md)
|
||||
@@ -991,10 +991,13 @@ git push origin your-branch --force-with-lease
|
||||
|
||||
## Adding a Model Recipe
|
||||
|
||||
After implementing and testing your model, add a model recipe to the
|
||||
[vllm-project/recipes](https://github.com/vllm-project/recipes) repository so users can
|
||||
get started quickly. See [Adding an Omni-Modality Model](./adding_omni_model.md#adding-a-model-recipe)
|
||||
for the expected format.
|
||||
After implementing and testing your model, add a recipe under this repository's
|
||||
`recipes/` directory. Follow the
|
||||
[`add-recipe` skill](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/add-recipe/SKILL.md)
|
||||
and [`recipes/TEMPLATE.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/TEMPLATE.md),
|
||||
update the model row in
|
||||
[`recipes/README.md`](https://github.com/vllm-project/vllm-omni/blob/main/recipes/README.md),
|
||||
and synchronize the relevant TTS example and speech API documentation.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -1011,7 +1014,7 @@ Adding a TTS model to vLLM-Omni involves:
|
||||
8. **Online serving** - add all 5 integration points to `serving_speech.py` in one commit
|
||||
9. **Tests** - cover single request, batching, and streaming
|
||||
10. **Pre-commit + DCO** - run `pre-commit` before pushing; sign every commit with `git commit -s`
|
||||
11. **Model recipe** - add to [vllm-project/recipes](https://github.com/vllm-project/recipes)
|
||||
11. **Model recipe** - add under the in-repository `recipes/` directory and update its index
|
||||
12. **Invariants** - re-check I1–I5 (streaming contract, consumer hygiene, hot-loop discipline, validation pyramid, per-request state) at the end of every phase
|
||||
|
||||
### Qwen3-TTS Reference Files
|
||||
|
||||
@@ -418,7 +418,7 @@ The public interfaces map onto the same engine and stage boundaries:
|
||||
flowchart LR
|
||||
offline["Offline Python<br/>Omni.generate()"] --> engine["AsyncOmniEngine"]
|
||||
online["OpenAI-compatible serving<br/>vllm serve ... --omni"] --> engine
|
||||
duplex["Experimental duplex WebSocket<br/>/v1/duplex or realtime duplex"] --> engine
|
||||
duplex["Duplex WebSocket<br/>/v1/realtime?duplex=1 or /v1/duplex"] --> engine
|
||||
engine --> stages["Configured AR and diffusion stages"]
|
||||
stages --> result["Streaming or final multimodal output"]
|
||||
```
|
||||
|
||||
@@ -150,8 +150,17 @@ that do not belong in the shared contract should be stored in `state.extra`.
|
||||
Queueing and lifecycle metadata remain in the scheduler's request state.
|
||||
|
||||
Current native pipelines that explicitly enable step execution include
|
||||
Qwen-Image, HunyuanImage3, and Helios. Step execution alone does not imply
|
||||
continuous-batching support: Qwen-Image accepts batched step states.
|
||||
Qwen-Image, BAGEL, HunyuanImage3, and Helios. Step execution alone does not
|
||||
imply continuous-batching support: Qwen-Image accepts batched step states.
|
||||
BAGEL accepts batched step states for image generation with its default,
|
||||
think-mode, and single-stage deploy configs. Only the diffusion stage uses the
|
||||
step protocol in two-stage deployments; explicit single-stage text output
|
||||
retains complete-request execution. BAGEL requires at least two inference steps
|
||||
because it schedules `num_inference_steps - 1` denoising updates, and its step
|
||||
path does not currently support sequence parallelism or a diffusion cache
|
||||
backend. When a finished stepwise request is followed by an explicit text
|
||||
fallback wave, the runner retires its step state and paged diffusion-KV rows
|
||||
before the full forward dispatch.
|
||||
HunyuanImage3 accepts batched step states only when its resolved self-attention
|
||||
backend is `TORCH_SDPA`; otherwise it rejects groups larger than one request.
|
||||
Configure `DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA` or
|
||||
|
||||
@@ -42,6 +42,11 @@ V1 does not include:
|
||||
|
||||
## Motivation and use cases
|
||||
|
||||
For investigating CPU memory retained by dependency tensor materialization,
|
||||
see the [standalone safetensors diagnostic](https://github.com/vllm-project/vllm-omni/blob/main/benchmarks/host_weight_runtime/README.md).
|
||||
It distinguishes repeated dependency calls from reuse of cached views and does
|
||||
not establish per-request HWR leakage.
|
||||
|
||||
Model loading can create the same final host representation repeatedly. This
|
||||
is especially expensive when loading performs checkpoint decoding, tensor
|
||||
renaming, TP slicing, quantization, packing, or scale construction before GPU
|
||||
|
||||
@@ -5,23 +5,27 @@ The three strategies have separate user and design pages:
|
||||
|
||||
| Strategy | User guide | Design contract |
|
||||
| --- | --- | --- |
|
||||
| Model-level | [Guide](../../../user_guide/diffusion/offloader/cpu_offload.md) | [Design](cpu_offload.md) |
|
||||
| Model-level | [Guide](../../../user_guide/diffusion/offloader/module_offload.md) | [Design](module_offload.md) |
|
||||
| Layerwise | [Guide](../../../user_guide/diffusion/offloader/layerwise_offload.md) | [Design](layerwise_offload.md) |
|
||||
| Distributed layerwise | [Guide](../../../user_guide/diffusion/offloader/distributed_layerwise_offload.md) | [Design](distributed_layerwise_offload.md) |
|
||||
|
||||
## Strategy selection
|
||||
|
||||
`OffloadConfig.from_od_config()` converts diffusion configuration into one
|
||||
`OffloadStrategy`. If callers set multiple flags, selection is deterministic:
|
||||
`OffloadStrategy`. The public config separates component selection from
|
||||
policy:
|
||||
|
||||
1. `DISTRIBUTED_LAYERWISE`;
|
||||
2. `LAYERWISE`;
|
||||
3. `MODEL_LEVEL`; and
|
||||
4. `NONE`.
|
||||
- `mode="module"` selects model-level offload;
|
||||
- `mode="layer"` with rank-local transfers selects ordinary layerwise
|
||||
offload; and
|
||||
- AllGather transfer or resident layers selects the distributed layerwise
|
||||
backend that implements those capabilities.
|
||||
|
||||
The factory logs when a higher-priority strategy disables another. It derives
|
||||
parallel and HSDP state from `DiffusionParallelConfig`; callers do not provide
|
||||
a separate offload group size.
|
||||
The compatibility boolean flags retain their historical priority (distributed
|
||||
layerwise, layerwise, then model-level). A compact config rejects a conflicting
|
||||
legacy strategy or non-default legacy DLO tuning. The factory derives parallel
|
||||
and HSDP state from `DiffusionParallelConfig`; callers do not provide a
|
||||
separate offload group size.
|
||||
|
||||
`get_offload_backend()` then validates platform offload support, resolves the
|
||||
device, and creates exactly one backend. Returning `None` means offloading is
|
||||
@@ -39,6 +43,11 @@ Every backend implements `OffloadBackend`:
|
||||
`disable()` does not promise to restore the pipeline's original device
|
||||
placement. The caller owns any subsequent rematerialization.
|
||||
|
||||
An enable failure must remove every partially installed hook. Rank-local
|
||||
backends restore ordinary tensors so the pipeline can be retried. A failed
|
||||
multi-rank AllGather startup must not enter unmatched recovery collectives;
|
||||
that worker is safely discarded after local resource cleanup.
|
||||
|
||||
Hooks are registered through `HookRegistry` and `ModelHook`; offload backends
|
||||
must use distinct hook names and remove only hooks they own.
|
||||
|
||||
@@ -55,13 +64,13 @@ Dotted paths are supported. Legacy pipelines may use the fallback scan of
|
||||
well-known attribute names, but new integrations should declare components
|
||||
explicitly.
|
||||
|
||||
Ordinary layerwise offload obtains topology from declared
|
||||
`_layerwise_offload_blocks_attrs` (with a deprecated singular-name
|
||||
compatibility path). Distributed layerwise offload may instead use a pipeline
|
||||
`OffloadPlan`, then falls back to declared block attributes and its documented
|
||||
compatibility discovery. Discovery metadata describes structure only; the
|
||||
backend remains responsible for transfer, synchronization, and storage
|
||||
ownership.
|
||||
Both layerwise backends first consume pipeline `OffloadPlan` metadata. A
|
||||
plan's `block_attrs` maps each DiT path to its ordered block containers, while
|
||||
`encoder_block_attrs` declares streamable encoder stacks. DiTs absent from the
|
||||
plan fall back to `_layerwise_offload_blocks_attrs` (including the deprecated
|
||||
singular-name compatibility path). Discovery metadata describes structure
|
||||
only; the backend remains responsible for transfer, synchronization, and
|
||||
storage ownership.
|
||||
|
||||
## Cross-strategy invariants
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Distributed Layerwise Offload
|
||||
|
||||
This document describes distributed layerwise offload (DLO) for diffusion
|
||||
models. DLO keeps only a small number of DiT blocks on the accelerator and
|
||||
streams the remaining blocks from host memory. The distributed backend can
|
||||
either shard those host-side weights across an existing parallel group or keep
|
||||
complete rank-local block sources and avoid an additional collective.
|
||||
models. DLO keeps only a small number of selected DiT or text-encoder blocks
|
||||
on the accelerator and streams the remaining blocks from host memory. The
|
||||
distributed backend can either shard those host-side weights across an
|
||||
existing parallel group or keep complete rank-local block sources and avoid
|
||||
an additional collective.
|
||||
|
||||
For user-facing commands, see the
|
||||
[distributed layerwise offloading guide](../../../user_guide/diffusion/offloader/distributed_layerwise_offload.md)
|
||||
@@ -38,10 +39,11 @@ detailed contracts and validation boundaries.
|
||||
|
||||
## Status
|
||||
|
||||
DLO is implemented for multi-device diffusion execution. The default
|
||||
AllGather path is the primary path for DP and SP deployments. The
|
||||
`--dlo-no-use-allgather` path streams complete blocks independently and adds no
|
||||
DLO weight collective.
|
||||
DLO is implemented for multi-device diffusion execution. The default public
|
||||
transfer is `rank-local`; setting a selected component's
|
||||
`layer_options.NAME.weight_transfer` to `allgather` enables weight sharding across a
|
||||
compatible DP or SP group. Transfers are configured independently for `dit`
|
||||
and `text_encoder`.
|
||||
|
||||
Host storage is selected separately from the transfer protocol. The loader can
|
||||
produce a direct-checkpoint mmap plan for a proven-compatible runtime layout;
|
||||
@@ -109,7 +111,7 @@ checkpoint adapter beside the ordinary loader.
|
||||
|
||||
### AllGather path
|
||||
|
||||
With the default `dlo_use_allgather=True`, each rank stores approximately
|
||||
For a component configured with `weight_transfer="allgather"`, each rank stores approximately
|
||||
`1 / group_size` of each streamable block in pinned host memory. The next
|
||||
block's shard is copied to a device buffer and reconstructed with
|
||||
`all_gather_into_tensor` on a communication stream while the current block is
|
||||
@@ -132,9 +134,8 @@ source used to prepare each rank's persistent shard. They can be closed after
|
||||
shard preparation. Across the AllGather group, those private shards total
|
||||
approximately one runtime model copy.
|
||||
|
||||
An effective DLO group size of one performs no collective, even when
|
||||
`dlo_use_allgather=True`; it follows the rank-local transfer path described
|
||||
below.
|
||||
An effective DLO group size of one performs no collective; it follows the
|
||||
rank-local transfer path described below.
|
||||
|
||||
When DP is greater than one, the engine can process one request per DP rank in
|
||||
the same denoising wave. Because AllGather is a collective, all participating
|
||||
@@ -142,9 +143,10 @@ requests must take the same execution path at every denoising step.
|
||||
|
||||
### Rank-local path without DLO AllGather
|
||||
|
||||
With `--dlo-no-use-allgather`, DLO forces its internal offload shard size to
|
||||
one and streams complete blocks using H2D copies only. The host backing may be
|
||||
either a loader-approved checkpoint mapping or ordinary runtime tensors.
|
||||
With `weight_transfer="rank-local"`, DLO forces the affected component's internal
|
||||
offload shard size to one and streams complete blocks using H2D copies only.
|
||||
The host backing may be either a loader-approved checkpoint mapping or
|
||||
ordinary runtime tensors.
|
||||
|
||||
For direct mmap, each process retains immutable safetensors views and uses two
|
||||
bounded pinned host staging slots. Processes on the same node that map the same
|
||||
@@ -204,7 +206,7 @@ adopt it.
|
||||
> optimizations without changing AllGather behavior.
|
||||
|
||||
Final-layout Host Weight Runtime (HWR) backing is opt-in and currently applies
|
||||
only to no-AllGather DLO. Enable it with
|
||||
only to a selected rank-local DiT. Enable it with
|
||||
`--host-weight-runtime-mode preferred` (or `required`) and
|
||||
`--host-weight-runtime-root <node-local-root>`.
|
||||
|
||||
@@ -422,8 +424,8 @@ failure.
|
||||
collective. When the model declares the final-layout contract, HWR keys the
|
||||
reusable artifact by TP rank/size and SP layout.
|
||||
- **HSDP + SP:** the general parallel configuration permits HSDP over SP, but
|
||||
DLO must use `--dlo-no-use-allgather`. HSDP remains responsible for weight
|
||||
materialization and synchronization.
|
||||
each selected component must use `weight_transfer="rank-local"`. HSDP remains
|
||||
responsible for weight materialization and synchronization.
|
||||
- **HSDP + DP or TP:** rejected independently by the diffusion parallel
|
||||
configuration.
|
||||
|
||||
@@ -447,8 +449,9 @@ recorded layouts (they all keep plain 1-byte dtypes over ordinary strided
|
||||
views: online INT8 has an int8 weight plus fp32 scale, online MXFP8 has an
|
||||
fp8 weight plus e8m0 block scale, each with a contiguous or transposed-view
|
||||
finalized tensor, all covered by the same physical-order packing as online
|
||||
FP8). Other online methods must use `--dlo-no-use-allgather` or disable
|
||||
online quantization until their runtime layouts are validated.
|
||||
FP8). Other online methods must set
|
||||
`layer_options.dit.weight_transfer="rank-local"` or disable online quantization
|
||||
until their runtime layouts are validated.
|
||||
|
||||
The Host Weight Runtime representation, publication, and no-AllGather consumer
|
||||
contracts are merged; see
|
||||
|
||||
@@ -36,12 +36,14 @@ dependencies, not incidental global synchronization, define correctness.
|
||||
## Topology contract
|
||||
|
||||
A DiT declares ordered block-container attributes using
|
||||
`_layerwise_offload_blocks_attrs` or an `OffloadPlan`. Each resolved item must
|
||||
be an executable `nn.Module`; ordering must match forward execution. Multiple
|
||||
containers are concatenated in execution order.
|
||||
`_layerwise_offload_blocks_attrs` or an `OffloadPlan`. A text encoder declares
|
||||
its block-list paths through `OffloadPlan.encoder_block_attrs`. Each resolved
|
||||
item must be an executable `nn.Module`; ordering must match forward execution.
|
||||
Multiple containers are streamed as separate hook rings.
|
||||
|
||||
Non-block DiT modules, encoders, VAEs, and declared resident modules are not
|
||||
streamed by this backend.
|
||||
Non-block state of a selected DiT or text encoder remains device resident.
|
||||
Unselected components, VAEs, and declared resident modules are not streamed by
|
||||
this backend.
|
||||
|
||||
## Invariants and limitations
|
||||
|
||||
@@ -50,7 +52,8 @@ streamed by this backend.
|
||||
- Blocks with heterogeneous shapes and dtypes use their recorded metadata;
|
||||
implementations must not assume identical blocks.
|
||||
- The ring is initialized before the first forward so block zero is available.
|
||||
- This backend is single-device; distributed weight reconstruction belongs to
|
||||
- This backend uses rank-local host weights. Multi-device compute may remain
|
||||
active, but cross-rank weight reconstruction belongs to
|
||||
[Distributed Layerwise Offload](distributed_layerwise_offload.md).
|
||||
|
||||
Shared selection and lifecycle behavior is defined in the
|
||||
|
||||
+11
-4
@@ -2,7 +2,7 @@
|
||||
|
||||
Model-level offload enforces mutual exclusion between pipeline component
|
||||
groups. For commands and model support, see the
|
||||
[model-level user guide](../../../user_guide/diffusion/offloader/cpu_offload.md).
|
||||
[model-level user guide](../../../user_guide/diffusion/offloader/module_offload.md).
|
||||
|
||||
## Generic hook path
|
||||
|
||||
@@ -10,10 +10,15 @@ groups. For commands and model support, see the
|
||||
makes encoders and VAEs device resident for initialization, then installs
|
||||
`SequentialOffloadHook` instances:
|
||||
|
||||
- each DiT hook offloads all encoders and other DiTs before loading itself;
|
||||
- each encoder hook offloads every DiT before loading itself; and
|
||||
- each DiT hook offloads selected encoders and other selected DiTs before
|
||||
loading itself;
|
||||
- each encoder hook offloads selected DiTs before loading itself; and
|
||||
- VAEs are not part of the mutual-exclusion hook set.
|
||||
|
||||
All execution-stage modules still receive a hook so an unselected resident
|
||||
stage can evict a selected component before it runs. Only components listed in
|
||||
`diffusion_offload_config.components` are eligible to move to CPU.
|
||||
|
||||
The hook moves parameters and buffers without recursively calling
|
||||
`module.to()`. This avoids recursion through modules that retain references to
|
||||
other transformer blocks. Host tensors are pinned when configured; DTensors
|
||||
@@ -36,7 +41,9 @@ contract:
|
||||
|
||||
- only the executing component is device resident;
|
||||
- enable and disable are idempotent;
|
||||
- transfers use the supplied device, pinning, and HSDP settings; and
|
||||
- transfers use the supplied device, pinning, and HSDP settings;
|
||||
- explicit component selection is either honored or rejected before partial
|
||||
setup; and
|
||||
- disabling removes all model-local offload hooks or contexts.
|
||||
|
||||
The delegation is currently duck typed. If another independent implementation
|
||||
@@ -84,8 +84,8 @@ dictionary narrows this global scope through runtime layer-prefix routing.
|
||||
|
||||
The factory uses Omni-specific overrides for methods whose diffusion behavior
|
||||
or platform dispatch is not provided by the vLLM registry. The current override
|
||||
set includes INT8, BitsAndBytes, MXFP8, MXFP4, MXFP4 dual-scale, and INC/
|
||||
AutoRound. Other methods, such as FP8, GGUF, and ModelOpt, are resolved through
|
||||
set includes INT8, BitsAndBytes, MXFP8, MXFP4, MXFP4 dual-scale, INC/AutoRound,
|
||||
and TorchAO. Other methods, such as FP8, GGUF, and ModelOpt, are resolved through
|
||||
vLLM's registry when their configuration is compatible.
|
||||
|
||||
## Per-component routing
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="460" viewBox="0 0 760 460" role="img" aria-labelledby="title desc">
|
||||
<title id="title">MAGI-2 DLO one-step Pareto frontier</title>
|
||||
<desc id="desc">Warm user latency in seconds versus throughput per device. DP1SP4 is 5.735 seconds and 0.0436 outputs per second per device, DP2SP2 is 6.785 and 0.0737, and DP4 is 9.997 and 0.1000.</desc>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<style>
|
||||
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.title { font-size: 20px; font-weight: 600; }
|
||||
.axis-label { font-size: 14px; font-weight: 500; }
|
||||
.tick { font-size: 12px; fill: #4b5563; }
|
||||
.point-label { font-size: 13px; font-weight: 600; }
|
||||
.value-label { font-size: 11px; fill: #4b5563; }
|
||||
.grid { stroke: #e5e7eb; stroke-width: 1; }
|
||||
.axis { stroke: #6b7280; stroke-width: 1.5; }
|
||||
.frontier { fill: none; stroke: #2563eb; stroke-width: 3; }
|
||||
.point { fill: #2563eb; stroke: #ffffff; stroke-width: 2; }
|
||||
</style>
|
||||
|
||||
<text x="380" y="28" text-anchor="middle" class="title">One diffusion step</text>
|
||||
|
||||
<g aria-hidden="true">
|
||||
<line x1="85" y1="309" x2="700" y2="309" class="grid"/>
|
||||
<line x1="85" y1="226" x2="700" y2="226" class="grid"/>
|
||||
<line x1="85" y1="143" x2="700" y2="143" class="grid"/>
|
||||
<line x1="85" y1="61" x2="700" y2="61" class="grid"/>
|
||||
<line x1="85" y1="40" x2="85" y2="350" class="axis"/>
|
||||
<line x1="85" y1="350" x2="700" y2="350" class="axis"/>
|
||||
|
||||
<text x="73" y="313" text-anchor="end" class="tick">0.04</text>
|
||||
<text x="73" y="230" text-anchor="end" class="tick">0.06</text>
|
||||
<text x="73" y="147" text-anchor="end" class="tick">0.08</text>
|
||||
<text x="73" y="65" text-anchor="end" class="tick">0.10</text>
|
||||
|
||||
<text x="85" y="370" text-anchor="middle" class="tick">5</text>
|
||||
<text x="197" y="370" text-anchor="middle" class="tick">6</text>
|
||||
<text x="309" y="370" text-anchor="middle" class="tick">7</text>
|
||||
<text x="421" y="370" text-anchor="middle" class="tick">8</text>
|
||||
<text x="532" y="370" text-anchor="middle" class="tick">9</text>
|
||||
<text x="644" y="370" text-anchor="middle" class="tick">10</text>
|
||||
</g>
|
||||
|
||||
<text x="392" y="414" text-anchor="middle" class="axis-label">Warm user latency (seconds) — lower is better</text>
|
||||
<text x="20" y="195" transform="rotate(-90 20 195)" text-anchor="middle" class="axis-label">Throughput/device (outputs/s) — higher is better</text>
|
||||
|
||||
<polyline points="167,294 285,169 644,61" class="frontier"/>
|
||||
|
||||
<circle cx="167" cy="294" r="7" class="point"/>
|
||||
<text x="179" y="288" class="point-label">DP1SP4</text>
|
||||
<text x="179" y="305" class="value-label">5.735 s, 0.0436</text>
|
||||
|
||||
<circle cx="285" cy="169" r="7" class="point"/>
|
||||
<text x="297" y="163" class="point-label">DP2SP2</text>
|
||||
<text x="297" y="180" class="value-label">6.785 s, 0.0737</text>
|
||||
|
||||
<circle cx="644" cy="61" r="7" class="point"/>
|
||||
<text x="632" y="84" text-anchor="end" class="point-label">DP4</text>
|
||||
<text x="632" y="101" text-anchor="end" class="value-label">9.997 s, 0.1000</text>
|
||||
|
||||
<text x="700" y="444" text-anchor="end" class="tick">4x NVIDIA L20X · warm qualification run</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,65 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="460" viewBox="0 0 760 460" role="img" aria-labelledby="title desc">
|
||||
<title id="title">MAGI-2 DLO four-step Pareto frontier</title>
|
||||
<desc id="desc">Warm user latency in seconds versus throughput per device. DP1SP4 is 8.647 seconds and 0.0289 outputs per second per device, DP2SP2 is 11.682 and 0.0428, and DP4 is 20.957 and 0.0477.</desc>
|
||||
<rect width="760" height="460" fill="#ffffff"/>
|
||||
<style>
|
||||
text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
|
||||
.title { font-size: 20px; font-weight: 600; }
|
||||
.axis-label { font-size: 14px; font-weight: 500; }
|
||||
.tick { font-size: 12px; fill: #4b5563; }
|
||||
.point-label { font-size: 13px; font-weight: 600; }
|
||||
.value-label { font-size: 11px; fill: #4b5563; }
|
||||
.grid { stroke: #e5e7eb; stroke-width: 1; }
|
||||
.axis { stroke: #6b7280; stroke-width: 1.5; }
|
||||
.frontier { fill: none; stroke: #2563eb; stroke-width: 3; }
|
||||
.point { fill: #2563eb; stroke: #ffffff; stroke-width: 2; }
|
||||
</style>
|
||||
|
||||
<text x="380" y="28" text-anchor="middle" class="title">Four diffusion steps</text>
|
||||
|
||||
<g aria-hidden="true">
|
||||
<line x1="85" y1="350" x2="700" y2="350" class="grid"/>
|
||||
<line x1="85" y1="288" x2="700" y2="288" class="grid"/>
|
||||
<line x1="85" y1="226" x2="700" y2="226" class="grid"/>
|
||||
<line x1="85" y1="164" x2="700" y2="164" class="grid"/>
|
||||
<line x1="85" y1="102" x2="700" y2="102" class="grid"/>
|
||||
<line x1="85" y1="40" x2="700" y2="40" class="grid"/>
|
||||
<line x1="85" y1="40" x2="85" y2="350" class="axis"/>
|
||||
<line x1="85" y1="350" x2="700" y2="350" class="axis"/>
|
||||
|
||||
<text x="73" y="354" text-anchor="end" class="tick">0.025</text>
|
||||
<text x="73" y="292" text-anchor="end" class="tick">0.030</text>
|
||||
<text x="73" y="230" text-anchor="end" class="tick">0.035</text>
|
||||
<text x="73" y="168" text-anchor="end" class="tick">0.040</text>
|
||||
<text x="73" y="106" text-anchor="end" class="tick">0.045</text>
|
||||
<text x="73" y="44" text-anchor="end" class="tick">0.050</text>
|
||||
|
||||
<text x="85" y="370" text-anchor="middle" class="tick">8</text>
|
||||
<text x="173" y="370" text-anchor="middle" class="tick">10</text>
|
||||
<text x="261" y="370" text-anchor="middle" class="tick">12</text>
|
||||
<text x="349" y="370" text-anchor="middle" class="tick">14</text>
|
||||
<text x="436" y="370" text-anchor="middle" class="tick">16</text>
|
||||
<text x="524" y="370" text-anchor="middle" class="tick">18</text>
|
||||
<text x="612" y="370" text-anchor="middle" class="tick">20</text>
|
||||
<text x="700" y="370" text-anchor="middle" class="tick">22</text>
|
||||
</g>
|
||||
|
||||
<text x="392" y="414" text-anchor="middle" class="axis-label">Warm user latency (seconds) — lower is better</text>
|
||||
<text x="20" y="195" transform="rotate(-90 20 195)" text-anchor="middle" class="axis-label">Throughput/device (outputs/s) — higher is better</text>
|
||||
|
||||
<polyline points="113,302 247,129 654,69" class="frontier"/>
|
||||
|
||||
<circle cx="113" cy="302" r="7" class="point"/>
|
||||
<text x="125" y="296" class="point-label">DP1SP4</text>
|
||||
<text x="125" y="313" class="value-label">8.647 s, 0.0289</text>
|
||||
|
||||
<circle cx="247" cy="129" r="7" class="point"/>
|
||||
<text x="259" y="123" class="point-label">DP2SP2</text>
|
||||
<text x="259" y="140" class="value-label">11.682 s, 0.0428</text>
|
||||
|
||||
<circle cx="654" cy="69" r="7" class="point"/>
|
||||
<text x="642" y="92" text-anchor="end" class="point-label">DP4</text>
|
||||
<text x="642" y="109" text-anchor="end" class="value-label">20.957 s, 0.0477</text>
|
||||
|
||||
<text x="700" y="444" text-anchor="end" class="tick">4x NVIDIA L20X · warm qualification run</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
+29
-15
@@ -23,16 +23,18 @@ The target is the engine-native path:
|
||||
-> response.audio.delta + response.audio_transcript.delta
|
||||
```
|
||||
|
||||
The standalone `/api/chat` and `/v1/audio/duplex` server remains a compatibility
|
||||
surface. It is not evidence that the unified engine path works.
|
||||
The standalone `/api/chat` and `/v1/audio/duplex` server that accompanied the
|
||||
original PR was demo-only and has been removed from the tree; the unified
|
||||
engine path above is the only serving surface. It was never evidence that the
|
||||
unified engine path works.
|
||||
|
||||
## Why configuration-only enablement is invalid
|
||||
## Why configuration-only enablement was invalid
|
||||
|
||||
The current staged pipeline is explicitly turn based. Its Talker reads
|
||||
`pplex_user_codes`, `pplex_prefill_text`, and `pplex_silence_codes`, but no
|
||||
production staged input path writes those fields. The voice prompt, persona
|
||||
prefill, and streaming Mimi state live only in the standalone
|
||||
`PersonaPlexEngine`.
|
||||
The staged pipeline as shipped by the original PR was explicitly turn based.
|
||||
Its Talker read `pplex_user_codes`, `pplex_prefill_text`, and
|
||||
`pplex_silence_codes`, but no production staged input path wrote those fields.
|
||||
The voice prompt, persona prefill, and streaming Mimi state lived only in the
|
||||
standalone `PersonaPlexEngine` (demo-only, since removed).
|
||||
|
||||
Setting only the following fields would therefore advertise an endpoint whose
|
||||
model never receives the live microphone stream:
|
||||
@@ -56,7 +58,12 @@ The unified implementation supports:
|
||||
- continuous user input while assistant audio is generated or played;
|
||||
- bundled `.pt` voice prompts and a session persona;
|
||||
- greedy text and depformer sampling, matching the current PersonaPlex port;
|
||||
- `/v1/realtime?duplex=1`;
|
||||
- `/v1/realtime?duplex=1` (the wire vocabulary is catalogued in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) serving guide);
|
||||
- the public client preset
|
||||
`vllm_omni.clients.personaplex.create_duplex_session_config()` (24 kHz
|
||||
`pcm_f32le` input format, voice prompt, persona) as the canonical
|
||||
session-config source for `DuplexClient` consumers;
|
||||
- engine lease close, disconnect cleanup, reconnect after cleanup, and explicit
|
||||
response cancellation without cross-session state reuse.
|
||||
|
||||
@@ -189,10 +196,17 @@ model-neutral native results:
|
||||
```
|
||||
|
||||
It owns per-request audio and text cursors so a cumulative output cannot replay
|
||||
old audio. PersonaPlex keeps one visible response open while continuous output
|
||||
arrives. Session close or cancellation terminates that response through the
|
||||
generic Realtime lifecycle; a codec segment finishing is not a conversational
|
||||
turn boundary.
|
||||
old audio. The generic projector turns each native result into the Realtime
|
||||
`response.audio.delta` / `response.audio_transcript.delta` pair (and
|
||||
`response.output_text.delta` for text) under one `response_id`; the full
|
||||
mapping is the name map in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) serving guide.
|
||||
PersonaPlex keeps one visible response open while continuous output arrives.
|
||||
Session close or cancellation terminates that response through the generic
|
||||
Realtime lifecycle; a codec segment finishing is not a conversational turn
|
||||
boundary. PersonaPlex advertises `supports_barge_in=false` and
|
||||
`supports_session_resume=false`, so `barge_in`, `turn_detection.server_vad`,
|
||||
and `session.resume` are rejected on this model.
|
||||
|
||||
## Error and lifecycle contracts
|
||||
|
||||
@@ -210,8 +224,8 @@ turn boundary.
|
||||
- New sessions cannot observe the previous voice, persona, PCM tail, Mimi
|
||||
convolution state, or Talker delayed code frame.
|
||||
|
||||
The legacy `PersonaPlexDuplexRuntime.run()` and standalone server drain path
|
||||
must follow the same exception-safe rule while they remain in the tree.
|
||||
(The legacy `PersonaPlexDuplexRuntime.run()` and standalone server drain path,
|
||||
which followed the same exception-safe rule, have been removed from the tree.)
|
||||
|
||||
## Testing and acceptance
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# MiniCPM-o 4.5 Full-Duplex Runtime Architecture
|
||||
|
||||
> Relocation note: after the H20 validation recorded below, the full-duplex
|
||||
> implementation moved out of `vllm_omni/experimental/fullduplex/` into the
|
||||
> stable tree (`vllm_omni/engine/duplex/`, `vllm_omni/entrypoints/duplex/`,
|
||||
> `vllm_omni/model_executor/models/minicpmo_4_5/duplex/`, and companion
|
||||
> modules). Module paths in this document reflect the new layout; the runtime
|
||||
> evidence was produced on the pre-move tree, which is file-identical apart
|
||||
> from module paths.
|
||||
|
||||
## Purpose
|
||||
|
||||
This document records the runtime architecture implemented by PR #3907, the
|
||||
@@ -31,8 +39,8 @@ session was rejected and capacity returned after one session closed. The two
|
||||
accepted sessions independently completed their audio, transcript, response,
|
||||
playback, and close lifecycles. This is isolation and admission evidence for the
|
||||
tested two-session deployment, not a production fairness, arbitrary-capacity,
|
||||
or failure-recovery claim. The validated paths include the experimental runtime
|
||||
extension, typed resumable policy, extracted control plane and clients, request
|
||||
or failure-recovery claim. The validated paths include the MiniCPM duplex
|
||||
runtime extension, typed resumable policy, extracted control plane and clients, request
|
||||
preregistration, typed direct-output decision, engine-managed two-session
|
||||
capacity in the checked-in deploy profile, separate public/runtime configuration
|
||||
channels, private Session ledgers, ordered `session.update`, the MiniCPM runner
|
||||
@@ -67,7 +75,9 @@ The checkpoint does not claim:
|
||||
- an acoustic-onset-to-cancel latency target across arbitrary client chunk sizes;
|
||||
- production multi-session admission, fairness, capacity, or failure recovery;
|
||||
- bounded long-session KV;
|
||||
- video input or audio/video synchronization.
|
||||
- audio/video synchronization beyond one camera frame per model unit (camera
|
||||
frames ride the audio appends as `video_frames`; the wire contract is in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) guide).
|
||||
|
||||
## Active Runtime Path
|
||||
|
||||
@@ -81,7 +91,7 @@ WebSocket
|
||||
-> AsyncOmni thin open/append/signal/close proxies
|
||||
-> DuplexControlClient over the engine-owned correlated RPC transport
|
||||
-> DuplexControlPlane + DuplexSessionRuntimeManager
|
||||
-> experimental DuplexRuntimeExtension + engine session/stage bindings
|
||||
-> MiniCPM DuplexRuntimeExtension + engine session/stage bindings
|
||||
-> StagePool
|
||||
-> resumable scheduler request
|
||||
-> MiniCPM Stage0
|
||||
@@ -177,21 +187,21 @@ ordering guarantee.
|
||||
|
||||
### Extracted control ownership
|
||||
|
||||
The generic engine mechanism and the experimental duplex plugin are split by
|
||||
The generic engine mechanism and the duplex plugin are split by
|
||||
responsibility:
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `engine.messages` | ordinary engine queue messages and collective RPC envelopes |
|
||||
| `engine.rpc_result_router` | generic correlation routing through each result message's `rpc_correlation_key` |
|
||||
| `experimental.fullduplex.engine.contracts` | immutable duplex DTOs, extension/stage protocols, typed decisions, request-ID codec |
|
||||
| `experimental.fullduplex.engine.lease` | duplex lease activity, TTL/grace configuration, and expiry records |
|
||||
| `experimental.fullduplex.engine.messages` | identity fence and duplex control message/result envelopes |
|
||||
| `experimental.fullduplex.engine.duplex_runtime` | extension loading, validation, and compatibility exports |
|
||||
| `experimental.fullduplex.engine.duplex_session` | session resources, fences, append reservations, idempotency cache, binding cleanup |
|
||||
| `experimental.fullduplex.engine.duplex_control_plane` | control algorithms and the narrow `DuplexStagePort` used to submit and clean up stage requests |
|
||||
| `experimental.fullduplex.engine.duplex_control_client` | typed control-message construction and calls through the engine-owned correlated RPC transport |
|
||||
| `experimental.fullduplex.request_client` | deterministic request identity, client-state preregistration, acknowledgement validation, data-plane collection, rollback |
|
||||
| `engine.duplex.contracts` | immutable duplex DTOs, extension/stage protocols, typed decisions, request-ID codec |
|
||||
| `engine.duplex.lease` | duplex lease activity, TTL/grace configuration, and expiry records |
|
||||
| `engine.duplex.messages` | identity fence and duplex control message/result envelopes |
|
||||
| `engine.duplex.runtime` | extension loading, validation, and compatibility exports |
|
||||
| `engine.duplex.session` | session resources, fences, append reservations, idempotency cache, binding cleanup |
|
||||
| `engine.duplex.control_plane` | control algorithms and the narrow `DuplexStagePort` used to submit and clean up stage requests |
|
||||
| `engine.duplex.control_client` | typed control-message construction and calls through the engine-owned correlated RPC transport |
|
||||
| `entrypoints.duplex_request_client` | deterministic request identity, client-state preregistration, acknowledgement validation, data-plane collection, rollback |
|
||||
|
||||
`Orchestrator`, `AsyncOmniEngine`, and `AsyncOmni` retain explicit wiring and
|
||||
compatibility proxies. They no longer own the corresponding duplex algorithms.
|
||||
@@ -362,33 +372,34 @@ vllm_omni.engine.rpc_result_router # generic correlation routing
|
||||
vllm_omni.engine.async_engine_utils # ordinary engine lifecycle helpers
|
||||
```
|
||||
|
||||
The full-duplex implementation remains experimental:
|
||||
The full-duplex implementation lives in dedicated duplex modules:
|
||||
|
||||
```text
|
||||
vllm_omni.experimental.fullduplex.engine.duplex_runtime
|
||||
vllm_omni.engine.duplex.runtime
|
||||
# extension loading and validation
|
||||
vllm_omni.experimental.fullduplex.engine.contracts
|
||||
vllm_omni.engine.duplex.contracts
|
||||
# immutable DTOs and narrow protocols
|
||||
vllm_omni.experimental.fullduplex.engine.lease
|
||||
vllm_omni.engine.duplex.lease
|
||||
# duplex lease primitives
|
||||
vllm_omni.experimental.fullduplex.engine.messages
|
||||
vllm_omni.engine.duplex.messages
|
||||
# duplex identity and queue envelopes
|
||||
vllm_omni.experimental.fullduplex.engine.duplex_session
|
||||
vllm_omni.engine.duplex.session
|
||||
# session transaction implementation
|
||||
vllm_omni.experimental.fullduplex.engine.duplex_control_plane
|
||||
vllm_omni.engine.duplex.control_plane
|
||||
# engine-side control algorithms
|
||||
vllm_omni.experimental.fullduplex.engine.duplex_control_client
|
||||
vllm_omni.engine.duplex.control_client
|
||||
# correlated control client
|
||||
vllm_omni.experimental.fullduplex.request_client
|
||||
vllm_omni.entrypoints.duplex_request_client
|
||||
# entrypoint request/output lifecycle
|
||||
```
|
||||
|
||||
Stable engine and entrypoint modules import only the kernel at module load.
|
||||
They load the experimental ControlPlane, ControlClient, request client, and
|
||||
runtime extension inside duplex-only construction or call paths. Importing
|
||||
`Orchestrator`, `AsyncOmniEngine`, or `AsyncOmni` for an ordinary deployment
|
||||
therefore does not import `vllm_omni.experimental.fullduplex`. Compatibility
|
||||
exports remain lazy and are not part of ordinary startup.
|
||||
Stable engine and entrypoint modules import the duplex kernel eagerly at
|
||||
module load (top-of-file imports). Duplex behavior remains opt-in: the
|
||||
ControlPlane, ControlClient, and request client are *constructed* only on
|
||||
duplex-enabled deployments, and the model runtime extension still loads only
|
||||
through the dotted-string plugin path. The client-side package
|
||||
(`vllm_omni/clients/`, home of the public `DuplexClient`) is never imported
|
||||
by runtime code.
|
||||
|
||||
`PipelineConfig.duplex_control_enabled` enables the generic mechanism, while
|
||||
`PipelineConfig.duplex_runtime_extension` separately selects the model adapter.
|
||||
@@ -452,7 +463,7 @@ sampling generation unchanged.
|
||||
`extra_body` or session payloads:
|
||||
|
||||
| Field | Default | Owner | Lifecycle or resource effect |
|
||||
|---|---:|---|---|
|
||||
| --- | ---: | --- | --- |
|
||||
| `idle_ttl_s` | `300.0` | Engine lease manager | Expires an attached or detached session after inactivity; `null` disables only idle expiry. |
|
||||
| `disconnect_grace_s` | `30.0` | Engine lease manager and API attachment registry | Bounds how long a detached session may retain engine resources before cleanup. |
|
||||
| `reaper_interval_s` | `5.0` | Orchestrator | Sets the cadence for lease expiry and pending cleanup retries. |
|
||||
@@ -467,6 +478,30 @@ Deploy profiles may lower or raise these values to match scheduler capacity,
|
||||
but clients cannot override them. The engine remains authoritative for
|
||||
admission and cleanup ownership even when the API performs an early check.
|
||||
|
||||
### Client-side companion package
|
||||
|
||||
The public client surface ships under `vllm_omni/clients/` and stays on the
|
||||
consumer side of the boundary above: `vllm_omni.clients.duplex` provides the
|
||||
model-agnostic `DuplexClient` (typed events, per-response demux via
|
||||
`ResponseHandle`, `session.resume` with acknowledgement-based replay dedup)
|
||||
and the `EventCollector` helper used for timing and request-metric summaries.
|
||||
Model-specific session defaults live in thin per-model preset modules —
|
||||
`vllm_omni.clients.minicpmo_4_5.create_duplex_session_config()` and
|
||||
`vllm_omni.clients.personaplex.create_duplex_session_config()` — so the core
|
||||
client never imports model packages and the model duplex plugin packages
|
||||
never import the client (both directions are enforced by tests:
|
||||
`tests/engine/test_duplex_import_boundary.py`,
|
||||
`tests/entrypoints/openai_api/test_duplex_capability.py`, and the subprocess
|
||||
purity guards in `tests/clients/test_model_session_configs.py`).
|
||||
`examples/online_serving/barge_in_client.py` (with its flow companion
|
||||
`barge_in_client_flow.md`) is the runnable reference for the client library
|
||||
against a live duplex deployment. The wire vocabulary the client speaks —
|
||||
every `/v1/realtime?duplex=1` event with its OpenAI-compatibility tier and a
|
||||
JSON example, plus the native-to-Realtime name map — is catalogued in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) serving guide; that
|
||||
page derives from the normative contract table above and must be updated
|
||||
with it.
|
||||
|
||||
## Generic-Path Cleanup
|
||||
|
||||
Model-specific `MINICPMO45_PROFILE_LOGS` probes were removed from:
|
||||
@@ -508,14 +543,16 @@ The MiniCPM Stage1 input processor consumes that capability for token-faithful
|
||||
delta transcripts. The generic orchestrator no longer installs or names a
|
||||
MiniCPM-specific bridge-state key.
|
||||
|
||||
The stable API server inspects the deployment `session_mode` before importing or
|
||||
constructing the experimental duplex handler. Ordinary deployments do not load
|
||||
the full-duplex serving package or create its registries and background state.
|
||||
The stable API server imports the duplex serving package eagerly and inspects
|
||||
the deployment `session_mode` before constructing the duplex handler. Ordinary
|
||||
deployments do not create its registries and background state.
|
||||
For an enabled deployment, the client still selects the Realtime duplex route
|
||||
with `?duplex=1` (or an equivalent explicit true value). Model-name matching is
|
||||
not used for routing or native-runtime activation. MiniCPM clients explicitly
|
||||
set `extra_body.minicpmo45_native_duplex=true`; repository demos do so in their
|
||||
session payloads.
|
||||
set `extra_body.native_duplex=true`; the canonical way to do so is
|
||||
the `vllm_omni.clients.minicpmo_4_5.create_duplex_session_config()` preset,
|
||||
which the repository's barge-in example
|
||||
(`examples/online_serving/barge_in_client.py`) uses in its session payload.
|
||||
|
||||
`DuplexRequestClient` derives the data-plane request identity from the accepted
|
||||
fence and preregisters a resumable `ClientRequestState` before the append crosses
|
||||
@@ -623,7 +660,7 @@ continues to return `input_audio_buffer_empty`.
|
||||
The commit-to-response contract is:
|
||||
|
||||
| Session state at `input_audio_buffer.commit` | Runtime action | Required event outcome |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| Auto-response disabled, valid idle input | Commit and retain the input item. | Emit `input_audio_buffer.committed`; emit no response until an explicit `response.create`. |
|
||||
| Auto-response enabled, valid idle speech | Submit the final native append for the current model turn. | Emit `input_audio_buffer.committed` before model output. Create a response only on the first visible text/audio output; a model listen decision may end with `response.listen` and no response. |
|
||||
| Auto-response enabled, short overlap during an active response | Discard the short overlap as an acknowledgement. | Emit the committed/no-response acknowledgement; do not create or defer a response. |
|
||||
@@ -770,9 +807,12 @@ Graph capture. This is a model execution constraint, not an E2E-only override.
|
||||
|
||||
### Resource capabilities
|
||||
|
||||
The checked-in MiniCPM duplex deploy profile sets Stage0 and Stage1
|
||||
`max_num_seqs` to two and configures the engine runtime manager with
|
||||
`max_sessions=2`. The Realtime capability response therefore advertises
|
||||
At the time of this snapshot the checked-in MiniCPM duplex deploy profile set
|
||||
Stage0 and Stage1 `max_num_seqs` to two and configured the engine runtime
|
||||
manager with `max_sessions=2`; the duplex settings have since been folded into
|
||||
`vllm_omni/deploy/minicpmo_4_5.yaml`, which admits four sessions
|
||||
(`duplex_session.max_sessions: 4`, matching its stage `max_num_seqs`). The
|
||||
Realtime capability response advertises
|
||||
multi-session and same-replica multi-session support with
|
||||
`session_admission_mode="engine_managed"`. Client session fields cannot raise
|
||||
this server-owned limit. Fresh E2E evidence covers two concurrent sessions in
|
||||
@@ -1025,4 +1065,6 @@ Passing this checkpoint supports the statement:
|
||||
|
||||
It does not support claims for an acoustic-onset barge-in latency target across
|
||||
arbitrary client chunk sizes, multi-session production concurrency, bounded
|
||||
long-session KV, scheduler-native append, or video input.
|
||||
long-session KV, or scheduler-native append. Camera-frame input
|
||||
(`video_frames`) landed after this snapshot and is covered by its own E2E
|
||||
test rather than by the evidence above.
|
||||
@@ -17,6 +17,8 @@ implementation contract; it is not, by itself, a general support claim.
|
||||
|
||||
### Runtime and stage execution
|
||||
|
||||
- [Full-Duplex Runtime (MiniCPM-o 4.5)](fullduplex.md)
|
||||
- [Full-Duplex Runtime (PersonaPlex)](fullduplex-personaplex.md)
|
||||
- [Disaggregated Inference](feature/disaggregated_inference.md)
|
||||
- [Host Weight Runtime](feature/host_weight_runtime.md)
|
||||
- [Async Chunk](feature/async_chunk.md)
|
||||
@@ -64,7 +66,7 @@ The design contracts separate selection mechanics from backend algorithms:
|
||||
#### CPU offloading
|
||||
|
||||
- [Overview and Shared Contracts](feature/offloader/README.md)
|
||||
- [Model-Level Offload](feature/offloader/cpu_offload.md)
|
||||
- [Model-Level Offload](feature/offloader/module_offload.md)
|
||||
- [Layerwise Offload](feature/offloader/layerwise_offload.md)
|
||||
- [Distributed Layerwise Offload](feature/offloader/distributed_layerwise_offload.md)
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ related_code_paths:
|
||||
- vllm_omni/engine/stage_pool.py
|
||||
- vllm_omni/outputs/output_processor.py
|
||||
- vllm_omni/distributed/omni_coordinator/**
|
||||
# Duplex kernel design is owned by ../fullduplex.md; listed here because the
|
||||
# orchestrator hosts the duplex control plane.
|
||||
- vllm_omni/engine/duplex/**
|
||||
depends_on:
|
||||
- input_output_modality_contracts.md
|
||||
- error_contracts.md
|
||||
@@ -44,7 +47,8 @@ validation_paths:
|
||||
- tests/engine/test_orchestrator_stage_input_bridge.py
|
||||
- tests/engine/test_cfg_companion_lifecycle.py
|
||||
- tests/engine/test_rpc_result_router.py
|
||||
- tests/e2e/features/fullduplex/engine/**
|
||||
- tests/engine/duplex/**
|
||||
- tests/engine/test_duplex_import_boundary.py
|
||||
upstream_refs:
|
||||
- vllm.v1.engine.EngineCoreRequest
|
||||
- vllm.v1.engine.EngineCoreOutputs
|
||||
|
||||
@@ -26,9 +26,18 @@ primary_code_paths:
|
||||
- vllm_omni/entrypoints/client_request_state.py
|
||||
- vllm_omni/entrypoints/stage_utils.py
|
||||
- vllm_omni/entrypoints/utils.py
|
||||
- vllm_omni/entrypoints/duplex/**
|
||||
- vllm_omni/entrypoints/duplex_request_client.py
|
||||
- vllm_omni/clients/**
|
||||
primary_path_exceptions:
|
||||
- path: vllm_omni/entrypoints/openai/errors.py
|
||||
owner: error_contracts.md
|
||||
- path: vllm_omni/entrypoints/duplex/**
|
||||
owner: ../fullduplex.md
|
||||
- path: vllm_omni/entrypoints/duplex_request_client.py
|
||||
owner: ../fullduplex.md
|
||||
- path: vllm_omni/clients/**
|
||||
owner: ../fullduplex.md
|
||||
related_code_paths:
|
||||
- vllm_omni/errors.py
|
||||
- vllm_omni/inputs/**
|
||||
|
||||
@@ -410,8 +410,8 @@ Notes:
|
||||
- `stages[].max_num_seqs` controls stage-level batching.
|
||||
- Thinker/Talker commonly use `enforce_eager: false` for CUDA Graph paths.
|
||||
- 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.
|
||||
- The Qwen3-Omni deploy config pins `moe_backend: triton` for the Thinker and Talker stages. Triton has been more
|
||||
stable and faster than the FlashInfer CUTLASS unquantized MoE backend on recent vLLM rebases.
|
||||
|
||||
#### 2) Enable async chunk
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ CATEGORIES = {
|
||||
"name": "Entry Points",
|
||||
"description": "Main entry points for vLLM-Omni inference and serving.",
|
||||
},
|
||||
"clients": {
|
||||
"name": "Clients",
|
||||
"description": (
|
||||
"Client libraries that talk to a running vLLM-Omni server "
|
||||
"(the full-duplex Realtime client and its per-model session presets)."
|
||||
),
|
||||
},
|
||||
"inputs": {
|
||||
"name": "Inputs",
|
||||
"description": "Input data structures for multi-modal inputs.",
|
||||
|
||||
@@ -18,7 +18,7 @@ th {
|
||||
</style>
|
||||
|
||||
| Architecture | Models | Example HF Models | NVIDIA GPU | AMD GPU | Ascend NPU | Intel GPU | Recipe |
|
||||
|--------------|--------|-------------------|------------|---------|-----|-----------|--------|
|
||||
| -------------- | -------- | ------------------- | ------------ | --------- | ----- | ----------- | -------- |
|
||||
| `Qwen3OmniMoeForConditionalGeneration` | Qwen3-Omni | `Qwen/Qwen3-Omni-30B-A3B-Instruct` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Qwen/Qwen3-Omni.md) |
|
||||
| `Qwen2_5OmniForConditionalGeneration` | Qwen2.5-Omni | `Qwen/Qwen2.5-Omni-7B`, `Qwen/Qwen2.5-Omni-3B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `MingFlashOmniForConditionalGeneration` + `MingImagePipeline` | Ming-flash-omni-2.0 (omni-speech + imagegen<sup>1</sup>) | `Jonathan1909/Ming-flash-omni-2.0` | ✅︎ | | | | — |
|
||||
@@ -42,7 +42,7 @@ th {
|
||||
| `SanaWmPipeline` | SANA-WM | `BBBBruce/SANA-WM_bidirectional-stage1-diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/NVIDIA/SANA-WM.md) |
|
||||
| `Wan22S2VPipeline` | Wan2.2-S2V | `Wan-AI/Wan2.2-S2V-14B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Wan22VACEPipeline` | Wan2.1-VACE | `Wan-AI/Wan2.1-VACE-1.3B-diffusers`, `Wan-AI/Wan2.1-VACE-14B-diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Wan22VACEPipeline` | Wan2.2-VACE | `Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers` | ✅︎ | | | ✅︎ | — |
|
||||
| `Wan22VACEPipeline` | Wan2.2-VACE | `Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers` | ✅︎ | | | ✅︎ | — |
|
||||
| `LTX2Pipeline` | LTX-2 / LTX-2.3 one-stage T2V and I2V | `Lightricks/LTX-2`, `diffusers/LTX-2.3-Diffusers` | ✅︎ | ✅︎ | | | — |
|
||||
| `LTX2TwoStagePipeline` | LTX-2 / LTX-2.3 ordinary two-stage T2V and I2V | `Lightricks/LTX-2`, `diffusers/LTX-2.3-Diffusers` + matching Lightricks LoRA and upsampler | ✅︎ | ✅︎ | | | — |
|
||||
| `LTX2DistilledOneStagePipeline` | LTX-2 / LTX-2.3 merged-distilled one-stage T2V and I2V | `rootonchair/LTX-2-19b-distilled`, `diffusers/LTX-2.3-Distilled-Diffusers` | ✅︎ | ✅︎ | | | — |
|
||||
@@ -55,6 +55,7 @@ th {
|
||||
| `SanaVideoPipeline` | SANA-Video-2B T2V (native 480p/720p; Diffusers adapter validated at both resolutions) | `Efficient-Large-Model/SANA-Video_2B_480p_diffusers`, `Efficient-Large-Model/SANA-Video_2B_720p_diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/NVIDIA/SANA-Video-2B.md) |
|
||||
| `SanaImageToVideoPipeline` | SANA-Video-2B I2V (native and Diffusers adapter validated at 480p/720p) | `Efficient-Large-Model/SANA-Video_2B_480p_diffusers`, `Efficient-Large-Model/SANA-Video_2B_720p_diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/NVIDIA/SANA-Video-2B.md) |
|
||||
| `MiniMaxH3Pipeline` | MiniMax H3 T2VA, first/last-keyframe FL2VA, and mixed-reference Ref2VA | `MiniMaxAI/MiniMax-H3` | ✅︎ | ✅︎ | | | [Published](https://recipes.vllm.ai/MiniMaxAI/MiniMax-H3) |
|
||||
| `Magi2Pipeline` | MAGI-2 native Preview-stage T2VA and I2VA (272p/540p) | `sand-ai/MAGI-2-preview` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/SandAI/MAGI-2-preview-L20X.md) |
|
||||
| `DreamZeroPipeline` | DreamZero-DROID | `GEAR-Dreams/DreamZero-DROID` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Pi0Pipeline` | π0 (Pi-Zero) VLA | `lerobot/pi0_base` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/lerobot/Pi0.md) |
|
||||
| `HeliosPipeline`, `HeliosPyramidPipeline` | Helios | `BestWishYsh/Helios-Base`, `BestWishYsh/Helios-Mid`, `BestWishYsh/Helios-Distilled` | ✅︎ | ✅︎ | ✅︎ | | — |
|
||||
@@ -68,7 +69,7 @@ th {
|
||||
| `CosyVoice3Model` | CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `OmniVoiceModel` | OmniVoice | `k2-fsa/OmniVoice` | ✅︎ | | | ✅︎ | — |
|
||||
| `VoxCPM2TalkerForConditionalGeneration` | VoxCPM2 | `openbmb/VoxCPM2` | ✅︎ | | | ✅︎ | — |
|
||||
| `DotsTTSForConditionalGeneration` | dots.tts | `rednote-hilab/dots.tts-soar` | ✅︎ | | | | — |
|
||||
| `DotsTTSForConditionalGeneration` | dots.tts | `dots-studio/dots.tts-soar` | ✅︎ | | | | — |
|
||||
| `MammothModa2ForConditionalGeneration` | MammothModa2-Preview | `bytedance-research/MammothModa2-Preview` | ✅︎ | ✅︎ | | | — |
|
||||
| `MammothModa2ForConditionalGeneration` | MammothModa2-Dev (AR-only image understanding) | `bytedance-research/MammothModa2-Dev` | ✅︎ | | | | — |
|
||||
| `Flux2KleinPipeline` | FLUX.2-klein | `black-forest-labs/FLUX.2-klein-4B`, `black-forest-labs/FLUX.2-klein-9B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
@@ -101,11 +102,11 @@ th {
|
||||
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | | — |
|
||||
| `VoxtralTTSForConditionalGeneration` | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✅︎ | ✅︎ | | | — |
|
||||
| `CovoAudioForConditionalGeneration` | Covo-Audio-Chat | `tencent/Covo-Audio-Chat` | ✅︎ | | | | — |
|
||||
|`DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | ✅︎ | — |
|
||||
| `DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | ✅︎ | — |
|
||||
| `MiniCPMO45OmniForConditionalGeneration` | MiniCPM-o 4.5 | `openbmb/MiniCPM-o-4_5` | ✅︎ | | ✅︎ | | — |
|
||||
| `ErnieImagePipeline` | ERNIE-Image | `baidu/ERNIE-Image`, `baidu/ERNIE-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `GepardTalkerForConditionalGeneration` | Gepard-1.0 | `nineninesix/gepard-1.0` | ✅︎ | | | ✅︎ | — |
|
||||
|`HiDreamImagePipeline` | HiDream-I1-Full | `HiDream-ai/HiDream-I1-Full` | ✅︎ | ✅︎ | | | — |
|
||||
| `HiDreamImagePipeline` | HiDream-I1-Full | `HiDream-ai/HiDream-I1-Full` | ✅︎ | ✅︎ | | | — |
|
||||
| `HiDreamO1ImagePipeline` | HiDream-O1-Image | `HiDream-ai/HiDream-O1-Image` | ✅︎ | | | | — |
|
||||
|
||||
The `Recipe` column links the corresponding deployment recipe. `Published` is
|
||||
|
||||
@@ -43,7 +43,7 @@ Completions for conversational or heterogeneous omni pipelines, rather than as
|
||||
the default wrapper for every generation task.
|
||||
|
||||
| Task | Endpoint | Request | Response | Details |
|
||||
|------|----------|---------|----------|---------|
|
||||
| ------ | ---------- | --------- | ---------- | --------- |
|
||||
| Conversation or multimodal understanding/generation | `POST /v1/chat/completions` | JSON | OpenAI-style JSON or SSE | [Chat Completions](chat_completions_api.md) |
|
||||
| Text-to-speech | `POST /v1/audio/speech` | JSON | Audio bytes or SSE | [Speech](speech_api.md) |
|
||||
| Sound, music, or ambient audio generation | `POST /v1/audio/generate` | JSON | Audio bytes | [Audio Generation](audio_generate_api.md) |
|
||||
@@ -141,7 +141,7 @@ These WebSocket APIs have different event schemas and cannot be used
|
||||
interchangeably.
|
||||
|
||||
| Workload | Endpoint | Interaction model | Details |
|
||||
|----------|----------|-------------------|---------|
|
||||
| ---------- | ---------- | ------------------- | --------- |
|
||||
| Incremental text input for speech synthesis | `WS /v1/audio/speech/stream` | Send text events and receive audio | [Streaming Text to Speech](speech_api.md#streaming-text-input-websocket) |
|
||||
| Live video understanding | `WS /v1/video/chat/stream` | Send video frames and receive text/audio | [Streaming Video Input](video_stream_api.md) |
|
||||
| Turn-based realtime audio | `WS /v1/realtime` | Stream one audio input and receive transcript/audio events | [Realtime Audio](realtime_api.md) |
|
||||
@@ -158,7 +158,7 @@ ordinary realtime handler when duplex is unavailable.
|
||||
## Related Endpoints
|
||||
|
||||
| Purpose | Endpoints | Reference |
|
||||
|---------|-----------|-----------|
|
||||
| --------- | ----------- | ----------- |
|
||||
| Discovery and readiness | `GET /health`, `GET /v1/models` | This page |
|
||||
| Batched conversations | `POST /v1/chat/completions/batch` | [Batch requests](chat_completions_api.md#batch-requests) |
|
||||
| Batched speech | `POST /v1/audio/speech/batch` | [Batch speech generation](speech_api.md#batch-speech-generation) |
|
||||
@@ -168,11 +168,10 @@ ordinary realtime handler when duplex is unavailable.
|
||||
|
||||
## Standalone Experimental Servers
|
||||
|
||||
PersonaPlex also provides a Moshi-compatible WebSocket server, while JoyVL
|
||||
provides a stateful interaction orchestrator in front of another model server.
|
||||
These are separate processes with their own routes; they are not additional
|
||||
JoyVL provides a stateful interaction orchestrator in front of another model
|
||||
server. It is a separate process with its own routes; they are not additional
|
||||
paths on every `vllm serve --omni` instance. See [Standalone Experimental
|
||||
Servers](standalone_servers.md) before deploying either one.
|
||||
Servers](standalone_servers.md) before deploying it.
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ with open("cat.wav", "wb") as f:
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/audio/generate
|
||||
Content-Type: application/json
|
||||
```
|
||||
@@ -63,7 +63,7 @@ Content-Type: application/json
|
||||
### Request Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `input` | string | **required** | Text prompt describing the audio to generate |
|
||||
| `model` | string | server's model | Model to use (optional, should match server if specified) |
|
||||
| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, opus |
|
||||
@@ -72,7 +72,7 @@ Content-Type: application/json
|
||||
#### Diffusion Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `audio_length` | float | null | Audio duration in seconds (default value is the max ~47s for `stable-audio-open-1.0`) |
|
||||
| `audio_start` | float | 0.0 | Audio start time in seconds |
|
||||
| `negative_prompt` | string | null | Text describing what to avoid in generation |
|
||||
@@ -85,7 +85,7 @@ Content-Type: application/json
|
||||
Returns binary audio data with the appropriate `Content-Type` header:
|
||||
|
||||
| `response_format` | Content-Type |
|
||||
|--------------------|--------------|
|
||||
| -------------------- | -------------- |
|
||||
| `wav` | `audio/wav` |
|
||||
| `mp3` | `audio/mpeg` |
|
||||
| `flac` | `audio/flac` |
|
||||
@@ -210,7 +210,7 @@ with open("thunder.wav", "wb") as f:
|
||||
Controls how closely the generated audio follows the text prompt.
|
||||
|
||||
| Range | Behaviour |
|
||||
|-------|-----------|
|
||||
| ------- | ----------- |
|
||||
| 3 - 5 | More creative / varied output |
|
||||
| 7 (default) | Balanced adherence |
|
||||
| 10+ | Strict adherence to the prompt |
|
||||
@@ -220,7 +220,7 @@ Controls how closely the generated audio follows the text prompt.
|
||||
Controls the number of denoising steps in the diffusion process.
|
||||
|
||||
| Steps | Quality | Speed | Use Case |
|
||||
|-------|---------|-------|----------|
|
||||
| ------- | --------- | ------- | ---------- |
|
||||
| 50 | Good | Fast | Quick previews |
|
||||
| 100 | Very Good | Medium | General purpose |
|
||||
| 150+ | Excellent | Slow | Final / critical audio |
|
||||
@@ -240,7 +240,7 @@ Describes characteristics to avoid. Common negative prompts include:
|
||||
## Supported Models
|
||||
|
||||
| Model | Description |
|
||||
|-------|-------------|
|
||||
| ----- | ----------- |
|
||||
| `stabilityai/stable-audio-open-1.0` | Open-source audio generation model, up to ~47 seconds, 44.1 kHz stereo |
|
||||
|
||||
## Error Responses
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
# Full-Duplex WebSocket API
|
||||
|
||||
vLLM-Omni provides an experimental full-duplex runtime for models that can
|
||||
vLLM-Omni provides a full-duplex runtime for models that can
|
||||
continue receiving speech while producing speech. It adds persistent session
|
||||
state, model-specific turn policy, overlap handling, playback acknowledgement,
|
||||
and optional session resume.
|
||||
|
||||
Full duplex is distinct from the turn-based [Realtime Audio API](realtime_api.md).
|
||||
|
||||
This page is the endpoint overview. For the complete wire contract, the
|
||||
`vllm_omni.clients.duplex.DuplexClient` Python library, and the per-model
|
||||
capability gates, see the [Realtime Duplex API guide](realtime_duplex_api.md).
|
||||
|
||||
## Choose an Endpoint
|
||||
|
||||
| Endpoint | Protocol | Recommended use |
|
||||
|----------|----------|-----------------|
|
||||
| --- | --- | --- |
|
||||
| `WS /v1/realtime?duplex=1` | OpenAI Realtime-style event projection | Applications and browser clients |
|
||||
| `WS /v1/duplex` | Native vLLM-Omni duplex events | Runtime integration and low-level testing |
|
||||
|
||||
@@ -41,8 +45,10 @@ and control plane as part of their registered pipeline.
|
||||
|
||||
The current unified-runtime integrations are:
|
||||
|
||||
- MiniCPM-o 4.5, using `vllm_omni/deploy/minicpmo_4_5_duplex.yaml`;
|
||||
- PersonaPlex, whose default `vllm_omni/deploy/personaplex.yaml` enables duplex.
|
||||
- MiniCPM-o 4.5, using `vllm_omni/deploy/minicpmo_4_5.yaml`
|
||||
(`session_mode: duplex`);
|
||||
- PersonaPlex, whose default `vllm_omni/deploy/personaplex.yaml` enables duplex;
|
||||
- Nemotron VoiceChat, via its registered duplex plugin package.
|
||||
|
||||
JoyVL is a separate HTTP interaction orchestrator and does not use these
|
||||
WebSocket endpoints. See [Standalone Experimental Servers](standalone_servers.md).
|
||||
@@ -53,7 +59,7 @@ Start the duplex deployment:
|
||||
|
||||
```bash
|
||||
vllm serve openbmb/MiniCPM-o-4_5 --omni \
|
||||
--deploy-config vllm_omni/deploy/minicpmo_4_5_duplex.yaml \
|
||||
--deploy-config vllm_omni/deploy/minicpmo_4_5.yaml \
|
||||
--trust-remote-code \
|
||||
--port 8091
|
||||
```
|
||||
@@ -112,5 +118,5 @@ applications should use the provided Realtime client where possible.
|
||||
|
||||
See the [MiniCPM-o example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/minicpmo),
|
||||
[PersonaPlex example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/personaplex),
|
||||
and [full-duplex runtime design](https://github.com/vllm-project/vllm-omni/blob/main/vllm_omni/experimental/fullduplex/DESIGN.md)
|
||||
and the [full-duplex runtime design](../design/fullduplex.md)
|
||||
for model-specific validation and architecture details.
|
||||
@@ -30,7 +30,6 @@ curl -s -D >(grep -i x-request-id >&2) \
|
||||
-F "output_format=png"
|
||||
```
|
||||
|
||||
|
||||
**Using OpenAI SDK:**
|
||||
|
||||
```python
|
||||
@@ -72,7 +71,7 @@ with open("edit_out_http.jpeg", "wb") as f:
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/images/edits
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
@@ -82,7 +81,7 @@ Content-Type: multipart/form-data
|
||||
#### OpenAI Standard Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `prompt` | string | **required** | A text description of the desired image |
|
||||
| `model` | string | server's model | Model to use (optional, should match server if specified) |
|
||||
| `image` | string or array | **required** | The image(s) to edit. |
|
||||
@@ -98,7 +97,7 @@ Content-Type: multipart/form-data
|
||||
#### vllm-omni Extension Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `url` | string or array | None | The image(s) to edit. |
|
||||
| `negative_prompt` | string | null | Text describing what to avoid in the image |
|
||||
| `num_inference_steps` | integer | model defaults | Number of diffusion steps |
|
||||
@@ -212,7 +211,6 @@ Use the `ar_delta` events for progressive display of the AR-generated
|
||||
recaption. Decode the `b64_json` field from the final `image` event to get the
|
||||
edited image.
|
||||
|
||||
|
||||
## Parameter Handling
|
||||
|
||||
The API passes parameters directly to the diffusion pipeline without model-specific transformation:
|
||||
@@ -266,6 +264,7 @@ curl http://localhost:8000/v1/images/edits \
|
||||
### Out of Memory
|
||||
|
||||
If you encounter OOM errors:
|
||||
|
||||
1. Reduce image size: `"size": "512x512"`
|
||||
2. Reduce inference steps: `"num_inference_steps": 25`
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ curl -o dragon.png -X POST http://localhost:8000/v1/images/generations \
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
**Using Python:**
|
||||
|
||||
```python
|
||||
@@ -123,7 +122,7 @@ response = client.images.generate(
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
```text
|
||||
POST /v1/images/generations
|
||||
Content-Type: application/json
|
||||
```
|
||||
@@ -133,7 +132,7 @@ Content-Type: application/json
|
||||
#### OpenAI Standard Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `prompt` | string | **required** | Text description of the desired image |
|
||||
| `model` | string | server's model | Model to use (optional, should match server if specified) |
|
||||
| `n` | integer | 1 | Number of images to generate (1-10) |
|
||||
@@ -144,7 +143,7 @@ Content-Type: application/json
|
||||
#### vllm-omni Extension Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `negative_prompt` | string | null | Text describing what to avoid in the image |
|
||||
| `num_inference_steps` | integer | model defaults | Number of diffusion steps |
|
||||
| `guidance_scale` | float | model defaults | Classifier-free guidance scale (typically 0.0-20.0) |
|
||||
@@ -267,6 +266,7 @@ curl http://localhost:8000/v1/images/generations \
|
||||
### Out of Memory
|
||||
|
||||
If you encounter OOM errors:
|
||||
|
||||
1. Reduce image size: `"size": "512x512"`
|
||||
2. Reduce inference steps: `"num_inference_steps": 25`
|
||||
3. Generate fewer images: `"n": 1`
|
||||
|
||||
@@ -5,7 +5,7 @@ receive incremental transcription and synthesized response audio. This is a
|
||||
turn-based streaming API: it starts one generation over an audio stream and
|
||||
finishes after the client closes that input stream.
|
||||
|
||||
For a model that listens while it is already speaking, use the experimental
|
||||
For a model that listens while it is already speaking, use the
|
||||
[Full-Duplex API](full_duplex_api.md) instead.
|
||||
|
||||
## Quick Start
|
||||
@@ -35,7 +35,7 @@ Messages in both directions are JSON text frames. Audio carried inside an
|
||||
event is base64-encoded raw PCM16.
|
||||
|
||||
| Direction | Event | Purpose |
|
||||
|-----------|-------|---------|
|
||||
| ----------- | ------- | --------- |
|
||||
| Server to client | `session.created` | Confirms that the WebSocket connection is ready |
|
||||
| Client to server | `session.update` | Selects and validates the served model |
|
||||
| Client to server | `input_audio_buffer.commit` with `final: false` | Starts generation over the incoming stream |
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,61 +1,19 @@
|
||||
# Standalone Experimental Servers
|
||||
|
||||
Most vLLM-Omni endpoints run on the unified server started with
|
||||
`vllm serve <model> --omni`. The experimental full-duplex package also contains
|
||||
two separate user-facing server processes. Their routes are not added to the
|
||||
unified API server.
|
||||
`vllm serve <model> --omni`. The experimental package also contains one
|
||||
separate user-facing server process; its routes are not added to the unified
|
||||
API server.
|
||||
|
||||
| Server | Transport | Role |
|
||||
|--------|-----------|------|
|
||||
| PersonaPlex compatibility server | WebSocket | Hosts the official PersonaPlex browser protocol and a raw-PCM alternative |
|
||||
| --- | --- | --- |
|
||||
| JoyVL interaction server | HTTP | Adds state, memory, proactive decisions, and delegation in front of a separate OpenAI-compatible model backend |
|
||||
|
||||
## PersonaPlex Compatibility Server
|
||||
|
||||
Use this server when a client requires the official Moshi/PersonaPlex wire
|
||||
protocol or the bundled browser UI. For the engine-owned unified duplex path,
|
||||
use [`WS /v1/realtime?duplex=1`](full_duplex_api.md) instead.
|
||||
|
||||
Start it with:
|
||||
|
||||
```bash
|
||||
HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 python -m \
|
||||
vllm_omni.experimental.fullduplex.personaplex.serving.server \
|
||||
--port 8091
|
||||
```
|
||||
|
||||
| Route | Purpose |
|
||||
|-------|---------|
|
||||
| `GET /` | Official PersonaPlex browser client, when its assets are available |
|
||||
| `GET /health` | Readiness check |
|
||||
| `WS /api/chat` | Moshi-compatible tagged binary protocol with Opus audio |
|
||||
| `WS /v1/audio/duplex` | Simpler float32 raw-PCM protocol |
|
||||
|
||||
### `/api/chat` Protocol
|
||||
|
||||
The first byte of every binary frame is a message tag:
|
||||
|
||||
| Direction | Tag | Payload |
|
||||
|-----------|-----|---------|
|
||||
| Server to client | `0x00` | Ready handshake |
|
||||
| Client to server | `0x01` | Opus microphone audio at 24 kHz |
|
||||
| Server to client | `0x01` | Opus response audio |
|
||||
| Server to client | `0x02` | UTF-8 inner-monologue text |
|
||||
|
||||
The optional `text_prompt` and `voice_prompt` query parameters select a persona
|
||||
and voice. The default server admits one conversation; use `--batch-size N` to
|
||||
configure multiple lockstep slots. Connections beyond capacity close with
|
||||
WebSocket code 1013.
|
||||
|
||||
### `/v1/audio/duplex` Protocol
|
||||
|
||||
Send `{"type":"open","persona":"...","voice":"..."}` as a JSON text frame,
|
||||
then exchange binary float32 PCM frames. The server returns generated float32
|
||||
audio as binary frames and text as `{"type":"text","text":"..."}`. Send
|
||||
`{"type":"close"}` and wait for `{"type":"done"}` to flush and finish.
|
||||
|
||||
See the [PersonaPlex example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/personaplex)
|
||||
for the browser and headless clients.
|
||||
PersonaPlex no longer ships a standalone compatibility server: it is served
|
||||
through the unified duplex path (`vllm_omni/deploy/personaplex.yaml`) over
|
||||
[`WS /v1/realtime?duplex=1`](full_duplex_api.md); see the
|
||||
[PersonaPlex example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/personaplex)
|
||||
for its clients.
|
||||
|
||||
## JoyVL Interaction Server
|
||||
|
||||
@@ -80,7 +38,7 @@ python -m vllm_omni.experimental.fullduplex.joyvl.serving.server \
|
||||
```
|
||||
|
||||
| Method and route | Purpose |
|
||||
|------------------|---------|
|
||||
| ------------------ | --------- |
|
||||
| `GET /health` | Readiness check |
|
||||
| `GET /v1/models` | Reports the configured interaction model |
|
||||
| `POST /v1/chat/completions` | Processes one frame or interaction tick |
|
||||
|
||||
@@ -36,7 +36,7 @@ immediately followed by one binary WebSocket frame containing fragmented MP4
|
||||
bytes.
|
||||
|
||||
| Direction | Event or frame | Purpose |
|
||||
|-----------|----------------|---------|
|
||||
| ----------- | ---------------- | --------- |
|
||||
| Client to server | `session.start` | Starts generation with a prompt and video parameters |
|
||||
| Server to client | `video.start` | Confirms the request ID, format, and accepted configuration |
|
||||
| Server to client | `video.chunk_metadata` | Describes the next binary media frame |
|
||||
|
||||
@@ -36,7 +36,7 @@ WebSocket /v1/video/chat/stream
|
||||
### Protocol
|
||||
|
||||
| Direction | Type | Required fields | Description |
|
||||
|-----------|------|-----------------|-------------|
|
||||
| ----------- | ------ | ----------------- | ------------- |
|
||||
| Client -> Server | `session.config` | none | First message. Configures output modalities, frame sampling, EVS, and prompts. |
|
||||
| Client -> Server | `video.frame` | `data` | Base64 JPEG/PNG frame. |
|
||||
| Client -> Server | `audio.chunk` | `data` | Base64 PCM16 16 kHz mono audio bytes. |
|
||||
@@ -53,7 +53,7 @@ WebSocket /v1/video/chat/stream
|
||||
### `session.config` Fields
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| ------- | ------ | --------- | ------------- |
|
||||
| `model` | string or null | null | Optional model name. Usually omitted because the server hosts one model. |
|
||||
| `modalities` | list[string] | `["text", "audio"]` | Output modalities. Use `["text"]`, `["audio"]`, or both. |
|
||||
| `num_frames` | integer, 1-128 | `4` | Number of buffered frames sampled for each query. |
|
||||
@@ -69,7 +69,7 @@ WebSocket /v1/video/chat/stream
|
||||
The server accepts these legacy field names and rewrites them before validation. New clients should send the canonical names above.
|
||||
|
||||
| Legacy field | Canonical field |
|
||||
|--------------|-----------------|
|
||||
| -------------- | ----------------- |
|
||||
| `num_sample_frames` | `num_frames` |
|
||||
| `evs_enabled` | `enable_frame_filter` |
|
||||
| `evs_threshold` | `frame_filter_threshold` |
|
||||
@@ -77,7 +77,7 @@ The server accepts these legacy field names and rewrites them before validation.
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Values | Default | Description |
|
||||
|----------|--------|---------|-------------|
|
||||
| -------- | ------ | ------- | ----------- |
|
||||
| `VLLM_VIDEO_ASYNC_CHUNK` | `on`, `off` | `on` | Wire-level streaming switch. `off` buffers server-side deltas and emits coalesced outputs at the end of a query. |
|
||||
| `VLLM_VIDEO_AUDIO_DELTA_MODE` | `fast`, `slow` | `fast` | Audio delta extraction strategy. `fast` emits only newly produced chunks; `slow` recomputes from accumulated audio and exists for A/B verification. |
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ curl -L "http://localhost:8091/v1/videos/${video_id}/content" -o output.mp4
|
||||
### Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| ---------- | -------- | ------------- |
|
||||
| `/v1/videos` | `POST` | Create an asynchronous video generation job |
|
||||
| `/v1/videos/sync` | `POST` | Generate a video synchronously and return raw video bytes |
|
||||
| `/v1/videos/{video_id}` | `GET` | Retrieve job status and metadata |
|
||||
@@ -56,7 +56,7 @@ curl -L "http://localhost:8091/v1/videos/${video_id}/content" -o output.mp4
|
||||
#### OpenAI-style fields
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `prompt` | string | **required** | Text prompt for video generation |
|
||||
| `model` | string | server's model | Optional model name |
|
||||
| `seconds` | string | null | Requested clip duration in seconds |
|
||||
@@ -66,7 +66,7 @@ curl -L "http://localhost:8091/v1/videos/${video_id}/content" -o output.mp4
|
||||
#### vLLM-Omni extension fields
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `input_reference` | file | null | Uploaded reference image or video for image-to-video/video-to-video requests |
|
||||
| `image_reference` | string | null | JSON-encoded reference image payload; do not combine with `input_reference` or `video_reference` |
|
||||
| `video_reference` | string | null | JSON-encoded reference video payload; do not combine with `input_reference` or `image_reference` |
|
||||
@@ -203,7 +203,6 @@ curl -s http://localhost:8091/v1/videos \
|
||||
-F "fps=16"
|
||||
```
|
||||
|
||||
|
||||
### Synchronous Generation
|
||||
|
||||
```bash
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@ A: AWQ is available through the inherited vLLM quantization registry, and vLLM-O
|
||||
|
||||
> Q: Does vLLM-Omni support multimodal streaming input and output?
|
||||
|
||||
A: Yes, for specific models and serving paths. Current capabilities include [streaming video input](../serving/video_stream_api.md), [streaming diffusion output](../user_guide/diffusion/execution_modes.md#streaming-output), and experimental full-duplex real-time audio described in the [project overview](../README.md). Check the model and API documentation because input and output support is model-specific.
|
||||
A: Yes, for specific models and serving paths. Current capabilities include [streaming video input](../serving/video_stream_api.md), [streaming diffusion output](../user_guide/diffusion/execution_modes.md#streaming-output), and [full-duplex realtime audio](../serving/realtime_duplex_api.md). Check the model and API documentation because input and output support is model-specific.
|
||||
@@ -12,7 +12,7 @@ implementations to reduce numerical drift and sampling divergence.
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install kernels==0.14.1
|
||||
pip install kernels==0.15.2
|
||||
```
|
||||
|
||||
If `kernels` is unavailable, an explicit `FLASH_ATTN_HUB` or
|
||||
|
||||
@@ -11,7 +11,7 @@ For the shared factory, discovery, and lifecycle contract, see the
|
||||
|
||||
| Strategy | Device residency | Parallel scope | Guide |
|
||||
| --- | --- | --- | --- |
|
||||
| Model-level (sequential) | One pipeline component group at a time | Single device | [Model-Level Offloading](offloader/cpu_offload.md) |
|
||||
| Model-level (sequential) | One pipeline component group at a time | Single device | [Model-Level Offloading](offloader/module_offload.md) |
|
||||
| Layerwise (blockwise) | One transformer block, with next-block prefetch | Single device | [Layerwise Offloading](offloader/layerwise_offload.md) |
|
||||
| Distributed layerwise | Fixed two-block device buffer; optional host-weight sharding and AllGather | Multiple GPU/NPU ranks | [Distributed Layerwise Offloading](offloader/distributed_layerwise_offload.md) |
|
||||
|
||||
@@ -27,7 +27,7 @@ higher-priority strategy is selected.
|
||||
|
||||
## Quick selection
|
||||
|
||||
- Use [model-level offloading](offloader/cpu_offload.md) when swapping whole
|
||||
- Use [model-level offloading](offloader/module_offload.md) when swapping whole
|
||||
encoders and DiTs is enough to fit and phase-boundary transfers are
|
||||
acceptable.
|
||||
- Use [layerwise offloading](offloader/layerwise_offload.md) for compute-heavy video
|
||||
@@ -63,7 +63,7 @@ These headings preserve links to sections that moved into dedicated guides.
|
||||
|
||||
## Model-level (Sequential) Offloading
|
||||
|
||||
See [Model-Level Offloading](offloader/cpu_offload.md).
|
||||
See [Model-Level Offloading](offloader/module_offload.md).
|
||||
|
||||
## Layerwise (Blockwise) Offloading
|
||||
|
||||
|
||||
@@ -94,8 +94,16 @@ validating a model or debugging correctness, then increase it for
|
||||
multi-request throughput.
|
||||
|
||||
Step execution is capability-based, not a generic switch for every diffusion
|
||||
model. Qwen-Image supports step-wise continuous batching. HunyuanImage3 also
|
||||
supports it, but only when its resolved self-attention backend is `TORCH_SDPA`;
|
||||
model. Qwen-Image supports step-wise continuous batching. BAGEL supports
|
||||
step-wise continuous batching for image generation with `bagel.yaml`,
|
||||
`bagel_think.yaml`, and `bagel_single_stage.yaml`. In the two-stage topologies,
|
||||
only the diffusion stage uses step execution; the autoregressive Thinker is
|
||||
unchanged. Explicit text-output requests in the single-stage topology retain
|
||||
the complete-request path. Use at least two inference steps for BAGEL image
|
||||
requests because BAGEL schedules `num_inference_steps - 1` denoising updates.
|
||||
BAGEL step execution does not currently support sequence parallelism or a
|
||||
diffusion cache backend. HunyuanImage3 also supports step execution, but only
|
||||
when its resolved self-attention backend is `TORCH_SDPA`;
|
||||
set `DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA` or configure
|
||||
`diffusion_attention_config.default.backend=TORCH_SDPA` before using
|
||||
`--max-num-seqs >1`. See the
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Distributed Layerwise Offloading
|
||||
|
||||
Distributed layerwise offloading (DLO) extends block streaming to multi-device
|
||||
deployments. With AllGather enabled, each rank stores roughly `1 / dp_size` of
|
||||
the host weights and reconstructs each layer at runtime. Without AllGather,
|
||||
each rank streams a complete block independently. Compatible TP1 deployments
|
||||
can share checkpoint-backed host pages among processes on the same node;
|
||||
otherwise DLO streams the ordinary loader's rank-local tensors.
|
||||
deployments. Each selected component chooses its transfer independently. With
|
||||
`allgather`, each rank stores roughly one shard per member of the effective DLO
|
||||
group (DP, or SP when DP is one) and reconstructs each layer at runtime. With
|
||||
`rank-local`, each rank streams a complete loader-produced block independently.
|
||||
Compatible TP1 DiT deployments can share checkpoint-backed host pages among
|
||||
processes on the same node; otherwise DLO streams the ordinary loader's
|
||||
rank-local tensors.
|
||||
|
||||
See the [DLO feature design](../../../design/feature/offloader/distributed_layerwise_offload.md)
|
||||
for the implementation contract and compatibility matrix.
|
||||
@@ -24,52 +26,120 @@ Slots: slot 0: Layer N slot 1: Layer N+1
|
||||
AllGather communicates only request-independent weight shards, so data-
|
||||
parallel ranks may process different requests concurrently.
|
||||
|
||||
The public API describes intent in one nested configuration:
|
||||
|
||||
| Level | Setting | Question answered |
|
||||
| --- | --- | --- |
|
||||
| Granularity | `mode` | move a complete component (`module`) or stream its blocks (`layer`) |
|
||||
| Selection | `components` list | whether `dit` and/or `text_encoder` may move to CPU |
|
||||
| Per-component layer options | `layer_options` | choose rank-local or AllGather weight transfer and optionally retain leading DiT blocks |
|
||||
|
||||
`weight_transfer` applies only to model weights. It does not change encoder
|
||||
activations or the encoder's compute parallelism. `rank-local` copies each
|
||||
rank's loader-produced block to its device. `allgather` stores one shard of
|
||||
that block per rank, copies each shard to its device, and reconstructs the
|
||||
complete block with a device-side AllGather before compute.
|
||||
|
||||
The historical `enable_cpu_offload`, `enable_layerwise_offload`, and
|
||||
`enable_distributed_layerwise_offload` fields remain compatibility aliases
|
||||
for existing callers and model-specific stage lifecycles. Existing
|
||||
combinations retain their priority (`distributed layerwise` > `layerwise` >
|
||||
`module`). A compact config rejects conflicting compatibility strategies and
|
||||
non-default legacy DLO tuning.
|
||||
`distributed-layerwise` remains an internal backend name; users select
|
||||
`mode="layer"`, and the runtime chooses the capable backend.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Four ranks with sharded host weights and AllGather
|
||||
vllm serve /path/to/model --omni \
|
||||
--enable-distributed-layerwise-offload \
|
||||
--data-parallel-size 4
|
||||
# Four SP ranks; Wan declares replicated text-encoder weights, so both
|
||||
# selected components can use AllGather safely.
|
||||
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers --omni \
|
||||
--diffusion-offload-config \
|
||||
'{"mode":"layer","components":["dit","text_encoder"],"layer_options":{"dit":{"weight_transfer":"allgather"},"text_encoder":{"weight_transfer":"allgather"}}}' \
|
||||
--usp 4
|
||||
|
||||
# Standard-loader rank-local weights, without DLO AllGather
|
||||
# Mix transfers independently: sharded DiT, loader-produced encoder layout
|
||||
vllm serve /path/to/model --omni \
|
||||
--enable-distributed-layerwise-offload \
|
||||
--data-parallel-size 4 \
|
||||
--dlo-no-use-allgather
|
||||
--diffusion-offload-config \
|
||||
'{"mode":"layer","components":["dit","text_encoder"],"layer_options":{"dit":{"weight_transfer":"allgather"},"text_encoder":{"weight_transfer":"rank-local"}}}' \
|
||||
--usp 4
|
||||
|
||||
# Sequence parallel deployment
|
||||
# Full-topology rank-local DLO for an existing model-specific lifecycle
|
||||
vllm serve /path/to/model --omni \
|
||||
--enable-distributed-layerwise-offload \
|
||||
--dlo-no-use-allgather \
|
||||
--usp 4
|
||||
```
|
||||
|
||||
A compact layer config whose selected components are all `rank-local` and
|
||||
whose `resident_layers` is zero resolves to the ordinary layerwise backend. It
|
||||
uses the ordinary loader and does not request DLO's direct-checkpoint mmap.
|
||||
Keep the compatibility DLO flags for a model-specific full-topology rank-local
|
||||
lifecycle; do not add resident layers solely to force backend selection.
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
|
||||
omni = Omni(
|
||||
model="/path/to/model",
|
||||
enable_distributed_layerwise_offload=True,
|
||||
dlo_use_allgather=True,
|
||||
diffusion_offload_config={
|
||||
"mode": "layer",
|
||||
"components": ["dit", "text_encoder"],
|
||||
"layer_options": {
|
||||
"dit": {
|
||||
"weight_transfer": "rank-local",
|
||||
"resident_layers": 20,
|
||||
},
|
||||
"text_encoder": {
|
||||
"weight_transfer": "allgather",
|
||||
},
|
||||
},
|
||||
"pin_memory": True,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Meaning | Default |
|
||||
| Setting | Meaning | Default |
|
||||
| --- | --- | --- |
|
||||
| `--enable-distributed-layerwise-offload` | Enable DLO | `false` |
|
||||
| `--data-parallel-size N` | DP ranks and AllGather weight-sharding group | `1` |
|
||||
| `--dlo-use-allgather` | Shard host weights and reconstruct with AllGather | `true` |
|
||||
| `--dlo-no-use-allgather` | Stream complete rank-local blocks without a DLO weight collective | `false` |
|
||||
| `--dlo-resident-layers N` | Keep N leading main-DiT blocks on device; requires no-AllGather and model-declared resident paths | `0` |
|
||||
| `diffusion_offload_config.mode` | `module` or `layer` granularity | required |
|
||||
| `diffusion_offload_config.components` | Non-empty list containing `dit`, `text_encoder`, or both | required |
|
||||
| `layer_options.NAME.weight_transfer` | `rank-local` or `allgather` | `rank-local` |
|
||||
| `layer_options.dit.resident_layers` | Leading main-DiT blocks kept on device; requires `rank-local` and model-declared resident paths | `0` |
|
||||
| `diffusion_offload_config.pin_memory` | Pin streamed host memory for faster H2D copies | `true` |
|
||||
| `--data-parallel-size N` | DP ranks; DP is the DLO group when greater than one, otherwise SP is used | `1` |
|
||||
| `--host-weight-runtime-mode {disabled,preferred,required}` | HWR policy: no interaction, populate on a miss, or require an exact hit | `disabled` |
|
||||
| `--host-weight-runtime-root PATH` | Writable node-local HWR store shared by workers in one storage domain; required for `preferred` and `required` | unset |
|
||||
| `--dlo-host-registration-limit-gib N` | Optional per-worker ceiling for registering an HWR mmap; zero adds no ceiling | `0` |
|
||||
|
||||
HWR and host-registration tuning remain on the compatibility DLO interface and
|
||||
cannot be combined with `diffusion_offload_config` in this release.
|
||||
|
||||
## Component and weight-transfer matrix
|
||||
|
||||
| Component | `allgather` | `rank-local` |
|
||||
| --- | --- | --- |
|
||||
| DiT | Shard each loader-visible block across the DLO DP group, or the SP group when DP is one | Stream each rank's complete loader-produced block |
|
||||
| Text encoder | Same shard + AllGather path when the model declares identical encoder weights across the DLO group | Stream each rank's complete encoder block, including encoder-TP shards |
|
||||
|
||||
The text encoder's compute group and weight-transfer group are separate
|
||||
concepts. An encoder TP group owns different parameter shards, so it is not a
|
||||
valid AllGather offload group. Models opt in through
|
||||
`OffloadPlan.encoder_dlo_weight_replication`; otherwise multi-rank encoder
|
||||
AllGather fails at startup with guidance to use `rank-local`. This makes SP
|
||||
AllGather available to replicated encoders without silently corrupting an
|
||||
encoder-TP layout.
|
||||
|
||||
`resident_layers` remains a DiT-only setting and currently requires the DiT
|
||||
transfer to be `rank-local`.
|
||||
|
||||
## Host-weight loading
|
||||
|
||||
The diffusion loader chooses host storage before DLO is enabled. It first
|
||||
When the distributed backend is selected by AllGather, resident DiT layers, or
|
||||
the compatibility DLO flag, the diffusion loader chooses host storage before
|
||||
DLO is enabled. It first
|
||||
attempts to build a complete, validated direct-checkpoint mmap plan. If names,
|
||||
coverage, shape, dtype, topology, or loader-callback compatibility cannot be
|
||||
proven, it runs the ordinary model loader instead. DLO consumes that result and
|
||||
@@ -97,20 +167,20 @@ With direct checkpoint mmap, the loader:
|
||||
6. preserves `post_load_weights()` and `validate_loaded_weights()` lifecycle
|
||||
hooks.
|
||||
|
||||
For AllGather with a group larger than one, each process copies only its
|
||||
persistent shard and then releases the source mapping. For no-AllGather, each
|
||||
process keeps the mapping open and packs complete blocks through two bounded
|
||||
pinned staging slots. Processes mapping the same files on one node share the
|
||||
immutable pages; no-AllGather still performs a complete-block H2D copy in each
|
||||
process.
|
||||
For a DiT using `allgather` with a group larger than one, each process copies
|
||||
only its persistent shard and then releases the source mapping. For a DiT using
|
||||
`rank-local`, each process keeps the mapping open and packs complete blocks
|
||||
through two bounded pinned staging slots. Processes mapping the same files on
|
||||
one node share the immutable pages; rank-local transfer still performs a
|
||||
complete-block H2D copy in each process.
|
||||
|
||||
When the effective DLO group size is one, `dlo_use_allgather=True` does not
|
||||
When the effective DLO group size is one, `weight_transfer="allgather"` does not
|
||||
perform a collective and uses the same rank-local transfer behavior.
|
||||
|
||||
### Final-layout Host Weight Runtime
|
||||
|
||||
HWR is an opt-in startup optimization for models that declare the final-layout
|
||||
BF16 restore contract. Use it only with no-AllGather DLO:
|
||||
BF16 restore contract. Use it only when the selected DiT uses `rank-local`:
|
||||
|
||||
The validated BF16 model contracts currently cover MiniMax H3 and
|
||||
`black-forest-labs/FLUX.2-klein-4B`. FLUX.2-klein-9B shares the same model
|
||||
@@ -204,7 +274,7 @@ H2D behavior.
|
||||
#### Registered direct H2D
|
||||
|
||||
Registration is attempted automatically only for an eligible warm HWR hit when
|
||||
`pin_cpu_memory` is enabled. A successful path is:
|
||||
the existing `pin_cpu_memory` policy is enabled. A successful path is:
|
||||
|
||||
```text
|
||||
shared read-only HWR mmap -> existing rotating HBM block buffer -> GPU kernel
|
||||
@@ -217,10 +287,10 @@ kernels access host memory directly.
|
||||
CUDA requires read-only host-registration capability for the immutable HWR
|
||||
mapping. Unsupported capability, a positive registration limit smaller than
|
||||
the complete page-aligned mapping, or a safely rolled-back registration error
|
||||
falls back to bounded staging. Programmatic configurations can set
|
||||
`pin_cpu_memory=False` to disable registration explicitly. A successful
|
||||
registration locks the complete mapped range in host memory for that worker's
|
||||
lifetime, so use
|
||||
falls back to bounded staging. Programmatic compatibility configurations can
|
||||
set `pin_cpu_memory=False` to disable registration explicitly.
|
||||
A successful registration locks the complete mapped range in host memory for
|
||||
that worker's lifetime, so use
|
||||
`--dlo-host-registration-limit-gib` when an operator-enforced ceiling is
|
||||
required.
|
||||
|
||||
@@ -228,8 +298,8 @@ Shutdown drains H2D work, releases hook/source references, unregisters every
|
||||
range, and only then closes the HWR lease. AllGather and direct checkpoint mmap
|
||||
retain their existing transfer paths.
|
||||
|
||||
When HWR is disabled, DLO is disabled, or AllGather is enabled, the loader does
|
||||
not resolve HWR sources or construct its store.
|
||||
When HWR is disabled, DLO is disabled, or the DiT uses `allgather`, the loader
|
||||
does not resolve HWR sources or construct its store.
|
||||
|
||||
## Declarative topology
|
||||
|
||||
@@ -241,38 +311,54 @@ from vllm_omni.diffusion.offloader import OffloadPlan
|
||||
|
||||
class MyPipeline(nn.Module):
|
||||
_dit_modules = ["transformer"]
|
||||
_encoder_modules = ["prompt_model"]
|
||||
_offload_plan = OffloadPlan(
|
||||
block_attrs={"transformer": ("blocks",)},
|
||||
offload_submodules={"context_encoder": "layers"},
|
||||
encoder_component_types={"prompt_model": "text_encoder"},
|
||||
encoder_block_attrs={"prompt_model": ("encoder.layers",)},
|
||||
encoder_dlo_weight_replication=frozenset({"prompt_model"}),
|
||||
on_demand_component_paths=frozenset({"prompt_model"}),
|
||||
)
|
||||
```
|
||||
|
||||
When no plan exists, discovery falls back to
|
||||
`_layerwise_offload_blocks_attrs` and then heuristic attribute lookup.
|
||||
`encoder_component_types` maps arbitrary encoder paths to the public
|
||||
`text_encoder` selector. `encoder_dlo_weight_replication` is the explicit
|
||||
safety declaration for reusing the DiT DLO group. On-demand encoder non-block
|
||||
state is loaded and offloaded by its pipeline phase.
|
||||
|
||||
When no plan exists, DiT discovery falls back to
|
||||
`_layerwise_offload_blocks_attrs` and then heuristic attribute lookup;
|
||||
undeclared auxiliary components remain resident.
|
||||
|
||||
## Data-parallel concurrency
|
||||
|
||||
With `data_parallel_size > 1` and AllGather enabled, the scheduler can process
|
||||
up to `dp_size` requests per denoising step. Every concurrent request must set
|
||||
the same explicit `num_inference_steps`; `None` is rejected because every rank
|
||||
must enter each collective.
|
||||
up to `dp_size` requests per denoising step. Concurrent requests must resolve
|
||||
to the same denoise schedule, so every request must provide the same explicit
|
||||
`num_inference_steps`. Pipeline defaults are rejected because different request
|
||||
modes can resolve the same `None` value to different schedules.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Direct checkpoint mmap currently requires TP1. TP greater than one is
|
||||
outside the Phase A shared-mmap support scope and falls back before model
|
||||
mutation to the ordinary TP-aware loader. DLO can stream that runtime layout,
|
||||
and eligible no-AllGather configurations may use HWR final-layout artifacts,
|
||||
and eligible DiT rank-local configurations may use HWR final-layout artifacts,
|
||||
but direct checkpoint mmap provides no shared-mmap host-memory guarantee for
|
||||
TP greater than one.
|
||||
- HSDP plus AllGather is rejected to avoid double sharding. HSDP without
|
||||
AllGather has limited end-to-end validation.
|
||||
- HSDP with `allgather` on any selected component is rejected to avoid double
|
||||
sharding. HSDP with rank-local transfers has limited end-to-end validation.
|
||||
- With data parallelism, `allgather` cannot be combined with TeaCache or
|
||||
Cache-DiT on any selected component. Prompt-embedding cache is also
|
||||
incompatible when the text encoder uses `allgather`. Rank-local cache hits
|
||||
could otherwise make ranks enter different weight collectives.
|
||||
- Per-tensor online FP8 linears use the ordinary loader and can run with either
|
||||
DLO transfer path. With AllGather, every rank temporarily materializes the
|
||||
complete FP8 model in host memory before DLO retains only its shard. Other
|
||||
online quantization methods require no-AllGather until their runtime layouts
|
||||
are validated.
|
||||
- Resident leading layers require `--dlo-no-use-allgather` and a model
|
||||
DiT transfer path. With DiT `allgather`, every rank temporarily materializes
|
||||
the complete FP8 model in host memory before DLO retains only its shard.
|
||||
Other online quantization methods require rank-local transfer for the
|
||||
affected component until their runtime layouts are validated.
|
||||
- Resident leading layers require DiT `rank-local` transfer and a model
|
||||
`OffloadPlan` that declares eligible `resident_dit_paths`.
|
||||
- DP concurrency requires an explicit, identical inference-step count.
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ stream.
|
||||
| ... | ... | ... | ... |
|
||||
| last block | Prefetch block 0 | Compute last block | Free last block |
|
||||
|
||||
Encoders, VAE modules, and non-block DiT modules such as embeddings and norms
|
||||
remain device resident.
|
||||
Selected, plan-declared text-encoder blocks can use the same rank-local
|
||||
streaming mechanism. Image encoders, VAEs, and non-block DiT modules remain
|
||||
device resident.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -28,15 +29,44 @@ from vllm_omni import Omni
|
||||
|
||||
omni = Omni(
|
||||
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
enable_layerwise_offload=True,
|
||||
diffusion_offload_config={
|
||||
"mode": "layer",
|
||||
"components": ["dit"],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
```bash
|
||||
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers \
|
||||
--omni --enable-layerwise-offload
|
||||
--omni \
|
||||
--diffusion-offload-config '{"mode":"layer","components":["dit"]}'
|
||||
```
|
||||
|
||||
## Component selection
|
||||
|
||||
Add `dit`, `text_encoder`, or both to the `components` list. Omitting
|
||||
`layer_options` uses safe rank-local defaults:
|
||||
|
||||
- `["dit"]` streams only DiT blocks.
|
||||
- `["text_encoder"]` streams only declared text-encoder blocks.
|
||||
- Listing both streams both components.
|
||||
|
||||
```bash
|
||||
# DiT-only layer offload
|
||||
vllm serve /path/to/model --omni \
|
||||
--diffusion-offload-config \
|
||||
'{"mode":"layer","components":["dit"]}'
|
||||
|
||||
# Stream a model-declared text encoder while keeping the DiT resident
|
||||
vllm serve /path/to/model --omni \
|
||||
--diffusion-offload-config \
|
||||
'{"mode":"layer","components":["text_encoder"]}'
|
||||
```
|
||||
|
||||
Encoder categories are resolved from `OffloadPlan.encoder_component_types`
|
||||
first. A name-based fallback is retained for pipelines that predate
|
||||
`OffloadPlan`.
|
||||
|
||||
## Model integration
|
||||
|
||||
Transformer classes declare containers of executable blocks:
|
||||
@@ -53,13 +83,29 @@ class Flux2Transformer2DModel(nn.Module):
|
||||
]
|
||||
```
|
||||
|
||||
See the [layerwise design](../../../design/feature/offloader/layerwise_offload.md) for
|
||||
the discovery and hook invariants. `OffloadPlan` is a separate declarative
|
||||
topology path for distributed layerwise offload.
|
||||
Auxiliary components use declarative pipeline metadata:
|
||||
|
||||
```python
|
||||
from vllm_omni.diffusion.offloader import OffloadPlan
|
||||
|
||||
|
||||
class MyPipeline(nn.Module):
|
||||
_encoder_modules = ["prompt_model"]
|
||||
_offload_plan = OffloadPlan(
|
||||
encoder_component_types={"prompt_model": "text_encoder"},
|
||||
encoder_block_attrs={"prompt_model": ("encoder.layers",)},
|
||||
)
|
||||
```
|
||||
|
||||
See the [layerwise design](../../../design/feature/offloader/layerwise_offload.md)
|
||||
for the discovery and hook invariants. Both ordinary and distributed layerwise
|
||||
offload consume the same `OffloadPlan` metadata.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Single device only.
|
||||
- The default weight transfer is `rank-local`. Set a selected component's
|
||||
`weight_transfer` to `allgather` to shard its host weights across a
|
||||
compatible multi-device group; backend selection is automatic.
|
||||
- Setup consolidates and pins block parameters, increasing cold-start time.
|
||||
- Performance depends on block compute time and host-to-device bandwidth;
|
||||
lightweight blocks may not hide transfers.
|
||||
+21
-7
@@ -2,14 +2,14 @@
|
||||
|
||||
Model-level, or sequential, offloading keeps only the pipeline component group
|
||||
currently executing on the accelerator. It is the simplest offload strategy
|
||||
and is selected with `--enable-cpu-offload`.
|
||||
and is selected with `diffusion_offload_config.mode="module"`.
|
||||
|
||||
## How it works
|
||||
|
||||
Pre-forward hooks enforce mutual exclusion between DiT and encoder modules:
|
||||
|
||||
- before an encoder runs, the DiT moves to CPU;
|
||||
- before a DiT runs, encoders and other DiTs move to CPU; and
|
||||
- before an encoder runs, selected DiTs move to CPU;
|
||||
- before a DiT runs, selected encoders and other selected DiTs move to CPU; and
|
||||
- VAE modules remain on the accelerator.
|
||||
|
||||
Pinned host memory reduces transfer overhead. Transfers occur at phase
|
||||
@@ -22,15 +22,28 @@ from vllm_omni import Omni
|
||||
|
||||
omni = Omni(
|
||||
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
|
||||
enable_cpu_offload=True,
|
||||
diffusion_offload_config={
|
||||
"mode": "module",
|
||||
"components": ["dit", "text_encoder"],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
```bash
|
||||
vllm serve Wan-AI/Wan2.2-T2V-A14B-Diffusers \
|
||||
--omni --enable-cpu-offload
|
||||
--omni \
|
||||
--diffusion-offload-config \
|
||||
'{"mode":"module","components":["dit","text_encoder"]}'
|
||||
```
|
||||
|
||||
List a component to select it for offload. Module mode rejects `layer_options`
|
||||
such as `weight_transfer` and `resident_layers`. The
|
||||
`enable_cpu_offload=True` compatibility entry point remains supported. New
|
||||
integrations should prefer the explicit config; existing model-specific stage
|
||||
lifecycles do not need to migrate until equivalent component coverage exists.
|
||||
For example, MiniMax-H3's compatibility lifecycle also stages its VAEs, while
|
||||
the compact selector intentionally covers only `dit` and `text_encoder`.
|
||||
|
||||
## Model integration
|
||||
|
||||
Pipelines should implement `SupportsComponentDiscovery`:
|
||||
@@ -73,9 +86,10 @@ while reusing sequential `.to()` movers.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Single device only.
|
||||
- Transfers are rank-local; module mode does not shard host payloads or add a
|
||||
weight AllGather across ranks.
|
||||
- Higher cold-start latency.
|
||||
- Transfers between encoder and denoising phases add latency.
|
||||
|
||||
See the [model-level design](../../../design/feature/offloader/cpu_offload.md)
|
||||
See the [model-level design](../../../design/feature/offloader/module_offload.md)
|
||||
for lifecycle and extension invariants.
|
||||
@@ -1,5 +1,7 @@
|
||||
# Diffusion Advanced Features
|
||||
|
||||
<!-- markdownlint-disable MD028 MD060 -->
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
@@ -30,7 +32,6 @@ Cache methods trade minimal quality for significant speedup. Quality loss is typ
|
||||
| **[TeaCache](diffusion/cache_acceleration/teacache.md)** | Adaptive caching using modulated inputs | Quick setup, balanced quality/speed on single GPU |
|
||||
| **[Cache-DiT](diffusion/cache_acceleration/cache_dit.md)** | Multiple caching techniques: DBCache, TaylorSeer, SCM | Fine-grained control, tunable quality-speed tradeoff |
|
||||
|
||||
|
||||
#### Lossless Acceleration
|
||||
|
||||
Parallelism methods distribute computation across GPUs without quality loss (mathematically equivalent to single-GPU).
|
||||
@@ -72,7 +73,6 @@ Extension methods add specialized capabilities to diffusion models beyond standa
|
||||
| **[LoRA Inference](diffusion/lora.md)** | Enables inference with Low-Rank Adaptation (LoRA) adapters weights | Reinforcement learning extensions |
|
||||
| **[Frame Interpolation](diffusion/frame_interpolation.md)** | Inserts intermediate video frames after generation for smoother motion | Video generation pipelines that need higher temporal smoothness |
|
||||
|
||||
|
||||
### Execution Modes
|
||||
|
||||
Execution modes control how the diffusion pipeline processes requests and
|
||||
@@ -116,7 +116,7 @@ The following tables show which models support each feature:
|
||||
|
||||
| Model | ⚡TeaCache | ⚡Cache-DiT | 🔀SP (Ulysses & Ring) | 🔀CFG-Parallel | 🔀Tensor-Parallel | 🔀Pipeline-Parallel | 🔀HSDP | 💾CPU Offload (Layerwise) | 💾VAE-Patch-Parallel | 💾Quantization | 🔄Step Execution |
|
||||
|--------------------------|:---------:|:----------:|:---------------------:|:--------------:|:-----------------:|:-------------------:|:------:|:-------------------------:|:--------------------:|:--------------:|:----------------:|
|
||||
| **Bagel** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **Bagel** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
| **Boogu-Image** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **FLUX.1-dev** | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
|
||||
| **FLUX.1-schnell** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
|
||||
@@ -147,11 +147,13 @@ The following tables show which models support each feature:
|
||||
| **Cosmos3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ✅ | ❌ |
|
||||
|
||||
> Notes:
|
||||
>
|
||||
> 1. Nextstep_1(T2I) does not support cache acceleration methods such as TeaCache or Cache-DiT.
|
||||
> 2. `Tongyi-MAI/Z-Image-Turbo` is a distilled model with minimal NFEs; CFG-Parallel is not necessary.
|
||||
> 3. Cosmos3 T2I uses `Cosmos3OmniDiffusersPipeline` with `modalities=["image"]`. Model-level CPU offload swaps the nested UND reasoner and GEN generator pathways; layerwise offload remains available for blockwise GEN/UND offload.
|
||||
> 4. Krea 2 currently supports single-GPU inference plus LoRA, Cache-DiT, HSDP, CPU/layerwise offload, and VAE-patch-parallel (decode). TP/SP/CFG-Parallel are not yet wired. The few-step distilled (Turbo) checkpoint uses `is_distilled=true` (fixed timestep shift `mu=1.15`); generate at 2048x2048 by default with `num_inference_steps≈8` and `guidance_scale=0`. The Raw checkpoint uses 1024x1024, `num_inference_steps=28`, and `guidance_scale=4.5`.
|
||||
> 5. HunyuanImage3 supports step execution. Multi-request step execution requires `TORCH_SDPA`; see [Diffusion Execution Modes](diffusion/execution_modes.md#step-execution).
|
||||
> 6. BAGEL step execution supports image generation with `bagel.yaml`, `bagel_think.yaml`, and `bagel_single_stage.yaml`; two-stage Thinker execution and explicit single-stage text output remain on their existing complete-request paths. Image requests require `num_inference_steps >= 2`. BAGEL step execution cannot currently be combined with sequence parallelism or a diffusion cache backend; see [Diffusion Execution Modes](diffusion/execution_modes.md#step-execution).
|
||||
|
||||
### VideoGen
|
||||
|
||||
@@ -163,24 +165,24 @@ The following tables show which models support each feature:
|
||||
| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||
| **LTX-2.3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||
| **LTX-2.5** | ❌ | ❓ (one-stage) | ❓ (Ulysses only) | ✅ (Full only) | ❓ | ❌ | ✅ | ✅ | ✅ (decode) | ❓ (FP8) | ❌ |
|
||||
| **SANA-Video-2B T2V I2V** | ❌ | ❌ | ❌ | ✅ | ✅ (TP=2 only) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **SANA-Video-2B T2V I2V** | ❌ | ❌ | ✅ (`--usp`, frame-sharded) | ✅ | ✅ (TP=2 only) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Helios** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅* |
|
||||
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
|
||||
| **Cosmos3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
|
||||
| **LongCat-Video-Avatar-1.5** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **MiniMax-H3** | ✅ (FL2VA) | ✅ | ✅ | ❌ | ✅ (DiT/TE) | ❌ | ✅ | ✅ | ✅ (tile) | ✅ (DiT) | ❌ |
|
||||
| **MAGI-2 Preview** | ❌ | ✅ | ✅ (Ulysses) | ✅ (2-way) | ✅ | ❌ | ✅ | ✅ (1-GPU/LW; DLO DP-AG/SP no-AG) | ✅ (tile) | ❌ | ❌ |
|
||||
| **SANA-WM** | ❌ | ❌ | ❌<sup>5</sup> | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
> Notes:
|
||||
> 5. SANA-WM cannot support sequence parallelism: its bidirectional gated delta
|
||||
> recurrence carries state across frames, so a rank cannot denoise a slice of
|
||||
> the token sequence in isolation. Doing so would need a distributed scan or
|
||||
> an all-gather before every GDN block. The remaining ❌ columns are simply
|
||||
> unvalidated on this model, not known-broken.
|
||||
> recurrence carries state across frames, so a rank cannot denoise a slice of
|
||||
> the token sequence in isolation. Doing so would need a distributed scan or
|
||||
> an all-gather before every GDN block. The remaining ❌ columns are simply
|
||||
> unvalidated on this model, not known-broken.
|
||||
|
||||
> **Step execution note:** Helios supports single-request step execution only;
|
||||
> use `max_num_seqs=1`.
|
||||
|
||||
**Frame Interpolation Support**
|
||||
|
||||
- **Supported**: Wan2.2 text-to-video, image-to-video, and TI2V pipelines; SANA-Video-2B native text-to-video and image-to-video pipelines
|
||||
@@ -192,7 +194,6 @@ The following tables show which models support each feature:
|
||||
|-----------------------|:---------:|:----------:|:---------------------:|:--------------:|:-----------------:|:-------------------:|:------:|:-------------------------:|:--------------------:|:--------------:|:----------------:|
|
||||
| **Stable-Audio-Open** | ✅ | ❌ | ❓ | ❓ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ |
|
||||
|
||||
|
||||
## Feature Compatibility
|
||||
|
||||
**Legend:**
|
||||
@@ -231,7 +232,6 @@ The following tables show which models support each feature:
|
||||
5. The compatibility matrix uses FP8 as the representative quantization method.
|
||||
6. Step Execution is not compatible with any diffusion cache backend. LoRA is supported, but each scheduled batch must use a single adapter (requests with different `lora_request` or `lora_scale` are kept in separate batches).
|
||||
|
||||
|
||||
## Multi-Thread Weight Loading
|
||||
|
||||
The loading guide now lives at [Diffusion Startup and
|
||||
|
||||
@@ -60,6 +60,50 @@ vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 \
|
||||
|
||||
See [`bagel_single_stage.yaml`](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/deploy/bagel_single_stage.yaml) for configuration. The `pipeline: bagel_single_stage` field selects the single-stage topology from the pipeline registry.
|
||||
|
||||
### Step Execution and Continuous Batching
|
||||
|
||||
Image generation supports step execution with all three BAGEL deploy configs.
|
||||
The global flag is applied only to diffusion stages:
|
||||
|
||||
- With `bagel.yaml` or `bagel_think.yaml`, Stage 1 uses step execution and the
|
||||
Stage 0 Thinker remains autoregressive.
|
||||
- With `bagel_single_stage.yaml`, image requests use step execution in Stage 0.
|
||||
Explicit `text2text` and `img2text` requests use the same complete-request
|
||||
text path as they do without `--step-execution`.
|
||||
|
||||
Start with the deploy file's default diffusion capacity of one:
|
||||
|
||||
```bash
|
||||
vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 \
|
||||
--deploy-config vllm_omni/deploy/bagel.yaml \
|
||||
--step-execution
|
||||
```
|
||||
|
||||
To enable step-wise continuous batching, override `max_num_seqs` on the
|
||||
diffusion stage. Use Stage 1 for either two-stage config:
|
||||
|
||||
```bash
|
||||
vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 \
|
||||
--deploy-config vllm_omni/deploy/bagel.yaml \
|
||||
--step-execution \
|
||||
--stage-overrides '{"1":{"max_num_seqs":4}}'
|
||||
```
|
||||
|
||||
Use Stage 0 for the single-stage config:
|
||||
|
||||
```bash
|
||||
vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 \
|
||||
--deploy-config vllm_omni/deploy/bagel_single_stage.yaml \
|
||||
--step-execution \
|
||||
--stage-overrides '{"0":{"max_num_seqs":4}}'
|
||||
```
|
||||
|
||||
BAGEL image requests require `num_inference_steps >= 2`: the original BAGEL
|
||||
schedule uses `num_inference_steps - 1` denoising updates. The scheduler batches
|
||||
only requests with compatible effective image sizes and BAGEL CFG/renormalization
|
||||
settings. Sequence parallelism and diffusion cache backends such as TeaCache or
|
||||
Cache-DiT are not currently supported together with BAGEL step execution.
|
||||
|
||||
### Tensor Parallelism (TP)
|
||||
|
||||
For larger models or multi-GPU environments, enable TP via CLI:
|
||||
@@ -203,6 +247,13 @@ curl http://localhost:8091/v1/chat/completions \
|
||||
-d @payload.json
|
||||
```
|
||||
|
||||
BAGEL img2img derives its generated image size from the input image. It
|
||||
preserves the input aspect ratio, aligns dimensions to the latent stride, and
|
||||
applies the checkpoint size limit; explicit `height` and `width` values do not
|
||||
override this model-specific resize policy. In step mode, the effective size is
|
||||
resolved before scheduler admission so requests with different resulting
|
||||
shapes are not combined.
|
||||
|
||||
### Image to Text (img2text)
|
||||
|
||||
**Python client:**
|
||||
@@ -267,8 +318,8 @@ curl http://localhost:8091/v1/chat/completions \
|
||||
| `--server` / `-s` | `http://localhost:8091` | Server URL |
|
||||
| `--image-url` / `-i` | `None` | Input image URL or local path (img2img/img2text) |
|
||||
| `--modality` / `-m` | `text2img` | `text2img`, `img2img`, `img2text`, `text2text` |
|
||||
| `--height` | `512` | Image height in pixels |
|
||||
| `--width` | `512` | Image width in pixels |
|
||||
| `--height` | `512` | Text-to-image output height; BAGEL img2img derives its size from the input image |
|
||||
| `--width` | `512` | Text-to-image output width; BAGEL img2img derives its size from the input image |
|
||||
| `--steps` | `25` | Number of inference steps |
|
||||
| `--seed` | `42` | Random seed |
|
||||
| `--negative` | `None` | Negative prompt for CFG |
|
||||
@@ -293,6 +344,7 @@ python openai_chat_client.py \
|
||||
| File | Description |
|
||||
| :--- | :---------- |
|
||||
| [`bagel.yaml`](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/deploy/bagel.yaml) | Two-stage default (Thinker + DiT on GPU 0) |
|
||||
| [`bagel_think.yaml`](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/deploy/bagel_think.yaml) | Two-stage think mode (Thinker + DiT) |
|
||||
| [`bagel_single_stage.yaml`](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/deploy/bagel_single_stage.yaml) | Single-stage (DiT only) |
|
||||
|
||||
### Key Deploy YAML Fields
|
||||
|
||||
@@ -16,6 +16,7 @@ implemented with clear Diffusers or vLLM-Omni behavior.
|
||||
- Sequence parallel execution
|
||||
- TeaCache / Cache-DiT acceleration
|
||||
- Step-wise execution (continuous batching)
|
||||
- Component-selective `diffusion_offload_config` (the legacy whole-pipeline CPU-offload options remain supported)
|
||||
|
||||
For these features, it is recommended to use natively supported pipelines instead.
|
||||
|
||||
|
||||
@@ -17,9 +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
|
||||
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.
|
||||
The bundled Qwen3-Omni setup explicitly sets `moe_backend: triton` for the Thinker and Talker stages. This keeps
|
||||
both stages on vLLM's Triton unquantized MoE path and avoids the unstable FlashInfer CUTLASS unquantized MoE path on
|
||||
some GPUs.
|
||||
Asynchronous chunk streaming is **enabled by default** within the bundled configuration.
|
||||
|
||||
To explicitly utilize a custom deployment YAML, specify the configuration path:
|
||||
@@ -87,15 +87,9 @@ vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \
|
||||
--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
|
||||
that kind of change. Since each command launches one stage, just pass the knob
|
||||
directly on that stage command:
|
||||
To experiment with another supported MoE backend, set `moe_backend` in the
|
||||
deployment YAML for both the Thinker and Talker stages. For the stage-based
|
||||
CLI, pass `--moe-backend` directly on each stage command.
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
|
||||
@@ -7,16 +7,16 @@ loading the model. Use it when you want memory savings without preparing a
|
||||
separate quantized checkpoint.
|
||||
|
||||
This mode is different from pre-quantized checkpoint formats such as GGUF,
|
||||
AutoRound, msModelSlim, or serialized Int8 checkpoints. Those formats are
|
||||
prepared before serving and are documented in their method-specific guides.
|
||||
For MXFP8 and MXFP4, use this page for load-time quantization from BF16
|
||||
checkpoints, and use the method-specific pages for offline checkpoints produced
|
||||
by msModelSlim and the merge tools.
|
||||
AutoRound, msModelSlim, serialized TorchAO checkpoints, or serialized Int8
|
||||
checkpoints. Those formats are prepared before serving and are documented in
|
||||
their method-specific guides. For MXFP8 and MXFP4, use this page for load-time
|
||||
quantization from BF16 checkpoints, and use the method-specific pages for
|
||||
offline checkpoints produced by msModelSlim and the merge tools.
|
||||
|
||||
## Hardware Support
|
||||
|
||||
| Device | FP8 W8A8 | Int8 W8A8 | MXFP8 W8A8 | MXFP4 W4A4 |
|
||||
|--------|----------|-----------|------------|------------|
|
||||
| -------- | ---------- | ----------- | ------------ | ------------ |
|
||||
| NVIDIA Blackwell GPU (SM 100+) | ✅ | ✅ | ⭕ | ⭕ |
|
||||
| NVIDIA Ada/Hopper GPU (SM 89+) | ✅ | ✅ | ⭕ | ⭕ |
|
||||
| NVIDIA Ampere GPU (SM 80+) | ✅ | ✅ | ⭕ | ⭕ |
|
||||
@@ -33,7 +33,7 @@ MXFP4 are documented for the Ascend NPU path.
|
||||
### Diffusion Model (Qwen-Image, Wan2.2)
|
||||
|
||||
| Method | Guide | Example models | Status |
|
||||
|--------|-------|----------------|--------|
|
||||
| -------- | ------- | ---------------- | -------- |
|
||||
| FP8 W8A8 | [FP8](fp8.md) | Qwen-Image; Wan2.2 is not validated | Validated for Qwen-Image family and other DiT models |
|
||||
| Int8 W8A8 | [Int8](int8.md) | Qwen-Image; Wan2.2 is not validated | Validated for Qwen-Image and Z-Image |
|
||||
| MXFP8 W8A8 | [MXFP8](mxfp8.md) | Wan2.2-T2V-A14B, Wan2.2-I2V-A14B, Wan2.2-TI2V-5B | Validated on Ascend NPU and Intel XPU |
|
||||
@@ -88,7 +88,7 @@ config = build_quant_config({
|
||||
## Parameters
|
||||
|
||||
| Parameter | Methods | Description |
|
||||
|-----------|---------|-------------|
|
||||
| ----------- | --------- | ------------- |
|
||||
| `method` | FP8, Int8, MXFP8, MXFP4 | Quantization method: `"fp8"`, `"int8"`, `"mxfp8"`, `"mxfp4"`, or `"mxfp4_dualscale"` |
|
||||
| `ignored_layers` | FP8, Int8, MXFP8, MXFP4 | Layer name patterns to keep in BF16/FP16 |
|
||||
| `activation_scheme` | FP8, Int8 | The runtime value `"dynamic"` selects online activation scaling |
|
||||
|
||||
@@ -11,21 +11,21 @@ For the internal architecture and backend extension points, see the
|
||||
## Quantization Modes
|
||||
|
||||
| Mode | Guide | Description | Methods |
|
||||
|------|-------|-------------|---------|
|
||||
| ------ | ------- | ------------- | --------- |
|
||||
| Online quantization | [Online Quantization](online.md) | vLLM-Omni computes quantized weights and scales while loading the model. | FP8 W8A8, Int8 W8A8, BitsAndBytes W4, MXFP8 W8A8, MXFP4 W4A4 |
|
||||
| Runtime attention quantization | [Quantized KV Cache](quantized_kvcache.md) | vLLM-Omni dynamically quantizes eligible diffusion Flash Attention tensors during inference. | FP8 FA |
|
||||
| Pre-quantized checkpoints | Method-specific guides | The checkpoint or an offline quantizer provides quantized weights and scales before serving. | ModelOpt, AutoRound, msModelSlim, serialized Int8, offline MXFP8, offline MXFP4 DualScale |
|
||||
| Pre-quantized checkpoints | Method-specific guides | The checkpoint or an offline quantizer provides quantized weights and scales before serving. | ModelOpt, AutoRound, TorchAO, msModelSlim, serialized Int8, offline MXFP8, offline MXFP4 DualScale |
|
||||
|
||||
## Hardware Support
|
||||
|
||||
| Device | FP8 W8A8 | Int8 W8A8 | BitsAndBytes W4 | ModelOpt | MXFP8 W8A8 | MXFP4 W4A4 | AutoRound | msModelSlim |
|
||||
|--------|----------|-----------|------------------|----------|------------|------------|-----------|-------------|
|
||||
| NVIDIA Blackwell GPU (SM 100+) | ✅ | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ❌ |
|
||||
| NVIDIA Ada/Hopper GPU (SM 89+) | ✅ | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ❌ |
|
||||
| NVIDIA Ampere GPU (SM 80+) | ✅ | ✅ | ✅ | ⭕ | ⭕ | ⭕ | ✅ | ❌ |
|
||||
| AMD ROCm | ⭕ | ⭕ | ❌ | ⭕ | ⭕ | ✅ | ⭕ | ❌ |
|
||||
| Intel XPU | ⭕ | ⭕ | ❌ | ⭕ | ⭕ | ⭕ | ✅ | ❌ |
|
||||
| Ascend NPU | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
| Device | FP8 W8A8 | Int8 W8A8 | BitsAndBytes W4 | ModelOpt | MXFP8 W8A8 | MXFP4 W4A4 | AutoRound | msModelSlim | TorchAO FP8 weight-only |
|
||||
| -------- | ---------- | ----------- | ------------------ | ---------- | ------------ | ------------ | ----------- | ------------- | --------- |
|
||||
| NVIDIA Blackwell GPU (SM 100+) | ✅ | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ❌ | ⭕ |
|
||||
| NVIDIA Ada/Hopper GPU (SM 89+) | ✅ | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ❌ | ✅ |
|
||||
| NVIDIA Ampere GPU (SM 80+) | ✅ | ✅ | ✅ | ⭕ | ⭕ | ⭕ | ✅ | ❌ | ⭕ |
|
||||
| AMD ROCm | ⭕ | ⭕ | ❌ | ⭕ | ⭕ | ✅ | ⭕ | ❌ | ⭕ |
|
||||
| Intel XPU | ⭕ | ⭕ | ❌ | ⭕ | ⭕ | ⭕ | ✅ | ❌ | ⭕ |
|
||||
| Ascend NPU | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ⭕ |
|
||||
|
||||
Legend: `✅` supported, `❌` unsupported, `⭕` not verified in this
|
||||
guide. FP8 on Ampere may use a weight-only path where available.
|
||||
@@ -40,7 +40,7 @@ encoder, and VAE stay on the base checkpoint unless a method guide says
|
||||
otherwise.
|
||||
|
||||
| Method | Guide | Mode | Example models | Status |
|
||||
|--------|-------|------|----------------|--------|
|
||||
| -------- | ------- | ------ | ---------------- | -------- |
|
||||
| FP8 W8A8 | [FP8](fp8.md) | Online W8A8 or checkpoint FP8 | Qwen-Image; Wan2.2 is not validated | Validated for Qwen-Image family and other DiT models |
|
||||
| Int8 W8A8 | [Int8](int8.md) | Online or serialized W8A8 | Qwen-Image; Wan2.2 is not validated | Validated for Qwen-Image and Z-Image |
|
||||
| BitsAndBytes W4 | [BitsAndBytes](bitsandbytes.md) | Online W4 weight-only | Z-Image-Turbo; Qwen-Image/Wan2.2 not validated | Validated for Z-Image-Turbo |
|
||||
@@ -49,6 +49,7 @@ otherwise.
|
||||
| MXFP4 W4A4 | [MXFP4](mxfp4.md) | `mxfp4`: online single-scale only; `mxfp4_dualscale`: online or offline dual-scale (offline recommended) | Wan2.2-T2V-A14B, I2V-A14B | Ascend NPU only; validated for Wan2.2 A14B cascade models; TI2V-5B not supported; offline `mxfp4_dualscale` uses calibrated `mul_scale` for best accuracy |
|
||||
| AutoRound | [AutoRound](autoround.md) | Pre-quantized W4A16 checkpoints | FLUX.1-dev; Qwen-Image/Wan2.2 not validated | Checkpoint-driven |
|
||||
| msModelSlim | [msModelSlim](msmodelslim.md) | Pre-quantized Ascend checkpoints | Wan2.2 recipe; HunyuanImage-3.0 inference target | Ascend/NPU path |
|
||||
| TorchAO | [TorchAO](torchao.md) | Pre-quantized FP8 weight-only (W8A16) | Boogu-Image Base/Edit `-fp8` | Validated for the Boogu-Image diffusion transformer |
|
||||
|
||||
### Multi-Stage Omni/TTS Model (Qwen3-Omni, Qwen3-TTS)
|
||||
|
||||
@@ -58,13 +59,14 @@ checkpoint contains a supported `quantization_config`; the non-AR stages stay
|
||||
in BF16 unless the model guide explicitly adds support.
|
||||
|
||||
| Method | Guide | Scope | Example models | Status |
|
||||
|--------|-------|-------|----------------|--------|
|
||||
| -------- | ------- | ------- | ---------------- | -------- |
|
||||
| ModelOpt | [ModelOpt](modelopt.md) | Thinker or language-model checkpoint config | Qwen3-Omni thinker | ModelOpt checkpoint path |
|
||||
| Int8 | [Int8](int8.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
|
||||
| BitsAndBytes | [BitsAndBytes](bitsandbytes.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
|
||||
| MXFP8 | [MXFP8](mxfp8.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
|
||||
| MXFP4 | [MXFP4](mxfp4.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
|
||||
| AutoRound | [AutoRound](autoround.md) | Thinker or language-model checkpoint config | Qwen2.5-Omni, Qwen3-Omni | Supported through AutoRound checkpoints |
|
||||
| TorchAO | [TorchAO](torchao.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
|
||||
| msModelSlim | [msModelSlim](msmodelslim.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
|
||||
|
||||
### Multi-Stage Diffusion Model (BAGEL, GLM-Image)
|
||||
@@ -73,7 +75,7 @@ These models split generation across multiple stages. Quantization must be
|
||||
attached to the intended stage rather than applied globally.
|
||||
|
||||
| Method | Guide | Scope | Example models | Status |
|
||||
|--------|-------|-------|----------------|--------|
|
||||
| -------- | ------- | ------- | ---------------- | -------- |
|
||||
| FP8 | [FP8](fp8.md) | Stage-specific DiT or transformer module | BAGEL, GLM-Image | Requires model-specific validation |
|
||||
| Int8 | [Int8](int8.md) | Stage-specific DiT or transformer module | BAGEL, GLM-Image | Requires model-specific validation |
|
||||
| BitsAndBytes | [BitsAndBytes](bitsandbytes.md) | Stage-specific transformer or DiT module | BAGEL, GLM-Image | Not validated |
|
||||
@@ -81,6 +83,7 @@ attached to the intended stage rather than applied globally.
|
||||
| MXFP8 | [MXFP8](mxfp8.md) | Stage-specific DiT or transformer module | BAGEL, GLM-Image | Not validated |
|
||||
| MXFP4 | [MXFP4](mxfp4.md) | Stage-specific DiT or transformer module | BAGEL, GLM-Image | Not validated |
|
||||
| AutoRound | [AutoRound](autoround.md) | Checkpoint-defined stage | BAGEL, GLM-Image | No validated checkpoint listed |
|
||||
| TorchAO | [TorchAO](torchao.md) | Stage-specific serialized checkpoint | BAGEL, GLM-Image | Not validated |
|
||||
| msModelSlim | [msModelSlim](msmodelslim.md) | Ascend-generated stage weights | GLM-Image | Requires model-specific adaptation |
|
||||
|
||||
!!! note
|
||||
@@ -105,8 +108,8 @@ config = build_quant_config({
|
||||
```
|
||||
|
||||
| Component | Default quantized? | Notes |
|
||||
|-----------|--------------------|-------|
|
||||
| Diffusion transformer | Yes | Primary target for FP8, Int8, BitsAndBytes, ModelOpt, MXFP8, MXFP4, AutoRound, and msModelSlim |
|
||||
| ----------- | -------------------- | ------- |
|
||||
| Diffusion transformer | Yes | Primary target for FP8, Int8, BitsAndBytes, ModelOpt, MXFP8, MXFP4, AutoRound, TorchAO, and msModelSlim |
|
||||
| Text encoder | No | Keep BF16 unless a method-specific guide documents support |
|
||||
| VAE | No | Keep BF16; storage-only paths are method-specific |
|
||||
| Scheduler/tokenizer | No | Loaded from the base model repository |
|
||||
@@ -114,7 +117,7 @@ config = build_quant_config({
|
||||
### Multi-Stage Omni/TTS Model (Qwen3-Omni, Qwen3-TTS)
|
||||
|
||||
| Component | Default quantized? | Notes |
|
||||
|-----------|--------------------|-------|
|
||||
| ----------- | -------------------- | ------- |
|
||||
| Thinker or AR language model | Yes, when checkpoint config is supported | ModelOpt FP8/NVFP4 or AutoRound checkpoint config |
|
||||
| Audio encoder | No | BF16 |
|
||||
| Vision encoder | No | BF16 |
|
||||
@@ -124,7 +127,7 @@ config = build_quant_config({
|
||||
### Multi-Stage Diffusion Model (BAGEL, GLM-Image)
|
||||
|
||||
| Component | Default quantized? | Notes |
|
||||
|-----------|--------------------|-------|
|
||||
| ----------- | -------------------- | ------- |
|
||||
| Selected diffusion or transformer stage | Method-specific | Must be routed to the intended stage |
|
||||
| Other generation stages | No | Keep BF16 unless separately validated |
|
||||
| VAE, tokenizer, scheduler | No | Loaded from the base checkpoint |
|
||||
@@ -204,7 +207,7 @@ The output JSON includes `output_metrics`, `reference_generation`, and
|
||||
`candidate_generation`.
|
||||
|
||||
| Metric | Direction | Meaning |
|
||||
|--------|-----------|---------|
|
||||
| -------- | ----------- | --------- |
|
||||
| `cosine_similarity` | Higher is better | Vector direction similarity between output pixels or frames. Useful as a broad sanity check. |
|
||||
| `mae` | Lower is better | Mean absolute pixel or frame error. For decoded outputs, values are in uint8 pixel units. |
|
||||
| `mse` / `rmse` | Lower is better | Squared error and its square root. These penalize localized large differences more than `mae`. |
|
||||
@@ -217,7 +220,7 @@ The output JSON includes `output_metrics`, `reference_generation`, and
|
||||
Recommended starting thresholds for same-seed diffusion comparisons:
|
||||
|
||||
| Metric | Smoke threshold | Stricter target | Notes |
|
||||
|--------|-----------------|-----------------|-------|
|
||||
| -------- | ----------------- | ----------------- | ------- |
|
||||
| `psnr_db` | `>= 20.0` | `>= 25.0` | Good for quick image or frame regression checks. |
|
||||
| `mae` | `<= 12.0` | `<= 6.0` | Interpreted in decoded uint8 pixel units. |
|
||||
| `cosine_similarity` | `>= 0.98` | `>= 0.995` | Less sensitive to global scale than L2-style metrics. |
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# TorchAO Quantization
|
||||
|
||||
## Overview
|
||||
|
||||
[TorchAO](https://github.com/pytorch/ao) provides quantization tools for
|
||||
PyTorch and supports both pre-quantized checkpoint loading and runtime
|
||||
quantization.
|
||||
|
||||
## Hardware Support
|
||||
|
||||
| Device | Support |
|
||||
| -------- | --------- |
|
||||
| NVIDIA Blackwell GPU (SM 100+) | ⭕ |
|
||||
| NVIDIA Ada GPU (SM 89) | ✅ |
|
||||
| NVIDIA Hopper GPU (SM 90) | ⭕ |
|
||||
| NVIDIA Ampere GPU (SM 80+) | ⭕ |
|
||||
| AMD ROCm | ⭕ |
|
||||
| Intel XPU | ⭕ |
|
||||
| Ascend NPU | ⭕ |
|
||||
|
||||
Legend: `✅` supported, `❌` unsupported, `⭕` not verified in this
|
||||
guide.
|
||||
|
||||
## Model Type Support
|
||||
|
||||
### Diffusion Model (Boogu-Image)
|
||||
|
||||
| Model | Checkpoint | Scope | Scheme |
|
||||
| --------------------- | ------------------------------------------------------------------ | -------------------------- | ----------------------- |
|
||||
| Boogu-Image Base/Edit | `Boogu/Boogu-Image-0.1-Base-fp8`, `Boogu/Boogu-Image-0.1-Edit-fp8` | Diffusion transformer only | FP8 weight-only (W8A16) |
|
||||
|
||||
## Configuration
|
||||
|
||||
Using Boogu-Image as an example:
|
||||
|
||||
```bash
|
||||
vllm serve Boogu/Boogu-Image-0.1-Base-fp8 \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--diffusion-quantization-config \
|
||||
'{"transformer":{"method":"torchao_float8_weight_only"}}'
|
||||
```
|
||||
|
||||
The equivalent command using the complete serialized TorchAO configuration is:
|
||||
|
||||
```bash
|
||||
vllm serve Boogu/Boogu-Image-0.1-Base-fp8 \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--diffusion-quantization-config \
|
||||
'{
|
||||
"transformer": {
|
||||
"method": "torchao",
|
||||
"quant_type": {
|
||||
"default": {
|
||||
"_type": "Float8WeightOnlyConfig",
|
||||
"_version": 2,
|
||||
"_data": {
|
||||
"weight_dtype": {
|
||||
"_type": "torch.dtype",
|
||||
"_data": "float8_e4m3fn"
|
||||
},
|
||||
"set_inductor_config": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| ------------ | ---- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `method` | str | - | Use `torchao_float8_weight_only` for the serialized FP8 weight-only shorthand, or `torchao` with a full `quant_type` configuration. |
|
||||
| `quant_type` | dict | - | Complete serialized TorchAO configuration used with `method: "torchao"`. |
|
||||
|
||||
See TorchAO's [vLLM integration guide](https://docs.pytorch.org/ao/0.17/eager_tutorials/torchao_vllm_integration.html#configuration-system)
|
||||
for the serialized form and [workflow configs](https://docs.pytorch.org/ao/0.17/api_reference/api_ref_quantization.html#workflow-configs)
|
||||
for available configuration classes and parameters.
|
||||
|
||||
## Validation and Notes
|
||||
|
||||
At load time, vLLM-Omni builds a `TorchAOConfig` from
|
||||
`--diffusion-quantization-config`. For the Boogu-Image checkpoints documented
|
||||
here, indexed PyTorch `.bin` shards are loaded through `pt_weights_iterator`.
|
||||
|
||||
When a component map is used, only the components included in the
|
||||
configuration use TorchAO. Other components keep their own checkpoint and
|
||||
runtime settings.
|
||||
|
||||
Use either the `torchao_float8_weight_only` shorthand or the equivalent full
|
||||
`quant_type` configuration shown above. The checkpoint must already contain
|
||||
weights quantized with TorchAO.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Image-To-Video
|
||||
|
||||
This shared example generates videos from images with VACE, Wan2.2, LTX-2,
|
||||
HunyuanVideo-1.5, SANA-Video, Cosmos3, and other compatible pipelines.
|
||||
HunyuanVideo-1.5, SANA-Video, Cosmos3, MAGI-2, and other compatible pipelines.
|
||||
|
||||
- `image_to_video.py`: command-line script for single video generation with advanced options.
|
||||
|
||||
@@ -27,9 +27,14 @@ This folder provides a unified CLI script for image-to-video generation using vL
|
||||
| `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | 480 x 832 | 81 | 50 | 4.0 | Around 20–25 GiB BF16, smallest I2V model |
|
||||
| `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v` | 480 x 832 | 121 | 50 | 6.0 | Around 100 GiB at default settings; the example enables `--enable-cpu-offload` + VAE tiling/slicing to fit an 80 GiB card |
|
||||
| `Lightricks/LTX-2` | 512 x 768 | 121 | 40 | video 3.0 / audio 7.0 | Memory use depends on frame count and tensor parallelism |
|
||||
| `sand-ai/MAGI-2-preview` | 512 x 896 | 125 | 100 | Model-fixed | Native four-GPU TP/SP; resident SP4 default; DLO available |
|
||||
| `Efficient-Large-Model/SANA-Video_2B_480p_diffusers` | 480 x 832 | 81 | 50 | 6.0 | Native `SanaImageToVideoPipeline`; Wan VAE |
|
||||
| `Efficient-Large-Model/SANA-Video_2B_720p_diffusers` | 704 x 1280 | 81 | 50 | 6.0 | Native `SanaImageToVideoPipeline`; LTX-2 Video VAE |
|
||||
|
||||
MAGI-2 native Preview setup, four-GPU topology and DLO choices, I2VA commands,
|
||||
and eight-GPU validation status are documented in the
|
||||
[`MAGI-2 Preview L20X recipe`](../../../recipes/SandAI/MAGI-2-preview-L20X.md).
|
||||
|
||||
!!! info
|
||||
Peak VRAM: based on basic single-card usage, batch size = 1, without any acceleration/optimization features. Some model weights cannot fit into one card with 80 GiB VRAM, which may need to use CPU offloading.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
"""
|
||||
Image-to-Video generation example using Wan2.2 I2V/TI2V models, LTX2/LTX-2.3,
|
||||
HunyuanVideo-1.5, SANA-Video, Cosmos3, or Wan2.1 VACE.
|
||||
HunyuanVideo-1.5, SANA-Video, Cosmos3, MAGI-2, or Wan2.1 VACE.
|
||||
|
||||
Supports:
|
||||
- Wan2.2-I2V-A14B-Diffusers: MoE model with CLIP image encoder
|
||||
@@ -64,6 +64,7 @@ import numpy as np
|
||||
import PIL.Image
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.data import resolve_model_class_name
|
||||
from vllm_omni.diffusion.utils.param_utils import apply_declared_extra_args
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
@@ -75,6 +76,7 @@ from vllm_omni.model_extras import (
|
||||
from vllm_omni.model_extras import (
|
||||
get_extra_body_params,
|
||||
get_model_class_name,
|
||||
get_video_generation_defaults,
|
||||
should_preserve_reference_image_size,
|
||||
)
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
@@ -122,15 +124,15 @@ def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Generate a video from one or more images "
|
||||
"(Wan2.2, LTX2/LTX-2.3, HunyuanVideo-1.5, SANA-Video, Cosmos3, or Wan2.1 VACE)."
|
||||
"(Wan2.2, LTX2/LTX-2.3, HunyuanVideo-1.5, SANA-Video, Cosmos3, MAGI-2, or Wan2.1 VACE)."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
|
||||
help=(
|
||||
"Diffusers I2V model ID or local path "
|
||||
"(Wan2.2, LTX2/LTX-2.3, HunyuanVideo-1.5, SANA-Video, Cosmos3, or Wan2.1 VACE)."
|
||||
"I2V model ID or local path "
|
||||
"(Wan2.2, LTX2/LTX-2.3, HunyuanVideo-1.5, SANA-Video, Cosmos3, MAGI-2, or Wan2.1 VACE)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -209,7 +211,7 @@ def parse_args() -> argparse.Namespace:
|
||||
help="Diffusion KV-cache quantization skip-layer selector, e.g. '0,1,4-8'.",
|
||||
)
|
||||
parser.add_argument("--output", type=str, default="i2v_output.mp4", help="Path to save the video (mp4).")
|
||||
parser.add_argument("--fps", type=int, default=None, help="Frames per second for the output video.")
|
||||
parser.add_argument("--fps", type=float, default=None, help="Frames per second for the output video.")
|
||||
parser.add_argument(
|
||||
"--vae-use-slicing",
|
||||
action="store_true",
|
||||
@@ -230,6 +232,30 @@ def parse_args() -> argparse.Namespace:
|
||||
action="store_true",
|
||||
help="Enable layerwise (blockwise) offloading on DiT modules.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-distributed-layerwise-offload",
|
||||
action="store_true",
|
||||
help="Enable distributed layerwise offloading with overlapped host-to-device weight streaming.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dlo-use-allgather",
|
||||
dest="dlo_use_allgather",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Use shard + AllGather weight reconstruction for distributed layerwise offload (default: enabled).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dlo-no-use-allgather",
|
||||
dest="dlo_use_allgather",
|
||||
action="store_false",
|
||||
help="Stream standard-loader rank-local weights without DLO sharding or AllGather.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dlo-resident-layers",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Number of leading main-DiT blocks to keep device-resident during distributed layerwise offload.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enforce-eager",
|
||||
action="store_true",
|
||||
@@ -391,12 +417,14 @@ def main():
|
||||
generator = torch.Generator(device=current_omni_platform.device_type).manual_seed(args.seed)
|
||||
model_name = str(args.model).lower() if args.model is not None else ""
|
||||
model_class_name = args.model_class_name
|
||||
model_class_name_lower = (model_class_name or "").lower()
|
||||
resolved_model_class_name = model_class_name or resolve_model_class_name(args.model)
|
||||
model_class_name_lower = (resolved_model_class_name or "").lower()
|
||||
video_defaults = get_video_generation_defaults(resolved_model_class_name, args.extra_body)
|
||||
is_ltx2_distilled = "distilled" in model_class_name_lower or "distilled" in model_name
|
||||
is_ltx23 = "ltx23" in model_class_name_lower or "ltx-2.3" in model_name
|
||||
is_ltx2 = is_ltx2_distilled or is_ltx23 or "ltx2" in model_class_name_lower or "ltx-2" in model_name
|
||||
is_sana = model_class_name == "SanaImageToVideoPipeline" or "sana-video" in model_name
|
||||
is_cosmos = "cosmos" in model_name or (model_class_name is not None and "cosmos" in model_class_name.lower())
|
||||
is_sana = resolved_model_class_name == "SanaImageToVideoPipeline" or "sana-video" in model_name
|
||||
is_cosmos = "cosmos" in model_name or "cosmos" in model_class_name_lower
|
||||
is_cosmos_edge = is_cosmos and ("edge" in model_name or "edge" in model_class_name_lower)
|
||||
|
||||
image = PIL.Image.open(args.image).convert("RGB") if args.image else None
|
||||
@@ -411,7 +439,17 @@ def main():
|
||||
|
||||
# Per-model generation defaults, applied only when the matching flag is omitted.
|
||||
# Cosmos3 would otherwise silently inherit the Wan2.2 defaults (wrong size/steps/shift).
|
||||
if is_cosmos_edge:
|
||||
if video_defaults is not None:
|
||||
d_fps, d_guidance, d_num_frames, d_steps, d_flow_shift, d_max_area, d_mod = (
|
||||
video_defaults.fps,
|
||||
video_defaults.guidance_scale,
|
||||
video_defaults.num_frames,
|
||||
video_defaults.num_inference_steps,
|
||||
video_defaults.flow_shift,
|
||||
video_defaults.max_area,
|
||||
video_defaults.dimension_multiple,
|
||||
)
|
||||
elif is_cosmos_edge:
|
||||
# Cosmos3-Edge native defaults: 480x832, gs 5.0, flow_shift 3.0 — NOT the Nano/Super
|
||||
# 720p / gs 6.0 / flow_shift 10.0 below, which produce degenerate output on Edge.
|
||||
d_fps, d_guidance, d_num_frames, d_steps, d_flow_shift, d_max_area, d_mod = (
|
||||
@@ -477,7 +515,10 @@ def main():
|
||||
# Calculate dimensions if not provided (model-aware max area).
|
||||
height = args.height
|
||||
width = args.width
|
||||
if height is None or width is None:
|
||||
if video_defaults is not None:
|
||||
height = height or video_defaults.height
|
||||
width = width or video_defaults.width
|
||||
elif height is None or width is None:
|
||||
calc_height, calc_width = calculate_dimensions(dimension_image, max_area=d_max_area, mod_value=d_mod)
|
||||
height = height or calc_height
|
||||
width = width or calc_width
|
||||
@@ -557,6 +598,9 @@ def main():
|
||||
cache_config=cache_config,
|
||||
enable_diffusion_pipeline_profiler=args.enable_diffusion_pipeline_profiler,
|
||||
profiler_config=args.profiler_config,
|
||||
enable_distributed_layerwise_offload=args.enable_distributed_layerwise_offload,
|
||||
dlo_use_allgather=args.dlo_use_allgather,
|
||||
dlo_resident_layers=args.dlo_resident_layers,
|
||||
)
|
||||
if args.deploy_config:
|
||||
omni_kwargs["deploy_config"] = args.deploy_config
|
||||
@@ -613,9 +657,12 @@ def main():
|
||||
)
|
||||
|
||||
negative_prompt = args.negative_prompt
|
||||
if negative_prompt is None and not is_ltx2:
|
||||
# Preserve the historical empty-prompt behavior for non-LTX examples.
|
||||
negative_prompt = ""
|
||||
if negative_prompt is None:
|
||||
if video_defaults is not None:
|
||||
negative_prompt = video_defaults.default_negative_prompt
|
||||
elif not is_ltx2:
|
||||
# Preserve the historical empty-prompt behavior for non-LTX examples.
|
||||
negative_prompt = ""
|
||||
prompt_dict = build_image_to_video_prompt(
|
||||
prompt=args.prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
@@ -642,6 +689,7 @@ def main():
|
||||
boundary_ratio=args.boundary_ratio,
|
||||
num_inference_steps=num_inference_steps,
|
||||
num_frames=num_frames,
|
||||
fps=fps,
|
||||
frame_rate=frame_rate,
|
||||
extra_args=sampling_extra_args,
|
||||
)
|
||||
|
||||
@@ -121,7 +121,6 @@ def engine_kwargs(args: argparse.Namespace) -> dict[str, Any]:
|
||||
data_parallel_size=1,
|
||||
text_encoder_tp_size=args.tp_size,
|
||||
vae_patch_parallel_size=args.tp_size,
|
||||
enable_distributed_layerwise_offload=False,
|
||||
)
|
||||
return common
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Offline PersonaPlex inference through the vllm-omni fullduplex backend.
|
||||
|
||||
This is the runnable end-to-end proof for the integration: it drives a user WAV
|
||||
through :class:`PersonaPlexEngine` + :class:`PersonaPlexSession` (our wrappers
|
||||
around the Moshi/Mimi lockstep loop) and writes the agent's audio + inner-monologue
|
||||
text. It mirrors ``python -m moshi.offline`` but goes through our ``FrameStepper``
|
||||
seam, so a green run validates that the session/adapter plumbing is faithful.
|
||||
|
||||
Prereqs: HF access to ``nvidia/personaplex-7b-v1`` (``export HF_TOKEN=...``). The
|
||||
stack is moshi-free (native temporal + depformer + streaming Mimi via
|
||||
``transformers``); decoding is greedy.
|
||||
|
||||
Example:
|
||||
python examples/offline_inference/personaplex/personaplex_offline.py \\
|
||||
--input-wav input_assistant.wav --output-wav out.wav \\
|
||||
--voice-prompt NATF2.pt --persona "You are a wise and friendly teacher."
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
|
||||
from vllm_omni.experimental.fullduplex.personaplex import (
|
||||
PersonaPlexConfig,
|
||||
PersonaPlexEngine,
|
||||
PersonaPlexSession,
|
||||
)
|
||||
from vllm_omni.experimental.fullduplex.personaplex.config import DEFAULT_PERSONA
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--input-wav", required=True, help="User-side input WAV")
|
||||
parser.add_argument("--output-wav", required=True, help="Agent audio output WAV")
|
||||
parser.add_argument("--output-text", default=None, help="Optional JSON for agent text")
|
||||
parser.add_argument("--voice-prompt", default="NATF2.pt", help="Voice clone (bundled .pt or .wav)")
|
||||
parser.add_argument("--persona", default=DEFAULT_PERSONA, help="System role text")
|
||||
parser.add_argument("--hf-repo", default="nvidia/personaplex-7b-v1")
|
||||
parser.add_argument("--device", default="cuda")
|
||||
parser.add_argument("--cpu-offload", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
import sphn # local import: only the runnable driver needs the audio I/O dep
|
||||
|
||||
config = PersonaPlexConfig(
|
||||
hf_repo=args.hf_repo,
|
||||
voice_prompt=args.voice_prompt,
|
||||
persona=args.persona,
|
||||
device=args.device,
|
||||
cpu_offload=args.cpu_offload,
|
||||
)
|
||||
|
||||
engine = PersonaPlexEngine(config).load()
|
||||
session = PersonaPlexSession(engine, config)
|
||||
session.open()
|
||||
|
||||
# Load exactly like the reference (moshi.lm.load_audio): native read then
|
||||
# explicit resample, so greedy runs stay bit-faithful to `python -m moshi.offline`.
|
||||
user_pcm, src_sr = sphn.read(args.input_wav)
|
||||
user_pcm = sphn.resample(user_pcm, src_sample_rate=src_sr, dst_sample_rate=config.sample_rate)
|
||||
audio, text = session.run(np.asarray(user_pcm[0], dtype=np.float32))
|
||||
|
||||
sphn.write_wav(args.output_wav, audio, config.sample_rate)
|
||||
print(f"wrote {audio.shape[0] / config.sample_rate:.2f}s of agent audio to {args.output_wav}")
|
||||
print(f"agent text: {text!r}")
|
||||
if args.output_text:
|
||||
with open(args.output_text, "w") as f:
|
||||
json.dump({"text": text}, f, ensure_ascii=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -24,7 +24,7 @@ list of supported architectures across all modalities, see
|
||||
| OmniVoice | `k2-fsa/OmniVoice` | 2 (gen + dec) | ✓ | — | voice design, language hint | 24 kHz |
|
||||
| Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | 2 (talker + code2wav) | ✓ (Base) | ✓ | 3 task variants | 24 kHz |
|
||||
| VoxCPM2 | `openbmb/VoxCPM2` | single (native AR) | ✓ | ✓ (online) | continuation | 48 kHz |
|
||||
| dots.tts | `rednote-hilab/dots.tts-soar` | single (native AR) | — (not wired yet) | — | — | 48 kHz |
|
||||
| dots.tts | `dots-studio/dots.tts-soar` | single (native AR) | — (text-only) | ✓ (online) | no-reference synthesis | 48 kHz |
|
||||
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | 2 (AR talker + S2Mel DiT + BigVGAN) | ✓ (required) | — | emotion control (`--emo-audio`, `--emo-text`, `--emo-vector`) | 22.05 kHz |
|
||||
| IndexTTS-2.5 | native `checkpoints/` bundle | 2 (AR talker + EnhancedCodec + S2Mel DiT + BigVGAN) | ✓ (required) | — | multilingual (`--lang`) + emotion control | 22.05 kHz |
|
||||
| Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | varies | ✓ | ✓ | voice presets | 24 kHz |
|
||||
@@ -490,20 +490,20 @@ Single-stage native AR TTS at 48 kHz (rednote-hilab). Pipeline: `Qwen2.5-1.5B ba
|
||||
### Quick start
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/dots_tts/end2end.py \
|
||||
--model rednote-hilab/dots.tts-soar \
|
||||
--model dots-studio/dots.tts-soar \
|
||||
--text "Hello, this is a test of dots TTS running on vLLM Omni."
|
||||
```
|
||||
|
||||
### Voice cloning
|
||||
Not wired in this release — generation is zero-shot only. The CAM++ x-vector speaker encoder weights load, but `end2end.py` has no `--ref-audio`/`--ref-text` flags yet.
|
||||
Not wired in this release. Generation is text-only and does not consume reference audio, a named voice, or a precomputed speaker embedding. The CAM++ x-vector speaker encoder weights load, but `end2end.py` has no `--ref-audio`/`--ref-text` flags yet.
|
||||
|
||||
### Streaming
|
||||
The AudioVAE decoder has an internal streaming path (`init_stream_state` / `stream_step` / `stream_flush`) used to avoid boundary artifacts between 160 ms patches, but it is not yet exposed through an online serving endpoint or example.
|
||||
The AudioVAE decoder emits incremental audio through its internal streaming path (`init_stream_state` / `stream_step` / `stream_flush`). Online streaming is exposed through the OpenAI-compatible `/v1/audio/speech` endpoint with `stream=true`; see the [online serving guide](../../online_serving/text_to_speech/README.md#dotstts). The offline `end2end.py` script returns the consolidated waveform.
|
||||
|
||||
### Notes
|
||||
- Output: 48 kHz mono WAV.
|
||||
- Deploy config: `vllm_omni/deploy/dots_tts.yaml` (auto-loaded by HF `model_type`).
|
||||
- Checkpoints: `rednote-hilab/dots.tts-soar` is the validated default. `dots.tts-base` shares the same architecture but is unvalidated in this repo. `dots.tts-mf` (MeanFlow, 2-4 step) is not supported yet.
|
||||
- Checkpoints: `dots-studio/dots.tts-soar` is the validated default. `dots.tts-base` shares the same architecture but is unvalidated in this repo. `dots.tts-mf` (MeanFlow, 2-4 step) is not supported yet.
|
||||
- Known limitation: no CUDA graph capture and no batched side-path computation yet, so concurrent requests do not currently scale (each request's DiT Euler steps run serially). See `recipes/rednote-hilab/dots.tts.md` for details and the roadmap.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Offline dots.tts inference example (single-stage native AR pipeline).
|
||||
|
||||
Uses the single-stage native AR config (vllm_omni/deploy/dots_tts.yaml).
|
||||
Reference checkpoint: rednote-hilab/dots.tts-soar.
|
||||
Reference checkpoint: dots-studio/dots.tts-soar.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -25,7 +25,7 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
default="rednote-hilab/dots.tts-soar",
|
||||
default="dots-studio/dots.tts-soar",
|
||||
help="dots.tts model path or HuggingFace repo ID.",
|
||||
)
|
||||
parser.add_argument(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Offline higgs-audio v3 inference example.
|
||||
|
||||
Runs Stage 0 (Qwen3 talker) + Stage 1 (HiggsAudio codec) end-to-end
|
||||
@@ -171,11 +171,13 @@ def main():
|
||||
num_ref_tokens=int(ref_codes_delayed.shape[0]),
|
||||
reference_text=args.ref_text,
|
||||
)
|
||||
prompt_ids, audio_placeholder_positions = adapter.prepare_prompt_for_engine(prompt_ids)
|
||||
prompt = {
|
||||
"prompt_token_ids": prompt_ids,
|
||||
"additional_information": {
|
||||
"audio_input_ids": ref_codes_delayed.to(torch.long),
|
||||
"audio_input_ids_mask": torch.ones(ref_codes_delayed.shape[0], dtype=torch.bool),
|
||||
"audio_placeholder_positions": audio_placeholder_positions,
|
||||
},
|
||||
}
|
||||
else:
|
||||
|
||||
@@ -14,9 +14,14 @@ A unified script for text-to-video generation. Supports multiple models with mod
|
||||
| `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | 720x1280 | 121 | 50 | 6.0 | FP8 + VAE tiling required |
|
||||
| `nvidia/Cosmos3-Nano` | 720x1280 | 189 | 35 | 6.0 | ~46 GiB (peak, 720p) |
|
||||
| `BestWishYsh/Helios-Base` / `Helios-Mid` / `Helios-Distilled` | 384x640 | 99 | 50 | 5.0 / 5.0 / 1.0 | — |
|
||||
| `sand-ai/MAGI-2-preview` | 512x896 | 125 | 100 | Model-fixed | Native four-GPU TP/SP; resident SP4 default; DLO available |
|
||||
| `Efficient-Large-Model/SANA-Video_2B_480p_diffusers` | 480x832 | 81 | 50 | 6.0 | BF16 DiT + FP32 Wan VAE wrapper |
|
||||
| `Efficient-Large-Model/SANA-Video_2B_720p_diffusers` | 704x1280 | 81 | 50 | 6.0 | BF16 DiT + LTX-2 Video VAE wrapper |
|
||||
|
||||
MAGI-2 native Preview setup, four-GPU topology and DLO choices, request
|
||||
constraints, and eight-GPU validation status are documented in the
|
||||
[`MAGI-2 Preview L20X recipe`](../../../recipes/SandAI/MAGI-2-preview-L20X.md).
|
||||
|
||||
## Local CLI Usage
|
||||
|
||||
### Wan2.2 (default)
|
||||
@@ -257,13 +262,14 @@ python text_to_video.py \
|
||||
- `--num-inference-steps`: sampling steps. Default depends on model.
|
||||
- `--fps`: frames per second for the saved MP4.
|
||||
- `--output`: path to save the generated video.
|
||||
- `--extra-body`: JSON dict of model-specific knobs (declared in `vllm_omni/model_extras/`), merged into sampling `extra_args`. See the Helios recipes above.
|
||||
- `--extra-body`: JSON object of model-specific generation knobs, filtered against the model's declared `extra_body_params` and merged into sampling `extra_args` (see the Helios and Cosmos3 examples above and [`vllm_omni/model_extras`](../../../vllm_omni/model_extras)).
|
||||
- `--vae-use-slicing`: enable VAE slicing for memory optimization.
|
||||
- `--vae-use-tiling`: enable VAE tiling for memory optimization.
|
||||
- `--cfg-parallel-size`: set it to 2 to enable CFG Parallel. See more examples in [`user_guide`](../../../docs/user_guide/diffusion/parallelism_acceleration.md#cfg-parallel).
|
||||
- `--tensor-parallel-size`: tensor parallel size (effective for models that support TP, e.g. LTX2).
|
||||
- `--enable-cpu-offload`: enable CPU offloading for diffusion models.
|
||||
- `--enable-layerwise-offload`: enable layerwise offloading on DiT modules.
|
||||
- `--diffusion-offload-config`: component-selective offload JSON; do not combine it with the legacy flags above.
|
||||
- `--frame-rate`: generation FPS for pipelines that require it (e.g., LTX2).
|
||||
- `--audio-sample-rate`: fallback audio sample rate when the pipeline returns audio.
|
||||
- `--quantization`: quantization method (such as `fp8` for FP8).
|
||||
@@ -271,7 +277,6 @@ python text_to_video.py \
|
||||
- `--lora-path`: path to PEFT LoRA adapter folder or checkpoint file.
|
||||
- `--lora-scale`: scale factor for LoRA weights.
|
||||
- `--lora-backend`: backend for loading LoRA adapters. Default: peft. Available options: peft, distill.
|
||||
- `--extra-body`: JSON object of model-specific generation params, filtered against the model's declared `extra_body_params` (see [`vllm_omni/model_extras`](../../../vllm_omni/model_extras)). Used by Cosmos3 (see above).
|
||||
|
||||
### Wan2.2-specific
|
||||
|
||||
@@ -291,4 +296,6 @@ python text_to_video.py \
|
||||
| 720p I2V | 7.0 | 6.0 | 50 |
|
||||
| CFG-distilled | (same) | 1.0 | 50 |
|
||||
|
||||
> If you encounter OOM errors, try `--vae-use-slicing`, `--vae-use-tiling`, `--enable-cpu-offload`, or `--quantization fp8`.
|
||||
> If you encounter OOM errors, try `--vae-use-slicing`, `--vae-use-tiling`,
|
||||
> module/layer offload through `--diffusion-offload-config`, or
|
||||
> `--quantization fp8`.
|
||||
@@ -10,12 +10,18 @@ from typing import Any
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.data import resolve_model_class_name
|
||||
from vllm_omni.diffusion.utils.param_utils import apply_declared_extra_args
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.lora.request import LoRARequest
|
||||
from vllm_omni.lora.utils import stable_lora_int_id
|
||||
from vllm_omni.model_extras import get_extra_body_params, get_model_class_name, get_output_tensor_range
|
||||
from vllm_omni.model_extras import (
|
||||
get_extra_body_params,
|
||||
get_model_class_name,
|
||||
get_output_tensor_range,
|
||||
get_video_generation_defaults,
|
||||
)
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
@@ -135,9 +141,17 @@ _MODEL_PRESETS = {
|
||||
}
|
||||
|
||||
|
||||
def _detect_preset(model: str, model_class_name: str | None = None) -> dict:
|
||||
def _detect_preset(
|
||||
model: str,
|
||||
model_class_name: str | None = None,
|
||||
extra_body: dict[str, Any] | None = None,
|
||||
) -> dict:
|
||||
model_lower = model.lower()
|
||||
class_lower = (model_class_name or "").lower()
|
||||
resolved_model_class_name = model_class_name or resolve_model_class_name(model)
|
||||
class_lower = (resolved_model_class_name or "").lower()
|
||||
video_defaults = get_video_generation_defaults(resolved_model_class_name, extra_body)
|
||||
if video_defaults is not None:
|
||||
return video_defaults.cli_defaults()
|
||||
if "sana-video" in model_lower or "sana_video" in model_lower or "sanavideo" in class_lower:
|
||||
return _MODEL_PRESETS["sana_720p" if "720p" in model_lower else "sana_480p"]
|
||||
if "lingbot" in model_lower or "lingbotvideo" in class_lower:
|
||||
@@ -221,7 +235,8 @@ def parse_extra_body(value: str) -> dict[str, Any]:
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate a video from a text prompt. "
|
||||
"Supports Wan2.2, HunyuanVideo-1.5, Helios, LingBot-Video, and other text-to-video models."
|
||||
"Supports Wan2.2, HunyuanVideo-1.5, Helios, LingBot-Video, MAGI-2, "
|
||||
"and other text-to-video models."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
@@ -288,7 +303,7 @@ def parse_args() -> argparse.Namespace:
|
||||
type=str,
|
||||
default=None,
|
||||
choices=["cache_dit"],
|
||||
help="Cache backend for acceleration (Wan2.2). Default: None.",
|
||||
help="Cache backend for supported diffusion pipelines. Default: None.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-cache-dit-summary",
|
||||
@@ -296,7 +311,7 @@ def parse_args() -> argparse.Namespace:
|
||||
help="Enable cache-dit summary logging after diffusion forward passes.",
|
||||
)
|
||||
parser.add_argument("--output", type=str, default=None, help="Output path (mp4). Default: model-specific.")
|
||||
parser.add_argument("--fps", type=int, default=None, help="Frames per second for the output video.")
|
||||
parser.add_argument("--fps", type=float, default=None, help="Frames per second for the output video.")
|
||||
parser.add_argument(
|
||||
"--vae-use-slicing",
|
||||
action="store_true",
|
||||
@@ -322,6 +337,36 @@ def parse_args() -> argparse.Namespace:
|
||||
action="store_true",
|
||||
help="Enable layerwise (blockwise) offloading on DiT modules.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diffusion-offload-config",
|
||||
type=json.loads,
|
||||
default=None,
|
||||
help="Component-selective diffusion offload config as JSON.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-distributed-layerwise-offload",
|
||||
action="store_true",
|
||||
help="Enable distributed layerwise offloading with overlapped host-to-device weight streaming.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dlo-use-allgather",
|
||||
dest="dlo_use_allgather",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Use shard + AllGather weight reconstruction for distributed layerwise offload (default: enabled).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dlo-no-use-allgather",
|
||||
dest="dlo_use_allgather",
|
||||
action="store_false",
|
||||
help="Stream standard-loader rank-local weights without DLO sharding or AllGather.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dlo-resident-layers",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Number of leading main-DiT blocks to keep device-resident during distributed layerwise offload.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--audio-sample-rate",
|
||||
type=int,
|
||||
@@ -428,7 +473,7 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--hsdp-shard-size",
|
||||
type=int,
|
||||
default=1,
|
||||
default=-1,
|
||||
help="Number of GPUs to shard weights across for HSDP.",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -464,15 +509,17 @@ def _extract_peak_memory_mb(result: Any) -> float:
|
||||
def main():
|
||||
args = parse_args()
|
||||
model_class_name = args.model_class_name
|
||||
resolved_model_class_name = model_class_name or resolve_model_class_name(args.model)
|
||||
video_defaults = get_video_generation_defaults(resolved_model_class_name, args.extra_body)
|
||||
|
||||
preset = _detect_preset(args.model, model_class_name)
|
||||
preset = _detect_preset(args.model, resolved_model_class_name, args.extra_body)
|
||||
for key, default_val in preset.items():
|
||||
if getattr(args, key.replace("-", "_"), None) is None:
|
||||
setattr(args, key.replace("-", "_"), default_val)
|
||||
model_class_name = args.model_class_name
|
||||
|
||||
generator = torch.Generator(device=current_omni_platform.device_type).manual_seed(args.seed)
|
||||
# Cache-dit config (Wan2.2 only)
|
||||
# Shared Cache-DiT defaults for pipelines that declare an adapter.
|
||||
cache_config = None
|
||||
if args.cache_backend == "cache_dit":
|
||||
cache_config = {
|
||||
@@ -493,6 +540,7 @@ def main():
|
||||
omni_kwargs = dict(
|
||||
model=args.model,
|
||||
enable_layerwise_offload=args.enable_layerwise_offload,
|
||||
diffusion_offload_config=args.diffusion_offload_config,
|
||||
vae_use_slicing=args.vae_use_slicing,
|
||||
vae_use_tiling=args.vae_use_tiling,
|
||||
enable_cpu_offload=args.enable_cpu_offload,
|
||||
@@ -501,6 +549,9 @@ def main():
|
||||
cfg_parallel_size=args.cfg_parallel_size,
|
||||
tensor_parallel_size=args.tensor_parallel_size,
|
||||
vae_patch_parallel_size=args.vae_patch_parallel_size,
|
||||
use_hsdp=args.use_hsdp,
|
||||
hsdp_shard_size=args.hsdp_shard_size,
|
||||
hsdp_replicate_size=args.hsdp_replicate_size,
|
||||
pipeline_parallel_size=args.pipeline_parallel_size,
|
||||
enable_expert_parallel=args.enable_expert_parallel,
|
||||
enforce_eager=args.enforce_eager,
|
||||
@@ -509,6 +560,9 @@ def main():
|
||||
cache_config=cache_config,
|
||||
enable_diffusion_pipeline_profiler=args.enable_diffusion_pipeline_profiler,
|
||||
profiler_config=args.profiler_config,
|
||||
enable_distributed_layerwise_offload=args.enable_distributed_layerwise_offload,
|
||||
dlo_use_allgather=args.dlo_use_allgather,
|
||||
dlo_resident_layers=args.dlo_resident_layers,
|
||||
)
|
||||
if args.deploy_config:
|
||||
omni_kwargs["deploy_config"] = args.deploy_config
|
||||
@@ -528,6 +582,10 @@ def main():
|
||||
lora_path = lora_path[0]
|
||||
omni_kwargs["lora_path"] = lora_path
|
||||
omni_kwargs["lora_backend"] = args.lora_backend
|
||||
if args.use_hsdp:
|
||||
omni_kwargs["use_hsdp"] = args.use_hsdp
|
||||
omni_kwargs["hsdp_shard_size"] = args.hsdp_shard_size
|
||||
omni_kwargs["hsdp_replicate_size"] = args.hsdp_replicate_size
|
||||
|
||||
# Cosmos3 loads its (gated) guardrail models at build time, so the guardrails
|
||||
# gate is an engine-level config (offline analog of the server's --no-guardrails).
|
||||
@@ -572,6 +630,8 @@ def main():
|
||||
)
|
||||
|
||||
negative_prompt = args.negative_prompt
|
||||
if negative_prompt is None and video_defaults is not None:
|
||||
negative_prompt = video_defaults.default_negative_prompt
|
||||
if negative_prompt is None and all(preset is not _MODEL_PRESETS[name] for name in ("lingbot", "ltx2", "ltx23")):
|
||||
# Preserve the historical empty-prompt behavior for non-LTX examples.
|
||||
negative_prompt = ""
|
||||
@@ -591,7 +651,10 @@ def main():
|
||||
num_frames=args.num_frames,
|
||||
extra_args=extra_args,
|
||||
)
|
||||
if args.frame_rate is not None:
|
||||
if video_defaults is not None:
|
||||
sampling_kwargs["fps"] = args.fps
|
||||
sampling_kwargs["frame_rate"] = args.frame_rate or args.fps
|
||||
elif args.frame_rate is not None:
|
||||
sampling_kwargs["frame_rate"] = args.frame_rate
|
||||
if args.guidance_scale_high is not None:
|
||||
sampling_kwargs["guidance_scale_2"] = args.guidance_scale_high
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Full-duplex use case: talk over the assistant (barge-in).
|
||||
|
||||
Scenario (see ``barge_in_client_flow.md`` for the diagram)
|
||||
---------------------------------------------------------------
|
||||
1. The user asks a question (``--question-wav``); the model answers in speech.
|
||||
2. Mid-answer, the user starts talking again (``--interrupt-wav``) — a
|
||||
follow-up or correction — while assistant audio is still streaming.
|
||||
3. The model handles the overlap: depending on the session overlap policy and
|
||||
speech length, the in-flight response is barged in (cancelled) or the
|
||||
commit is deferred to the turn end, and a new response answers the
|
||||
interruption. Either way the user never waits for the first answer.
|
||||
|
||||
The demo is model-neutral: it drives any duplex model through the public
|
||||
:class:`vllm_omni.clients.duplex.DuplexClient` over
|
||||
``/v1/realtime?duplex=1``. Model-specific session shape comes from the
|
||||
per-model presets under ``vllm_omni.clients.<model>`` selected with
|
||||
``--preset`` (default ``minicpmo_4_5``, whose model decides listen/speak on
|
||||
its own — no VAD).
|
||||
|
||||
Run the server first (for MiniCPM-o 4.5: ``bash minicpmo/barge_in_serve.sh``, which
|
||||
starts the ``vllm_omni/deploy/minicpmo_4_5.yaml`` profile with ``session_mode: duplex``),
|
||||
then:
|
||||
|
||||
python barge_in_client.py \
|
||||
--url ws://127.0.0.1:8099/v1/realtime \
|
||||
--model openbmb/MiniCPM-o-4_5 \
|
||||
--ref-audio /path/to/reference_voice.wav \
|
||||
--question-wav question_16k.wav \
|
||||
--interrupt-wav follow_up_16k.wav \
|
||||
--output-dir ./duplex_out
|
||||
|
||||
Input WAVs must be mono 16 kHz PCM16; when a preset's session takes a
|
||||
different capture format (the PersonaPlex preset streams 24 kHz float32),
|
||||
the audio is converted before streaming. ``--ref-audio`` is required by the
|
||||
MiniCPM-o preset (without it the session is rejected with
|
||||
``ref_audio_required``). Outputs land in ``--output-dir`` as one WAV per
|
||||
response — ``response_1_cancelled.wav`` for a barged-in answer,
|
||||
``response_1_completed.wav`` for one that finished — plus ``summary.json``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from vllm_omni.clients.duplex import ( # noqa: E402
|
||||
AudioFormat,
|
||||
DuplexClient,
|
||||
EventCollector,
|
||||
SessionConfig,
|
||||
acknowledge_collected_playback,
|
||||
audio_data_url,
|
||||
read_pcm16_wav,
|
||||
wait_for_condition,
|
||||
write_pcm16_wav,
|
||||
)
|
||||
|
||||
_TRANSCRIPT_DELTA_EVENTS = {
|
||||
"response.audio_transcript.delta",
|
||||
"response.output_text.delta",
|
||||
"response.text.delta",
|
||||
}
|
||||
|
||||
# Input WAVs are mono 16 kHz PCM16; each preset's session may negotiate a
|
||||
# different capture format (PersonaPlex takes 24 kHz float32) and a different
|
||||
# model-unit-aligned chunk size (PersonaPlex frames are exactly 80 ms).
|
||||
_PRESET_DEFAULT_CHUNK_MS = {"minicpmo_4_5": 100, "personaplex": 80, "none": 100}
|
||||
|
||||
|
||||
def _convert_to_session_format(pcm16_16k: bytes, target: AudioFormat) -> bytes:
|
||||
"""Convert mono 16 kHz PCM16 to the session's input format."""
|
||||
if target.encoding == "pcm16" and target.sample_rate_hz == 16_000:
|
||||
return pcm16_16k
|
||||
import numpy as np
|
||||
|
||||
samples = np.frombuffer(pcm16_16k, dtype=np.int16).astype(np.float32) / 32768.0
|
||||
if target.sample_rate_hz != 16_000 and len(samples) > 1:
|
||||
target_len = round(len(samples) * target.sample_rate_hz / 16_000)
|
||||
positions = np.linspace(0.0, len(samples) - 1, num=target_len)
|
||||
samples = np.interp(positions, np.arange(len(samples)), samples).astype(np.float32)
|
||||
if target.encoding == "pcm_f32le":
|
||||
return samples.tobytes()
|
||||
if target.encoding == "pcm16":
|
||||
return (np.clip(samples, -1.0, 1.0) * 32767.0).astype(np.int16).tobytes()
|
||||
raise ValueError(f"Unsupported session input encoding: {target.encoding!r}")
|
||||
|
||||
|
||||
def _session_config(preset: str, *, ref_audio_path: str | None) -> SessionConfig:
|
||||
ref_audio = audio_data_url(Path(ref_audio_path).expanduser()) if ref_audio_path else None
|
||||
if preset == "minicpmo_4_5":
|
||||
from vllm_omni.clients.minicpmo_4_5 import create_duplex_session_config
|
||||
|
||||
return create_duplex_session_config(ref_audio=ref_audio)
|
||||
if preset == "personaplex":
|
||||
from vllm_omni.clients.personaplex import create_duplex_session_config
|
||||
|
||||
return create_duplex_session_config()
|
||||
return SessionConfig(ref_audio=ref_audio)
|
||||
|
||||
|
||||
def _fold_responses(collector: EventCollector) -> dict[str, dict[str, object]]:
|
||||
"""Fold the event stream into per-response audio, text, and status."""
|
||||
out: dict[str, dict[str, object]] = {}
|
||||
for response_id in collector.response_ids:
|
||||
out[response_id] = {
|
||||
"audio": collector.audio_bytes(response_id),
|
||||
"text": "",
|
||||
"status": "in_progress",
|
||||
"sample_rate_hz": collector.output_sample_rate_hz,
|
||||
}
|
||||
|
||||
for event in collector.events:
|
||||
etype = event.get("type")
|
||||
response_id = collector.response_id(event)
|
||||
item = out.get(response_id) if isinstance(response_id, str) else None
|
||||
if etype in _TRANSCRIPT_DELTA_EVENTS and item is not None:
|
||||
delta = event.get("delta")
|
||||
if isinstance(delta, str):
|
||||
item["text"] += delta
|
||||
elif etype == "response.done" and item is not None:
|
||||
response = event.get("response")
|
||||
status = response.get("status") if isinstance(response, dict) else None
|
||||
if item["status"] == "in_progress":
|
||||
item["status"] = "cancelled" if status == "cancelled" else "completed"
|
||||
elif etype == "audio.cancelled":
|
||||
targets = [item] if item is not None else list(out.values())
|
||||
for target in targets:
|
||||
if target["status"] == "in_progress":
|
||||
target["status"] = "cancelled"
|
||||
return out
|
||||
|
||||
|
||||
async def run(args: argparse.Namespace) -> int:
|
||||
config = _session_config(args.preset, ref_audio_path=args.ref_audio)
|
||||
question = _convert_to_session_format(read_pcm16_wav(Path(args.question_wav)), config.input_audio)
|
||||
interrupt = _convert_to_session_format(read_pcm16_wav(Path(args.interrupt_wav)), config.input_audio)
|
||||
chunk_ms = args.chunk_ms if args.chunk_ms is not None else _PRESET_DEFAULT_CHUNK_MS.get(args.preset, 100)
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
collector = EventCollector()
|
||||
client = DuplexClient(
|
||||
args.url,
|
||||
model=args.model,
|
||||
config=config,
|
||||
reconnect=None,
|
||||
heartbeat_interval_s=None,
|
||||
handshake_timeout_s=args.timeout_s,
|
||||
)
|
||||
# GREET: open the voice session (handshake happens on enter).
|
||||
async with client:
|
||||
consume_task = asyncio.create_task(collector.consume(client))
|
||||
|
||||
# ASK: stream the question at realtime pace, then commit the turn.
|
||||
# The model decides on its own to answer (no VAD).
|
||||
print("[1] streaming question ...", file=sys.stderr)
|
||||
await client.stream_pcm(question, chunk_ms=chunk_ms)
|
||||
await client.commit(create_response=False)
|
||||
await wait_for_condition(
|
||||
lambda: collector.count("response.created") > 0,
|
||||
timeout_s=args.timeout_s,
|
||||
label="first response.created",
|
||||
)
|
||||
print("[1] assistant is answering", file=sys.stderr)
|
||||
|
||||
# Let the answer play for a moment, as a listening user would.
|
||||
await asyncio.sleep(args.listen_s)
|
||||
|
||||
# INTERRUPT: talk over the assistant while its audio still streams.
|
||||
# The serving overlap policy decides: enough overlapped speech barges
|
||||
# in (the first response is cancelled) or a short remark is deferred
|
||||
# until the model yields the turn.
|
||||
print("[2] interrupting mid-answer ...", file=sys.stderr)
|
||||
ids_before_interrupt = set(collector.response_ids)
|
||||
await client.stream_pcm(interrupt, chunk_ms=chunk_ms)
|
||||
await client.commit(create_response=False)
|
||||
|
||||
# ANSWER AGAIN: wait for a response created *after* the interruption
|
||||
# to finish. The first answer may still be streaming here; matching on
|
||||
# any response.done would let its completion satisfy the wait and cut
|
||||
# off the actual follow-up answer at close.
|
||||
def follow_up_answer_done() -> bool:
|
||||
return any(
|
||||
event.get("type") == "response.done"
|
||||
and collector.response_id(event) is not None
|
||||
and collector.response_id(event) not in ids_before_interrupt
|
||||
for event in collector.events
|
||||
)
|
||||
|
||||
await wait_for_condition(
|
||||
follow_up_answer_done,
|
||||
timeout_s=args.timeout_s,
|
||||
label="response.done for the response answering the interruption",
|
||||
)
|
||||
|
||||
# HANG UP: report playback, close the session cleanly.
|
||||
await acknowledge_collected_playback(client, collector)
|
||||
await client.close(timeout_s=args.timeout_s)
|
||||
try:
|
||||
await asyncio.wait_for(consume_task, timeout=5.0)
|
||||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||||
consume_task.cancel()
|
||||
|
||||
# Report: one WAV per response; statuses show what the overlap did.
|
||||
responses = _fold_responses(collector)
|
||||
summary: list[dict[str, object]] = []
|
||||
for index, (response_id, item) in enumerate(responses.items(), start=1):
|
||||
audio = item["audio"]
|
||||
assert isinstance(audio, bytes)
|
||||
sample_rate_hz = int(item["sample_rate_hz"]) # type: ignore[arg-type]
|
||||
wav_path = output_dir / f"response_{index}_{item['status']}.wav"
|
||||
if audio:
|
||||
write_pcm16_wav(wav_path, audio, sample_rate_hz=sample_rate_hz)
|
||||
summary.append(
|
||||
{
|
||||
"response_id": response_id,
|
||||
"status": item["status"],
|
||||
"audio_s": round(len(audio) / (sample_rate_hz * 2), 2),
|
||||
"text": item["text"],
|
||||
"wav": wav_path.name if audio else None,
|
||||
}
|
||||
)
|
||||
(output_dir / "summary.json").write_text(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||
(output_dir / "events.jsonl").write_text(
|
||||
"".join(json.dumps(event, ensure_ascii=False) + "\n" for event in collector.events),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||
cancelled = sum(1 for item in summary if item["status"] == "cancelled")
|
||||
print(
|
||||
f"\n{len(summary)} responses ({cancelled} interrupted); outputs in {output_dir}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--url", default="ws://127.0.0.1:8099/v1/realtime")
|
||||
parser.add_argument("--model", default="openbmb/MiniCPM-o-4_5")
|
||||
parser.add_argument(
|
||||
"--preset",
|
||||
choices=["minicpmo_4_5", "personaplex", "none"],
|
||||
default="minicpmo_4_5",
|
||||
help="per-model session preset from vllm_omni.clients.<model>",
|
||||
)
|
||||
parser.add_argument("--ref-audio", help="reference voice WAV (the MiniCPM-o preset requires it)")
|
||||
parser.add_argument("--question-wav", required=True, help="mono 16 kHz PCM16 question")
|
||||
parser.add_argument("--interrupt-wav", required=True, help="mono 16 kHz PCM16 follow-up spoken mid-answer")
|
||||
parser.add_argument("--output-dir", default="./duplex_out")
|
||||
parser.add_argument(
|
||||
"--chunk-ms",
|
||||
type=int,
|
||||
default=None,
|
||||
help="append chunk size; defaults to the preset's model-unit-aligned size (minicpmo_4_5: 100, personaplex: 80)",
|
||||
)
|
||||
parser.add_argument("--listen-s", type=float, default=2.0, help="how long to listen before interrupting")
|
||||
parser.add_argument("--timeout-s", type=float, default=120.0)
|
||||
return asyncio.run(run(parser.parse_args()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,54 @@
|
||||
# `barge_in_client.py` — what happens, at a glance
|
||||
|
||||
The demo plays one everyday conversation pattern against the full-duplex
|
||||
server: ask something, get talked-over mid-answer, and see the assistant
|
||||
cope. High-level flow below — for the exact wire events, read the
|
||||
docstring in `barge_in_client.py`.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as user (client script)
|
||||
participant A as assistant (server + model)
|
||||
|
||||
Note over U,A: GREET
|
||||
U->>A: start a voice session (with the reference voice)
|
||||
A-->>U: ready
|
||||
|
||||
Note over U,A: ASK
|
||||
U->>A: ask a question (speak the question WAV)
|
||||
Note right of A: decides on its own to answer<br/>(no VAD — the model chooses)
|
||||
A-->>U: starts answering aloud
|
||||
|
||||
Note over U,A: INTERRUPT
|
||||
Note over U: listens for ~2 seconds
|
||||
par both talk at once
|
||||
U->>A: interrupts with a follow-up
|
||||
and
|
||||
A-->>U: still speaking the first answer
|
||||
end
|
||||
|
||||
alt the interruption is substantial
|
||||
Note right of A: stops mid-sentence (barge-in),<br/>first answer is cut off
|
||||
else it was just a short remark
|
||||
Note right of A: finishes the first answer,<br/>then takes the follow-up
|
||||
end
|
||||
|
||||
Note over U,A: ANSWER AGAIN
|
||||
A-->>U: answers the follow-up aloud
|
||||
|
||||
Note over U,A: HANG UP
|
||||
U->>A: goodbye (close the session)
|
||||
Note over U: saves one recording per answer<br/>plus a summary of what happened
|
||||
```
|
||||
|
||||
Outcome: the output directory shows which branch ran — a cut-off first
|
||||
answer is saved as `response_1_cancelled.wav`, a completed one as
|
||||
`response_1_completed.wav`, and `summary.json` records status, text, and
|
||||
duration per answer.
|
||||
|
||||
Companion: the runtime architecture lives in
|
||||
[`docs/design/fullduplex.md`](../../docs/design/fullduplex.md); the client
|
||||
API is `vllm_omni.clients.duplex.DuplexClient`, documented with the wire
|
||||
protocol in
|
||||
[`docs/serving/realtime_duplex_api.md`](../../docs/serving/realtime_duplex_api.md).
|
||||
@@ -13,6 +13,7 @@ implemented with clear Diffusers or vLLM-Omni behavior.
|
||||
- Sequence parallel execution
|
||||
- TeaCache / Cache-DiT acceleration
|
||||
- Step-wise execution (continuous batching)
|
||||
- Component-selective `diffusion_offload_config` (the legacy whole-pipeline CPU-offload options remain supported)
|
||||
|
||||
For these features, it is recommended to use natively supported pipelines instead.
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ This directory contains MiniCPM-o 4.5 online serving demos for vLLM-Omni.
|
||||
Inputs can include text, image, audio, or video; outputs are text and optional
|
||||
24 kHz speech.
|
||||
|
||||
For the experimental native duplex runtime architecture, lifecycle invariants,
|
||||
For the native duplex runtime architecture, lifecycle invariants,
|
||||
capability boundary, and validation scope, see
|
||||
[`vllm_omni/experimental/fullduplex/DESIGN.md`](../../../vllm_omni/experimental/fullduplex/DESIGN.md).
|
||||
[`docs/design/fullduplex.md`](../../../docs/design/fullduplex.md); for the
|
||||
`DuplexClient` API and the `/v1/realtime?duplex=1` wire protocol, see
|
||||
[`docs/serving/realtime_duplex_api.md`](../../../docs/serving/realtime_duplex_api.md).
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -231,7 +233,7 @@ Start the duplex generation server:
|
||||
|
||||
```bash
|
||||
vllm serve openbmb/MiniCPM-o-4_5 --omni --trust-remote-code \
|
||||
--deploy-config vllm_omni/deploy/minicpmo_4_5_duplex.yaml \
|
||||
--deploy-config vllm_omni/deploy/minicpmo_4_5.yaml \
|
||||
--served-model-name openbmb/MiniCPM-o-4_5 \
|
||||
--host 0.0.0.0 --port 8099
|
||||
```
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
# Server side for barge_in_client.py: start MiniCPM-o 4.5 in native
|
||||
# full-duplex mode.
|
||||
#
|
||||
# The duplex deploy profile registers the WS /v1/duplex endpoint (with
|
||||
# /v1/realtime?duplex=1 projecting onto the same handler), enables the
|
||||
# engine duplex control plane, and bounds the Thinker/Talker/Code2Wav
|
||||
# stages to four live duplex sessions on one GPU. See
|
||||
# vllm_omni/deploy/minicpmo_4_5.yaml (session_mode: duplex) for the session
|
||||
# limits; every other duplex_session knob keeps its runtime default
|
||||
# (idle TTL 300s, disconnect grace 30s, 16MiB pending input per session).
|
||||
set -euo pipefail
|
||||
|
||||
MODEL="${MODEL:-openbmb/MiniCPM-o-4_5}"
|
||||
PORT="${PORT:-8099}"
|
||||
|
||||
exec vllm-omni serve "$MODEL" \
|
||||
--omni \
|
||||
--deploy-config vllm_omni/deploy/minicpmo_4_5.yaml \
|
||||
--trust-remote-code \
|
||||
--host 0.0.0.0 --port "$PORT"
|
||||
# duplex endpoints: ws://<host>:$PORT/v1/duplex and /v1/realtime?duplex=1
|
||||
@@ -23,23 +23,26 @@ REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from vllm_omni.experimental.fullduplex.client import ( # noqa: E402
|
||||
from vllm_omni.clients.duplex import ( # noqa: E402
|
||||
PCM16_BYTES_PER_SAMPLE,
|
||||
PCM16_SAMPLE_RATE,
|
||||
RealtimeDuplexClient,
|
||||
RealtimeEventCollector,
|
||||
build_realtime_url,
|
||||
DuplexClient,
|
||||
EventCollector,
|
||||
acknowledge_collected_playback,
|
||||
read_pcm16_wav,
|
||||
wait_for,
|
||||
wait_for_condition,
|
||||
write_pcm16_wav,
|
||||
)
|
||||
from vllm_omni.experimental.fullduplex.client import chunk_period_ms as _chunk_period_ms # noqa: E402
|
||||
from vllm_omni.experimental.fullduplex.client import ( # noqa: E402
|
||||
from vllm_omni.clients.duplex import chunk_period_ms as _chunk_period_ms # noqa: E402
|
||||
from vllm_omni.clients.duplex import ( # noqa: E402
|
||||
has_residual_model_unit as _has_residual_model_unit,
|
||||
)
|
||||
from vllm_omni.experimental.fullduplex.client import ( # noqa: E402
|
||||
from vllm_omni.clients.duplex import ( # noqa: E402
|
||||
reference_audio_data_url as _ref_audio_data_url,
|
||||
)
|
||||
from vllm_omni.clients.minicpmo_4_5 import ( # noqa: E402
|
||||
create_duplex_session_config,
|
||||
)
|
||||
from vllm_omni.experimental.fullduplex.video_stacking import ( # noqa: E402
|
||||
concat_frames_b64,
|
||||
unit_subframe_offsets,
|
||||
@@ -96,7 +99,7 @@ class _StreamingOutputWriter:
|
||||
)
|
||||
|
||||
|
||||
class _StreamingEventCollector(RealtimeEventCollector):
|
||||
class _StreamingEventCollector(EventCollector):
|
||||
def __init__(self, writer: _StreamingOutputWriter) -> None:
|
||||
super().__init__()
|
||||
self._writer = writer
|
||||
@@ -155,7 +158,7 @@ def _response_in_progress(events: list[dict[str, object]]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
async def _commit_input_after_playback_checkpoint(client: RealtimeDuplexClient) -> float:
|
||||
async def _commit_input_after_playback_checkpoint(client: DuplexClient, collector: EventCollector) -> float:
|
||||
"""Commit input only after preserving already-played assistant history.
|
||||
|
||||
Native duplex can finish several responses while the user is still
|
||||
@@ -166,7 +169,7 @@ async def _commit_input_after_playback_checkpoint(client: RealtimeDuplexClient)
|
||||
original history position. A second acknowledgement after the commit can
|
||||
then safely advance response-local playback that drained in the meantime.
|
||||
"""
|
||||
await client.acknowledge_playback()
|
||||
await acknowledge_collected_playback(client, collector)
|
||||
commit_sent_at_s = time.monotonic()
|
||||
await client.commit()
|
||||
return commit_sent_at_s
|
||||
@@ -434,87 +437,95 @@ async def run_demo(args: argparse.Namespace) -> dict[str, object]:
|
||||
if not input_pcm16:
|
||||
raise ValueError("input WAV has no audio")
|
||||
|
||||
url = build_realtime_url(
|
||||
collector = _StreamingEventCollector(stream_writer)
|
||||
client = DuplexClient(
|
||||
args.url,
|
||||
args.model,
|
||||
autostart=False if args.ref_audio else None,
|
||||
session_id=args.session_id,
|
||||
)
|
||||
client = RealtimeDuplexClient(url)
|
||||
client.events = _StreamingEventCollector(stream_writer)
|
||||
async with client:
|
||||
await client.configure(
|
||||
args.model,
|
||||
model=args.model,
|
||||
config=create_duplex_session_config(
|
||||
ref_audio=_ref_audio_data_url(args.ref_audio),
|
||||
session_id=args.session_id,
|
||||
temperature=args.temperature,
|
||||
timeout_s=args.timeout_s,
|
||||
)
|
||||
stream_event_cursor = len(client.events.events)
|
||||
frames_sent = await client.stream_pcm16(
|
||||
),
|
||||
session_id=args.session_id,
|
||||
reconnect=None,
|
||||
heartbeat_interval_s=None,
|
||||
handshake_timeout_s=args.timeout_s,
|
||||
)
|
||||
async with client:
|
||||
consume_task = asyncio.create_task(collector.consume(client))
|
||||
# The handshake consumed session.created before the collector
|
||||
# subscribed; seed it so counts and capability lookups still work.
|
||||
collector.add({"type": "session.created", "session": client.session_info})
|
||||
stream_event_cursor = len(collector.events)
|
||||
frames_sent = await client.stream_pcm(
|
||||
input_pcm16,
|
||||
chunk_ms=args.chunk_ms,
|
||||
realtime=not args.no_realtime_pacing,
|
||||
video_frames=video_frames,
|
||||
stacked_video_frames=stacked_video_frames,
|
||||
)
|
||||
commit_event_cursor = len(client.events.events)
|
||||
stream_decision = _latest_model_decision(client.events.events, stream_event_cursor)
|
||||
commit_event_cursor = len(collector.events)
|
||||
stream_decision = _latest_model_decision(collector.events, stream_event_cursor)
|
||||
input_has_residual_model_unit = _has_residual_model_unit(
|
||||
input_pcm16,
|
||||
chunk_period_ms=_chunk_period_ms(client.events.events),
|
||||
chunk_period_ms=_chunk_period_ms(collector.events),
|
||||
)
|
||||
wait_for_post_commit_decision = False
|
||||
commit_sent_at_s = await _commit_input_after_playback_checkpoint(client)
|
||||
commit_sent_at_s = await _commit_input_after_playback_checkpoint(client, collector)
|
||||
wait_error: str | None = None
|
||||
committed_index: int | None = None
|
||||
post_commit_decision: str | None = None
|
||||
try:
|
||||
await wait_for(
|
||||
lambda: _input_committed_index(client.events.events, commit_event_cursor) is not None,
|
||||
await wait_for_condition(
|
||||
lambda: _input_committed_index(collector.events, commit_event_cursor) is not None,
|
||||
timeout_s=args.timeout_s,
|
||||
label="input_audio_buffer.committed",
|
||||
)
|
||||
committed_index = _input_committed_index(client.events.events, commit_event_cursor)
|
||||
stream_decision = _latest_model_decision(client.events.events[: committed_index + 1], stream_event_cursor)
|
||||
committed_index = _input_committed_index(collector.events, commit_event_cursor)
|
||||
stream_decision = _latest_model_decision(collector.events[: committed_index + 1], stream_event_cursor)
|
||||
wait_for_post_commit_decision = input_has_residual_model_unit or _response_in_progress(
|
||||
client.events.events[: committed_index + 1]
|
||||
collector.events[: committed_index + 1]
|
||||
)
|
||||
if wait_for_post_commit_decision:
|
||||
await wait_for(
|
||||
lambda: _post_commit_model_decision(client.events.events, committed_index) is not None,
|
||||
await wait_for_condition(
|
||||
lambda: _post_commit_model_decision(collector.events, committed_index) is not None,
|
||||
timeout_s=args.timeout_s,
|
||||
label="post-commit model decision or response drain",
|
||||
)
|
||||
post_commit_decision = _post_commit_model_decision(client.events.events, committed_index)
|
||||
post_commit_decision = _post_commit_model_decision(collector.events, committed_index)
|
||||
except TimeoutError as exc:
|
||||
wait_error = str(exc)
|
||||
await client.acknowledge_playback()
|
||||
await acknowledge_collected_playback(client, collector)
|
||||
close_error: str | None = None
|
||||
await client.close(timeout_s=args.timeout_s)
|
||||
# close() returns when the client's reader observed session.closed;
|
||||
# the collector is a separate subscriber task, so wait for it to
|
||||
# drain before checking, or a normal close reads as a failure.
|
||||
try:
|
||||
await client.close_session(timeout_s=args.timeout_s)
|
||||
except TimeoutError as exc:
|
||||
close_error = str(exc)
|
||||
await asyncio.wait_for(consume_task, timeout=5.0)
|
||||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||||
consume_task.cancel()
|
||||
if collector.count("session.closed") == 0:
|
||||
close_error = "Timed out waiting for session.closed"
|
||||
|
||||
audio = client.events.audio_bytes()
|
||||
first_text_at_s = client.events.first_received_at(
|
||||
audio = collector.audio_bytes()
|
||||
first_text_at_s = collector.first_received_at(
|
||||
"response.audio_transcript.delta",
|
||||
"response.output_text.delta",
|
||||
after_s=commit_sent_at_s,
|
||||
)
|
||||
first_audio_at_s = client.events.first_received_at(
|
||||
first_audio_at_s = collector.first_received_at(
|
||||
"response.audio.delta",
|
||||
after_s=commit_sent_at_s,
|
||||
)
|
||||
response_created_at_s = client.events.first_received_at(
|
||||
response_created_at_s = collector.first_received_at(
|
||||
"response.created",
|
||||
after_s=commit_sent_at_s,
|
||||
)
|
||||
response_done_at_s = client.events.first_received_at(
|
||||
response_done_at_s = collector.first_received_at(
|
||||
"response.done",
|
||||
after_s=commit_sent_at_s,
|
||||
)
|
||||
audio_duration_s = len(audio) / (client.events.output_sample_rate_hz * 2)
|
||||
audio_duration_s = len(audio) / (collector.output_sample_rate_hz * 2)
|
||||
response_generation_s = (
|
||||
response_done_at_s - response_created_at_s
|
||||
if response_done_at_s is not None and response_created_at_s is not None
|
||||
@@ -522,27 +533,27 @@ async def run_demo(args: argparse.Namespace) -> dict[str, object]:
|
||||
)
|
||||
transcript_deltas = [
|
||||
str(event.get("delta", ""))
|
||||
for event in client.events.events
|
||||
for event in collector.events
|
||||
if event.get("type")
|
||||
in {
|
||||
"response.audio_transcript.delta",
|
||||
"response.output_text.delta",
|
||||
}
|
||||
]
|
||||
response_id = client.events.response_ids[0] if client.events.response_ids else None
|
||||
timing = client.events.timing_summary(
|
||||
response_id = collector.response_ids[0] if collector.response_ids else None
|
||||
timing = collector.timing_summary(
|
||||
after_s=commit_sent_at_s,
|
||||
input_committed_at_s=commit_sent_at_s,
|
||||
response_id=response_id,
|
||||
)
|
||||
errors = client.events.errors()
|
||||
errors = collector.errors()
|
||||
if wait_error:
|
||||
errors.append({"type": "client.timeout", "message": wait_error})
|
||||
if close_error:
|
||||
errors.append({"type": "client.timeout", "message": close_error})
|
||||
model_decision = post_commit_decision or stream_decision
|
||||
(output_dir / "events.jsonl").write_text(
|
||||
"".join(json.dumps(event, ensure_ascii=False) + "\n" for event in client.events.events),
|
||||
"".join(json.dumps(event, ensure_ascii=False) + "\n" for event in collector.events),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(output_dir / "output.pcm").write_bytes(audio)
|
||||
@@ -550,13 +561,13 @@ async def run_demo(args: argparse.Namespace) -> dict[str, object]:
|
||||
write_pcm16_wav(
|
||||
output_dir / "output.wav",
|
||||
audio,
|
||||
sample_rate_hz=client.events.output_sample_rate_hz,
|
||||
sample_rate_hz=collector.output_sample_rate_hz,
|
||||
)
|
||||
|
||||
result = {
|
||||
"ok": (
|
||||
client.events.count("session.created") > 0
|
||||
and client.events.count("session.closed") > 0
|
||||
collector.count("session.created") > 0
|
||||
and collector.count("session.closed") > 0
|
||||
and not errors
|
||||
and model_decision is not None
|
||||
and (bool(audio) or not args.require_audio)
|
||||
@@ -568,12 +579,12 @@ async def run_demo(args: argparse.Namespace) -> dict[str, object]:
|
||||
"decision_required": wait_for_post_commit_decision,
|
||||
"input_had_residual_model_unit": input_has_residual_model_unit,
|
||||
"response_listen_count": _event_count_after(
|
||||
client.events.events,
|
||||
collector.events,
|
||||
"response.listen",
|
||||
committed_index,
|
||||
),
|
||||
"response_done_count": _event_count_after(
|
||||
client.events.events,
|
||||
collector.events,
|
||||
"response.done",
|
||||
committed_index,
|
||||
),
|
||||
@@ -585,7 +596,7 @@ async def run_demo(args: argparse.Namespace) -> dict[str, object]:
|
||||
"audio_bytes": len(audio),
|
||||
"audio_chunk_count": len(stream_writer.audio_chunk_paths),
|
||||
"audio_chunk_files": [str(path) for path in stream_writer.audio_chunk_paths],
|
||||
"output_sample_rate_hz": client.events.output_sample_rate_hz,
|
||||
"output_sample_rate_hz": collector.output_sample_rate_hz,
|
||||
"latency": {
|
||||
"ttft_ms": (
|
||||
round((first_text_at_s - commit_sent_at_s) * 1000, 2) if first_text_at_s is not None else None
|
||||
@@ -611,7 +622,7 @@ async def run_demo(args: argparse.Namespace) -> dict[str, object]:
|
||||
"measurement_origin": "input_audio_buffer.commit send",
|
||||
},
|
||||
"timing": timing,
|
||||
"response_ids": client.events.response_ids,
|
||||
"response_ids": collector.response_ids,
|
||||
"transcript": "".join(transcript_deltas),
|
||||
"errors": errors,
|
||||
"output_dir": str(output_dir),
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
url.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
url.searchParams.set('duplex', '1');
|
||||
url.searchParams.set('model', config.model || 'openbmb/MiniCPM-o-4_5');
|
||||
url.searchParams.set('minicpmo45_native_duplex', '1');
|
||||
url.searchParams.set('native_duplex', '1');
|
||||
url.searchParams.set('autostart', '0');
|
||||
return url.toString();
|
||||
}
|
||||
@@ -458,7 +458,7 @@
|
||||
settled = true;
|
||||
const extraBody = {
|
||||
auto_response: true,
|
||||
minicpmo45_native_duplex: true,
|
||||
native_duplex: true,
|
||||
};
|
||||
const session = {
|
||||
modalities: ['audio', 'text'],
|
||||
|
||||
@@ -1,47 +1,40 @@
|
||||
# PersonaPlex full-duplex online serving
|
||||
|
||||
Serve [`nvidia/personaplex-7b-v1`](https://huggingface.co/nvidia/personaplex-7b-v1)
|
||||
(a Moshi-based full-duplex speech-to-speech model) with the native vLLM-Omni engine.
|
||||
(a Moshi-based full-duplex speech-to-speech model) with the native vLLM-Omni engine
|
||||
through the unified duplex serving stack (`/v1/duplex` and `/v1/realtime?duplex=1`).
|
||||
|
||||
The server hosts the **official PersonaPlex web client** at `/` (auto-downloaded from
|
||||
the model repo) and implements its WebSocket protocol at `/api/chat`, so you talk to the
|
||||
model live in the browser — mic in, agent speech + inner-monologue text out.
|
||||
|
||||
> Experimental. Requires a GPU and Hugging Face access to the gated repo
|
||||
> Requires a GPU and Hugging Face access to the gated repo
|
||||
> (`HF_TOKEN` with access to `nvidia/personaplex-7b-v1`).
|
||||
|
||||
## Start the server
|
||||
|
||||
```bash
|
||||
HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 bash run_server.sh
|
||||
# or directly:
|
||||
HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 python -m \
|
||||
vllm_omni.experimental.fullduplex.personaplex.serving.server --port 8124
|
||||
```
|
||||
|
||||
## Talk to it
|
||||
|
||||
- **Browser (recommended):** open `http://localhost:8124/` and allow the microphone.
|
||||
Use headphones so the agent does not hear itself.
|
||||
- **Headless:** stream a 24 kHz mono WAV and save the reply:
|
||||
|
||||
```bash
|
||||
python duplex_client.py --url ws://localhost:8124/api/chat --input user.wav --out reply.wav
|
||||
```
|
||||
|
||||
## Unified Realtime API
|
||||
|
||||
The default `vllm_omni/deploy/personaplex.yaml` also enables main's engine-owned
|
||||
full-duplex control plane:
|
||||
The default `vllm_omni/deploy/personaplex.yaml` enables the engine-owned
|
||||
full-duplex control plane (`session_mode: duplex`):
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 python -m vllm_omni.entrypoints.cli.main serve \
|
||||
HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 python -m vllm_omni.entrypoints.cli.main serve \
|
||||
/path/to/personaplex-7b-v1 \
|
||||
--omni \
|
||||
--deploy-config vllm_omni/deploy/personaplex.yaml
|
||||
```
|
||||
|
||||
Validate the actual `/v1/realtime?duplex=1` scheduler path with paced 24 kHz
|
||||
This exposes:
|
||||
|
||||
- `WS /v1/duplex` — the native duplex session dialect
|
||||
(`session.create` / `input_audio_buffer.append` / `response.output_audio.delta` ...);
|
||||
- `WS /v1/realtime?duplex=1` — the same sessions projected onto the
|
||||
OpenAI Realtime event vocabulary (client API and wire protocol:
|
||||
[`docs/serving/realtime_duplex_api.md`](../../../docs/serving/realtime_duplex_api.md)).
|
||||
|
||||
PersonaPlex is a pure-lockstep model: every session on a PersonaPlex deployment is
|
||||
native duplex (`is_enabled()` is unconditionally true), audio flows continuously in
|
||||
both directions, and there are no client commits or external turn signals
|
||||
(`supports_client_commit=false`, `supports_external_turn_signal=false`).
|
||||
|
||||
## Validate the serving path
|
||||
|
||||
Validate the `/v1/realtime?duplex=1` scheduler path with paced 24 kHz
|
||||
PCM, two concurrent sessions, overflow admission, per-session slot recycling,
|
||||
and non-silent output:
|
||||
|
||||
@@ -52,33 +45,19 @@ python tests/e2e/online_serving/personaplex_realtime_duplex.py \
|
||||
--output-dir /tmp/personaplex-realtime-duplex
|
||||
```
|
||||
|
||||
This is distinct from the standalone `/api/chat` compatibility server above.
|
||||
The unified endpoint advertises `supports_barge_in=false`: overlapping speech
|
||||
is native model behavior, but destructive output interruption and model-state
|
||||
rewind have not been validated for PersonaPlex.
|
||||
|
||||
## Protocol (`/api/chat`)
|
||||
|
||||
Binary WebSocket messages, first byte is a tag (identical to `moshi.server`):
|
||||
|
||||
| Direction | Tag | Payload |
|
||||
| --- | --- | --- |
|
||||
| server → client | `\x00` | ready handshake (after the system-prompt prefill) |
|
||||
| client → server | `\x01` | Opus-encoded mic audio (24 kHz) |
|
||||
| server → client | `\x01` | Opus-encoded agent audio |
|
||||
| server → client | `\x02` | UTF-8 inner-monologue text |
|
||||
|
||||
Connect with query params `text_prompt` (persona) and `voice_prompt` (voice file).
|
||||
A raw-PCM endpoint (`/v1/audio/duplex`, JSON `open` + float32 frames) is also available
|
||||
for clients that do not want an Opus dependency.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Run the client near the server.** Real-time 80 ms audio is sensitive to network
|
||||
latency/jitter; over a high-latency remote link playback can stutter regardless of
|
||||
engine speed. On localhost it is smooth.
|
||||
- Greedy decoding by default. One conversation per server by default (a second
|
||||
concurrent connection is rejected with WS close 1013 until the first ends);
|
||||
pass `--batch-size N` to host N concurrent conversations on one engine
|
||||
(elastic batching; connections beyond N are rejected until a slot frees).
|
||||
- **Run the client near the server.** Real-time 80 ms frame audio is sensitive to
|
||||
network latency/jitter; over a high-latency remote link playback can stutter
|
||||
regardless of engine speed. On localhost it is smooth.
|
||||
- The earlier standalone Moshi-web compatibility server (browser client at `/`,
|
||||
binary WS protocol at `/api/chat`, raw-PCM `/v1/audio/duplex`) was demo-only
|
||||
and has been removed; use the unified endpoints above.
|
||||
- Session config (voice / persona / sampling) is passed per session via
|
||||
`extra_body`; see
|
||||
`vllm_omni/model_executor/models/personaplex/duplex/serving_adapter.py`.
|
||||
- Full runbook: [`recipes/NVIDIA/PersonaPlex.md`](../../../recipes/NVIDIA/PersonaPlex.md).
|
||||
@@ -1,114 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Headless client for the PersonaPlex duplex server (official Moshi protocol).
|
||||
|
||||
Streams a 24 kHz mono WAV to ``/api/chat`` exactly as the browser client does
|
||||
(Opus over WebSocket, tags ``\\x00`` ready / ``\\x01`` audio / ``\\x02`` text),
|
||||
collects the agent's reply audio + inner-monologue text, and writes a WAV.
|
||||
|
||||
The browser UI at ``http://<host>:<port>/`` is the primary demo; this script is
|
||||
for headless testing / scripting.
|
||||
|
||||
python duplex_client.py --url ws://localhost:8124/api/chat \
|
||||
--input user.wav --out reply.wav
|
||||
|
||||
Deps: ``websockets``, ``sphn``, ``soundfile``, ``numpy``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import sphn
|
||||
|
||||
SR = 24000 # PersonaPlex / Mimi codec rate
|
||||
|
||||
|
||||
async def converse(url: str, pcm: np.ndarray) -> tuple[np.ndarray, str]:
|
||||
import websockets
|
||||
|
||||
reader = sphn.OpusStreamReader(SR) # decode agent audio
|
||||
writer = sphn.OpusStreamWriter(SR) # encode our mic audio
|
||||
text: list[str] = []
|
||||
ready = asyncio.Event()
|
||||
last = {"t": 0.0}
|
||||
|
||||
async with websockets.connect(url, max_size=None) as ws:
|
||||
|
||||
async def receive() -> None:
|
||||
loop = asyncio.get_event_loop()
|
||||
while True:
|
||||
try:
|
||||
msg = await ws.recv()
|
||||
except Exception:
|
||||
return
|
||||
if not isinstance(msg, (bytes, bytearray)) or not msg:
|
||||
continue
|
||||
tag, payload = msg[0], msg[1:]
|
||||
if tag == 0:
|
||||
ready.set()
|
||||
elif tag == 1:
|
||||
reader.append_bytes(bytes(payload))
|
||||
last["t"] = loop.time()
|
||||
elif tag == 2:
|
||||
text.append(payload.decode("utf8", errors="replace"))
|
||||
|
||||
recv_task = asyncio.create_task(receive())
|
||||
await asyncio.wait_for(ready.wait(), timeout=120) # waits for system-prompt prefill
|
||||
|
||||
frame = 1920 # 80 ms
|
||||
for i in range(0, len(pcm), frame):
|
||||
chunk = pcm[i : i + frame]
|
||||
if len(chunk) < frame:
|
||||
chunk = np.concatenate([chunk, np.zeros(frame - len(chunk), dtype=np.float32)])
|
||||
writer.append_pcm(chunk)
|
||||
data = writer.read_bytes()
|
||||
if data:
|
||||
await ws.send(b"\x01" + data)
|
||||
await asyncio.sleep(frame / SR) # pace at real time
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
last["t"] = loop.time()
|
||||
while loop.time() - last["t"] < 2.5: # drain until the agent stops talking
|
||||
await asyncio.sleep(0.1)
|
||||
recv_task.cancel()
|
||||
|
||||
chunks: list[np.ndarray] = []
|
||||
while True:
|
||||
out = reader.read_pcm()
|
||||
if out is None or out.shape[-1] == 0:
|
||||
break
|
||||
chunks.append(np.asarray(out, dtype=np.float32))
|
||||
audio = np.concatenate(chunks) if chunks else np.zeros(0, dtype=np.float32)
|
||||
return audio, "".join(text)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--url", default="ws://localhost:8124/api/chat")
|
||||
ap.add_argument("--input", required=True, help="24 kHz mono WAV of the user turn")
|
||||
ap.add_argument("--out", default="personaplex_reply.wav")
|
||||
ap.add_argument("--tail", type=float, default=10.0, help="trailing silence (s) for the agent reply")
|
||||
ap.add_argument("--persona", default="")
|
||||
ap.add_argument("--voice", default="NATF2.pt")
|
||||
args = ap.parse_args()
|
||||
|
||||
wav, sr = sf.read(args.input, dtype="float32")
|
||||
if wav.ndim > 1:
|
||||
wav = wav.mean(axis=1)
|
||||
if sr != SR:
|
||||
raise SystemExit(f"input must be {SR} Hz mono; got {sr} Hz. Resample it first.")
|
||||
wav = np.concatenate([wav, np.zeros(int(args.tail * SR), dtype=np.float32)])
|
||||
|
||||
url = f"{args.url}?text_prompt={args.persona}&voice_prompt={args.voice}"
|
||||
audio, text = asyncio.run(converse(url, wav))
|
||||
sf.write(args.out, audio if audio.size else np.zeros(SR, dtype=np.float32), SR)
|
||||
print(f"agent reply: {audio.size / SR:.2f}s -> {args.out}")
|
||||
print(f"inner-monologue: {text.strip()!r}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
# PersonaPlex full-duplex online serving.
|
||||
#
|
||||
# Serves the OFFICIAL PersonaPlex web client at / (auto-downloaded from
|
||||
# nvidia/personaplex-7b-v1) and the Moshi WebSocket protocol at /api/chat,
|
||||
# backed by the native vLLM-Omni engine. Requires HF access to the gated repo.
|
||||
|
||||
HOST="${HOST:-0.0.0.0}"
|
||||
PORT="${PORT:-8124}"
|
||||
|
||||
echo "Starting PersonaPlex duplex server on ${HOST}:${PORT}..."
|
||||
echo "Open http://localhost:${PORT}/ for the official web client (run it near the server)."
|
||||
|
||||
python -m vllm_omni.experimental.fullduplex.personaplex.serving.server \
|
||||
--host "$HOST" --port "$PORT"
|
||||
@@ -24,6 +24,7 @@ For the full list of supported architectures across all modalities, see
|
||||
| OmniVoice | `k2-fsa/OmniVoice` | ✓ | — | — | — |
|
||||
| Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | ✓ (Base) | ✓ (PCM + WebSocket) | ✓ (presets + `/v1/audio/voices` upload) | ✓ (standard + FastRTC) |
|
||||
| VoxCPM2 | `openbmb/VoxCPM2` | ✓ | ✓ (AudioWorklet via gradio) | — | ✓ |
|
||||
| dots.tts | `dots-studio/dots.tts-soar` | — (text-only) | ✓ (PCM stream) | — (default placeholder only) | — |
|
||||
| Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✓ (gated upstream) | ✓ | ✓ (presets) | ✓ |
|
||||
|
||||
CosyVoice3 is intentionally absent: no online example exists for it yet. See its [offline section](../../offline_inference/text_to_speech/README.md#cosyvoice3) instead.
|
||||
@@ -99,6 +100,44 @@ For full request-shape documentation (all parameters, response formats, error co
|
||||
|
||||
---
|
||||
|
||||
## dots.tts
|
||||
|
||||
Single-stage, text-only TTS at 48 kHz. The current integration supports
|
||||
no-reference synthesis; voice cloning, reference audio, named voices, and
|
||||
precomputed speaker embeddings are not supported yet.
|
||||
|
||||
### Launch
|
||||
|
||||
```bash
|
||||
vllm serve dots-studio/dots.tts-soar --omni --trust-remote-code --port 8091
|
||||
```
|
||||
|
||||
### Sending requests
|
||||
|
||||
```bash
|
||||
# Non-streaming WAV
|
||||
curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"input": "Hello, this is a dots TTS online-serving test.",
|
||||
"voice": "default",
|
||||
"response_format": "wav"
|
||||
}' --output output.wav
|
||||
|
||||
# Streaming 48 kHz mono PCM
|
||||
curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"input": "Hello, this is a streaming dots TTS test.",
|
||||
"voice": "default",
|
||||
"stream": true,
|
||||
"stream_format": "audio",
|
||||
"response_format": "pcm"
|
||||
}' --no-buffer | play -t raw -r 48000 -e signed -b 16 -c 1 -
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GLM-TTS
|
||||
|
||||
2-stage TTS (AR + DiT flow-matching) at 24 kHz. Every request requires `ref_audio` + `ref_text`.
|
||||
|
||||
+3
-1
@@ -83,7 +83,9 @@ plugins:
|
||||
design/feature/diffusion_request_level_batching.md: design/feature/diffusion_continuous_batching.md
|
||||
design/feature/diffusion_step_execution.md: design/feature/diffusion_continuous_batching.md
|
||||
design/feature/cpu_offloading.md: design/feature/offloader/README.md
|
||||
design/feature/model_level_offload.md: design/feature/offloader/cpu_offload.md
|
||||
user_guide/diffusion/offloader/cpu_offload.md: user_guide/diffusion/offloader/module_offload.md
|
||||
design/feature/model_level_offload.md: design/feature/offloader/module_offload.md
|
||||
design/feature/offloader/cpu_offload.md: design/feature/offloader/module_offload.md
|
||||
design/feature/layerwise_offload.md: design/feature/offloader/layerwise_offload.md
|
||||
design/feature/distributed_layerwise_offload.md: design/feature/offloader/distributed_layerwise_offload.md
|
||||
- glightbox
|
||||
|
||||
@@ -351,6 +351,8 @@ extend-ignore-re = [
|
||||
]
|
||||
[tool.typos.default.extend-words]
|
||||
ue = "ue"
|
||||
# Einstein-summation index tuple for head/expert/dimension axes.
|
||||
hed = "hed"
|
||||
semantics = "semantics"
|
||||
fullset = "fullset"
|
||||
Vai = "Vai"
|
||||
|
||||
@@ -34,6 +34,7 @@ parameters through the pipeline-declared `extra_args` contract.
|
||||
[`examples/online_serving/image_to_image/openai_chat_client.py`](../../examples/online_serving/image_to_image/openai_chat_client.py)
|
||||
- Default deploy configs:
|
||||
[`vllm_omni/deploy/bagel.yaml`](../../vllm_omni/deploy/bagel.yaml),
|
||||
[`vllm_omni/deploy/bagel_think.yaml`](../../vllm_omni/deploy/bagel_think.yaml),
|
||||
[`vllm_omni/deploy/bagel_single_stage.yaml`](../../vllm_omni/deploy/bagel_single_stage.yaml)
|
||||
|
||||
## Hardware Support
|
||||
@@ -119,6 +120,12 @@ python examples/offline_inference/image_to_image/image_edit.py \
|
||||
The `--extra-args` JSON forwards BAGEL-specific parameters (e.g. `cfg_text_scale`,
|
||||
`cfg_img_scale`, `cfg_interval`, `cfg_renorm_type`) into
|
||||
`OmniDiffusionSamplingParams.extra_args` via the model-extras registry.
|
||||
For BAGEL img2img, the pipeline derives the generated image size from the input
|
||||
image: it preserves the input aspect ratio, aligns dimensions to the latent
|
||||
stride, and applies the checkpoint size limit. Explicit `--height` and
|
||||
`--width` values do not override that BAGEL-specific resize policy. Step-mode
|
||||
admission resolves this effective size before batching so differently shaped
|
||||
requests are scheduled separately.
|
||||
|
||||
Run text-to-text with the shared understanding example. BAGEL's default
|
||||
`bagel.yaml` deploy config is discovered from the checkpoint, so no model-specific
|
||||
@@ -156,6 +163,100 @@ vllm serve ByteDance-Seed/BAGEL-7B-MoT \
|
||||
--deploy-config vllm_omni/deploy/bagel_single_stage.yaml
|
||||
```
|
||||
|
||||
#### Step Execution and Continuous Batching
|
||||
|
||||
BAGEL supports step execution for image generation with all three in-repo
|
||||
deploy configs:
|
||||
|
||||
- `bagel.yaml`: the flag applies to the Stage 1 diffusion engine; Stage 0
|
||||
remains on the normal autoregressive path.
|
||||
- `bagel_think.yaml`: the flag also applies only to the Stage 1 diffusion
|
||||
engine; Thinker decoding in Stage 0 is unchanged.
|
||||
- `bagel_single_stage.yaml`: image requests use step execution in Stage 0.
|
||||
Explicit text-output requests keep the existing complete-request path, so
|
||||
`text2text` and `img2text` behavior is unchanged.
|
||||
|
||||
Start with one active request when validating correctness. For example, the
|
||||
following command uses the single-stage topology:
|
||||
|
||||
```bash
|
||||
vllm serve ByteDance-Seed/BAGEL-7B-MoT \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--deploy-config vllm_omni/deploy/bagel_single_stage.yaml \
|
||||
--step-execution
|
||||
```
|
||||
|
||||
To let compatible requests share denoising waves, override the diffusion-stage
|
||||
capacity. The deploy files set `max_num_seqs: 1` for their diffusion stages, so
|
||||
the per-stage value takes precedence over the global `--max-num-seqs` flag.
|
||||
Use Stage 0 for `bagel_single_stage.yaml`:
|
||||
|
||||
```bash
|
||||
vllm serve ByteDance-Seed/BAGEL-7B-MoT \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--deploy-config vllm_omni/deploy/bagel_single_stage.yaml \
|
||||
--step-execution \
|
||||
--stage-overrides '{"0":{"max_num_seqs":4}}'
|
||||
```
|
||||
|
||||
For either two-stage config, use Stage 1 instead:
|
||||
|
||||
```bash
|
||||
vllm serve ByteDance-Seed/BAGEL-7B-MoT \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--deploy-config vllm_omni/deploy/bagel.yaml \
|
||||
--step-execution \
|
||||
--stage-overrides '{"1":{"max_num_seqs":4}}'
|
||||
```
|
||||
|
||||
Use at least two inference steps for image requests. BAGEL builds
|
||||
`num_inference_steps - 1` scheduler updates, so a one-step image request has no
|
||||
denoising iteration to schedule. The scheduler automatically separates
|
||||
requests with incompatible image shapes or BAGEL CFG/renormalization settings.
|
||||
|
||||
Step execution currently does not support BAGEL sequence parallelism or a
|
||||
diffusion cache backend. Do not combine it with BAGEL SP, TeaCache, or
|
||||
Cache-DiT. Tensor parallelism and CFG parallelism are separate features. For
|
||||
best utilization, match the CFG parallel world size to the number of active
|
||||
BAGEL CFG branches. A larger world size is supported, but extra ranks run dummy
|
||||
forward passes to participate in collectives and their outputs are ignored.
|
||||
BAGEL currently does not support two CFG ranks when three CFG branches are
|
||||
active.
|
||||
|
||||
For a matched throughput comparison, start the first server with the default
|
||||
stage capacity of one and run:
|
||||
|
||||
```bash
|
||||
python3 benchmarks/diffusion/diffusion_benchmark_serving.py \
|
||||
--base-url http://localhost:8091 \
|
||||
--endpoint /v1/chat/completions \
|
||||
--model ByteDance-Seed/BAGEL-7B-MoT \
|
||||
--task t2i \
|
||||
--dataset random \
|
||||
--num-prompts 20 \
|
||||
--max-concurrency 4 \
|
||||
--width 512 \
|
||||
--height 512 \
|
||||
--num-inference-steps 20 \
|
||||
--seed 42 \
|
||||
--warmup-requests 4 \
|
||||
--warmup-concurrency 4 \
|
||||
--warmup-num-inference-steps 2 \
|
||||
--output-file /tmp/bagel_step_seq1.json \
|
||||
--disable-tqdm
|
||||
```
|
||||
|
||||
Restart the single-stage server with `--stage-overrides
|
||||
'{"0":{"max_num_seqs":4}}'` (or use Stage 1 for either two-stage config),
|
||||
rerun the same benchmark with
|
||||
`--output-file /tmp/bagel_step_seq4.json`, and compare completed requests,
|
||||
duration, throughput, and latency percentiles. Keep the model, device topology,
|
||||
request count, client concurrency, resolution, steps, seed, and warmup settings
|
||||
fixed between runs.
|
||||
|
||||
Send a text-to-image request with BAGEL-specific generation parameters:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -128,6 +128,109 @@ with `guidance_scale=1.0` remain valid on the same server: every rank evaluates
|
||||
only the positive branch, no negative embeddings are built, and no guidance is
|
||||
applied.
|
||||
|
||||
### 1 x RTX 4090 48 GB (FP8)
|
||||
|
||||
Boogu-Image supports two ways to quantize or load the DiT transformer:
|
||||
|
||||
| Path | Base model | Edit model | Method | Scheme |
|
||||
| ------------------ | -------------------------------- | -------------------------------- | ---------------------------- | ----------------------- |
|
||||
| Serialized TorchAO | `Boogu/Boogu-Image-0.1-Base-fp8` | `Boogu/Boogu-Image-0.1-Edit-fp8` | `torchao_float8_weight_only` | FP8 weight-only (W8A16) |
|
||||
| Native online FP8 | `Boogu/Boogu-Image-0.1-Base` | `Boogu/Boogu-Image-0.1-Edit` | `fp8` | Dynamic W8A8 |
|
||||
|
||||
#### Environment
|
||||
|
||||
The following configuration was validated:
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.12
|
||||
- Driver / runtime: NVIDIA CUDA environment with one RTX 4090 48 GB GPU
|
||||
- vLLM version: Match the vLLM-Omni checkout used for deployment
|
||||
- vLLM-Omni version or commit: Use a commit that contains TorchAO FP8
|
||||
checkpoint loading for diffusion models
|
||||
- TorchAO version: 0.17.0
|
||||
- `kernels` / `kernels-data`: 0.15.2 / 0.16.1
|
||||
|
||||
#### Command
|
||||
|
||||
Serve the official Base FP8 checkpoint using its pre-quantized TorchAO FP8
|
||||
weight-only transformer weights:
|
||||
|
||||
```bash
|
||||
vllm serve Boogu/Boogu-Image-0.1-Base-fp8 \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--diffusion-quantization-config \
|
||||
'{"transformer":{"method":"torchao_float8_weight_only"}}'
|
||||
```
|
||||
|
||||
Native online FP8 (currently applied only to eligible vLLM-quantizable linear
|
||||
layers in the DiT transformer):
|
||||
|
||||
```bash
|
||||
vllm serve Boogu/Boogu-Image-0.1-Base \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--diffusion-quantization-config \
|
||||
'{"transformer":{"method":"fp8"}}'
|
||||
```
|
||||
|
||||
For image editing, use the matching Edit model ID from the table and keep the
|
||||
corresponding quantization configuration.
|
||||
|
||||
#### Verification
|
||||
|
||||
The request format is shared by both FP8 paths. Set `<MODEL_ID>` to the model
|
||||
ID passed to `vllm serve`. For example, use
|
||||
`Boogu/Boogu-Image-0.1-Base-fp8` for serialized TorchAO FP8 or
|
||||
`Boogu/Boogu-Image-0.1-Base` for native online FP8. For image editing, use the
|
||||
corresponding Edit model.
|
||||
|
||||
Base text-to-image:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/images/generations \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "<MODEL_ID>",
|
||||
"prompt": "A mountain lake at sunset, photorealistic, cinematic lighting",
|
||||
"size": "1024x1024",
|
||||
"num_inference_steps": 28,
|
||||
"guidance_scale": 4.0,
|
||||
"seed": 42
|
||||
}' | jq -r '.data[0].b64_json' | base64 -d > output_fp8.png
|
||||
```
|
||||
|
||||
Image editing:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/images/edits \
|
||||
-F model="<MODEL_ID>" \
|
||||
-F image="@input.png" \
|
||||
-F prompt="Change the style to a colored pencil drawing." \
|
||||
-F num_inference_steps=28 \
|
||||
-F guidance_scale=4.0 \
|
||||
-F guidance_scale_2=1.0 \
|
||||
-F seed=42 \
|
||||
| jq -r '.data[0].b64_json' | base64 -d > edited_fp8.png
|
||||
```
|
||||
|
||||
#### Notes
|
||||
|
||||
- **Memory usage:**
|
||||
- The serialized TorchAO FP8 path uses approximately 21.1 GiB at 512x512
|
||||
and 24.2-24.6 GiB at 1024x1024.
|
||||
- The native online FP8 path uses approximately 31.5 GiB for Base and
|
||||
30.7 GiB for Edit at 1024x1024. It uses more memory because only eligible
|
||||
DiT linear layers are quantized online, while the MLLM from the regular
|
||||
Base/Edit checkpoint remains in BF16. Most of the observed difference
|
||||
comes from the MLLM precision, although differences in the two DiT FP8
|
||||
representations and kernels may also contribute.
|
||||
- **Quantization scope:** The transformer-scoped configuration applies to the
|
||||
DiT. With the serialized `-fp8` checkpoints, the MLLM follows its own
|
||||
checkpoint-declared FP8 configuration. With the regular checkpoints used for
|
||||
online FP8, the MLLM remains in BF16. The VAE and scheduler are outside this
|
||||
quantization routing.
|
||||
|
||||
## Image editing (Boogu-Image-0.1-Edit)
|
||||
|
||||
The Edit checkpoint is served by the same native pipeline (`BooguImagePipeline`);
|
||||
|
||||
@@ -42,7 +42,8 @@ encoder's BF16 configuration and the video/audio VAEs' FP32 precision. Select
|
||||
one offload strategy per deployment:
|
||||
|
||||
```bash
|
||||
# Stage 1 model-level CPU offload.
|
||||
# Stage 1 full-topology model offload, including MiniMax-H3's VAEs. Keep the
|
||||
# compatibility alias because the compact selector covers only DiT/text encoder.
|
||||
--stage-overrides '{"1":{"enable_cpu_offload":true}}'
|
||||
|
||||
# Stage 1 distributed layerwise offload. Tune resident layers for available RAM.
|
||||
|
||||
@@ -117,11 +117,12 @@ same time on a host sized for this minimum.
|
||||
The consumer-GPU profiles below are HBM budgets only. They still require the
|
||||
host-RAM budget above.
|
||||
|
||||
### Single GPU: accuracy and memory first
|
||||
### Single GPU: blockwise capacity path
|
||||
|
||||
The single-GPU configuration uses model-level CPU offload.
|
||||
This matches the accuracy-qualified reference path and prevents the Qwen3-VL
|
||||
encoder and DiT from being resident on the GPU at the same time.
|
||||
Use ordinary layerwise offload when one GPU cannot keep the Qwen3-VL encoder
|
||||
and DiT resident together. The component list below streams the active DiT,
|
||||
the Qwen vision blocks, and the first 50 Qwen text layers. Encoder blocks stay
|
||||
rank-local; the video/audio VAEs remain resident.
|
||||
|
||||
```bash
|
||||
export MODEL=MiniMaxAI/MiniMax-H3
|
||||
@@ -129,20 +130,37 @@ export PORT=8091
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0 \
|
||||
VLLM_WORKER_MULTIPROC_METHOD=spawn \
|
||||
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800 \
|
||||
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400 \
|
||||
vllm serve "${MODEL}" \
|
||||
--omni \
|
||||
--host 0.0.0.0 \
|
||||
--port "${PORT}" \
|
||||
--trust-remote-code \
|
||||
--task-type fl2va \
|
||||
--num-gpus 1 \
|
||||
--enable-cpu-offload \
|
||||
--diffusion-offload-config \
|
||||
'{"mode":"layer","components":["dit","text_encoder"]}' \
|
||||
--enforce-eager \
|
||||
--diffusion-attention-backend FLASH_ATTN
|
||||
```
|
||||
|
||||
Use a GPU with enough memory for the active H3 component and enough system RAM
|
||||
for both offloaded DiTs plus the shared components. Model-level offload keeps
|
||||
the two DiTs mutually exclusive on GPU, but adds PCIe/NVLink transfer latency.
|
||||
This is a capacity profile, not a latency profile: every denoising step streams
|
||||
DiT blocks over the host link, while encoder blocks are streamed only during
|
||||
the short conditioning phase. It requires host memory for the complete
|
||||
checkpoint plus pinned transfer buffers. Re-measure peak HBM on the target
|
||||
shape; this command does not claim a particular GPU model as validated.
|
||||
|
||||
A one-B300 correctness smoke selected only `text_encoder` (keeping the DiT and
|
||||
VAEs resident) and completed a 384x672, 5-second T2VA request with two denoise
|
||||
steps. It installed 77 encoder hooks across the vision and text stacks, used a
|
||||
77,728 MiB worker peak, and measured 2.803 seconds encode, 2.706 seconds
|
||||
denoise, and 4.503 seconds decode. These reduced-step numbers validate the
|
||||
execution path; they are not a quality or production-latency benchmark.
|
||||
|
||||
To use whole-component behavior, change the config to
|
||||
`{"mode":"module","components":["dit","text_encoder"]}`. Module
|
||||
offload swaps the complete encoder and DiT and therefore has a higher
|
||||
encode-phase peak than encoder blockwise offload.
|
||||
|
||||
### Two 24/32 GB GPUs: TP2 distributed layerwise offload
|
||||
|
||||
@@ -483,10 +501,9 @@ Add this option to an existing H3 server command:
|
||||
|
||||
Use the FL2VA-only partition for this capacity test. Loading the combined
|
||||
service would also load the Ref2VA DiT and would test a different memory
|
||||
budget. A no-offload capacity check should contain none of
|
||||
`--enable-cpu-offload`, `--enable-layerwise-offload`, or
|
||||
`--enable-distributed-layerwise-offload`. VAE tiling changes decode placement
|
||||
but does not offload model weights to the CPU.
|
||||
budget. A no-offload capacity check should omit
|
||||
`--diffusion-offload-config` and all legacy `--enable-*-offload` aliases. VAE
|
||||
tiling changes decode placement but does not offload model weights to the CPU.
|
||||
|
||||
The run passes the capacity check when the server initializes, the request
|
||||
finishes without CUDA OOM or Xid errors, `peak_used_mib` remains below the
|
||||
@@ -518,10 +535,11 @@ For example, keep the first main block's attention projections in BF16 with:
|
||||
```
|
||||
|
||||
The structured option replaces `--quantization fp8`. Online FP8 can be used
|
||||
with H3 layerwise offload and with both DLO transfer paths. The default
|
||||
AllGather path uses the ordinary loader to finalize FP8 weights and scales
|
||||
before sharding them across ranks. `--dlo-no-use-allgather` instead retains
|
||||
complete rank-local tensors and avoids the synchronized request-wave contract.
|
||||
with H3 layerwise offload and with either DiT DLO transfer. DiT `allgather`
|
||||
uses the ordinary loader to finalize FP8 weights and scales before sharding
|
||||
them across ranks. DiT `rank-local` instead retains complete loader-produced
|
||||
tensors and avoids the synchronized request-wave contract. H3's TP-sharded
|
||||
text encoder uses `rank-local`.
|
||||
|
||||
## AMD ROCm (gfx942 / gfx950)
|
||||
|
||||
@@ -567,6 +585,13 @@ vllm serve "${MODEL}" \
|
||||
--diffusion-attention-backend FLASH_ATTN
|
||||
```
|
||||
|
||||
This validated ROCm capacity recipe intentionally retains the compatibility
|
||||
full-topology alias because MiniMax-H3 also stages its VAEs on that path. The
|
||||
compact API in this release selects only `dit` and `text_encoder`, so replacing
|
||||
the flag would change residency rather than perform a mechanical migration.
|
||||
No removal deadline is assigned until the compact API offers equivalent
|
||||
component coverage.
|
||||
|
||||
### ROCm four GPUs
|
||||
|
||||
The best-practice CUDA four-GPU configuration works on ROCm with the changes above.
|
||||
|
||||
@@ -31,7 +31,8 @@ NeMo modules (`nemo_vendored/`), so no `nemo_toolkit` install is needed.
|
||||
[`examples/offline_inference/nemotron_voicechat/end2end.py`](../../examples/offline_inference/nemotron_voicechat/end2end.py)
|
||||
- Model modules (thinker / talker / code2wav / vendored NeMo):
|
||||
[`vllm_omni/model_executor/models/nemotron_voicechat/`](../../vllm_omni/model_executor/models/nemotron_voicechat/)
|
||||
- Staged pipeline config:
|
||||
- Staged pipeline config (fast default; "PARITY:" comments restore the
|
||||
NeMo-bit-exact reference):
|
||||
[`vllm_omni/deploy/nemotron_labs_voicechat.yaml`](../../vllm_omni/deploy/nemotron_labs_voicechat.yaml)
|
||||
- Native duplex config and E2E probe:
|
||||
[`vllm_omni/deploy/nemotron_labs_voicechat_duplex.yaml`](../../vllm_omni/deploy/nemotron_labs_voicechat_duplex.yaml),
|
||||
@@ -46,14 +47,60 @@ NeMo modules (`nemo_vendored/`), so no `nemo_toolkit` install is needed.
|
||||
|
||||
| stage | arch | dtype | role |
|
||||
|---|---|---|---|
|
||||
| 0 thinker | `NemotronVoiceChatThinkerForConditionalGeneration` (LLM_AR) | fp32 | WAV + system prompt -> frame-locked text-token timeline (+ function channel) |
|
||||
| 0 thinker | `NemotronVoiceChatThinkerForConditionalGeneration` (LLM_AR) | bf16 (PARITY: fp32) | WAV + system prompt -> frame-locked text-token timeline (+ function channel) |
|
||||
| 1 talker | `NemotronVoiceChatTalker` (LLM_AR) | fp32 | text timeline -> 31-quantizer RVQ code stacks (one per 80 ms frame) |
|
||||
| 2 code2wav | `NemotronVoiceChatCode2Wav` (LLM_GENERATION) | fp32 | RVQ-VAE decode -> 22.05 kHz PCM |
|
||||
|
||||
Stages 0/1 default to fp32 for exact parity with the NeMo reference
|
||||
implementation (greedy decoding matches it token for token on the acceptance
|
||||
fixture). The deploy yaml documents a ~2x-faster bf16 thinker option whose
|
||||
output stayed within one word of the reference in testing.
|
||||
The deploy yamls default to a fast execution profile; every fast setting
|
||||
carries a "PARITY:" comment whose edits restore the eager fp32 reference
|
||||
execution that matches the NeMo implementation token for token (see
|
||||
Performance below).
|
||||
|
||||
## Performance
|
||||
|
||||
Each deploy yaml defaults to a fast execution profile, mirroring how NVIDIA's
|
||||
own serving stack runs this model (bf16 LLM + CUDA-graph decode, EAR-TTS on a
|
||||
vLLM engine, incremental codec cache):
|
||||
|
||||
- `nemotron_labs_voicechat.yaml` (offline sync): bf16 + CUDA-graph thinker and
|
||||
the talker's Gemma3 backbone as a native vLLM model
|
||||
(`hf_overrides.use_native_talker`) — PagedAttention KV, vLLM decode CUDA
|
||||
graphs, and the MoG sampling step captured into a single graph. RTF ~0.23 on
|
||||
the 196-frame acceptance fixture (1x H100, warm).
|
||||
- `nemotron_labs_voicechat_streaming.yaml` (offline streaming): the vendored
|
||||
per-frame EAR-TTS step captured into CUDA graphs
|
||||
(`hf_overrides.use_talker_cuda_graphs`), 5-frame codec chunks decoded
|
||||
incrementally with a per-request causal-conv cache
|
||||
(`use_incremental_codec_cache`, O(T) instead of O(T^2) codec work). First
|
||||
audio chunk ~1.5 s after submission on the same fixture.
|
||||
- `nemotron_labs_voicechat_duplex.yaml` (realtime): native talker + bf16
|
||||
thinker; audio is delivered as one 80 ms packet per frame at the frame clock
|
||||
(median inter-chunk gap ~81 ms on the bundled fixture) and the session
|
||||
length is unbounded (paged KV, no StaticCache bucket cap).
|
||||
|
||||
Notes on the fast settings:
|
||||
|
||||
- Thinker: `dtype: bfloat16` + `enforce_eager: false`. The 9B NemotronH decode
|
||||
is memory- and launch-bound, so both are enabled together; weights fit in
|
||||
~21 GB, so the pipeline also runs on sub-80 GB parts. Text stays coherent
|
||||
but greedy decoding can legitimately diverge from the fp32 reference
|
||||
(verified turn-for-turn coherent and ASR-clean on the bundled fixtures).
|
||||
- Talker (captured-step, streaming yaml): the whole EAR-TTS frame step
|
||||
(Gemma3 backbone via HF StaticCache + CFG batch doubling + MoG iterative
|
||||
unmasking) replays as one CUDA graph per cache-size bucket. MoG noise draws
|
||||
from the CUDA-graph Philox stream: audio is equivalent but not
|
||||
bit-identical to the eager path. Capture failures and oversize sessions
|
||||
fall back to the eager step automatically.
|
||||
- Talker (native, sync/duplex yamls): engine positions map 1:1 onto timeline
|
||||
steps; fused input embeddings are numerically verified against the
|
||||
reference forward, and the reference's EOS-silence code feedback
|
||||
(`inference_force_speech_silence_on_eos`) is applied. Supports
|
||||
multi-session batching (`max_num_seqs > 1`).
|
||||
|
||||
Bit parity with the NeMo reference is preserved behind the `PARITY:` comments
|
||||
in each yaml: applying those edits runs the eager fp32 reference execution
|
||||
(md5-anchored WAV on the acceptance fixture; the drain-all talker scheduling
|
||||
added for performance was verified WAV-byte identical against it).
|
||||
|
||||
## Hardware Support
|
||||
|
||||
@@ -110,7 +157,7 @@ vllm-omni serve /path/to/NVIDIA-NemotronLabs-VoiceChat-11B \
|
||||
--deploy-config vllm_omni/deploy/nemotron_labs_voicechat_duplex.yaml
|
||||
```
|
||||
|
||||
The single-GPU profile runs three eager fp32 engine processes on one device.
|
||||
The single-GPU profile runs all three engine processes on one device.
|
||||
|
||||
In another shell, stream only the user channel of one of NVIDIA's bundled
|
||||
stereo conversation fixtures:
|
||||
@@ -142,14 +189,53 @@ python tests/e2e/online_serving/nemotron_voicechat_realtime_duplex.py \
|
||||
--expected-post-tool-text "random number"
|
||||
```
|
||||
|
||||
Probe caveats observed on the bundled `turn_taking.wav`: stream the FULL
|
||||
fixture (a `--max-frames` truncation cuts the conversation mid-turn, the model
|
||||
then never closes its last response, and the probe times out waiting for a
|
||||
`response.done`), and note that the pass/fail condition "a response completes
|
||||
after the final commit" is timing-sensitive — a faster pipeline can legitimately
|
||||
complete every turn before the commit lands (use
|
||||
`--allow-incomplete-response` when measuring latency rather than protocol
|
||||
completion).
|
||||
|
||||
#### Duplex performance profile
|
||||
|
||||
`nemotron_labs_voicechat_duplex.yaml` defaults to the fast profile: the
|
||||
talker's Gemma3 backbone on PagedAttention + vLLM CUDA graphs (MoG sampling
|
||||
in a captured step graph, unbounded session length) and a bf16 + CUDA-graph
|
||||
thinker inherited from the streaming base. On the bundled `turn_taking.wav`
|
||||
fixture (1x H100) the client receives one 80 ms packet per frame at the frame
|
||||
clock (median inter-chunk gap ~81 ms / p95 ~110 ms) with sub-second first-packet
|
||||
latency per turn; playback clients should still keep a small (~200-300 ms)
|
||||
prebuffer to absorb scheduling jitter.
|
||||
|
||||
The bf16 thinker's greedy text stream can legitimately diverge from fp32
|
||||
mid-conversation. On the bundled fixture it was verified turn-for-turn:
|
||||
coherent transcripts, every turn reaching EOS/`response.done`, ASR-clean
|
||||
output. Apply the PARITY edits in the yaml to bit-match the fp32 reference
|
||||
thinker instead.
|
||||
|
||||
Classifier-free guidance on the native talker
|
||||
(`hf_overrides.native_talker_guidance: true` + `gpu_memory_utilization: 0.18`
|
||||
on stage 1): the conditional stream stays on vLLM paged KV while the
|
||||
unconditional stream mirrors it on the vendored HF backbone (a captured
|
||||
StaticCache step), and the pair is blended with NeMo's `generate_step` math
|
||||
(hidden-space blends in `lm_head` and inside `MoGHead.infer`). The per-frame
|
||||
cost stays within the 80 ms duplex budget, and a paired 4-fixture ASR study
|
||||
matched the guided reference. Off by default since guidance at this
|
||||
checkpoint's scale (0.2) is ASR-indistinguishable; enable it for strict parity
|
||||
with NVIDIA's guided deployment or for listening tests. Under CFG the
|
||||
unconditional stream's StaticCache is bucketed (largest 4096 positions ≈ 5.4
|
||||
minutes); one session at a time uses the captured step, extra concurrent
|
||||
sessions fall back to eager stepping.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Memory usage: the shipped yaml runs all three stages on one GPU
|
||||
(`gpu_memory_utilization` 0.62 / 0.12 / 0.06); peak usage is dominated by the
|
||||
fp32 thinker. The fp32 default has a hard floor of roughly 43 GB of thinker
|
||||
weights alone (9B backbone + 587M `embed_tokens` + 587M `function_head` +
|
||||
0.6B Conformer), so 48 GB cards cannot run it — use the bf16 thinker option
|
||||
documented in the deploy yaml on anything smaller than an 80 GB part.
|
||||
- Memory usage: the shipped yamls run all three stages on one GPU
|
||||
(`gpu_memory_utilization` 0.62 / 0.12 / 0.06). The default bf16 thinker fits
|
||||
in ~21 GB of weights; the fp32 PARITY thinker needs roughly 43 GB of weights
|
||||
alone (9B backbone + 587M `embed_tokens` + 587M `function_head` + 0.6B
|
||||
Conformer), so the PARITY execution requires an 80 GB part.
|
||||
- Input sizing: the timeline is frame-locked, so the reply budget IS the input
|
||||
duration. The acoustic channel trails the text channel; if the WAV does not
|
||||
carry enough trailing silence for the reply to finish, the spoken answer is
|
||||
@@ -161,15 +247,17 @@ python tests/e2e/online_serving/nemotron_voicechat_realtime_duplex.py \
|
||||
set `min_tokens` on the thinker: the tokenizer's EOS token is also the
|
||||
frame-locked PAD/silence token, so masking it forces the model to babble
|
||||
instead of pausing.
|
||||
- The talker's `max_tokens` is 16383 (its stage prompt is one placeholder
|
||||
token, and the stage context is 16384).
|
||||
- The talker's `max_tokens` is 16347: its stage prompt is the 37-token
|
||||
speaker-prompt prefill (`talker_init_len: 37`), and prompt + max_tokens
|
||||
must fit the 16384 stage context.
|
||||
- The 80 ms frame period is a model/protocol contract, not a throughput
|
||||
guarantee. In an eager fp32 H200 profile, Stage 2 emitted 80 ms packets at
|
||||
roughly 0.87--0.88 s intervals (RTF about 10.8--10.9); placing the three
|
||||
stages on separate GPUs did not remove the serial 8-step EAR-TTS bottleneck.
|
||||
The current native duplex path is functionally streaming but not wall-clock
|
||||
realtime.
|
||||
- Known limitations: batch=1 only. The native duplex deployment allows one
|
||||
guarantee. The default duplex profile keeps up with the frame clock on 1x
|
||||
H100; the eager fp32 PARITY execution is functionally streaming but not
|
||||
wall-clock realtime (the serial 8-step EAR-TTS unmasking dominates, and
|
||||
placing the three stages on separate GPUs does not remove it).
|
||||
- Known limitations: the offline pipeline is single-request per call
|
||||
(the native talker supports engine-level multi-session batching via
|
||||
max_num_seqs > 1). The native duplex deployment allows one
|
||||
active session and does not support barge-in. Tool execution remains
|
||||
client-owned: the server emits function-call events, accepts a validated
|
||||
`function_call_output`, and resumes the live model with the returned result;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# PersonaPlex
|
||||
|
||||
> Live full-duplex speech-to-speech (talk to the model in your browser, it listens
|
||||
> while it speaks) with persona and voice control, on the native vLLM-Omni engine.
|
||||
> Live full-duplex speech-to-speech (the model listens while it speaks) with
|
||||
> persona and voice control, on the native vLLM-Omni engine.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
- Task: Full-duplex speech-to-speech. 24 kHz mic audio in, agent speech +
|
||||
inner-monologue text out, in 80 ms lockstep. Persona (role text) and voice
|
||||
(zero-shot clone) are set per session.
|
||||
- Mode: Live duplex WebSocket server with the official browser client, single or
|
||||
multi-session (`--batch-size`), plus an offline WAV-in / WAV-out example
|
||||
- Mode: Live duplex WebSocket serving through the unified duplex stack
|
||||
(`/v1/duplex` and `/v1/realtime?duplex=1`)
|
||||
- Maintainer: [`@linyueqian`](https://github.com/linyueqian)
|
||||
|
||||
## When to use this recipe
|
||||
@@ -24,23 +24,21 @@ streaming Mimi codec are all native vLLM-Omni modules (Mimi runs on
|
||||
matching the reference implementation frame for frame on the golden replays used
|
||||
as the acceptance gate.
|
||||
|
||||
PersonaPlex is the first Moshi-class (pure-lockstep) model in the experimental
|
||||
full-duplex framework (`vllm_omni/experimental/fullduplex/`), and the first with
|
||||
elastic multi-session batching: one engine hosts `--batch-size` concurrent
|
||||
conversations, callers join or leave on any 80 ms tick without disturbing live
|
||||
slots (greedy isolation is bit-exact).
|
||||
PersonaPlex is the first Moshi-class (pure-lockstep) model on the vLLM-Omni
|
||||
full-duplex serving stack: it plugs into the generic `/v1/duplex` handler
|
||||
through the standard plugin seams (`duplex_serving_adapter` /
|
||||
`duplex_runtime_extension` in its `pipeline.py`) with a model-specific
|
||||
package at `vllm_omni/model_executor/models/personaplex/duplex/`.
|
||||
|
||||
## References
|
||||
|
||||
- Serving package:
|
||||
[`vllm_omni/experimental/fullduplex/personaplex/`](../../vllm_omni/experimental/fullduplex/personaplex/)
|
||||
- Duplex serving package:
|
||||
[`vllm_omni/model_executor/models/personaplex/duplex/`](../../vllm_omni/model_executor/models/personaplex/duplex/)
|
||||
- Native model modules (temporal / depformer / streaming Mimi):
|
||||
[`vllm_omni/model_executor/models/personaplex/`](../../vllm_omni/model_executor/models/personaplex/)
|
||||
- Online example (server + headless client):
|
||||
- Online serving example:
|
||||
[`examples/online_serving/personaplex/`](../../examples/online_serving/personaplex/)
|
||||
- Offline example:
|
||||
[`examples/offline_inference/personaplex/personaplex_offline.py`](../../examples/offline_inference/personaplex/personaplex_offline.py)
|
||||
- Staged offline pipeline (talker -> Mimi code2wav, async-chunk streaming):
|
||||
- Staged pipeline (talker -> Mimi code2wav, async-chunk streaming):
|
||||
[`vllm_omni/deploy/personaplex.yaml`](../../vllm_omni/deploy/personaplex.yaml)
|
||||
- Integration PR: [vllm-project/vllm-omni#4771](https://github.com/vllm-project/vllm-omni/pull/4771)
|
||||
- Framework context: [RFC #3745](https://github.com/vllm-project/vllm-omni/issues/3745)
|
||||
@@ -53,18 +51,20 @@ slots (greedy isolation is bit-exact).
|
||||
## Why it is different from MiniCPM-o / JoyVL duplex
|
||||
|
||||
| | MiniCPM-o 4.5 (#3907) | JoyVL | **PersonaPlex (this)** |
|
||||
|---|---|---|---|
|
||||
| --- | --- | --- | --- |
|
||||
| Cadence | 1 s chunk groups | ~1 fps frames | **80 ms lockstep (12.5 Hz)** |
|
||||
| Turn control | learned `⟨listen⟩`/`⟨speak⟩` | `</silence>`/`</response>` | **none, pure lockstep** |
|
||||
| Per step | variable-length token group | text decision | **1 user frame in -> 1 agent frame + 1 text token** |
|
||||
| Barge-in | at chunk boundary | n/a | **native (model always hears the user)** |
|
||||
| Session state | chunk-group KV | per-tick HTTP | **persistent ring KV + streaming Mimi state** |
|
||||
|
||||
This is the lockstep ("parallel-frame joint") shape of the duplex adapter
|
||||
patterns: the adapter declares `DuplexCapability.continuous = True`, and
|
||||
`core.DuplexRuntime` runs ONE eternal response that consumes input frames as they
|
||||
arrive and drains on close. The flag is a small, default-off lifecycle mode, so
|
||||
turn-based adapters (JoyVL, MiniCPM-o) are unaffected.
|
||||
This is the lockstep ("parallel-frame joint") shape of full duplex: audio flows
|
||||
continuously in both directions with no client commits or external turn signals.
|
||||
On the unified stack this is expressed purely through the capability payload
|
||||
(`supports_client_commit=false`, `supports_external_turn_signal=false`,
|
||||
`supports_barge_in=false`); every session on a PersonaPlex deployment is native
|
||||
duplex (`is_enabled()` is unconditionally true). Turn-based models
|
||||
(MiniCPM-o 4.5) are unaffected.
|
||||
|
||||
## Architecture (Moshi RQ-Transformer, all native)
|
||||
|
||||
@@ -95,9 +95,7 @@ native modules, so other CUDA GPUs with enough memory are expected to work.
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+ (CI covers 3.11 / 3.12)
|
||||
- vLLM-Omni: PR #4771 branch or current `main` once merged
|
||||
- Extra dependency for the live server: `pip install sphn` (Opus framing;
|
||||
`aiohttp` already ships with vLLM)
|
||||
- vLLM-Omni: current `main`
|
||||
- `export HF_TOKEN=...` with access to the gated
|
||||
`nvidia/personaplex-7b-v1` repo (accept the license on the model page)
|
||||
|
||||
@@ -107,67 +105,49 @@ First run auto-downloads from the model repo: `model.safetensors`,
|
||||
|
||||
#### Command
|
||||
|
||||
Live duplex server (official browser client served at `/`):
|
||||
Live duplex serving through the unified stack:
|
||||
|
||||
```bash
|
||||
HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 \
|
||||
python -m vllm_omni.experimental.fullduplex.personaplex.serving.server \
|
||||
--port 8124 --voice NATF2.pt
|
||||
HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 python -m vllm_omni.entrypoints.cli.main serve \
|
||||
/path/to/personaplex-7b-v1 \
|
||||
--omni \
|
||||
--deploy-config vllm_omni/deploy/personaplex.yaml
|
||||
```
|
||||
|
||||
Multi-session on the same GPU (elastic batching, callers join/leave any tick):
|
||||
|
||||
```bash
|
||||
HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 \
|
||||
python -m vllm_omni.experimental.fullduplex.personaplex.serving.server \
|
||||
--port 8124 --batch-size 4
|
||||
```
|
||||
|
||||
Then open `http://localhost:8124/` and allow the microphone (use headphones so
|
||||
the agent does not hear itself). Headless alternative:
|
||||
|
||||
```bash
|
||||
python examples/online_serving/personaplex/duplex_client.py \
|
||||
--url ws://localhost:8124/api/chat --input user.wav --out reply.wav
|
||||
```
|
||||
|
||||
Offline WAV-in / WAV-out (no server):
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/personaplex/personaplex_offline.py \
|
||||
--input-wav question.wav --output-wav reply.wav --output-text reply.json \
|
||||
--voice-prompt NATF2.pt --persona "You are a wise and friendly teacher."
|
||||
```
|
||||
This exposes `WS /v1/duplex` (native duplex dialect) and
|
||||
`WS /v1/realtime?duplex=1` (OpenAI Realtime projection; client API and wire
|
||||
protocol in [`docs/serving/realtime_duplex_api.md`](../../docs/serving/realtime_duplex_api.md)).
|
||||
Voice and persona are set per session via `extra_body`.
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
# GPU-free contract tests (stubbed FrameStepper)
|
||||
pytest tests/e2e/features/fullduplex/test_personaplex_adapter.py \
|
||||
tests/e2e/features/fullduplex/test_personaplex_session.py \
|
||||
tests/e2e/features/fullduplex/test_personaplex_batched.py -q
|
||||
# GPU-free contract tests (stage0 runtime + unified serving adapter)
|
||||
pytest tests/model_executor/models/personaplex/duplex/ -q
|
||||
|
||||
# GPU e2e: batched isolation + slot recycle (coherence check needs a real
|
||||
# question WAV via PPLEX_QUESTION_WAV)
|
||||
pytest tests/e2e/features/fullduplex/test_personaplex_elastic.py -q
|
||||
# GPU e2e: paced 24 kHz PCM over /v1/realtime?duplex=1, two concurrent
|
||||
# sessions, overflow admission, slot recycling, non-silent output
|
||||
python tests/e2e/online_serving/personaplex_realtime_duplex.py \
|
||||
--model /path/to/personaplex-7b-v1 \
|
||||
--input-wav /path/to/speech.wav \
|
||||
--output-dir /tmp/personaplex-realtime-duplex
|
||||
```
|
||||
|
||||
A green offline run producing a coherent spoken reply (and inner-monologue text
|
||||
in `reply.json`) is the quickest end-to-end check.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Realtime budget is 80 ms/frame. Verified eager per-tick latency at
|
||||
`--batch-size 4` is ~70-74 ms on this hardware class, i.e. all four
|
||||
- Realtime budget is 80 ms/frame. Verified eager per-tick latency at four
|
||||
concurrent sessions is ~70-74 ms on this hardware class, i.e. all four
|
||||
conversations stay realtime; single-session has comfortable headroom.
|
||||
- Decoding is greedy only (temperature/top-k knobs are intentionally not
|
||||
exposed): greedy is what the parity gates pin against the reference
|
||||
implementation, and sampling amplifies sub-bit numeric drift into divergence.
|
||||
- With `--batch-size N`, connections beyond N are rejected until a slot frees;
|
||||
a recycled slot is bit-exact with a fresh engine for the same inputs.
|
||||
- Session concurrency is governed by the deploy config
|
||||
(`vllm_omni/deploy/personaplex.yaml`); connections beyond the limit are
|
||||
rejected until a slot frees. A recycled slot is bit-exact with a fresh
|
||||
engine for the same inputs.
|
||||
- Voice prompts: bundled `voices.tgz` names (`NATF*`/`NATM*` natural,
|
||||
`VARF*`/`VARM*` varied) or a path to your own `.pt`/`.wav`.
|
||||
- Run the browser client near the server. The 80 ms cadence is sensitive to
|
||||
- Run the client near the server. The 80 ms cadence is sensitive to
|
||||
network jitter; over a high-latency link playback can stutter regardless of
|
||||
engine speed. On localhost it is smooth.
|
||||
- Sliding window is 3000 frames (~4 min). The ring KV recycles beyond that;
|
||||
|
||||
@@ -121,8 +121,8 @@ bash examples/online_serving/text_to_video/run_curl_sana_video.sh
|
||||
|
||||
##### Parallel native serving
|
||||
|
||||
The native pipelines support tensor parallelism (up to 2 GPUs) and CFG
|
||||
parallelism. Tensor parallelism on 2 GPUs:
|
||||
The native pipelines support tensor parallelism (up to 2 GPUs), sequence
|
||||
parallelism (2 or 4 GPUs) and CFG parallelism. Tensor parallelism on 2 GPUs:
|
||||
|
||||
```bash
|
||||
vllm serve Efficient-Large-Model/SANA-Video_2B_480p_diffusers \
|
||||
@@ -133,15 +133,32 @@ vllm serve Efficient-Large-Model/SANA-Video_2B_480p_diffusers \
|
||||
--port 8091
|
||||
```
|
||||
|
||||
For CFG parallelism on 2 GPUs, replace `--tensor-parallel-size 2` with
|
||||
`--cfg-parallel-size 2`; it splits the guided and unguided branches across the
|
||||
two GPUs and only helps when `guidance_scale` is above 1. Passing both flags
|
||||
combines the two on 4 GPUs.
|
||||
Sequence parallelism on 2 GPUs (replace `--tensor-parallel-size 2` with):
|
||||
|
||||
Whether tensor parallelism lowers latency depends on the interconnect. Each
|
||||
transformer block adds an all-reduce, so it speeds up generation only on fast
|
||||
GPU links such as NVLink and can be slower than a single GPU on PCIe-only
|
||||
systems. Measure on your hardware before enabling it.
|
||||
```bash
|
||||
--usp 2
|
||||
```
|
||||
|
||||
The `--usp` flag carries the sequence parallel degree. On SANA-Video the
|
||||
implementation is not Ulysses all-to-all: video frames are sharded across
|
||||
ranks and the linear-attention state is combined with one small all-reduce
|
||||
per block, which is far less communication than tensor parallelism needs.
|
||||
Ring and AllGather-KV sequence parallel do not apply to SANA's linear
|
||||
attention and are rejected.
|
||||
|
||||
For CFG parallelism on 2 GPUs, use `--cfg-parallel-size 2` instead; it splits
|
||||
the guided and unguided branches across the two GPUs and only helps when
|
||||
`guidance_scale` is above 1. CFG parallelism combines with either tensor
|
||||
parallelism or sequence parallelism on 4 GPUs. Tensor and sequence parallelism
|
||||
also combine; on the measured 4-GPU NVLink box tp2 x sp2 reached 2.55x, below
|
||||
the pure sp4 (3.24x) and sp2 x cfg2 (3.56x) configurations, so prefer those
|
||||
when four GPUs serve one request.
|
||||
|
||||
Whether these lower latency depends on the interconnect. Each transformer
|
||||
block adds an all-reduce under tensor parallelism, so it speeds up generation
|
||||
only on fast GPU links such as NVLink and can be slower than a single GPU on
|
||||
PCIe-only systems. Sequence parallelism communicates far less per block, but
|
||||
was only measured on NVLink. Measure on your hardware before enabling either.
|
||||
|
||||
To run the black-box compatibility backend for T2V, replace the server script
|
||||
with `run_server_sana_video_diffusers.sh`. The same `/v1/videos` request
|
||||
@@ -287,9 +304,9 @@ for the tuning knobs.
|
||||
The denoising loop intentionally retains the checkpoint-compatible
|
||||
Diffusers `DPMSolverMultistepScheduler`.
|
||||
- Known limitations:
|
||||
- Tensor parallelism (up to 2 GPUs) and CFG parallelism are supported for
|
||||
the native pipeline. Sequence parallelism, TeaCache, and step execution
|
||||
are not validated.
|
||||
- Tensor parallelism (up to 2 GPUs), sequence parallelism (2 or 4 GPUs)
|
||||
and CFG parallelism are supported for the native pipeline. TeaCache and
|
||||
step execution are not validated.
|
||||
- Cache-DiT and CPU offload are limited to TP1/CFG1/SP1. Cache-DiT with
|
||||
distributed layerwise offload is not supported by the native pipeline.
|
||||
- Cache-DiT speedup and offload memory numbers above are single-GPU A800
|
||||
|
||||
+9
-8
@@ -10,7 +10,8 @@ keep naming and layout consistent, organize recipes by model vendor in a way
|
||||
that is aligned with
|
||||
[`vllm-project/recipes`](https://github.com/vllm-project/recipes), but treat
|
||||
that external repository as a reference for structure rather than the place to
|
||||
add files for this repo. Use one Markdown file per model family by default.
|
||||
add files for this repo. Use one Markdown file per model and hardware profile,
|
||||
named `<model>-<hardware>.md`.
|
||||
|
||||
> **Note:** The vLLM-Omni TTS recipes (12 models across 8 providers) are also
|
||||
> published on the rendered recipes site at
|
||||
@@ -29,16 +30,16 @@ Example layout:
|
||||
```text
|
||||
recipes/
|
||||
Qwen/
|
||||
Qwen3-Omni.md
|
||||
Qwen3-TTS.md
|
||||
Qwen3-Omni-H100.md
|
||||
Qwen3-TTS-A100.md
|
||||
Tencent/
|
||||
Covo-Audio-Chat.md
|
||||
Covo-Audio-Chat-L40S.md
|
||||
```
|
||||
|
||||
## Available Recipes
|
||||
|
||||
| Recipe | Task | Hardware |
|
||||
|--------|------|----------|
|
||||
| -------- | ------ | ---------- |
|
||||
| [`Baidu/ERNIE-Image.md`](./Baidu/ERNIE-Image.md) | Text-to-image online serving (ERNIE-Image 8B) | 1x or 2x RTX 4090 24GB |
|
||||
| [`Bagel/BAGEL-7B-MoT.md`](./Bagel/BAGEL-7B-MoT.md) | Text-to-image with shared online/offline examples | 1x A100 80GB / 2x CUDA GPUs |
|
||||
| [`Boogu/Boogu-Image.md`](./Boogu/Boogu-Image.md) | Text-to-image online serving (Boogu-Image-0.1-Base) | 1x A100/H100 40GB+ |
|
||||
@@ -62,6 +63,7 @@ recipes/
|
||||
| [`meituan-longcat/LongCat-Video-Avatar-1.5.md`](./meituan-longcat/LongCat-Video-Avatar-1.5.md) | Audio-driven avatar video generation (AT2V / AI2V, single- and multi-speaker, AVC continuation) | 1x H100 80GB |
|
||||
| [`mistralai/Voxtral-TTS.md`](./mistralai/Voxtral-TTS.md) | Online serving for TTS | 1x RTX 4090 24GB |
|
||||
| [`Robbyant/LingBot-Video.md`](./Robbyant/LingBot-Video.md) | Native dense and MoE text-to-video serving | Dense: 1x L20X; MoE: 1x L20X (~67.7 GiB peak) |
|
||||
| [`SandAI/MAGI-2-preview-L20X.md`](./SandAI/MAGI-2-preview-L20X.md) | Native Preview-stage text/image-to-video-and-audio generation | 1x/4x NVIDIA L20X 140 GiB qualified |
|
||||
| [`Robbyant/LingBot-World-2.0.md`](./Robbyant/LingBot-World-2.0.md) | Offline and experimental realtime interactive world generation | 1x H200/B200 or 2x B200 for TP=2 |
|
||||
| [`cosmos3/Cosmos3-Nano.md`](./cosmos3/Cosmos3-Nano.md) | Text-to-image, text-to-video, image-to-video, video-to-video generation, text to video with sound, action policy | 1x H200 141GB / B300 |
|
||||
| [`cosmos3/Cosmos3-Edge.md`](./cosmos3/Cosmos3-Edge.md) | T2I / T2V / I2V + action (Physical-AI) generation on the Nemotron-based Edge transformer | 1x GPU (~8 GiB) |
|
||||
@@ -92,8 +94,7 @@ recipes/
|
||||
| [`GLM/GLM-Image.md`](./GLM/GLM-Image.md) | Online serving for image generation | 1x A800 80GB / 2x A800 80GB |
|
||||
| [`JD/JoyAI-VL-Interaction.md`](./JD/JoyAI-VL-Interaction.md) | Real-time streaming video-language interaction (proactive speak/silence/delegate) | 1x GPU 24GB+ |
|
||||
|
||||
Within a single recipe file, include different hardware support sections such
|
||||
as `GPU`, `ROCm`, and `NPU`, and add concrete tested configurations like
|
||||
`1x A100 80GB` or `2x L40S` inside those sections when applicable.
|
||||
Keep one accelerator model or platform per recipe file. Add a separately
|
||||
suffixed recipe and table row for another GPU, ROCm accelerator, or NPU.
|
||||
|
||||
See [TEMPLATE.md](./TEMPLATE.md) for the recommended format.
|
||||
@@ -0,0 +1,288 @@
|
||||
# MAGI-2 Preview — NVIDIA L20X
|
||||
|
||||
> Native text/image-to-video-and-audio generation with the released Preview stage
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: SandAI
|
||||
- Model: `sand-ai/MAGI-2-preview`
|
||||
- Runtime: Native vLLM-Omni pipeline; no SandAI runtime import
|
||||
- Modes: Offline shared examples and OpenAI-compatible video serving
|
||||
- Hardware: NVIDIA L20X 140 GiB
|
||||
- Recommended deployment: Four-GPU resident SP4 (`TP=1`, `SP=4`)
|
||||
- Maintainer: Community
|
||||
|
||||
## Supported model contract
|
||||
|
||||
### Tasks and inputs
|
||||
|
||||
| Task | Text prompt | Media condition | Shared entrypoint |
|
||||
| --- | --- | --- | --- |
|
||||
| T2VA | Required | None | [`text_to_video.py`](../../examples/offline_inference/text_to_video/text_to_video.py) |
|
||||
| I2VA | Required | Exactly one still image | [`image_to_video.py`](../../examples/offline_inference/image_to_video/image_to_video.py) |
|
||||
|
||||
The I2VA path preserves the source aspect ratio and applies MAGI-2's native
|
||||
resize/pad transform. Video or audio conditions, multiple images, and fused
|
||||
per-rank request batches are not supported.
|
||||
|
||||
### Output
|
||||
|
||||
| Category | Supported Preview specification |
|
||||
| --- | --- |
|
||||
| Duration | Exactly 10 seconds |
|
||||
| Video | 125 frames at 12.5 fps |
|
||||
| Native tiers | `272p` (448x256) and `540p` (896x512) |
|
||||
| Optional final resize | `output_width` and `output_height` must be supplied together |
|
||||
| Audio | 44.1 kHz stereo, generated with the video |
|
||||
| Refiner | The separate 1080p refiner is not included in this integration |
|
||||
|
||||
### Profiles provided by this recipe
|
||||
|
||||
| Profile | Devices | Purpose | Qualification |
|
||||
| --- | ---: | --- | --- |
|
||||
| Resident SP4 | 4 | Recommended fidelity/default deployment | Released smoke and reference parity |
|
||||
| Rank-local DLO SP4 | 4 | Full-quality lower-HBM transformer streaming | Released checkpoint, 100-step T2VA and I2VA |
|
||||
| DLO DP4 / DP2SP2 | 4 | Concurrent request throughput | Bounded one- and four-step coverage |
|
||||
| Ordinary layerwise | 1 | Memory-constrained single request | Released checkpoint, one and four steps |
|
||||
| Online resident SP4 | 4 | OpenAI-compatible video API | Serving path covered |
|
||||
|
||||
## References
|
||||
|
||||
- Official model card: <https://huggingface.co/sand-ai/MAGI-2-preview>
|
||||
- Architecture blog: <https://sand.ai/blog/magi-2-preview>
|
||||
- Supported models: [`docs/models/supported_models.md`](../../docs/models/supported_models.md)
|
||||
- Diffusion feature matrix: [`docs/user_guide/diffusion_features.md`](../../docs/user_guide/diffusion_features.md)
|
||||
|
||||
## Prepare the checkpoint
|
||||
|
||||
The native Preview path uses approximately 274 GiB on disk. The separate
|
||||
refiner is not required.
|
||||
|
||||
```bash
|
||||
export MAGI2_CKPT_ROOT=/path/to/MAGI-2-preview
|
||||
|
||||
hf download sand-ai/MAGI-2-preview \
|
||||
preview/ text_encoder/ vae/ turbo_vae/ stable-audio-open-1.0/ \
|
||||
--revision 2dea51b64db47ee5b4402d36fd90829a0c58913b \
|
||||
--local-dir "$MAGI2_CKPT_ROOT"
|
||||
```
|
||||
|
||||
Install vLLM-Omni and ensure `ffmpeg` is on `PATH`. No second model runtime is
|
||||
required.
|
||||
|
||||
## Hardware support
|
||||
|
||||
### Locally qualified NVIDIA configuration
|
||||
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| Accelerator | NVIDIA L20X |
|
||||
| Per-device memory | 143,771 MiB (about 140.4 GiB) |
|
||||
| Device count | 4; the one-device profile uses one of the same GPUs |
|
||||
| Device interconnect | All-to-all NV18 links (`nvidia-smi topo -m`) |
|
||||
| Host memory | 2.0 TiB |
|
||||
|
||||
The official SandAI runtime requires eight NVIDIA Hopper GPUs. That official
|
||||
eight-GPU system was unavailable for this PR; compatible eight-worker
|
||||
topologies pass configuration validation but are not locally runtime-qualified.
|
||||
No H100, H200, B200, or B300 runtime claim is made here.
|
||||
|
||||
### Software environment
|
||||
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| OS | Ubuntu 22.04.5 LTS, Linux 5.10.134 |
|
||||
| Python | 3.12.13 |
|
||||
| NVIDIA driver / CUDA runtime | 570.133.20 / 13.0 |
|
||||
| PyTorch | 2.11.0+cu130 |
|
||||
| vLLM | 0.26.0 |
|
||||
| vLLM-Omni | This PR branch; evidence commits `a5af2a8c` and `42bbfb57` |
|
||||
| Precision | BF16 |
|
||||
|
||||
Set `MAGI2_DETERMINISTIC=1` before worker startup when deterministic kernels
|
||||
are required. The setting is fixed for the worker lifetime.
|
||||
|
||||
Each worker process supports one MAGI-2 pipeline instance. Pipeline startup
|
||||
sets process-wide deterministic state, so multi-pipeline tests and deployments
|
||||
must use separate worker processes rather than constructing multiple instances
|
||||
inside one process.
|
||||
|
||||
MAGI-2 uses vLLM's bundled FlashAttention 3 kernels by default on Hopper
|
||||
(compute capability 9.x), FlashAttention 4 on supported Blackwell devices
|
||||
with FA3 as the fallback, and FlashAttention 2 on earlier architectures. Set
|
||||
`MAGI2_FLASH_ATTN_VERSION=2`, `3`, or `4` before worker startup to force an
|
||||
available kernel for diagnostics or output comparisons.
|
||||
|
||||
## Offline generation
|
||||
|
||||
### Recommended four-GPU T2VA
|
||||
|
||||
```bash
|
||||
export CUDA_VISIBLE_DEVICES=0,1,2,3
|
||||
|
||||
python examples/offline_inference/text_to_video/text_to_video.py \
|
||||
--model "$MAGI2_CKPT_ROOT" \
|
||||
--model-class-name Magi2Pipeline \
|
||||
--prompt "A red fox walks through fresh snow while wind moves the pine branches." \
|
||||
--height 512 --width 896 --num-frames 125 \
|
||||
--num-inference-steps 100 --fps 12.5 \
|
||||
--tensor-parallel-size 1 --ulysses-degree 4 \
|
||||
--extra-body '{"seconds":10,"resolution":"540p"}' \
|
||||
--output magi2_540p_t2va.mp4
|
||||
```
|
||||
|
||||
For a load-and-kernel smoke, use one inference step. A one-step output is not a
|
||||
quality evaluation.
|
||||
|
||||
### Recommended four-GPU I2VA
|
||||
|
||||
Use the same geometry and parallelism with the shared I2V entrypoint:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/image_to_video/image_to_video.py \
|
||||
--model "$MAGI2_CKPT_ROOT" \
|
||||
--model-class-name Magi2Pipeline \
|
||||
--image /path/to/first_frame.png \
|
||||
--prompt "The fox looks up, then walks forward as snow falls around it." \
|
||||
--height 512 --width 896 --num-frames 125 \
|
||||
--num-inference-steps 100 --fps 12.5 \
|
||||
--tensor-parallel-size 1 --ulysses-degree 4 \
|
||||
--extra-body '{"seconds":10,"resolution":"540p"}' \
|
||||
--output magi2_540p_i2va.mp4
|
||||
```
|
||||
|
||||
### One-device layerwise offload
|
||||
|
||||
MAGI-2 already stages Qwen, the image VAE, TurboVAE, and Oobleck from pinned
|
||||
CPU memory. Ordinary layerwise offload streams the Preview DiT blocks so the
|
||||
complete transformer does not need to fit in HBM at once.
|
||||
|
||||
```bash
|
||||
export CUDA_VISIBLE_DEVICES=0
|
||||
|
||||
python examples/offline_inference/text_to_video/text_to_video.py \
|
||||
--model "$MAGI2_CKPT_ROOT" \
|
||||
--model-class-name Magi2Pipeline \
|
||||
--prompt "A red fox walks through fresh snow while wind moves the pine branches." \
|
||||
--height 256 --width 448 --num-frames 125 \
|
||||
--num-inference-steps 4 --fps 12.5 \
|
||||
--enable-cpu-offload --enable-layerwise-offload \
|
||||
--extra-body '{"seconds":10,"resolution":"272p"}' \
|
||||
--output magi2_272p_cpu_layerwise.mp4
|
||||
```
|
||||
|
||||
Standalone `--enable-cpu-offload` is rejected because staging whole modules
|
||||
does not make the complete Preview DiT fit on one qualified GPU. When both
|
||||
flags are supplied, the shared offloader selects layerwise mode and MAGI's
|
||||
native auxiliary staging remains active.
|
||||
|
||||
## Online serving
|
||||
|
||||
The OpenAI-compatible serving defaults are the full `540p` preset and `100`
|
||||
denoising steps. The model always generates a 10-second, 125-frame clip at
|
||||
12.5 fps; pass `resolution=272p` and `num_inference_steps=1` for a low-cost
|
||||
load smoke. Requests with another duration are rejected before generation.
|
||||
|
||||
```bash
|
||||
export CUDA_VISIBLE_DEVICES=0,1,2,3
|
||||
|
||||
vllm serve "$MAGI2_CKPT_ROOT" --omni \
|
||||
--model-class-name Magi2Pipeline \
|
||||
--num-gpus 4 \
|
||||
--tensor-parallel-size 1 \
|
||||
--ulysses-degree 4 \
|
||||
--port 8091
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/videos/sync \
|
||||
-F 'prompt=A red fox walks through fresh snow.' \
|
||||
-F 'seconds=10' \
|
||||
-F 'size=896x512' \
|
||||
-F 'num_frames=125' \
|
||||
-F 'fps=12.5' \
|
||||
-F 'num_inference_steps=100' \
|
||||
-F 'seed=42' \
|
||||
-F 'extra_params={"resolution":"540p"}' \
|
||||
-o magi2_online.mp4
|
||||
```
|
||||
|
||||
For I2VA, add `-F 'input_reference=@first_frame.png;type=image/png'`.
|
||||
|
||||
## Supported features
|
||||
|
||||
Use the shared guides for launch syntax and feature semantics. The flags below
|
||||
are the MAGI-2-specific qualified combinations.
|
||||
|
||||
| Feature | MAGI-2 status / topology | Guide |
|
||||
| --- | --- | --- |
|
||||
| Sequence parallel | Resident SP4 default; compatible SP8 configuration | [Sequence parallel](../../docs/user_guide/diffusion/parallelism/sequence_parallel.md) |
|
||||
| Tensor parallel | TP4 or TP2SP2 on four workers | [Tensor parallel](../../docs/user_guide/diffusion/parallelism/tensor_parallel.md) |
|
||||
| DLO | DP4/DP2SP2 AllGather; SP4 rank-local requires `--dlo-no-use-allgather` | [Distributed layerwise offload](../../docs/design/feature/distributed_layerwise_offload.md) |
|
||||
| HSDP | HSDP4+SP4; alternative to TP and DLO | [HSDP](../../docs/user_guide/diffusion/parallelism/hsdp.md) |
|
||||
| CFG parallel | CFG2xSP2 on four workers; CFG2xSP4 requires eight | [CFG parallel](../../docs/user_guide/diffusion/parallelism/cfg_parallel.md) |
|
||||
| VAE patch parallel | TurboVAE tile decode across the complete worker group | [VAE parallelism](../../docs/user_guide/diffusion/parallelism/vae_parallelism.md) |
|
||||
| Cache-DiT | Repeated Preview transformer layers; approximate | [Cache-DiT](../../docs/user_guide/diffusion/cache_acceleration/cache_dit.md) |
|
||||
| One-device offload | Ordinary layerwise DiT plus native auxiliary CPU staging | [CPU offload](../../docs/user_guide/diffusion/cpu_offload.md) |
|
||||
| Quantization | Not supported | [Feature matrix](../../docs/user_guide/diffusion_features.md) |
|
||||
| Ring / pipeline parallel | Not supported | [Feature matrix](../../docs/user_guide/diffusion_features.md) |
|
||||
|
||||
DP request waves must contain exactly `data_parallel_size` requests with the
|
||||
same explicit step count. Data-parallel replicas require TP=1. SP-only DLO
|
||||
cannot AllGather because SP ranks own different MoE-head shards.
|
||||
|
||||
## Request fields
|
||||
|
||||
Common geometry and sampling values use the shared CLI flags. The native
|
||||
Preview pipeline accepts these model-specific `--extra-body` fields:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `seconds` | Must be `10`. |
|
||||
| `resolution` | `272p` or `540p`; default `540p`. |
|
||||
| `output_width`, `output_height` | Optional final resize; supply both. |
|
||||
| `deterministic` | Must match `MAGI2_DETERMINISTIC` at worker startup. |
|
||||
|
||||
Use `--image` in the shared I2V entrypoint rather than the lower-level
|
||||
`image_path` field.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
ffprobe -v error \
|
||||
-show_entries stream=codec_type,width,height,avg_frame_rate,nb_frames,sample_rate,channels \
|
||||
-of json magi2_540p_t2va.mp4
|
||||
```
|
||||
|
||||
A 540p result contains 125 896x512 frames at 12.5 fps and stereo 44.1 kHz
|
||||
audio. A 272p result contains 125 448x256 frames with the same duration, rate,
|
||||
and audio contract.
|
||||
|
||||
## Local qualification evidence
|
||||
|
||||
### Released-checkpoint E2E
|
||||
|
||||
| Profile | Workload | Steps | E2E | Peak HBM | Peak host PSS | Output |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | --- |
|
||||
| DLO SP4 | 540p T2VA | 100 | 563.855 s | See PR evidence | See PR evidence | `3437d078e...f16fdea` |
|
||||
| DLO SP4 | 540p I2VA | 100 | 556.533 s | See PR evidence | See PR evidence | `b8c2a4ac...ff2de8` |
|
||||
| Layerwise 1 GPU | 272p T2VA | 1 | 8.86 s | 49.29 GiB | Not sampled | Valid video + audio |
|
||||
| Layerwise 1 GPU | 272p T2VA | 4 | 42.55 s | 49.29 GiB | 371.97 GiB | Valid video + audio |
|
||||
|
||||
The one-device four-step host-PSS sampler scans the full process tree and adds
|
||||
observable CPU overhead, so its stage timing is qualification evidence rather
|
||||
than a latency benchmark.
|
||||
|
||||
### Distributed correctness
|
||||
|
||||
- Resident TP4, TP2SP2, and SP4 match a single-rank native oracle within the
|
||||
documented BF16 tolerance; resident SP4 is the reference-aligned default.
|
||||
- Deterministic rank-local DLO SP4 output matched resident SP4 exactly.
|
||||
- HSDP4+SP4, HSDP4+CFG2xSP2, Cache-DiT forced hits, and TurboVAE PP4 have
|
||||
focused four-rank coverage. TurboVAE PP4 matched resident decode exactly.
|
||||
- Full 100-step 540p quality generation is qualified only for four-device DLO
|
||||
SP4. Eight-worker layouts remain configuration-only on this host.
|
||||
|
||||
These are bounded qualification runs, not committed benchmarks. Generated
|
||||
artifacts stay outside the repository, and this integration adds no
|
||||
model-specific example or benchmark script.
|
||||
+40
-36
@@ -1,6 +1,10 @@
|
||||
# Recipe Title
|
||||
# Recipe Title — Hardware
|
||||
|
||||
> Example: Qwen3-Omni for speech chat
|
||||
> Example: Qwen3-Omni for speech chat — H100
|
||||
|
||||
Name the file `recipes/<vendor>/<model>-<hardware>.md`. Keep exactly one
|
||||
hardware profile in each recipe; add a separate suffixed recipe when another
|
||||
accelerator is qualified.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -8,28 +12,44 @@
|
||||
- Model:
|
||||
- Task:
|
||||
- Mode:
|
||||
- Hardware:
|
||||
- Maintainer:
|
||||
|
||||
## When to use this recipe
|
||||
|
||||
Briefly describe the concrete scenario this recipe covers.
|
||||
|
||||
## Supported model contract
|
||||
|
||||
Put user-visible constraints before commands. Use compact tables for:
|
||||
|
||||
- supported tasks and shared entrypoints;
|
||||
- input modalities, counts, duration/geometry limits, and required combinations;
|
||||
- output duration, geometry, frame/audio rates, and variants;
|
||||
- the deployment profiles provided on this recipe's named hardware.
|
||||
|
||||
Source model specifications from the canonical model card or repository. Do
|
||||
not copy values from a sibling model or infer support from configuration alone.
|
||||
|
||||
## References
|
||||
|
||||
- Upstream or canonical docs:
|
||||
- Related example under `examples/`:
|
||||
- Related issue or discussion:
|
||||
|
||||
## Hardware Support
|
||||
## Hardware
|
||||
|
||||
Add one section per platform, such as `GPU`, `ROCm`, or `NPU`. Under each
|
||||
platform section, document one or more tested hardware configurations.
|
||||
- Accelerator model and per-device memory:
|
||||
- Number of devices:
|
||||
- Device interconnect (NVLink, PCIe, or other):
|
||||
- Host memory, when CPU offload or staging is relevant:
|
||||
- Qualification scope:
|
||||
|
||||
## GPU
|
||||
Do not add a second accelerator to this file. Create another
|
||||
`<model>-<hardware>.md` recipe instead. Mention unqualified upstream hardware
|
||||
only as a limitation, not as a provided profile.
|
||||
|
||||
### 1x A100 80GB
|
||||
|
||||
#### Environment
|
||||
## Software environment
|
||||
|
||||
- OS:
|
||||
- Python:
|
||||
@@ -37,7 +57,10 @@ platform section, document one or more tested hardware configurations.
|
||||
- vLLM version:
|
||||
- vLLM-Omni version or commit:
|
||||
|
||||
#### Command
|
||||
Keep worker counts, precision, and TP/SP/DP/PP sizes with the command or
|
||||
deployment profile; they are not software-environment properties.
|
||||
|
||||
## Command
|
||||
|
||||
Serve commands use `vllm serve <model> --omni`. The `--omni` flag is what
|
||||
selects the Omni pipeline, so it is required and cannot be dropped.
|
||||
@@ -46,40 +69,21 @@ selects the Omni pipeline, so it is required and cannot be dropped.
|
||||
# Add the exact command(s) here
|
||||
```
|
||||
|
||||
#### Verification
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Add a quick validation command or expected output here
|
||||
```
|
||||
|
||||
#### Notes
|
||||
## Notes
|
||||
|
||||
- Memory usage:
|
||||
- Key flags:
|
||||
- Known limitations:
|
||||
|
||||
### 2x L40S
|
||||
## Supported features
|
||||
|
||||
Repeat the same structure for other hardware setups as needed.
|
||||
|
||||
## ROCm
|
||||
|
||||
### Example hardware configuration
|
||||
|
||||
Repeat the same nested structure for ROCm setups as needed:
|
||||
|
||||
- `#### Environment`
|
||||
- `#### Command`
|
||||
- `#### Verification`
|
||||
- `#### Notes`
|
||||
|
||||
## NPU
|
||||
|
||||
### Example hardware configuration
|
||||
|
||||
Repeat the same nested structure for NPU setups as needed:
|
||||
|
||||
- `#### Environment`
|
||||
- `#### Command`
|
||||
- `#### Verification`
|
||||
- `#### Notes`
|
||||
Use a compact table containing the model-specific status/topology and a link to
|
||||
the corresponding shared feature guide. Do not repeat the general feature
|
||||
documentation in the recipe. Record unsupported combinations in the same table
|
||||
instead of a disconnected restrictions section.
|
||||
@@ -1,20 +1,19 @@
|
||||
# dots.tts
|
||||
|
||||
> Offline continuous-AR TTS at 48 kHz (rednote-hilab)
|
||||
> Continuous-AR TTS at 48 kHz (rednote-hilab)
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: rednote-hilab
|
||||
- Model: `rednote-hilab/dots.tts-soar`
|
||||
- Model: `dots-studio/dots.tts-soar`
|
||||
- Task: Text-to-speech, zero-shot synthesis only
|
||||
- Mode: Offline end-to-end example only (not yet wired for online serving —
|
||||
see [Known limitations](#known-limitations))
|
||||
- Mode: Offline inference and OpenAI-compatible online serving
|
||||
- Maintainer: Community
|
||||
|
||||
## When to use this recipe
|
||||
|
||||
Use this recipe as a known-good starting point for running
|
||||
`rednote-hilab/dots.tts-soar` offline on vLLM-Omni on consumer-class GPUs.
|
||||
`dots-studio/dots.tts-soar` on vLLM-Omni on consumer-class GPUs.
|
||||
dots.tts is a ~1.7B-parameter continuous-AR TTS model (Qwen2.5-1.5B base LM
|
||||
+ 344M DiT flow-matching head + 180M AudioVAE) that emits 48 kHz mono audio.
|
||||
It follows the same "vLLM-native base LM + side-path computation" pattern as
|
||||
@@ -23,8 +22,8 @@ VoxCPM2 — single-stage pipeline
|
||||
loopback → AudioVAE (streaming decode)` — with a plain Qwen2 backbone
|
||||
instead of MiniCPM4, and no FSQ / residual-LM stage.
|
||||
|
||||
This is an early integration (see [Known limitations](#known-limitations)
|
||||
below before depending on it for anything beyond offline experimentation).
|
||||
This is an early integration; review the [Known limitations](#known-limitations)
|
||||
before deploying it in production.
|
||||
|
||||
## References
|
||||
|
||||
@@ -32,6 +31,8 @@ below before depending on it for anything beyond offline experimentation).
|
||||
[`examples/offline_inference/text_to_speech/dots_tts/end2end.py`](../../examples/offline_inference/text_to_speech/dots_tts/end2end.py)
|
||||
- Example guide:
|
||||
[`examples/offline_inference/text_to_speech/README.md`](../../examples/offline_inference/text_to_speech/README.md#dotstts)
|
||||
- Online serving guide:
|
||||
[`examples/online_serving/text_to_speech/README.md`](../../examples/online_serving/text_to_speech/README.md#dotstts)
|
||||
- Default deploy config:
|
||||
[`vllm_omni/deploy/dots_tts.yaml`](../../vllm_omni/deploy/dots_tts.yaml)
|
||||
- Talker / pipeline source:
|
||||
@@ -73,7 +74,7 @@ release runs `enforce_eager: true`, so it doesn't apply today).
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/dots_tts/end2end.py \
|
||||
--model rednote-hilab/dots.tts-soar \
|
||||
--model dots-studio/dots.tts-soar \
|
||||
--text "Hello, this is a test of dots TTS running on vLLM Omni."
|
||||
```
|
||||
|
||||
@@ -88,7 +89,7 @@ Pass `--deploy-config <path>` to override.
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/dots_tts/end2end.py \
|
||||
--model rednote-hilab/dots.tts-soar \
|
||||
--model dots-studio/dots.tts-soar \
|
||||
--text "Hello, this is a test of dots TTS running on vLLM Omni."
|
||||
```
|
||||
|
||||
@@ -106,10 +107,29 @@ Whisper transcription of the output matched the input text with no
|
||||
dropped leading word (confirms the streaming-vocoder patch-boundary fix
|
||||
described in [Known limitations](#known-limitations)).
|
||||
|
||||
**T2 — online text-only synthesis**:
|
||||
|
||||
```bash
|
||||
vllm serve dots-studio/dots.tts-soar --omni --trust-remote-code --port 8091
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"input": "Hello, this is a test of dots TTS online serving.",
|
||||
"voice": "default",
|
||||
"response_format": "wav"
|
||||
}' --output output.wav
|
||||
```
|
||||
|
||||
The endpoint also supports raw streaming audio with `stream=true`,
|
||||
`stream_format="audio"`, and `response_format="pcm"`.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Output: 48 kHz mono WAV.
|
||||
- Checkpoints: `rednote-hilab/dots.tts-soar` is the validated default
|
||||
- Checkpoints: `dots-studio/dots.tts-soar` is the validated default
|
||||
used throughout this recipe. `rednote-hilab/dots.tts-base` shares the
|
||||
same architecture but is unvalidated in this repo. `rednote-hilab/dots.tts-mf`
|
||||
(MeanFlow, 2-4 step) is not supported — see below.
|
||||
@@ -123,17 +143,11 @@ described in [Known limitations](#known-limitations)).
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **No online serving yet.** dots.tts is not registered in
|
||||
`vllm_omni/entrypoints/openai/tts_adapters/` (the framework other TTS
|
||||
models in this repo use for `/v1/audio/speech`), so `vllm serve
|
||||
rednote-hilab/dots.tts-soar --omni` will start but the OpenAI-compatible
|
||||
speech endpoint does not know how to build request params for it yet.
|
||||
Offline `Omni()` (as used in `end2end.py`) is the only supported path
|
||||
today.
|
||||
- **Voice cloning is not wired.** The CAM++ x-vector speaker encoder
|
||||
weights load and are exercised by `load_weights()`, but nothing in the
|
||||
prompt builder consumes a reference audio yet — generation is zero-shot
|
||||
only. `end2end.py` has no `--ref-audio`/`--ref-text` flags.
|
||||
prompt builder consumes reference audio or a precomputed speaker embedding
|
||||
yet. Both offline and online generation are text-only; `ref_audio`,
|
||||
`ref_text`, named voices, and speaker embeddings are not supported.
|
||||
- **`dots.tts-mf` (MeanFlow, 2-4 step) checkpoint is not supported.** Only
|
||||
the fixed 10-step Euler DiT sampler used by `dots.tts-soar` /
|
||||
`dots.tts-base` is implemented.
|
||||
|
||||
@@ -13,8 +13,9 @@ transformers >= 5.10.1, < 5.15
|
||||
av>=14.0.0
|
||||
omegaconf>=2.3.0
|
||||
diffusers==0.40.0
|
||||
# Hub kernels, including LTX-2.5 NATTEN
|
||||
kernels==0.14.1
|
||||
# Hub kernels, including LTX-2.5 NATTEN. Keep this pin in sync with the
|
||||
# supported transformers range above.
|
||||
kernels==0.15.2
|
||||
safetensors>=0.8.0
|
||||
accelerate==1.12.0
|
||||
soundfile>=0.13.1
|
||||
|
||||
@@ -28,7 +28,7 @@ from vllm_omni.benchmarks.patch.patch import (
|
||||
async_request_openai_realtime_duplex,
|
||||
should_request_stage_metrics,
|
||||
)
|
||||
from vllm_omni.experimental.fullduplex.client import RealtimeEventCollector
|
||||
from vllm_omni.clients.duplex import EventCollector
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.benchmark, pytest.mark.cpu]
|
||||
|
||||
@@ -131,7 +131,7 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
|
||||
|
||||
def __init__(self, url):
|
||||
assert url == "ws://localhost:8000/v1/realtime?duplex=1"
|
||||
self.events = RealtimeEventCollector()
|
||||
self.events = EventCollector()
|
||||
self.configure_kwargs = None
|
||||
self.sent = []
|
||||
self.response_count = 0
|
||||
@@ -206,7 +206,7 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm_omni.benchmarks.patch.patch.RealtimeDuplexClient",
|
||||
"vllm_omni.benchmarks.patch.patch._RealtimeTTSProbe",
|
||||
FakeRealtimeClient,
|
||||
)
|
||||
request_input = RequestFuncInput(
|
||||
|
||||
Loaded 100 of 475 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user