Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d29a35bb65 | ||
|
|
f2441ddd2a | ||
|
|
9454e7987c | ||
|
|
2c43541fa7 | ||
|
|
bbee488da3 | ||
|
|
2560f91859 | ||
|
|
7b15d22f48 | ||
|
|
423f34326e | ||
|
|
a038b38179 | ||
|
|
27a6321a63 | ||
|
|
4af28f33bd | ||
|
|
56763cd6c3 | ||
|
|
b44b2f981c | ||
|
|
c524e1a879 | ||
|
|
d0f073686a | ||
|
|
004a5711dc | ||
|
|
e34f8c4f42 | ||
|
|
aa41a3ac97 | ||
|
|
989161c49d | ||
|
|
3a82c85880 | ||
|
|
c27a52a359 | ||
|
|
25bdb38b11 | ||
|
|
146ccc133a | ||
|
|
9097fa6547 | ||
|
|
35cf6ac355 | ||
|
|
3a28882946 | ||
|
|
8ad6e801f5 | ||
|
|
5af3a16526 | ||
|
|
43ef1e61fb | ||
|
|
764a109f66 | ||
|
|
1ea0ee3b38 | ||
|
|
c85f1a44fb | ||
|
|
f145f66c82 | ||
|
|
001f8a80b9 | ||
|
|
6b4173f50b | ||
|
|
7d6904aa94 | ||
|
|
235c430326 | ||
|
|
1c1fdf719c | ||
|
|
5b3abe2f4d | ||
|
|
3e62bba3cb | ||
|
|
0173cb3748 | ||
|
|
d31dbc5b0a | ||
|
|
7266fc6138 | ||
|
|
b656fcf45b | ||
|
|
12e9280010 | ||
|
|
a07384b953 | ||
|
|
965ad68f1d | ||
|
|
e823f0d909 | ||
|
|
29b818a785 | ||
|
|
79a46a4f54 | ||
|
|
2a453c5ad8 | ||
|
|
9c688aeba0 | ||
|
|
5ce6f5dc49 | ||
|
|
c0331048d0 | ||
|
|
f5bdb20e9d | ||
|
|
36b74f99f6 | ||
|
|
c8a1419e02 | ||
|
|
817f5d0e4d | ||
|
|
1b97ff1188 | ||
|
|
b63e35ab4f | ||
|
|
798d52ce55 | ||
|
|
7ab582ae25 | ||
|
|
6715c57b2d | ||
|
|
d418004b6b | ||
|
|
eb67caaaa9 | ||
|
|
e75278dccc | ||
|
|
e1303e5067 | ||
|
|
8a49c65bf9 | ||
|
|
acf6620233 | ||
|
|
ad025defe6 | ||
|
|
bcb4ae288f | ||
|
|
62ebad2d7c | ||
|
|
5e36b5a86b |
No files matched your search
@@ -41,7 +41,33 @@ steps:
|
||||
- export VLLM_ROCM_USE_AITER=0
|
||||
# ignore test_teacache_extractors.py because it use rocm gemm kernel from vLLM
|
||||
# that is not supported on CPU
|
||||
- "pytest -sv tests/diffusion -m 'core_model and cpu' --ignore=tests/diffusion/cache/test_teacache_extractors.py --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB"
|
||||
# Keep multi-GPU tests out of this one-GPU pool even when their module
|
||||
# also carries the cpu marker for unit tests.
|
||||
- "pytest -sv tests/diffusion -m 'core_model and cpu and not (cards_2 or cards_3 or cards_4 or cards_5 or cards_6 or cards_7 or cards_8)' --ignore=tests/diffusion/cache/test_teacache_extractors.py --ignore=tests/diffusion/models/pi05/test_pi05_units.py --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB"
|
||||
# This module takes about 22 minutes on MI300 and was consistently assigned to shard 4,
|
||||
# pushing that shard to the 45-minute timeout. Keep its module-scoped fixture in one lane.
|
||||
- label: "Simple · Pi0.5 CPU Test"
|
||||
agent_pool: mi300_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
timeout_in_minutes: 45
|
||||
commands:
|
||||
- export VLLM_ROCM_USE_AITER=0
|
||||
- "pytest -sv tests/diffusion/models/pi05/test_pi05_units.py -m 'core_model and cpu'"
|
||||
|
||||
- label: "Diffusion · AuK VAE GPU Test"
|
||||
agent_pool: mi300_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
timeout_in_minutes: 40
|
||||
commands:
|
||||
- export VLLM_ROCM_USE_AITER=0
|
||||
- >-
|
||||
timeout --signal=TERM --kill-after=2m 35m
|
||||
pytest -sv tests/diffusion/models/auk/test_auk_vae_fastpath.py
|
||||
-m 'core_model and rocm and cards_1' --run-level core_model
|
||||
|
||||
- label: "Simple · Engine&Entrypoints Test"
|
||||
agent_pool: mi300_1
|
||||
@@ -131,6 +157,11 @@ steps:
|
||||
grade: Blocking
|
||||
commands:
|
||||
- pytest -s -v tests/diffusion/distributed/test_tensor_parallel.py
|
||||
- >-
|
||||
timeout 20m pytest -s -v
|
||||
tests/diffusion/attention/test_ulysses_uaa.py::test_ulysses_uaa_2d_mask_layout_matches_baseline
|
||||
-m 'core_model and cards_2'
|
||||
--run-level "core_model"
|
||||
|
||||
- label: "Diffusion Model Test"
|
||||
agent_pool: mi300_1
|
||||
@@ -138,7 +169,9 @@ steps:
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- timeout 30m pytest -s -v tests/e2e/offline_inference/test_z_image.py
|
||||
# A cold Z-Image run spends about six minutes loading weights before
|
||||
# the first-request AITER compilation, which can itself exceed 23m.
|
||||
- timeout 55m pytest -s -v tests/e2e/offline_inference/test_z_image.py
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
agent_pool: mi300_2
|
||||
|
||||
@@ -41,7 +41,33 @@ steps:
|
||||
- export VLLM_ROCM_USE_AITER=0
|
||||
# ignore test_teacache_extractors.py because it use rocm gemm kernel from vLLM
|
||||
# that is not supported on CPU
|
||||
- "pytest -sv tests/diffusion -m 'core_model and cpu' --ignore=tests/diffusion/cache/test_teacache_extractors.py --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB"
|
||||
# Keep multi-GPU tests out of this one-GPU pool even when their module
|
||||
# also carries the cpu marker for unit tests.
|
||||
- "pytest -sv tests/diffusion -m 'core_model and cpu and not (cards_2 or cards_3 or cards_4 or cards_5 or cards_6 or cards_7 or cards_8)' --ignore=tests/diffusion/cache/test_teacache_extractors.py --ignore=tests/diffusion/models/pi05/test_pi05_units.py --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB"
|
||||
# This module takes about 22 minutes on MI300 and was consistently assigned to shard 4,
|
||||
# pushing that shard to the 45-minute timeout. Keep its module-scoped fixture in one lane.
|
||||
- label: "Simple · Pi0.5 CPU Test"
|
||||
agent_pool: mi300_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
timeout_in_minutes: 45
|
||||
commands:
|
||||
- export VLLM_ROCM_USE_AITER=0
|
||||
- "pytest -sv tests/diffusion/models/pi05/test_pi05_units.py -m 'core_model and cpu'"
|
||||
|
||||
- label: "Diffusion · AuK VAE GPU Test"
|
||||
agent_pool: mi300_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
timeout_in_minutes: 40
|
||||
commands:
|
||||
- export VLLM_ROCM_USE_AITER=0
|
||||
- >-
|
||||
timeout --signal=TERM --kill-after=2m 35m
|
||||
pytest -sv tests/diffusion/models/auk/test_auk_vae_fastpath.py
|
||||
-m 'core_model and rocm and cards_1' --run-level core_model
|
||||
|
||||
- label: "Simple · Engine&Entrypoints Test"
|
||||
agent_pool: mi300_1
|
||||
@@ -141,6 +167,11 @@ steps:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- timeout 20m pytest -s -v tests/diffusion/distributed/test_sequence_parallel.py -m core_model
|
||||
- >-
|
||||
timeout 20m pytest -s -v
|
||||
tests/diffusion/attention/test_ulysses_uaa.py::test_ulysses_uaa_2d_mask_layout_matches_baseline
|
||||
-m 'core_model and cards_2'
|
||||
--run-level "core_model"
|
||||
- >-
|
||||
timeout 20m pytest -s -v
|
||||
tests/diffusion/models/ltx2/test_ltx2_transformer_ulysses.py
|
||||
@@ -165,6 +196,7 @@ steps:
|
||||
# registered multi-GPU marker from this one-GPU job.
|
||||
- >-
|
||||
timeout 15m pytest -sv tests/diffusion/models/
|
||||
--ignore=tests/diffusion/models/auk/test_auk_vae_fastpath.py
|
||||
-m 'core_model and cuda and not
|
||||
(cards_2 or cards_3 or cards_4 or cards_5 or cards_6 or cards_7 or cards_8)'
|
||||
--run-level "core_model"
|
||||
|
||||
@@ -20,6 +20,7 @@ x-qwen3-tts: &qwen3_tts
|
||||
- vllm_omni/model_executor/models/common/alias_free_activation.py
|
||||
- vllm_omni/model_executor/stage_input_processors/qwen3_tts.py
|
||||
- vllm_omni/deploy/qwen3_tts.yaml
|
||||
- vllm_omni/deploy/qwen3_tts_high_concurrency_mrv2_single_gpu.yaml
|
||||
- vllm_omni/entrypoints/openai/serving_speech.py
|
||||
- vllm_omni/entrypoints/openai/tts_adapters/
|
||||
- vllm_omni/engine/stage_pool.py
|
||||
@@ -30,6 +31,9 @@ x-qwen3-tts: &qwen3_tts
|
||||
- vllm_omni/config/omni_config.py
|
||||
- vllm_omni/engine/orchestrator.py
|
||||
- vllm_omni/engine/stage_engine_core_proc.py
|
||||
- vllm_omni/engine/cuda_mps.py
|
||||
- vllm_omni/engine/stage_runtime.py
|
||||
- vllm_omni/engine/stage_init_utils.py
|
||||
- vllm_omni/deploy/qwen3_tts_mrv2.yaml
|
||||
- vllm_omni/deploy/qwen3_tts_high_concurrency_mrv2.yaml
|
||||
- vllm_omni/deploy/qwen3_tts_high_concurrency_mrv2_b4.yaml
|
||||
@@ -418,6 +422,7 @@ source_file_dependencies:
|
||||
|
||||
diffusion_minimax_h3_function:
|
||||
- *minimax_h3
|
||||
- tests/helpers/assertions.py
|
||||
- tests/e2e/online_serving/test_minimax_h3_dlo_dp2_t2va.py
|
||||
- tests/e2e/online_serving/minimax_h3/
|
||||
- tests/diffusion/models/minimax_h3/
|
||||
|
||||
@@ -208,3 +208,14 @@ steps:
|
||||
timeout 35m pytest -s -v tests/e2e/online_serving/minimax_h3/test_minimax_h3_turbo_lora.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
|
||||
timeout 35m pytest -s -v tests/e2e/online_serving/minimax_h3/test_minimax_h3_fasth3_lora.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
|
||||
mirror_hardwares: h100_2
|
||||
|
||||
- label: "Diffusion · MiniMax-H3 Latent Upscale and Refine"
|
||||
source_file_dependencies: diffusion_minimax_h3_function
|
||||
timeout_in_minutes: 40
|
||||
commands:
|
||||
- |
|
||||
set -eu
|
||||
VLLM_TEST_MINIMAX_H3_LATENT_UPSCALER=$(python3 -c 'from huggingface_hub import hf_hub_download; print(hf_hub_download(repo_id="LBH-123-AI/Minimax_h3_latent_Upscaler", filename="minimax_h3_latent_upscaler_3d_conv_v1/minimax_h3_latent_upscaler_3d_conv_v1_bf16.safetensors"))')
|
||||
export VLLM_TEST_MINIMAX_H3_LATENT_UPSCALER
|
||||
timeout 35m pytest -s -v tests/e2e/online_serving/minimax_h3/test_minimax_h3.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
|
||||
mirror_hardwares: h100_2
|
||||
@@ -21,7 +21,7 @@ steps:
|
||||
- omni_minicpmo_4_5_function
|
||||
timeout_in_minutes: 120
|
||||
commands:
|
||||
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and B200 and omni and cards_1" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
|
||||
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and B200 and omni and cards_1" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py --ignore=tests/e2e/online_serving/test_minicpmo_4_5_window.py
|
||||
|
||||
- label: ":full_moon: Omni · Function Test with H100 · 2-GPU"
|
||||
source_file_dependencies:
|
||||
@@ -33,9 +33,10 @@ steps:
|
||||
|
||||
- label: ":full_moon: Omni · MiniCPM-o 4.5 Duplex Test"
|
||||
source_file_dependencies: omni_minicpmo_4_5_duplex_function
|
||||
timeout_in_minutes: 50
|
||||
timeout_in_minutes: 120
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py -m "full_model and cuda and H100 and B200 and omni and cards_1" --run-level "full_model"
|
||||
- pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_window.py -m "full_model and cuda and H100 and B200 and omni and cards_1" --run-level "full_model"
|
||||
|
||||
- label: ":full_moon: Omni · Doc Test with L4 · 4-GPU"
|
||||
source_file_dependencies: omni_qwen2_5_omni_doc
|
||||
@@ -229,17 +230,6 @@ steps:
|
||||
-m "full_model and diffusion and H100 and B200 and cards_1"
|
||||
--run-level "full_model"
|
||||
|
||||
- label: ":robot_face: Robot Policy OpenPI · π0.5 · H100 · Single-GPU"
|
||||
timeout_in_minutes: 120
|
||||
commands:
|
||||
- pip install --no-deps "openpi-client @ git+https://github.com/Physical-Intelligence/openpi.git@215abfb217dbac7d5f1273282331b9b1866c0479#subdirectory=packages/openpi-client"
|
||||
- >-
|
||||
pytest -sv
|
||||
tests/e2e/online_serving/test_pi05_expansion.py
|
||||
-m "full_model and diffusion and H100 and cards_1"
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with H100 · 2-GPU"
|
||||
source_file_dependencies: diffusion_qwen_image_function
|
||||
timeout_in_minutes: 120
|
||||
|
||||
@@ -16,6 +16,15 @@ steps:
|
||||
- "pytest -sv tests/model_executor -m 'core_model and cpu'"
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "Simple · CosyVoice3 Packed Flow Test"
|
||||
timeout_in_minutes: 15
|
||||
source_file_dependencies:
|
||||
- tests/model_executor/models/cosyvoice3/test_cosyvoice3_full_batch.py
|
||||
- vllm_omni/model_executor/models/cosyvoice3/
|
||||
commands:
|
||||
- "pytest -sv tests/model_executor/models/cosyvoice3/test_cosyvoice3_full_batch.py -k packed -m 'core_model and cuda' --run-level=core_model"
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: "Simple · Diffusion Test"
|
||||
timeout_in_minutes: 30
|
||||
commands:
|
||||
@@ -196,6 +205,16 @@ steps:
|
||||
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minicpmo_4_5_ready.json -m 'core_model and cuda'
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: "TTS · Higgs v3 Full Response Test"
|
||||
source_file_dependencies:
|
||||
- tests/e2e/online_serving/test_higgs_audio_v3.py
|
||||
- vllm_omni/model_executor/models/higgs_audio_v3/
|
||||
- vllm_omni/model_executor/stage_input_processors/higgs_audio_v3.py
|
||||
- vllm_omni/deploy/higgs_multimodal_qwen3.yaml
|
||||
commands:
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_higgs_audio_v3.py::test_full_response_v1_preserves_audio -m 'core_model and cuda' --run-level 'core_model'
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: "TTS · Qwen3-TTS CustomVoice Test"
|
||||
source_file_dependencies: tts_qwen3_tts_function
|
||||
commands:
|
||||
|
||||
@@ -152,15 +152,122 @@ steps:
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: h100_4
|
||||
|
||||
- label: "TTS · L4"
|
||||
timeout_in_minutes: 120
|
||||
- label: "TTS · Moss · L4"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- export VLLM_USE_DEEP_GEMM="0"
|
||||
- export VLLM_MOE_USE_DEEP_GEMM="0"
|
||||
- |
|
||||
failed=0
|
||||
for f in $(pytest --collect-only -q tests/e2e/ -m "slow and L4 and tts" --run-level "full_model" | grep '\.py::' | sed -E 's|::.*||' | sort -u); do
|
||||
for f in \
|
||||
tests/e2e/offline_inference/test_moss_tts_expansion.py \
|
||||
tests/e2e/offline_inference/test_moss_tts_nano_expansion.py \
|
||||
tests/e2e/offline_inference/test_moss_tts_realtime.py \
|
||||
tests/e2e/offline_inference/test_moss_tts_realtime_expansion.py \
|
||||
tests/e2e/online_serving/test_moss_tts_expansion.py \
|
||||
tests/e2e/online_serving/test_moss_tts_nano_expansion.py
|
||||
do
|
||||
pytest -s -v "$$f" -m "slow and L4 and tts" --run-level "full_model" || failed=1
|
||||
done
|
||||
exit $$failed
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "TTS · VoxCPM2 · Voxtral · L4"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- export VLLM_USE_DEEP_GEMM="0"
|
||||
- export VLLM_MOE_USE_DEEP_GEMM="0"
|
||||
- |
|
||||
failed=0
|
||||
for f in \
|
||||
tests/e2e/offline_inference/test_voxcpm2_tts.py \
|
||||
tests/e2e/online_serving/test_voxcpm2_tts.py \
|
||||
tests/e2e/online_serving/test_voxcpm2_tts_expansion.py \
|
||||
tests/e2e/online_serving/test_voxcpm2_tts_lora.py \
|
||||
tests/e2e/offline_inference/test_voxtral_tts_expansion.py \
|
||||
tests/e2e/online_serving/test_voxtral_tts_expansion.py
|
||||
do
|
||||
pytest -s -v "$$f" -m "slow and L4 and tts" --run-level "full_model" || failed=1
|
||||
done
|
||||
exit $$failed
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "TTS · Higgs · L4"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- export VLLM_USE_DEEP_GEMM="0"
|
||||
- export VLLM_MOE_USE_DEEP_GEMM="0"
|
||||
- |
|
||||
failed=0
|
||||
for f in \
|
||||
tests/e2e/offline_inference/test_higgs_audio_v2_expansion.py \
|
||||
tests/e2e/online_serving/test_higgs_audio_v2_expansion.py \
|
||||
tests/e2e/online_serving/test_higgs_audio_v3.py
|
||||
do
|
||||
pytest -s -v "$$f" -m "slow and L4 and tts" --run-level "full_model" || failed=1
|
||||
done
|
||||
exit $$failed
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "TTS · OmniVoice · Step-Audio · L4"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- export VLLM_USE_DEEP_GEMM="0"
|
||||
- export VLLM_MOE_USE_DEEP_GEMM="0"
|
||||
- |
|
||||
failed=0
|
||||
for f in \
|
||||
tests/e2e/offline_inference/test_omnivoice_expansion.py \
|
||||
tests/e2e/online_serving/test_omnivoice_expansion.py \
|
||||
tests/e2e/online_serving/test_omnivoice_parity.py \
|
||||
tests/e2e/offline_inference/test_step_audio2_expansion.py
|
||||
do
|
||||
pytest -s -v "$$f" -m "slow and L4 and tts" --run-level "full_model" || failed=1
|
||||
done
|
||||
exit $$failed
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "TTS · Ming · GLM · IndexTTS · Breeze · L4"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- export VLLM_USE_DEEP_GEMM="0"
|
||||
- export VLLM_MOE_USE_DEEP_GEMM="0"
|
||||
- |
|
||||
failed=0
|
||||
for f in \
|
||||
tests/e2e/offline_inference/test_ming_tts_expansion.py \
|
||||
tests/e2e/online_serving/test_ming_tts_expansion.py \
|
||||
tests/e2e/offline_inference/test_glm_tts_expansion.py \
|
||||
tests/e2e/online_serving/test_glm_tts_expansion.py \
|
||||
tests/e2e/offline_inference/test_indextts2_expansion.py \
|
||||
tests/e2e/online_serving/test_indextts2_expansion.py \
|
||||
tests/e2e/online_serving/test_breeze_tts_2.py
|
||||
do
|
||||
pytest -s -v "$$f" -m "slow and L4 and tts" --run-level "full_model" || failed=1
|
||||
done
|
||||
exit $$failed
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- label: "TTS · Audex · CosyVoice · Qwen3-TTS · L4"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- export VLLM_USE_DEEP_GEMM="0"
|
||||
- export VLLM_MOE_USE_DEEP_GEMM="0"
|
||||
- |
|
||||
failed=0
|
||||
for f in \
|
||||
tests/e2e/offline_inference/test_audex_tts.py \
|
||||
tests/e2e/offline_inference/test_audex_s2t.py \
|
||||
tests/e2e/offline_inference/test_audex_tta.py \
|
||||
tests/e2e/offline_inference/test_cosyvoice3_expansion.py \
|
||||
tests/e2e/online_serving/test_qwen3_tts_word_timestamps.py
|
||||
do
|
||||
pytest -s -v "$$f" -m "slow and L4 and tts" --run-level "full_model" || failed=1
|
||||
done
|
||||
exit $$failed
|
||||
@@ -224,6 +331,17 @@ steps:
|
||||
|
||||
# Split the old shared H100 Single-GPU sweep: one pytest process collected
|
||||
# every slow+cards_1 diffusion file and exceeded 150 minutes.
|
||||
- label: ":robot_face: Robot Policy OpenPI · π0.5 · H100 · Single-GPU"
|
||||
timeout_in_minutes: 120
|
||||
commands:
|
||||
- pip install --no-deps "openpi-client @ git+https://github.com/Physical-Intelligence/openpi.git@215abfb217dbac7d5f1273282331b9b1866c0479#subdirectory=packages/openpi-client"
|
||||
- >-
|
||||
pytest -sv
|
||||
tests/e2e/online_serving/test_pi05_expansion.py
|
||||
-m "slow and diffusion and H100 and cards_1"
|
||||
--run-level "full_model"
|
||||
mirror_hardwares: h100_1
|
||||
|
||||
- label: "Diffusion · Qwen-Image · H100 · Single-GPU"
|
||||
timeout_in_minutes: 120
|
||||
commands:
|
||||
|
||||
@@ -64,6 +64,11 @@ steps:
|
||||
tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
|
||||
-m "npu and A3 and omni"
|
||||
--run-level full_model
|
||||
- >-
|
||||
pytest -s -v
|
||||
tests/e2e/online_serving/test_minicpmo_4_5_window.py
|
||||
-m "full_model and npu and A3 and omni"
|
||||
--run-level full_model
|
||||
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Accuracy Test"
|
||||
source_file_dependencies: omni_minicpmo_4_5_accuracy
|
||||
timeout_in_minutes: 180
|
||||
|
||||
@@ -9,6 +9,15 @@ Review like a maintainer: direct, selective, and focused on issues that CI does
|
||||
not prove. Prefer a few high-confidence findings over exhaustive commentary.
|
||||
Zero findings is a valid result.
|
||||
|
||||
## Target-version evidence
|
||||
|
||||
Resolve Python/dependency compatibility, pytest selectors and hardware needs
|
||||
from the frozen head's metadata, requirements, CI, and fixtures. Run levels such
|
||||
as `core_model` do not imply CPU-only execution. Read the target's accepted
|
||||
module contracts; draft or deferred architecture is not a merge requirement.
|
||||
Recheck only affected assumptions when reviewing another release. Historical
|
||||
coverage does not establish compatibility with a new snapshot.
|
||||
|
||||
## Quality contract
|
||||
|
||||
Make every finding:
|
||||
@@ -103,6 +112,12 @@ If docs and live code disagree, verify the code/tests and report the drift.
|
||||
| [examples-policy.md](../precheck-pr/references/examples-policy.md) | The PR adds, copies, or renames Python under `examples/`; apply the canonical policy shared with `precheck-pr`. |
|
||||
| [find-simplifications](../find-simplifications/SKILL.md) | Every review; run a diff-scoped subtraction and simplification pass after correctness blockers. |
|
||||
|
||||
### Batch sessions and review helpers
|
||||
|
||||
For a requested batch session, unanswered review replies, or inline-coordinate
|
||||
validation, read [batch-review.md](references/process/batch-review.md). The
|
||||
repository owns the helper scripts and their offline regression tests.
|
||||
|
||||
### Delivery and reviewer coordination
|
||||
|
||||
| Reference | Read when |
|
||||
@@ -110,6 +125,15 @@ If docs and live code disagree, verify the code/tests and report the drift.
|
||||
| [maintainer-style-study.md](references/delivery/maintainer-style-study.md) | Findings are ready for concise maintainer-style delivery. |
|
||||
| [review-requests.md](references/delivery/review-requests.md) | The user asks to identify, suggest, request, or ping code-owner reviewers. |
|
||||
|
||||
## Optional review workers
|
||||
|
||||
For independent defect discovery on a large diff, or when the user selects
|
||||
`auto`, `codex`, `claude`, or `none`, read
|
||||
[reviewer-backends.md](references/process/reviewer-backends.md). Pass immutable
|
||||
base/head SHAs and a bounded read-only scope. The parent owns domain routing,
|
||||
verification, deduplication, severity, and any separately authorized posting.
|
||||
Missing optional tooling falls back to direct review without blocking it.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Freeze and report the snapshot
|
||||
|
||||
@@ -20,11 +20,15 @@ were reverted or broken. Check:
|
||||
tolerances are deterministic or explicitly controlled;
|
||||
- normal, invalid, boundary, feature-off, failure/cancellation, and regression
|
||||
paths are covered where the diff changes them;
|
||||
- run-level and domain markers place hardware/model tests in the intended CI
|
||||
lane without silently skipping the contract.
|
||||
- read the target's pytest configuration, collection hooks, hardware helpers,
|
||||
and CI together: run-level/domain/backend/SKU/card-count selectors must place
|
||||
tests in the intended lane. `core_model` alone is not CPU-only; use explicit
|
||||
hardware selection such as `core_model and cpu` only when supported by the
|
||||
target. Zero selected or skipped accelerator tests are not validation.
|
||||
|
||||
Map source symbols to tests with bounded `rg` searches; do not assume the test
|
||||
tree mirrors production paths.
|
||||
Map source symbols to tests with bounded `rg` searches and inspect the target's
|
||||
test-writing guide. Components may mirror source while model E2E and feature
|
||||
suites live elsewhere; imports, fixtures, and assertions establish coverage.
|
||||
|
||||
## Runtime check
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Requested batch review and helper tools
|
||||
|
||||
Use only for a requested batch session, review follow-up, or proposed inline
|
||||
comments. Commands below run from this skill directory, require `gh`, `jq`,
|
||||
Python, and Bash, and only read GitHub. Resolve the connected reviewer's login;
|
||||
do not infer it from examples or repository ownership.
|
||||
|
||||
This is generic candidate discovery, not a daily eligibility policy. If the
|
||||
user's session specifies eligibility labels, a changed head since prior review,
|
||||
a daily cap, or another selection rule, build that eligible set from current
|
||||
metadata first. Do not let the legacy selector's own-PR/already-reviewed filters
|
||||
silently override it: select directly when those filters would discard an
|
||||
explicitly eligible re-review. User-selected PRs do not need to pass the helper.
|
||||
|
||||
1. Check outstanding replies first when the session includes follow-up:
|
||||
`bash scripts/check_replies.sh --reviewer <login> --days 14`.
|
||||
Reopen each returned thread and inspect its current resolution before
|
||||
deciding whether a response is needed. A reply can already be addressed by
|
||||
code or another maintainer; do not send an automatic acknowledgement.
|
||||
2. Select candidates with
|
||||
`bash scripts/select_prs.sh --reviewer <login> --days 7 --limit 5`.
|
||||
This preserves the existing selector: open recent PRs, excluding own,
|
||||
draft/WIP/"don't merge", and already-reviewed PRs, with low-review-count
|
||||
candidates first. It does not filter ordinary docs/CI PRs or prove
|
||||
mergeability, CI success, or complete search coverage.
|
||||
3. Freeze and review each selected PR using the normal execution contract.
|
||||
Keep CI conclusions separate from conflicts/mergeability. Match depth to
|
||||
changed behavior, not candidate rank; no quota requires manufacturing
|
||||
findings. Search/review APIs can be limited or fail; inspect raw status when
|
||||
coverage matters rather than treating a zero count as a verified clean state.
|
||||
4. Before separately authorized inline posting, validate the proposed payload:
|
||||
`printf '%s' "$REVIEW_JSON" | bash scripts/verify_line_numbers.sh <PR>`.
|
||||
Input is an object with a `comments` array containing `path`, `line`,
|
||||
optional `side` (default `RIGHT`), and optional same-side `start_line`.
|
||||
It checks actual diff hunks, including deletion/rename paths. It does not
|
||||
verify the finding or submit it. Re-read base/head before and after its
|
||||
live diff fetch; require equality with the reviewed snapshot and use that
|
||||
head as `commit_id` for any authorized post. Otherwise refresh the review
|
||||
and line mapping first.
|
||||
5. Preserve a short session record of pinned heads, findings, validation gaps,
|
||||
and already-posted comment links so a repeat pass reviews only the delta.
|
||||
|
||||
Run helper regression tests without project dependencies or GitHub access:
|
||||
|
||||
```bash
|
||||
python tests/test_helpers.py
|
||||
```
|
||||
|
||||
The helpers are bounded triage aids, not a scheduler. They do not authorize
|
||||
follow-up comments, review events, code changes, or recurring monitoring.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Reviewer Backends
|
||||
|
||||
Use this reference when delegating defect discovery to Codex, Claude, or another reviewer. The parent `review-pr` workflow remains responsible for gates, domain routing, verification, prioritization, and any GitHub writes.
|
||||
|
||||
## Backend Selection
|
||||
|
||||
| Choice | Behavior |
|
||||
|--------|----------|
|
||||
| `auto` | Use one available reviewer backend; fall back to direct review |
|
||||
| `codex` | Use an available Codex review agent or skill |
|
||||
| `claude` | Use an available Claude review agent or CLI |
|
||||
| `none` | Review directly without delegation |
|
||||
|
||||
Honor an explicit user choice. Do not block when the requested backend is unavailable; report the fallback locally and continue directly unless the user required that backend specifically.
|
||||
|
||||
Use one backend for ordinary code changes. Use at most two reviewers for large or high-risk changes, either with non-overlapping path scopes or as independent passes. Do not run multiple reviewers merely to vote on the same conclusion.
|
||||
|
||||
## Common Read-Only Handoff
|
||||
|
||||
Give every backend the same concrete target:
|
||||
|
||||
```yaml
|
||||
repository: /absolute/path/to/vllm-omni
|
||||
base_ref: origin/main
|
||||
base_sha: <resolved-base-sha>
|
||||
head_sha: <captured-pr-head-sha>
|
||||
merge_base_sha: <resolved-merge-base-sha>
|
||||
scope: complete-diff | path-partition
|
||||
paths: []
|
||||
focus: []
|
||||
constraints:
|
||||
read_only: true
|
||||
no_github_writes: true
|
||||
no_delegation: true
|
||||
output:
|
||||
finding: "[P0-P3] Imperative title — path:line"
|
||||
exhaustive: true
|
||||
include_assessment: true
|
||||
include_test_gaps: true
|
||||
include_residual_risks: true
|
||||
```
|
||||
|
||||
Capture immutable SHAs before delegation. Ask the reviewer to:
|
||||
|
||||
1. Read the applicable `AGENTS.md`.
|
||||
2. Inspect the complete assigned diff and enough surrounding code to understand it.
|
||||
3. Check relevant tests and call sites.
|
||||
4. Report only concrete regressions introduced by the change.
|
||||
5. Cite the smallest changed-line range that demonstrates each finding.
|
||||
6. Continue through the full assigned scope after finding the first issue.
|
||||
|
||||
The worker must not modify files, create commits, push branches, post comments, submit a review event, or delegate again.
|
||||
|
||||
## Backend Adapters
|
||||
|
||||
### Codex
|
||||
|
||||
Use an available Codex review agent or review skill; do not assume that a named system skill is installed. Give it the common handoff plus selected vLLM-Omni focus areas. Use it for a complete change or bounded path partition, not for a one-function context lookup.
|
||||
|
||||
### Claude
|
||||
|
||||
Pass the same handoff and output format to the available Claude review surface. Do not assume a particular command, model, or installation path. Explicitly include the read-only and no-GitHub-write constraints in the prompt.
|
||||
|
||||
### Direct fallback
|
||||
|
||||
If no backend is available, the parent performs the same checks. Keep the same evidence threshold and comment budget. Do not mention missing tooling in the GitHub review unless the user asks.
|
||||
|
||||
## Aggregate Results
|
||||
|
||||
The parent reviewer must:
|
||||
|
||||
1. Re-read the PR head SHA. If it changed, discard stale line mappings and rerun affected scopes.
|
||||
2. Verify each finding against code, tests, and the captured diff.
|
||||
3. Drop speculative, pre-existing, intentional, or style-only findings.
|
||||
4. Deduplicate by root cause and affected call path rather than by line alone.
|
||||
5. Classify `blocking` independently from `P0`-`P3`; priority alone does not determine the verdict.
|
||||
6. Apply the vLLM-Omni comment budget after collecting the complete result.
|
||||
7. Preserve material omitted findings, test gaps, and residual risks in the local report.
|
||||
|
||||
Only the parent may turn selected findings into GitHub comments, and only with user authorization.
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# check_replies.sh — Find unanswered contributor replies to your review comments.
|
||||
#
|
||||
# Usage: ./check_replies.sh --reviewer lishunyang12 [--days 14]
|
||||
#
|
||||
# Scans PRs you've reviewed for threads where a contributor replied
|
||||
# but you haven't responded yet. Outputs actionable list.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="vllm-project/vllm-omni"
|
||||
REVIEWER=""
|
||||
DAYS=14
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--reviewer) REVIEWER="$2"; shift 2 ;;
|
||||
--days) DAYS="$2"; shift 2 ;;
|
||||
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$REVIEWER" ]; then
|
||||
echo "Usage: $0 --reviewer <github_login>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SINCE=$(date -d "-${DAYS} days" +%Y-%m-%d 2>/dev/null || date -v-${DAYS}d +%Y-%m-%d)
|
||||
|
||||
echo "Scanning for unanswered replies to ${REVIEWER} (last ${DAYS} days)..." >&2
|
||||
|
||||
# Find PRs the reviewer has commented on recently
|
||||
REVIEWED_PRS=$(gh api "search/issues?q=repo:${REPO}+reviewed-by:${REVIEWER}+is:pr+is:open+updated:>=${SINCE}&per_page=50" \
|
||||
--jq '.items[].number' 2>/dev/null)
|
||||
|
||||
UNANSWERED=0
|
||||
|
||||
for PR_NUM in $REVIEWED_PRS; do
|
||||
# Get all review comments on this PR
|
||||
COMMENTS=$(gh api "repos/${REPO}/pulls/${PR_NUM}/comments" \
|
||||
--jq '[.[] | {id, user: .user.login, body: .body[:100], in_reply_to_id, created_at, path, url: .html_url}]' 2>/dev/null)
|
||||
|
||||
# Find threads: reviewer's comments that got a reply, but reviewer didn't reply back
|
||||
# 1. Get reviewer's comment IDs
|
||||
REVIEWER_IDS=$(echo "$COMMENTS" | jq -r --arg r "$REVIEWER" '[.[] | select(.user == $r) | .id]')
|
||||
|
||||
# 2. Find replies to reviewer's comments (from others)
|
||||
REPLIES=$(echo "$COMMENTS" | jq -c --arg r "$REVIEWER" --argjson rids "$REVIEWER_IDS" '
|
||||
[.[] |
|
||||
select(.user != $r) |
|
||||
select(.in_reply_to_id != null) |
|
||||
select([.in_reply_to_id] | inside($rids))
|
||||
]
|
||||
')
|
||||
|
||||
REPLY_COUNT=$(echo "$REPLIES" | jq 'length')
|
||||
if [ "$REPLY_COUNT" -eq 0 ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# 3. For each reply, check if reviewer replied after it
|
||||
while IFS= read -r reply; do
|
||||
THREAD_ID=$(echo "$reply" | jq -r '.in_reply_to_id')
|
||||
REPLY_DATE=$(echo "$reply" | jq -r '.created_at')
|
||||
REPLY_USER=$(echo "$reply" | jq -r '.user')
|
||||
REPLY_URL=$(echo "$reply" | jq -r '.url')
|
||||
|
||||
# Check if reviewer replied after this reply
|
||||
FOLLOWUP=$(echo "$COMMENTS" | jq --arg r "$REVIEWER" --arg date "$REPLY_DATE" --argjson tid "$THREAD_ID" '
|
||||
[.[] | select(.user == $r) | select(.in_reply_to_id == $tid) | select(.created_at > $date)] | length
|
||||
')
|
||||
|
||||
if [ "$FOLLOWUP" -eq 0 ]; then
|
||||
REPLY_BODY=$(echo "$reply" | jq -r '.body')
|
||||
echo "PR #${PR_NUM} | @${REPLY_USER} replied | ${REPLY_URL}"
|
||||
echo " > ${REPLY_BODY}"
|
||||
echo ""
|
||||
UNANSWERED=$((UNANSWERED + 1))
|
||||
fi
|
||||
done < <(echo "$REPLIES" | jq -c '.[]')
|
||||
done
|
||||
|
||||
if [ "$UNANSWERED" -eq 0 ]; then
|
||||
echo "No unanswered replies found." >&2
|
||||
fi
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# select_prs.sh — Pick reviewable PRs from vllm-omni for a review session.
|
||||
#
|
||||
# Usage: ./select_prs.sh [--days 7] [--limit 5] [--reviewer lishunyang12]
|
||||
#
|
||||
# Filters:
|
||||
# - Excludes your own PRs (--reviewer)
|
||||
# - Excludes WIP / Draft / "Don't merge" PRs
|
||||
# - Excludes pure docs/CI-only merged PRs
|
||||
# - Excludes PRs you already reviewed
|
||||
# - Prioritizes zero-review PRs
|
||||
#
|
||||
# Output: JSON array of {number, title, author, createdAt, reviewCount}
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO="vllm-project/vllm-omni"
|
||||
DAYS=7
|
||||
LIMIT=5
|
||||
REVIEWER=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--days) DAYS="$2"; shift 2 ;;
|
||||
--limit) LIMIT="$2"; shift 2 ;;
|
||||
--reviewer) REVIEWER="$2"; shift 2 ;;
|
||||
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
SINCE=$(date -d "-${DAYS} days" +%Y-%m-%d 2>/dev/null || date -v-${DAYS}d +%Y-%m-%d)
|
||||
|
||||
echo "Fetching open PRs from last ${DAYS} days..." >&2
|
||||
|
||||
# Fetch all open PRs from the time range
|
||||
ALL_PRS=$(gh pr list --repo "$REPO" --state open --limit 200 \
|
||||
--json number,title,author,createdAt,isDraft \
|
||||
--search "created:>=${SINCE}")
|
||||
|
||||
# Filter in a single jq pass
|
||||
FILTERED=$(echo "$ALL_PRS" | jq --arg reviewer "$REVIEWER" '
|
||||
[.[] |
|
||||
# Skip own PRs
|
||||
select(.author.login != $reviewer) |
|
||||
# Skip drafts
|
||||
select(.isDraft == false) |
|
||||
# Skip WIP / Dont merge
|
||||
select(.title | test("\\[WIP\\]|\\[Draft\\]|Don.t[ ]+merge|\\[Don.t\\]"; "i") | not)
|
||||
]
|
||||
')
|
||||
|
||||
# For each remaining PR, check review count and whether reviewer already reviewed
|
||||
echo "Checking review status for $(echo "$FILTERED" | jq length) PRs..." >&2
|
||||
|
||||
echo "$FILTERED" | jq -c '.[]' | while read -r pr; do
|
||||
NUM=$(echo "$pr" | jq -r '.number')
|
||||
|
||||
# Get review count
|
||||
REVIEW_COUNT=$(gh api "repos/${REPO}/pulls/${NUM}/reviews" --jq 'length' 2>/dev/null || echo "0")
|
||||
|
||||
# Check if reviewer already reviewed
|
||||
if [ -n "$REVIEWER" ]; then
|
||||
ALREADY=$(gh api "repos/${REPO}/pulls/${NUM}/reviews" \
|
||||
--jq "[.[] | select(.user.login==\"${REVIEWER}\")] | length" 2>/dev/null || echo "0")
|
||||
if [ "$ALREADY" -gt 0 ]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$pr" | jq --argjson rc "$REVIEW_COUNT" '. + {reviewCount: $rc}'
|
||||
done | jq -s "sort_by(.reviewCount) | .[:${LIMIT}]"
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Check review comment coordinates against the actual lines of a PR diff."""
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def parse_hunk(text):
|
||||
"""Parse a unified-diff hunk header without a runtime dependency."""
|
||||
parts = text.split(" ", 4)
|
||||
if len(parts) < 4 or parts[0] != "@@" or not parts[3].startswith("@@"):
|
||||
return None
|
||||
bounds = []
|
||||
for value, sign in zip(parts[1:3], ("-", "+")):
|
||||
if not value.startswith(sign):
|
||||
return None
|
||||
numbers = value[1:].split(",")
|
||||
if len(numbers) not in (1, 2) or not all(number.isdecimal() for number in numbers):
|
||||
return None
|
||||
bounds.extend((numbers[0], numbers[1] if len(numbers) == 2 else None))
|
||||
return tuple(bounds)
|
||||
|
||||
|
||||
def diff_path(value):
|
||||
"""Decode a git path, including C-quoted UTF-8 bytes."""
|
||||
if value.startswith('"'):
|
||||
value = ast.literal_eval(value).encode("latin1").decode("utf-8")
|
||||
return None if value == "/dev/null" else value[2:]
|
||||
|
||||
|
||||
def diff_lines(diff):
|
||||
"""Map each file/side to commentable lines, preserving rename/delete paths."""
|
||||
lines = {}
|
||||
old_path = new_path = None
|
||||
old_remaining = new_remaining = 0
|
||||
for text in diff.splitlines():
|
||||
if text.startswith("diff --git "):
|
||||
old_path = new_path = None
|
||||
old_remaining = new_remaining = 0
|
||||
elif hunk := parse_hunk(text):
|
||||
old_line, old_count, new_line, new_count = hunk
|
||||
old_line, new_line = int(old_line), int(new_line)
|
||||
old_remaining = int(old_count) if old_count is not None else 1
|
||||
new_remaining = int(new_count) if new_count is not None else 1
|
||||
elif old_remaining or new_remaining:
|
||||
# A hunk's content can itself start with --- or +++.
|
||||
path = new_path or old_path
|
||||
if text.startswith((" ", "-")):
|
||||
lines.setdefault((path, "LEFT"), set()).add(old_line)
|
||||
old_line += 1
|
||||
old_remaining -= 1
|
||||
if text.startswith((" ", "+")):
|
||||
lines.setdefault((path, "RIGHT"), set()).add(new_line)
|
||||
new_line += 1
|
||||
new_remaining -= 1
|
||||
elif text.startswith("--- "):
|
||||
old_path = diff_path(text[4:])
|
||||
elif text.startswith("+++ "):
|
||||
new_path = diff_path(text[4:])
|
||||
return lines
|
||||
|
||||
|
||||
def validate_comments(comments, lines):
|
||||
"""Validate single-line and same-side multiline review comments."""
|
||||
valid = True
|
||||
for comment in comments:
|
||||
path = comment["path"]
|
||||
side = comment.get("side", "RIGHT")
|
||||
line = comment["line"]
|
||||
start = comment.get("start_line", line)
|
||||
start_side = comment.get("start_side", side)
|
||||
available = lines.get((path, side), set())
|
||||
if (
|
||||
type(line) is not int
|
||||
or type(start) is not int
|
||||
or start < 1
|
||||
or line < start
|
||||
or start_side != side
|
||||
or line - start + 1 > len(available)
|
||||
or not all(number in available for number in range(start, line + 1))
|
||||
):
|
||||
print(
|
||||
f"ERROR: {path}:{start}-{line} ({side}) is outside diff lines",
|
||||
file=sys.stderr,
|
||||
)
|
||||
valid = False
|
||||
else:
|
||||
print(f"OK: {path}:{line} ({side})", file=sys.stderr)
|
||||
return valid
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("pr_number", type=int)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
review = json.load(sys.stdin)
|
||||
comments = review["comments"]
|
||||
if not isinstance(comments, list):
|
||||
raise TypeError("comments must be an array")
|
||||
result = subprocess.run(
|
||||
[
|
||||
"gh",
|
||||
"pr",
|
||||
"diff",
|
||||
str(args.pr_number),
|
||||
"--repo",
|
||||
"vllm-project/vllm-omni",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return 0 if validate_comments(comments, diff_lines(result.stdout)) else 1
|
||||
except subprocess.CalledProcessError as error:
|
||||
print(error.stderr, file=sys.stderr, end="")
|
||||
return 1
|
||||
except (OSError, ValueError, KeyError, TypeError, SyntaxError) as error:
|
||||
print(f"ERROR: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# Usage: echo '<review_json>' | ./verify_line_numbers.sh <pr_number>
|
||||
# Requires Python 3 and gh. Exits nonzero for invalid comments or fetch failures.
|
||||
set -euo pipefail
|
||||
|
||||
exec python3 "$(dirname "${BASH_SOURCE[0]}")/verify_line_numbers.py" "$@"
|
||||
@@ -0,0 +1,227 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Exercise repository review helpers without network access."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REVIEW = ROOT / "scripts"
|
||||
|
||||
|
||||
class HelperTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.root = Path(self.temporary.name)
|
||||
self.bin = self.root / "bin"
|
||||
self.bin.mkdir()
|
||||
gh = self.bin / "gh"
|
||||
gh.write_text("""#!/usr/bin/env python3
|
||||
import json, os, sys
|
||||
from pathlib import Path
|
||||
args = sys.argv[1:]
|
||||
root = Path(os.environ['FIXTURES'])
|
||||
if os.environ.get('FAIL_GH'):
|
||||
print('GitHub unavailable', file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
if args[:2] == ['pr', 'list']:
|
||||
print((root / 'prs.json').read_text())
|
||||
elif args[:2] == ['pr', 'diff']:
|
||||
print((root / 'diff').read_text(), end='')
|
||||
elif args[0] == 'api' and args[1].startswith('search/issues?'):
|
||||
print('1')
|
||||
elif args[0] == 'api' and args[1].endswith('/comments'):
|
||||
print((root / 'comments.json').read_text())
|
||||
elif args[0] == 'api' and args[1].endswith('/reviews'):
|
||||
print('0')
|
||||
else:
|
||||
raise SystemExit('Unexpected gh arguments: ' + repr(args))
|
||||
""")
|
||||
gh.chmod(0o755)
|
||||
self.env = dict(
|
||||
os.environ,
|
||||
PATH=f"{self.bin}{os.pathsep}{os.environ['PATH']}",
|
||||
FIXTURES=str(self.root),
|
||||
PYTHONDONTWRITEBYTECODE="1",
|
||||
)
|
||||
|
||||
def run_script(self, script, *args, data=None):
|
||||
return subprocess.run(
|
||||
["bash", str(script), *args],
|
||||
input=data,
|
||||
env=self.env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_select_filters_drafts_wip_and_own_prs(self):
|
||||
prs = [
|
||||
{"number": i, "title": title, "author": {"login": author}, "isDraft": draft}
|
||||
for i, (title, author, draft) in enumerate(
|
||||
[
|
||||
("Fix bug", "author", False),
|
||||
("[WIP] Feature", "author", False),
|
||||
("[Draft] Feature", "author", False),
|
||||
("Don't merge yet", "author", False),
|
||||
("Feature", "reviewer", False),
|
||||
("Feature", "author", True),
|
||||
],
|
||||
1,
|
||||
)
|
||||
]
|
||||
(self.root / "prs.json").write_text(json.dumps(prs))
|
||||
result = self.run_script(REVIEW / "select_prs.sh", "--reviewer", "reviewer")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual([pr["number"] for pr in json.loads(result.stdout)], [1])
|
||||
|
||||
def verify(self, comments, diff=None):
|
||||
(self.root / "diff").write_text(
|
||||
diff
|
||||
or (
|
||||
"diff --git a/src/example.py b/src/example.py\n"
|
||||
"--- a/src/example.py\n+++ b/src/example.py\n"
|
||||
"@@ -10,2 +20,2 @@\n-old\n+new\n context\n"
|
||||
)
|
||||
)
|
||||
return self.run_script(
|
||||
REVIEW / "verify_line_numbers.sh",
|
||||
"1",
|
||||
data=json.dumps({"comments": comments}),
|
||||
)
|
||||
|
||||
def test_verify_valid_nested_path_and_both_sides(self):
|
||||
result = self.verify(
|
||||
[
|
||||
{"path": "src/example.py", "line": line, "side": side, "body": "Check"}
|
||||
for side, line in [("RIGHT", 20), ("RIGHT", 21), ("LEFT", 10)]
|
||||
]
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stderr.count("OK:"), 3)
|
||||
|
||||
def test_verify_rejects_outside_hunk_or_missing_file(self):
|
||||
for path, line in [("src/example.py", 999), ("missing.py", 20)]:
|
||||
with self.subTest(path=path):
|
||||
result = self.verify([{"path": path, "line": line, "body": "Check"}])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
def test_verify_deleted_file_on_left(self):
|
||||
diff = "diff --git a/gone.py b/gone.py\n--- a/gone.py\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n"
|
||||
result = self.verify([{"path": "gone.py", "line": 1, "side": "LEFT", "body": "Check"}], diff)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = self.verify([{"path": "gone.py", "line": 1, "side": "RIGHT", "body": "Check"}], diff)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
def test_verify_new_file_with_explicit_zero_old_count(self):
|
||||
diff = "diff --git a/new.py b/new.py\n--- /dev/null\n+++ b/new.py\n@@ -0,0 +1,2 @@\n+one\n+two\n"
|
||||
result = self.verify([{"path": "new.py", "line": 2, "side": "RIGHT", "body": "Check"}], diff)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = self.verify([{"path": "new.py", "line": 1, "side": "LEFT", "body": "Check"}], diff)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
def test_verify_checks_multiline_comment_start(self):
|
||||
result = self.verify(
|
||||
[
|
||||
{
|
||||
"path": "src/example.py",
|
||||
"start_line": 1,
|
||||
"start_side": "RIGHT",
|
||||
"line": 20,
|
||||
"side": "RIGHT",
|
||||
"body": "Check",
|
||||
}
|
||||
]
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
def test_verify_multiple_files_hunks_and_renamed_path(self):
|
||||
diff = (
|
||||
"diff --git a/old.py b/new.py\n--- a/old.py\n+++ b/new.py\n"
|
||||
"@@ -1 +1 @@\n-old\n+new\n"
|
||||
"@@ -10 +10 @@\n-old\n+new\n"
|
||||
"diff --git a/second.py b/second.py\n--- a/second.py\n+++ b/second.py\n"
|
||||
"@@ -1 +1 @@\n-old\n+new\n"
|
||||
)
|
||||
result = self.verify(
|
||||
[
|
||||
{"path": path, "line": line, "body": "Check"}
|
||||
for path, line in [("new.py", 1), ("new.py", 10), ("second.py", 1)]
|
||||
],
|
||||
diff,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = self.verify([{"path": "second.py", "line": 10, "body": "Check"}], diff)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
def test_verify_quoted_path_and_header_like_content(self):
|
||||
diff = (
|
||||
'diff --git "a/a\\tb.py" "b/a\\tb.py"\n'
|
||||
'--- "a/a\\tb.py"\n+++ "b/a\\tb.py"\n'
|
||||
"@@ -1 +1 @@\n--- content\n+++ content\n"
|
||||
)
|
||||
result = self.verify([{"path": "a\tb.py", "line": 1, "body": "Check"}], diff)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_verify_rejects_malformed_input_and_fetch_failure(self):
|
||||
for data in ("not json", '{"comments": null}'):
|
||||
result = self.run_script(REVIEW / "verify_line_numbers.sh", "1", data=data)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.env["FAIL_GH"] = "1"
|
||||
result = self.verify([{"path": "src/example.py", "line": 20, "body": "Check"}])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("GitHub unavailable", result.stderr)
|
||||
|
||||
def replies(self, answered):
|
||||
comments = [
|
||||
{
|
||||
"id": 100,
|
||||
"user": "reviewer",
|
||||
"body": "Please fix",
|
||||
"in_reply_to_id": None,
|
||||
"created_at": "2026-09-01T00:00:00Z",
|
||||
"url": "root",
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"user": "author",
|
||||
"body": "Fixed",
|
||||
"in_reply_to_id": 100,
|
||||
"created_at": "2026-09-02T00:00:00Z",
|
||||
"url": "reply",
|
||||
},
|
||||
]
|
||||
if answered:
|
||||
comments.append(
|
||||
{
|
||||
"id": 102,
|
||||
"user": "reviewer",
|
||||
"body": "Thanks",
|
||||
"in_reply_to_id": 100,
|
||||
"created_at": "2026-09-03T00:00:00Z",
|
||||
"url": "answer",
|
||||
}
|
||||
)
|
||||
(self.root / "comments.json").write_text(json.dumps(comments))
|
||||
return self.run_script(REVIEW / "check_replies.sh", "--reviewer", "reviewer")
|
||||
|
||||
def test_replies_ignores_answered_thread(self):
|
||||
result = self.replies(answered=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout, "")
|
||||
self.assertIn("No unanswered replies found", result.stderr)
|
||||
|
||||
def test_replies_reports_unanswered_without_false_clean_summary(self):
|
||||
result = self.replies(answered=False)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("@author replied", result.stdout)
|
||||
self.assertNotIn("No unanswered replies found", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -56,6 +56,35 @@ jobs:
|
||||
run: |
|
||||
bash scripts/build_wheel.sh --python python
|
||||
|
||||
- name: Validate runtime package data from source archive
|
||||
run: |
|
||||
set -euo pipefail
|
||||
archive_dir="$(mktemp -d)"
|
||||
install_dir="$(mktemp -d)"
|
||||
vcs_wheels=(dist/*.whl)
|
||||
if [[ ${#vcs_wheels[@]} -ne 1 || ! -f "${vcs_wheels[0]}" ]]; then
|
||||
echo "Expected exactly one VCS-built wheel" >&2
|
||||
exit 1
|
||||
fi
|
||||
git archive HEAD | tar -x -C "${archive_dir}"
|
||||
(
|
||||
cd "${archive_dir}"
|
||||
VLLM_OMNI_TARGET_DEVICE=cuda \
|
||||
VLLM_OMNI_VERSION_OVERRIDE=0.0.0 \
|
||||
python -m build --wheel --outdir archive-dist
|
||||
)
|
||||
wheels=("${archive_dir}"/archive-dist/*.whl)
|
||||
if [[ ${#wheels[@]} -ne 1 || ! -f "${wheels[0]}" ]]; then
|
||||
echo "Expected exactly one archive-built wheel" >&2
|
||||
exit 1
|
||||
fi
|
||||
python -m pip install --no-deps --target "${install_dir}" "${wheels[0]}"
|
||||
python tools/check_wheel_package_data.py \
|
||||
--source-root "${archive_dir}" \
|
||||
--wheel "${wheels[0]}" \
|
||||
--vcs-wheel "${vcs_wheels[0]}" \
|
||||
--installed-root "${install_dir}"
|
||||
|
||||
- name: Upload Wheel Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
@@ -243,6 +243,7 @@ datasets/
|
||||
*.csv
|
||||
*.json
|
||||
!recipes/cosmos3/negative_prompt.json
|
||||
!benchmarks/mammoth_moda2/reference_results.json
|
||||
!tests/dfx/perf/tests/*.json
|
||||
!apps/ComfyUI-vLLM-Omni/example_workflows/*.json
|
||||
# Omni-DuplexEval CI config JSON (e.g. accuracy/omni_duplex_eval)
|
||||
@@ -250,6 +251,7 @@ datasets/
|
||||
*.jsonl
|
||||
# Shared Hunyuan IT2I input for accuracy tests and vllm bench.
|
||||
!tests/assets/hunyuan_image3/it2i.jsonl
|
||||
!tests/assets/livekit/client_vad_replay.jsonl
|
||||
*.parquet
|
||||
|
||||
# Output files
|
||||
|
||||
@@ -16,6 +16,7 @@ Easy, fast, and cheap omni-modality model serving for everyone
|
||||
|
||||
*Latest News* 🔥
|
||||
|
||||
- [2026/09] We released [0.30.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.30.0), rebased onto vLLM 0.30.0, featuring a unified full-duplex serving framework around engine-owned sessions for [MiniCPM-o 4.5](recipes/OpenBMB/MiniCPM-o-4_5.md) and AURA, native cross-stage KV and multimodal payload transfer (Mooncake AR-to-DiT handoff and NIXL connectors), interactive world-model serving with [LingBot World](recipes/Robbyant/LingBot-World-2.0.md), and realtime [MiniMax H3](recipes/MiniMaxAI/MiniMax-H3.md) Turbo inference on NVIDIA Blackwell and Ascend 950.
|
||||
- [2026/08] We released [0.28.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.28.0), featuring production-ready [MiniMax H3](recipes/MiniMaxAI/MiniMax-H3.md) serving on GPU and NPU, a unified AR/DiT paged KV cache runtime, and enhanced realtime full-duplex serving for the [MiniCPM-o series](recipes/OpenBMB/MiniCPM-o-4_5.md).
|
||||
- [2026/08] [VeRL-Omni](https://github.com/verl-project/verl-omni) `v0.2.0` is released: faster diffusion RL powered by vLLM-Omni (request-level/step-wise batching with FA3), rebuilt Qwen3-Omni multimodal training (DPO & GSPO), plus LTX-2.3, Qwen-Image-Edit support and more. See the [release notes](https://github.com/verl-project/verl-omni/releases/tag/v0.2.0).
|
||||
- [2026/08] We released [0.26.0](https://github.com/vllm-project/vllm-omni/releases/tag/v0.26.0) - aligned with the vLLM 0.26 release line, featuring [MiniMax H3](recipes/MiniMaxAI/MiniMax-H3.md) joint video/audio generation, an experimental full-duplex realtime runtime for [MiniCPM-o 4.5](recipes/OpenBMB/MiniCPM-o-4_5.md), distributed layerwise diffusion offload, and broader model, hardware, streaming, TTS, and quantization support.
|
||||
@@ -58,9 +59,9 @@ vLLM-Omni is flexible and easy to use with:
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
- **Omni-modality models** (e.g. Qwen3-Omni, MiniCPM-o 4.5, Cosmos3, HunyuanImage, BAGEL)
|
||||
- **TTS models** (e.g. Qwen3-TTS, IndexTTS 2.5, CosyVoice3)
|
||||
- **Diffusion models** — image, video, and audio generation (e.g. MiniMax H3, LTX-2.5, SANA-Video, Wan2.2)
|
||||
- **Robot-policy and action models** (e.g. π0, GR00T-N1.7, DreamZero-DROID, InternVLA-A1)
|
||||
- **TTS models** (e.g. Qwen3-TTS, Tencent AuK, Breeze-TTS-2, CosyVoice3)
|
||||
- **Diffusion models** — image, video, and audio generation (e.g. MiniMax H3, LingBot World, MAGI-2, LTX-2.5, Wan2.2)
|
||||
- **Robot-policy and action models** (e.g. π0.5, GR00T-N1.7, DreamZero-DROID, InternVLA-A1)
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -66,6 +66,13 @@ Accuracy benchmarks for image generation/editing models, adapting external suite
|
||||
- **Layout**: `accuracy/text_to_image/` (GEBench), `accuracy/image_to_image/` (GEdit-Bench)
|
||||
- **Method**: generation and judge scoring both run through local `vllm-omni serve` endpoints
|
||||
|
||||
### [MammothModa2](mammoth_moda2/README.md) — Startup and Weight Loading
|
||||
|
||||
Startup / model-loading benchmark for the two-stage MammothModa2 (AR → DiT) deployment, plus a raw safetensors loading micro-benchmark.
|
||||
|
||||
- **Layout**: `mammoth_moda2/bench_startup.py` (single-process startup and first/steady request timing), `mammoth_moda2/parse_startup_log.py` (per-stage breakdown from engine logs), `mammoth_moda2/bench_storage_scenarios.sh` (cold/warm page cache on local disk vs. network storage), `mammoth_moda2/raw_load_bench.py` (safetensors → GPU without vLLM)
|
||||
- **Key metrics**: time to engine ready, per-stage spawn/init/weight-load/profile time, first vs. steady-state request latency
|
||||
|
||||
### Common serving metrics framework
|
||||
|
||||
`vllm_omni/benchmarks/` extends `vllm bench serve --omni` with Omni-specific datasets, backends, and multimodal metrics. Key metrics include:
|
||||
|
||||
@@ -107,6 +107,8 @@ async def async_request_image_edits(
|
||||
|
||||
if extra_body.get("guidance_scale") is not None:
|
||||
form.add_field("guidance_scale", str(extra_body["guidance_scale"]))
|
||||
if extra_body.get("guidance_scale_2") is not None:
|
||||
form.add_field("guidance_scale_2", str(extra_body["guidance_scale_2"]))
|
||||
if extra_body.get("negative_prompt") is not None:
|
||||
form.add_field("negative_prompt", str(extra_body["negative_prompt"]))
|
||||
if extra_body.get("true_cfg_scale") is not None:
|
||||
|
||||
@@ -1107,6 +1107,11 @@ def calculate_metrics(
|
||||
error_outputs = [o for o in outputs if not o.success]
|
||||
|
||||
num_success = len(success_outputs)
|
||||
error_log_path = os.environ.get("BENCH_ERROR_LOG")
|
||||
if error_outputs and error_log_path:
|
||||
with open(error_log_path, "a") as f:
|
||||
for output in error_outputs:
|
||||
f.write(f"ERROR latency={output.latency:.3f} detail={output.error!r}\n")
|
||||
latencies = [o.latency for o in success_outputs]
|
||||
peak_memories = [o.peak_memory_mb for o in success_outputs if o.peak_memory_mb > 0]
|
||||
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Cold-cache torch.cuda timing and correctness gates for MAGI-2 microbenchmarks."""
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def emit(**fields: Any) -> None:
|
||||
LOGGER.info(json.dumps(fields, sort_keys=True))
|
||||
|
||||
|
||||
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--tokens", type=int, nargs="+", default=[1, 4, 16, 64, 129, 4096])
|
||||
parser.add_argument("--dtype", choices=["bfloat16", "float16", "float32"], default="bfloat16")
|
||||
parser.add_argument("--num-tests", type=int, default=30, help="Measured samples per implementation.")
|
||||
parser.add_argument("--warmup", type=int, default=10, help="Interleaved warmup calls before sampling.")
|
||||
parser.add_argument("--seed", type=int, default=42)
|
||||
|
||||
|
||||
def load_musa(args: argparse.Namespace, parser: argparse.ArgumentParser) -> Any:
|
||||
if min(*args.tokens, args.num_tests) <= 0 or args.warmup < 0:
|
||||
parser.error("tokens and num-tests must be positive; warmup must be nonnegative")
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stdout)
|
||||
torch = importlib.import_module("torch")
|
||||
cuda = getattr(torch, "cuda")
|
||||
# torchada is needed only on MUSA; after it is loaded, use torch.cuda for
|
||||
# both CUDA and MUSA as documented by torchada.
|
||||
if hasattr(torch.version, "musa") and torch.version.musa is not None:
|
||||
try:
|
||||
importlib.import_module("torchada")
|
||||
except ModuleNotFoundError as error:
|
||||
if error.name != "torchada":
|
||||
raise
|
||||
has_gpu = cuda.is_available() or (
|
||||
hasattr(torch.version, "musa") and torch.version.musa is not None and torch.musa.is_available()
|
||||
)
|
||||
if not has_gpu:
|
||||
emit(status="skip", reason="CUDA/MUSA device is unavailable", torch=torch.__version__)
|
||||
return None
|
||||
torch.manual_seed(args.seed)
|
||||
cuda.manual_seed_all(args.seed)
|
||||
return torch
|
||||
|
||||
|
||||
def report_environment(torch: Any, args: argparse.Namespace, module: ModuleType) -> None:
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
packages = {}
|
||||
for name in ("torch", "torch_musa", "torchada", "triton", "vllm", "vllm-omni"):
|
||||
try:
|
||||
packages[name] = importlib.metadata.version(name)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
packages[name] = None
|
||||
cuda = getattr(torch, "cuda")
|
||||
properties = cuda.get_device_properties(cuda.current_device())
|
||||
try:
|
||||
source_sha = subprocess.check_output(["git", "-C", str(root), "rev-parse", "HEAD"], text=True).strip()
|
||||
source_dirty = bool(subprocess.check_output(["git", "-C", str(root), "status", "--porcelain"], text=True))
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
source_sha = os.environ.get("MAGI2_BENCH_SOURCE_SHA", "unknown")
|
||||
source_dirty = None
|
||||
emit(
|
||||
status="environment",
|
||||
command=sys.argv,
|
||||
source_root=str(root),
|
||||
source_sha=source_sha,
|
||||
source_dirty=source_dirty,
|
||||
kernel_source=module.__file__,
|
||||
device=cuda.get_device_name(cuda.current_device()),
|
||||
sm_count=getattr(properties, "multi_processor_count", None),
|
||||
packages=packages,
|
||||
env={name: os.environ.get(name) for name in ("MUSA_VISIBLE_DEVICES", "TORCHDYNAMO_DISABLE")},
|
||||
config=vars(args),
|
||||
mode="eager operator diagnostic; no compile or graph capture",
|
||||
comparison="forward_native versus the default CustomOp dispatch",
|
||||
timing="torch.cuda.Event; alternating native/fused calls; vanilla 512 MiB flush outside timed regions",
|
||||
timing_scope="whole callable, including device launch gaps; not isolated kernel duration",
|
||||
l2_flush=True,
|
||||
l2_flush_bytes=512 * 1024 * 1024,
|
||||
)
|
||||
|
||||
|
||||
class _LaunchProbe:
|
||||
def __init__(self, kernel: Any) -> None:
|
||||
self.kernel = kernel
|
||||
self.count = 0
|
||||
|
||||
def __getitem__(self, grid: Any) -> Any:
|
||||
self.count += 1
|
||||
return self.kernel[grid]
|
||||
|
||||
|
||||
def check_pair(
|
||||
torch: Any,
|
||||
reference: Callable,
|
||||
fused: Callable,
|
||||
module: ModuleType,
|
||||
kernels: tuple[str, ...],
|
||||
*,
|
||||
rtol: float,
|
||||
atol: float,
|
||||
) -> dict:
|
||||
cuda = getattr(torch, "cuda")
|
||||
expected = reference()
|
||||
probes = {name: _LaunchProbe(getattr(module, name)) for name in kernels}
|
||||
try:
|
||||
for name, probe in probes.items():
|
||||
setattr(module, name, probe)
|
||||
actual = fused()
|
||||
cuda.synchronize()
|
||||
finally:
|
||||
for name, probe in probes.items():
|
||||
setattr(module, name, probe.kernel)
|
||||
if any(probe.count != 1 for probe in probes.values()):
|
||||
raise RuntimeError(f"Expected one launch per fused kernel; got {[(k, p.count) for k, p in probes.items()]}")
|
||||
expected = expected if isinstance(expected, tuple) else (expected,)
|
||||
actual = actual if isinstance(actual, tuple) else (actual,)
|
||||
errors = []
|
||||
for result, baseline in zip(actual, expected, strict=True):
|
||||
for value in (baseline, result):
|
||||
if not torch.isfinite(value).all().item():
|
||||
raise AssertionError("Nonfinite benchmark output")
|
||||
if value.numel() > 1 and (value == value.flatten()[0]).all().item():
|
||||
raise AssertionError("Constant benchmark output")
|
||||
torch.testing.assert_close(result, baseline, rtol=rtol, atol=atol)
|
||||
difference = (result.float() - baseline.float()).abs()
|
||||
errors.append(
|
||||
{
|
||||
"max_abs": difference.max().item(),
|
||||
"max_rel": (difference / baseline.float().abs().clamp_min(1e-6)).max().item(),
|
||||
}
|
||||
)
|
||||
return {"parity": "pass", "errors": errors, "rtol": rtol, "atol": atol, "kernels": list(kernels)}
|
||||
|
||||
|
||||
def _percentile(samples: list[float], quantile: float) -> float:
|
||||
ordered = sorted(samples)
|
||||
position = (len(ordered) - 1) * quantile
|
||||
lower, upper = math.floor(position), math.ceil(position)
|
||||
return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
|
||||
|
||||
|
||||
def measure_pair(torch: Any, reference: Callable, fused: Callable, args: argparse.Namespace) -> dict:
|
||||
cuda = getattr(torch, "cuda")
|
||||
functions = (reference, fused)
|
||||
order: list[int] = []
|
||||
|
||||
def alternate() -> Any:
|
||||
mode = len(order) % 2
|
||||
order.append(mode)
|
||||
return functions[mode]()
|
||||
|
||||
times_ms = []
|
||||
for _ in range(args.warmup):
|
||||
alternate()
|
||||
cuda.synchronize()
|
||||
order.clear()
|
||||
flush = torch.empty(512 * 1024 * 1024, dtype=torch.uint8, device="cuda")
|
||||
for _ in range(2 * args.num_tests):
|
||||
flush.zero_()
|
||||
cuda.synchronize()
|
||||
start = cuda.Event(enable_timing=True)
|
||||
end = cuda.Event(enable_timing=True)
|
||||
start.record()
|
||||
alternate()
|
||||
end.record()
|
||||
end.synchronize()
|
||||
times_ms.append(start.elapsed_time(end))
|
||||
samples: tuple[list[float], list[float]] = ([], [])
|
||||
for mode, milliseconds in zip(order[-len(times_ms) :], times_ms, strict=True):
|
||||
if not math.isfinite(milliseconds) or milliseconds <= 0:
|
||||
raise RuntimeError(f"Invalid GPU event sample: {milliseconds}")
|
||||
samples[mode].append(milliseconds * 1000)
|
||||
stats = {}
|
||||
for label, values in zip(("native", "fused"), samples, strict=True):
|
||||
if len(values) != args.num_tests:
|
||||
raise RuntimeError("GPU timing harness returned an unexpected sample count")
|
||||
p50 = _percentile(values, 0.5)
|
||||
stats[label] = {"median_us": p50, "p50_us": p50, "p90_us": _percentile(values, 0.9), "samples": len(values)}
|
||||
stats["speedup_p50"] = stats["native"]["p50_us"] / stats["fused"]["p50_us"]
|
||||
return stats
|
||||
@@ -0,0 +1,310 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Compare MAGI-2 routed MoE with equal preprocessing boundaries.
|
||||
|
||||
python -m benchmarks.kernels.benchmark_magi2_bf16_moe --tokens 2 4096 10000
|
||||
|
||||
Default (`--mode dispatch`): the same MAGI-2 layer is timed with its native
|
||||
reference and default BF16 fast path, including routing and all preprocessing.
|
||||
With `--mode routed`, routing probabilities/IDs are inputs to BOTH timed paths. Sorting,
|
||||
padding, packing, allocations and reduction remain inside their respective
|
||||
calls. The reference uses deterministic routed storage in diagnostic mode.
|
||||
Use --mode dispatch to measure the
|
||||
actual layer with the native and default fast paths, including the router. Unsupported
|
||||
dispatch fails explicitly; it is never silently replaced by another baseline.
|
||||
|
||||
Stage timings are standalone diagnostics, not additive or env speedup claims.
|
||||
These synthetic local shapes are not full-model performance measurements.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--tokens", type=int, nargs="+", default=[2, 4096])
|
||||
parser.add_argument("--heads", type=int, default=3, help="EP4 local head count for MAGI-2 Preview.")
|
||||
parser.add_argument("--experts", type=int, default=256, help="Released experts per head.")
|
||||
parser.add_argument("--top-k", type=int, default=6)
|
||||
parser.add_argument("--warmup", type=int, default=10)
|
||||
parser.add_argument("--iters", type=int, default=50)
|
||||
parser.add_argument("--seed", type=int, default=419)
|
||||
parser.add_argument("--mode", choices=["routed", "dispatch"], default="dispatch")
|
||||
parser.add_argument(
|
||||
"--native-deterministic",
|
||||
action="store_true",
|
||||
help="Use the portable deterministic native reference (needed by Triton-MUSA 3.2).",
|
||||
)
|
||||
parser.add_argument("--stages", action="store_true")
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args(argv)
|
||||
if min(*args.tokens, args.heads, args.experts, args.top_k, args.iters) < 1 or args.warmup < 0:
|
||||
parser.error("Shapes/iters must be positive; warmup must be nonnegative")
|
||||
if args.top_k > args.experts:
|
||||
parser.error("top-k must not exceed experts per head")
|
||||
return args
|
||||
|
||||
|
||||
@contextmanager
|
||||
def env_switch(enabled):
|
||||
# Keep the legacy benchmark context API; dispatch is no longer opt-in.
|
||||
with patch.dict(os.environ, MAGI2_DETERMINISTIC="0"):
|
||||
yield
|
||||
|
||||
|
||||
def stats(values):
|
||||
if not values or any(not math.isfinite(x) or x <= 0 for x in values):
|
||||
raise ValueError("Missing/non-positive/non-finite timing sample")
|
||||
ordered = sorted(values)
|
||||
return {
|
||||
"mean_us": statistics.mean(values),
|
||||
"p50_us": statistics.median(values),
|
||||
"p90_us": ordered[math.ceil(0.9 * len(ordered)) - 1],
|
||||
"samples_us": values,
|
||||
}
|
||||
|
||||
|
||||
def measure(torch, calls, args):
|
||||
"""Alternating AB/BA; cold-cache flush is outside each timed region."""
|
||||
cuda = getattr(torch, "cuda")
|
||||
names = list(calls)
|
||||
sequence = names + names[::-1]
|
||||
wall_samples = []
|
||||
labels = []
|
||||
counter = 0
|
||||
|
||||
def invoke():
|
||||
nonlocal counter
|
||||
name = sequence[counter % len(sequence)]
|
||||
counter += 1
|
||||
cuda.synchronize()
|
||||
start = time.perf_counter_ns()
|
||||
output = calls[name]()
|
||||
cuda.synchronize()
|
||||
elapsed = (time.perf_counter_ns() - start) / 1000
|
||||
wall_samples.append(elapsed)
|
||||
labels.append(name)
|
||||
return output
|
||||
|
||||
count = len(names) * args.iters
|
||||
cache = torch.empty(512 * 1024 * 1024, device="cuda", dtype=torch.uint8)
|
||||
gpu_ms = []
|
||||
for index in range(len(names) * args.warmup + count):
|
||||
cache.zero_()
|
||||
cuda.synchronize()
|
||||
begin, end = cuda.Event(enable_timing=True), cuda.Event(enable_timing=True)
|
||||
begin.record()
|
||||
invoke()
|
||||
end.record()
|
||||
end.synchronize()
|
||||
if index >= len(names) * args.warmup:
|
||||
gpu_ms.append(begin.elapsed_time(end))
|
||||
labels, wall_samples = labels[-count:], wall_samples[-count:]
|
||||
result = {}
|
||||
for name in names:
|
||||
wall = [x for label, x in zip(labels, wall_samples) if label == name]
|
||||
event = [float(x) * 1000 for label, x in zip(labels, gpu_ms) if label == name]
|
||||
if len(wall) != args.iters or len(event) != args.iters:
|
||||
raise RuntimeError("Timing harness did not return the requested sample counts")
|
||||
result[name] = {"synchronized_wall": stats(wall), "gpu_event": stats(event)}
|
||||
return result
|
||||
|
||||
|
||||
def make_inputs(torch, tokens, args):
|
||||
# Fan-in-scaled synthetic weights. Unlike the old script, W_up and W_gate
|
||||
# have the same distribution and top-k IDs are unique within each token.
|
||||
torch.manual_seed(args.seed)
|
||||
x = torch.randn(tokens, args.heads, 256, device="cuda").to(torch.bfloat16)
|
||||
shape = (args.heads * args.experts, 256, 1280)
|
||||
gate, up = [(torch.randn(shape, device="cuda") / 16).to(torch.bfloat16) for _ in range(2)]
|
||||
down = (torch.randn(shape[0], 1280, 256, device="cuda") / math.sqrt(1280)).to(torch.bfloat16)
|
||||
logits = torch.randn(args.heads, tokens, args.experts, device="cuda")
|
||||
selected = logits.topk(args.top_k, dim=-1)
|
||||
probabilities = selected.values.softmax(-1)
|
||||
return x, probabilities, selected.indices, gate, up, down
|
||||
|
||||
|
||||
def check_and_capture(torch, moe, kernels, data, args):
|
||||
x, probabilities, indices, gate, up, down = data
|
||||
gather, weights, offsets = moe.global_sort_routes(probabilities, indices, args.experts)
|
||||
sorted_routes, launches = [], []
|
||||
scatter, invoke = kernels._deterministic_scatter, moe.invoke_fused_moe_bf16
|
||||
|
||||
def capture_scatter(routes, *params):
|
||||
sorted_routes.append(routes.detach().clone())
|
||||
return scatter(routes, *params)
|
||||
|
||||
def capture_launch(*params, **kwargs):
|
||||
launches.append((params, kwargs))
|
||||
return invoke(*params, **kwargs)
|
||||
|
||||
with patch.object(kernels, "_deterministic_scatter", capture_scatter):
|
||||
moe.triton_mh_moe_forward(x, gather, weights, offsets, gate, up, down, deterministic=True)
|
||||
packed_w13 = moe._pack_bf16_w13(gate, up)
|
||||
route_buffers = moe._allocate_bf16_route_buffers(
|
||||
x.shape[0] * args.heads * args.top_k,
|
||||
args.heads * args.experts,
|
||||
128,
|
||||
x.device,
|
||||
)
|
||||
with patch.object(moe, "invoke_fused_moe_bf16", capture_launch):
|
||||
actual = moe._bf16_fused_moe_forward(x, probabilities, indices, packed_w13, down, route_buffers)
|
||||
if len(sorted_routes) != 1 or len(launches) != 2:
|
||||
raise RuntimeError("Unexpected kernel dispatch: benchmark contract needs updating")
|
||||
flat_experts = (indices + torch.arange(args.heads, device=x.device)[:, None, None] * args.experts).flatten()
|
||||
order = flat_experts.argsort(stable=True)
|
||||
new_routes = launches[1][0][2].reshape(-1, 256).index_select(0, order)
|
||||
scatter_ids = gather.long() * args.heads + flat_experts[order] // args.experts
|
||||
expected = torch.zeros(x.shape[0] * args.heads, 256, device=x.device, dtype=torch.float32)
|
||||
expected.index_add_(0, scatter_ids, sorted_routes[0].float())
|
||||
expected = expected.to(x.dtype).view_as(x)
|
||||
errors = []
|
||||
# Same expert/FP32 reduction contract as test_bf16_moe_wiring.py, rather
|
||||
# than relaxing tolerances on the inherited BF16 scatter reduction.
|
||||
for result, reference in ((new_routes, sorted_routes[0]), (actual, expected)):
|
||||
if not (torch.isfinite(result).all() and torch.isfinite(reference).all()):
|
||||
raise RuntimeError("Non-finite MoE output")
|
||||
torch.testing.assert_close(result, reference, rtol=2e-2, atol=0.5)
|
||||
relative_l2 = (result.float() - reference.float()).norm() / reference.float().norm().clamp_min(1e-6)
|
||||
if relative_l2.item() >= 1e-3:
|
||||
raise AssertionError(f"MoE relative L2 {relative_l2.item()} >= 1e-3")
|
||||
errors.append(
|
||||
{"max_abs": (result.float() - reference.float()).abs().max().item(), "relative_l2": relative_l2.item()}
|
||||
)
|
||||
return launches, errors
|
||||
|
||||
|
||||
def benchmark(torch, moe, kernels, tokens, args):
|
||||
data = make_inputs(torch, tokens, args)
|
||||
x, probabilities, indices, gate, up, down = data
|
||||
launches, errors = check_and_capture(torch, moe, kernels, data, args)
|
||||
packed_w13 = moe._pack_bf16_w13(gate, up)
|
||||
route_count = tokens * args.heads * args.top_k
|
||||
route_buffers = moe._allocate_bf16_route_buffers(route_count, args.heads * args.experts, 128, x.device)
|
||||
|
||||
def reference():
|
||||
gather, weights, offsets = moe.global_sort_routes(probabilities, indices, args.experts)
|
||||
return moe.triton_mh_moe_forward(x, gather, weights, offsets, gate, up, down, deterministic=True)
|
||||
|
||||
calls = {
|
||||
"reference_deterministic_routed": reference,
|
||||
"candidate_routed": lambda: moe._bf16_fused_moe_forward(
|
||||
x, probabilities, indices, packed_w13, down, route_buffers
|
||||
),
|
||||
}
|
||||
if args.mode == "dispatch":
|
||||
config = moe.Magi2MultiHeadMoEConfig(
|
||||
args.heads * 256, args.heads, args.experts, args.top_k, 1280, torch.bfloat16
|
||||
)
|
||||
layer = moe.Magi2MultiHeadMoE(config).to(x.device)
|
||||
for parameter, value in ((layer.W_gate, gate), (layer.W_up, up), (layer.W_down, down)):
|
||||
parameter.copy_(value)
|
||||
layer.gate.normal_(std=1 / 16)
|
||||
|
||||
flat_x = x.reshape(tokens, -1)
|
||||
|
||||
def native_dispatch():
|
||||
x_heads = flat_x.view(tokens, args.heads, 256)
|
||||
probs, idx = layer._route(x_heads)
|
||||
gather, weights, offsets = moe.global_sort_routes(probs, idx, args.experts)
|
||||
return moe.triton_mh_moe_forward(
|
||||
x_heads,
|
||||
gather,
|
||||
weights,
|
||||
offsets,
|
||||
layer.W_gate,
|
||||
layer.W_up,
|
||||
layer.W_down,
|
||||
deterministic=args.native_deterministic,
|
||||
).reshape(tokens, -1)
|
||||
|
||||
calls = {"native_dispatch": native_dispatch, "fused_dispatch": lambda: layer(flat_x)}
|
||||
torch.testing.assert_close(calls["fused_dispatch"](), calls["native_dispatch"](), rtol=2e-2, atol=0.5)
|
||||
timings = measure(torch, calls, args)
|
||||
names = list(calls)
|
||||
result = {"status": "pass", "tokens": tokens, "mode": args.mode, "correctness": errors, "timings": timings}
|
||||
result["speedup_p50_wall"] = (
|
||||
timings[names[0]]["synchronized_wall"]["p50_us"] / timings[names[1]]["synchronized_wall"]["p50_us"]
|
||||
)
|
||||
if args.stages:
|
||||
route_ids = (indices.int() + torch.arange(args.heads, device=x.device)[:, None, None] * args.experts).reshape(
|
||||
-1, args.top_k
|
||||
)
|
||||
|
||||
def gems():
|
||||
for params, kwargs in launches:
|
||||
moe.invoke_fused_moe_bf16(*params, **kwargs)
|
||||
|
||||
result["standalone_stages_not_additive"] = measure(
|
||||
torch,
|
||||
{
|
||||
"reference_sort": lambda: moe.global_sort_routes(probabilities, indices, args.experts),
|
||||
"candidate_align": lambda: moe._align_bf16_routes(route_ids, args.heads * args.experts, 128),
|
||||
"candidate_pack_w13": lambda: torch.stack((gate.transpose(1, 2), up.transpose(1, 2)), dim=2).reshape(
|
||||
args.heads * args.experts, 2560, 256
|
||||
),
|
||||
"candidate_two_gemms_prepared": gems,
|
||||
},
|
||||
args,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
import torch
|
||||
|
||||
if hasattr(torch.version, "musa") and torch.version.musa is not None:
|
||||
importlib.import_module("torchada") # before importing vLLM / capturing CUDA APIs
|
||||
cuda = getattr(torch, "cuda")
|
||||
has_gpu = cuda.is_available() or (
|
||||
hasattr(torch.version, "musa") and torch.version.musa is not None and torch.musa.is_available()
|
||||
)
|
||||
if not has_gpu:
|
||||
print(json.dumps({"status": "skip", "reason": "No visible CUDA/MUSA device"}))
|
||||
return
|
||||
import vllm_omni.diffusion.models.magi2.fused_moe_kernels as kernels
|
||||
import vllm_omni.diffusion.models.magi2.mh_moe as moe
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
try:
|
||||
sha = subprocess.check_output(
|
||||
["git", "-C", str(root), "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL
|
||||
).strip()
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
sha = os.environ.get("MAGI2_BENCH_SOURCE_SHA", "unknown")
|
||||
report = {
|
||||
"device": cuda.get_device_name(),
|
||||
"sm_count": cuda.get_device_properties(0).multi_processor_count,
|
||||
"source_sha": sha,
|
||||
"module_path": moe.__file__,
|
||||
"torch": torch.__version__,
|
||||
"args": {**vars(args), "output": str(args.output)},
|
||||
"cache": "512 MiB flush outside each region",
|
||||
"capture": "eager; JIT warmed; no graph/compile",
|
||||
"results": [],
|
||||
}
|
||||
with torch.inference_mode():
|
||||
for tokens in args.tokens:
|
||||
result = benchmark(torch, moe, kernels, tokens, args)
|
||||
report["results"].append(result)
|
||||
print(json.dumps(result), flush=True)
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(report, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,87 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Compare native and fused MAGI-2 SwiGLU7 operators on CUDA or MUSA.
|
||||
|
||||
Run from the checkout root in an environment with PyTorch and the selected
|
||||
backend dependencies. torchada is loaded automatically only on MUSA::
|
||||
|
||||
python -m benchmarks.kernels.benchmark_magi2_swiglu7 \
|
||||
--tokens 1 4 16 64 129 4096 --intermediate-size 1280 --num-tests 30
|
||||
|
||||
The released MAGI-2 config supplies the 1280 intermediate width. Token counts
|
||||
are a synthetic sweep; pass per-rank sizes from the target trace for production
|
||||
comparisons. Input is contiguous interleaved gate/up [tokens, 2 * intermediate].
|
||||
Both paths evaluate FP32 arithmetic with one cast to the requested output dtype.
|
||||
This measures eager operator latency, not compiled serving performance.
|
||||
No CUDA/MUSA device (or no PyTorch) produces a JSON skip record and exit status zero.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from functools import partial
|
||||
|
||||
from benchmarks.kernels._magi2_musa_bench import (
|
||||
add_arguments,
|
||||
check_pair,
|
||||
emit,
|
||||
load_musa,
|
||||
measure_pair,
|
||||
report_environment,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
add_arguments(parser)
|
||||
parser.add_argument("--intermediate-size", type=int, default=1280)
|
||||
parser.add_argument("--out-dtype", choices=["bfloat16", "float16", "float32"], default=None)
|
||||
args = parser.parse_args()
|
||||
if args.intermediate_size <= 0:
|
||||
parser.error("intermediate-size must be positive")
|
||||
torch = load_musa(args, parser)
|
||||
if torch is None:
|
||||
return
|
||||
cuda = getattr(torch, "cuda")
|
||||
from vllm_omni.diffusion.layers import swiglu7
|
||||
|
||||
report_environment(torch, args, swiglu7)
|
||||
dtype = getattr(torch, args.dtype)
|
||||
out_dtype = getattr(torch, args.out_dtype or args.dtype)
|
||||
device = torch.device("cuda")
|
||||
rtol = {torch.bfloat16: 1.6e-2, torch.float16: 2e-3, torch.float32: 1e-5}[out_dtype]
|
||||
atol = 1e-5 if out_dtype == torch.float32 else 2e-3
|
||||
op = swiglu7.SwiGLU7()
|
||||
with torch.inference_mode():
|
||||
for tokens in args.tokens:
|
||||
# Values outside both clamp boundaries exercise the released formula.
|
||||
x = (torch.randn(tokens, 2 * args.intermediate_size, device=device) * 10).to(dtype)
|
||||
reference = partial(op.forward_native, x, out_dtype=out_dtype)
|
||||
fused = partial(op, x, out_dtype=out_dtype)
|
||||
parity = check_pair(torch, reference, fused, swiglu7, ("_swiglu7_kernel",), rtol=rtol, atol=atol)
|
||||
timing = measure_pair(torch, reference, fused, args)
|
||||
output_elements = tokens * args.intermediate_size
|
||||
minimum_io_bytes = (
|
||||
x.numel() * x.element_size() + output_elements * torch.empty((), dtype=out_dtype).element_size()
|
||||
)
|
||||
emit(
|
||||
status="pass",
|
||||
op="swiglu7",
|
||||
input_shape=list(x.shape),
|
||||
input_stride=list(x.stride()),
|
||||
dtype=args.dtype,
|
||||
out_dtype=args.out_dtype or args.dtype,
|
||||
alpha=1.702,
|
||||
limit=7.0,
|
||||
minimum_io_bytes=minimum_io_bytes,
|
||||
# Six scalar arithmetic operations and one exp per output;
|
||||
# clamps and transcendental costs are not modeled as FLOPs.
|
||||
scalar_flops_excluding_exp_and_clamps=6 * output_elements,
|
||||
effective_fused_gbps=minimum_io_bytes / timing["fused"]["p50_us"] / 1000,
|
||||
**parity,
|
||||
**timing,
|
||||
)
|
||||
del x, reference, fused
|
||||
cuda.empty_cache()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,124 @@
|
||||
# MammothModa2 startup benchmark
|
||||
|
||||
Measures `Omni()` initialization and first/subsequent image requests for
|
||||
MammothModa2-Preview. This is a measurement baseline for
|
||||
[#7075](https://github.com/vllm-project/vllm-omni/issues/7075); it changes no runtime code.
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root, with the model downloaded and vLLM-Omni installed:
|
||||
|
||||
```bash
|
||||
set -o pipefail
|
||||
python benchmarks/mammoth_moda2/bench_startup.py \
|
||||
--model /path/MammothModa2-Preview \
|
||||
--deploy-config vllm_omni/deploy/mammoth_moda2.yaml \
|
||||
--height 512 --width 512 --seed 42 \
|
||||
--extra-body '{"text_guidance_scale": 4.0, "cfg_range": [0.0, 1.0], "num_inference_steps": 8}' \
|
||||
--repeat 1 --label baseline --output-json baseline.json --save-image baseline.png \
|
||||
2>&1 | tee baseline.log
|
||||
python benchmarks/mammoth_moda2/parse_startup_log.py baseline.log --require-complete --markdown
|
||||
```
|
||||
|
||||
Use `OMP_NUM_THREADS=16` to set CPU threads and `--parallel-stage-init` to
|
||||
initialize stages concurrently. For first/subsequent requests, use
|
||||
`--height 1024 --width 1024 --repeat 3` and 50 inference steps. Memory polling
|
||||
is opt-in (`--sample-memory`); collect it in separate runs to avoid perturbing
|
||||
the timing comparison. `--save-image` saves only the first request's image.
|
||||
Compare that image across runs; a fixed seed alone does not guarantee equality
|
||||
across execution modes or software versions.
|
||||
|
||||
## Measurement boundaries
|
||||
|
||||
| Metric | Boundary / limitation |
|
||||
| --- | --- |
|
||||
| Imports | Before torch import → benchmark imports complete; excludes interpreter startup |
|
||||
| Time to ready | `Omni()` call → return; excludes client imports and requests |
|
||||
| Time to first image | Benchmark clock start → first `generate()` return, including request setup |
|
||||
| First / subsequent request | Each `generate()` call → return; same prompt and settings |
|
||||
| Stage spawn / device init | Log timestamps: launch → `world_size=` → `Starting to load model`; 1 s resolution |
|
||||
| Weight load | Engine-reported `Loading weights took`; read/cast/copy are merged |
|
||||
| Model setup estimate | Reported model-load time minus weight-load time; not independently instrumented |
|
||||
| Profile + KV + warmup | One merged interval after loading; compilation/capture may be nested within it |
|
||||
| Compile / graph capture | Explicit engine log durations when present; N/A for eager execution |
|
||||
| Memory | Optional 1 s samples: sum of process-tree RSS and maximum device memory used across GPUs reported by `nvidia-smi`; includes other GPU users and can miss transient peaks |
|
||||
|
||||
Stage timelines overlap with parallel initialization; do not sum them to
|
||||
calculate time to ready. Missing log fields stay unmeasured. The parser marks
|
||||
logs without `BENCH_JSON` as incomplete; `--require-complete` makes this an error.
|
||||
|
||||
## Reference results
|
||||
|
||||
A800-SXM4-80GB; Ubuntu 22.04; 16-CPU cgroup quota on a 128-core host;
|
||||
vLLM 0.28.0+cu129, torch 2.13.0+cu129, vLLM-Omni `34aa1d2a`.
|
||||
Checkpoint: 34.5 GiB, 8 shards, mostly fp32, on local NVMe.
|
||||
Committed eager deploy config, GPU memory fractions 0.5 / 0.3.
|
||||
|
||||
The 2026-09-10 rerun used the corrected benchmark: one warm-up process,
|
||||
then three rounds of four configurations with order rotated between rounds.
|
||||
The checkpoint page cache was warmed before the matrix; no cache eviction
|
||||
was requested during it. Each fresh process sent one 512×512, 8-step request
|
||||
(guidance 4.0, seed 42). Memory polling was disabled. All 12 measured runs
|
||||
are retained in [the results](reference_results.json).
|
||||
Values below are `Omni()` seconds, mean ± sample standard deviation, n=3;
|
||||
client imports took approximately another 10 s.
|
||||
|
||||
| CPU threads | Serial init | Parallel init |
|
||||
| --- | --- | --- |
|
||||
| Default (64) | 60.1 ± 0.2 | 41.1 ± 1.0 |
|
||||
| 16 | 56.5 ± 0.9 | 36.7 ± 0.2 |
|
||||
|
||||
On this configuration, parallel initialization reduced time to ready by about
|
||||
32%; combining it with 16 threads reduced it by about 39%. This supports
|
||||
trying these settings on similar deployments, not changing defaults for all
|
||||
models or hardware. Fixed 512×512 samples matched across all 12 measured runs;
|
||||
this is a smoke check, not a broad quality evaluation.
|
||||
|
||||
For context, in the earlier serial baseline AR / DiT spent approximately 11 / 11 s in
|
||||
spawn and imports, 6 / 6 s in device setup, 6 / 4 s loading weights, and
|
||||
5 / 2 s in profile + KV + warmup. About 6 s followed the last stage becoming
|
||||
ready. These coarse log intervals locate work; they are not independent
|
||||
operator timings.
|
||||
|
||||
Separate observations from the earlier experiment (not rerun in this matrix):
|
||||
|
||||
- Requested page-cache eviction: 80.8 s; FlashInfer JIT cache removed:
|
||||
129.1 s (means, n=3 each). These are separate cache conditions, not guaranteed
|
||||
cold-machine startup or isolated disk/JIT costs.
|
||||
- Three 1024×1024, 50-step eager requests in one process took
|
||||
95.2 / 94.9 / 94.6 s. No large first-request penalty was observed in that run.
|
||||
- With AR `enforce_eager: false` and serial init, time to ready was
|
||||
62.5 ± 0.1 s (warm caches, n=3); logged graph capture was 4 s / 0.20 GiB.
|
||||
The model reported torch.compile unsupported. KV capacity fell from about
|
||||
15.7 to 2.0 GiB, and one parallel-init trial failed. The reported non-KV
|
||||
profiling difference is not a measurement of graph allocation. Graph-mode
|
||||
samples differed from eager. This needs separate memory and quality analysis
|
||||
before recommending the configuration.
|
||||
|
||||
## Loading diagnostic and storage scenarios
|
||||
|
||||
```bash
|
||||
OMP_NUM_THREADS=16 python benchmarks/mammoth_moda2/raw_load_bench.py \
|
||||
--model /path/MammothModa2-Preview --shards 6,7,8 --mode cpu_cast
|
||||
# Compare --mode gpu_cast in a fresh process with the same cache conditions.
|
||||
|
||||
MODEL_LOCAL=/local/MammothModa2-Preview OUT_DIR=./startup-bench \
|
||||
bash benchmarks/mammoth_moda2/bench_storage_scenarios.sh
|
||||
# Optional MODEL_NFS=/nfs/MammothModa2-Preview; SCENARIOS="local-cold local-warm".
|
||||
```
|
||||
|
||||
The diagnostic clones each mmap-backed tensor into owned CPU memory before
|
||||
casting. `materialize_s` includes page faults, CPU allocation and copying;
|
||||
`cast_s` measures dtype conversion; `h2d_s` includes GPU allocation and copy.
|
||||
CUDA context/operation warmup is outside the timer. Total also includes file
|
||||
opening and bookkeeping. This extra CPU copy differs from the engine loader,
|
||||
so these numbers do not decompose its loading time or measure disk bandwidth.
|
||||
The revised diagnostic was run three times each for CPU cast with 64 / 16
|
||||
threads and GPU cast with 16 threads; per-run values are in the results file.
|
||||
Both CPU materialization and conversion were faster with 16 threads here; this
|
||||
does not isolate how much of the engine-level improvement comes from either.
|
||||
|
||||
Storage scenarios explicitly read shards before warm runs and request
|
||||
`posix_fadvise(DONTNEED)` before cold runs. Eviction is not guaranteed, and
|
||||
server-side NFS caches are uncontrolled. The script stops on failure and
|
||||
summarizes only logs from the current invocation.
|
||||
-2
@@ -1,4 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Qwen3-Omni turn-based browser call UI."""
|
||||
@@ -0,0 +1,338 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Startup / loading benchmark for multi-stage omni models (MammothModa2 first).
|
||||
|
||||
Measures, from a single fresh process:
|
||||
* python import time (torch + vllm_omni)
|
||||
* Omni() construction wall time (all stages: weight load, profiling, KV cache, warmup)
|
||||
* first request latency (includes JIT / lazy init that only happens on first run)
|
||||
* steady-state request latency (repeat N-1 more requests)
|
||||
* optional host RSS / device memory samples (--sample-memory, once a second)
|
||||
|
||||
Per-stage breakdown (weight loading, model-load memory, engine init) is emitted by the
|
||||
stage engine-core subprocesses into the log; use parse_startup_log.py on the captured
|
||||
stdout/stderr to merge them with the JSON written here.
|
||||
|
||||
Usage (keep the log: the per-stage breakdown is parsed from it afterwards):
|
||||
python benchmarks/mammoth_moda2/bench_startup.py --model /path/MammothModa2-Preview \
|
||||
--deploy-config vllm_omni/deploy/mammoth_moda2.yaml \
|
||||
--height 1024 --width 1024 --seed 42 \
|
||||
--extra-body '{"text_guidance_scale": 4.0, "cfg_range": [0.0, 1.0], "num_inference_steps": 50}' \
|
||||
--repeat 2 --label nfs-warm --output-json out.json 2>&1 | tee run.log
|
||||
python benchmarks/mammoth_moda2/parse_startup_log.py run.log --markdown
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
T_PROCESS_START = time.perf_counter()
|
||||
|
||||
import torch # noqa: E402
|
||||
|
||||
from vllm_omni.diffusion.utils.param_utils import apply_declared_extra_args # noqa: E402
|
||||
from vllm_omni.entrypoints.omni import Omni # noqa: E402
|
||||
from vllm_omni.entrypoints.openai.stage_params import clone_sampling_params # noqa: E402
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams # noqa: E402
|
||||
from vllm_omni.model_extras import ( # noqa: E402
|
||||
build_text_to_image_prompt as build_model_text_to_image_prompt,
|
||||
)
|
||||
from vllm_omni.model_extras import ( # noqa: E402
|
||||
get_extra_body_params,
|
||||
get_model_class_name,
|
||||
should_init_extra_args_for_non_diffusion_stages,
|
||||
)
|
||||
from vllm_omni.platforms import current_omni_platform # noqa: E402
|
||||
|
||||
T_IMPORTS_DONE = time.perf_counter()
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
p.add_argument("--model", required=True)
|
||||
p.add_argument("--deploy-config", default=None)
|
||||
p.add_argument("--prompt", default="A stylish woman riding a motorcycle in NYC, movie poster style")
|
||||
p.add_argument("--negative-prompt", default=None)
|
||||
p.add_argument("--height", type=int, default=1024)
|
||||
p.add_argument("--width", type=int, default=1024)
|
||||
p.add_argument("--seed", type=int, default=42)
|
||||
p.add_argument("--extra-body", type=json.loads, default=None, help="JSON object forwarded as model extra_body")
|
||||
p.add_argument(
|
||||
"--repeat", type=int, default=2, help="number of generate() calls; 1st = first-request, rest = steady"
|
||||
)
|
||||
p.add_argument("--label", default="run", help="free-form label, e.g. nfs-cold / nfs-warm / local-nvme")
|
||||
p.add_argument("--output-json", default=None)
|
||||
p.add_argument("--save-image", default=None, help="optional path to save the first request's image")
|
||||
p.add_argument("--sample-memory", action="store_true", help="sample memory once a second (adds overhead)")
|
||||
p.add_argument(
|
||||
"--parallel-stage-init",
|
||||
action="store_true",
|
||||
help="initialize stages that share a GPU concurrently (VllmOmniOrchestratorConfig.parallel_stage_init)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
if args.repeat < 1:
|
||||
p.error("--repeat must be at least 1")
|
||||
return args
|
||||
|
||||
|
||||
def gpu_info() -> dict:
|
||||
info = {"torch": torch.__version__, "cuda": torch.version.cuda, "python": platform.python_version()}
|
||||
try:
|
||||
import vllm
|
||||
|
||||
info["vllm"] = vllm.__version__
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
try:
|
||||
import vllm_omni
|
||||
|
||||
info["vllm_omni"] = getattr(vllm_omni, "__version__", "src")
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["nvidia-smi", "--query-gpu=name,driver_version,memory.total", "--format=csv,noheader"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
info["gpu"] = out.stdout.strip()
|
||||
except Exception:
|
||||
info["gpu"] = "n/a"
|
||||
return info
|
||||
|
||||
|
||||
class PeakSampler:
|
||||
"""Poll host RSS of this process tree and GPU memory in use once a second (benchmark-side only)."""
|
||||
|
||||
def __init__(self, interval_s: float = 1.0):
|
||||
self.interval_s = interval_s
|
||||
self.host_rss_peak_gib = 0.0
|
||||
self.gpu_used_peak_gib = 0.0
|
||||
self._stop = threading.Event()
|
||||
self._thread = threading.Thread(target=self._run, daemon=True)
|
||||
|
||||
def _sample(self) -> None:
|
||||
try:
|
||||
import psutil
|
||||
|
||||
root = psutil.Process()
|
||||
rss = root.memory_info().rss + sum(
|
||||
c.memory_info().rss for c in root.children(recursive=True) if c.is_running()
|
||||
)
|
||||
self.host_rss_peak_gib = max(self.host_rss_peak_gib, rss / 2**30)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
used = max(float(x) for x in out.stdout.split() if x.strip())
|
||||
self.gpu_used_peak_gib = max(self.gpu_used_peak_gib, used / 1024)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _run(self) -> None:
|
||||
while not self._stop.is_set():
|
||||
self._sample()
|
||||
self._stop.wait(self.interval_s)
|
||||
|
||||
def start(self) -> "PeakSampler":
|
||||
self._thread.start()
|
||||
return self
|
||||
|
||||
def stop(self) -> dict:
|
||||
self._stop.set()
|
||||
self._thread.join(timeout=10)
|
||||
self._sample()
|
||||
return {
|
||||
"host_rss_peak_gib": round(self.host_rss_peak_gib, 2),
|
||||
"gpu_used_peak_gib": round(self.gpu_used_peak_gib, 2),
|
||||
}
|
||||
|
||||
|
||||
def fs_type(path: str) -> str:
|
||||
try:
|
||||
out = subprocess.run(["stat", "-f", "-c", "%T", path], capture_output=True, text=True, timeout=10)
|
||||
return out.stdout.strip()
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def build_request(omni: Omni, args: argparse.Namespace, generator: torch.Generator):
|
||||
model_class_name = get_model_class_name(omni)
|
||||
declared = get_extra_body_params(model_class_name)
|
||||
|
||||
prompt_dict = {"prompt": args.prompt}
|
||||
if args.negative_prompt:
|
||||
prompt_dict["negative_prompt"] = args.negative_prompt
|
||||
prompt_dict = build_model_text_to_image_prompt(
|
||||
model_class_name=model_class_name, prompt=prompt_dict, height=args.height, width=args.width
|
||||
)
|
||||
|
||||
diffusion_params = OmniDiffusionSamplingParams(
|
||||
height=args.height,
|
||||
width=args.width,
|
||||
seed=args.seed,
|
||||
generator=generator,
|
||||
)
|
||||
user_extra = dict(args.extra_body or {})
|
||||
if declared:
|
||||
apply_declared_extra_args(diffusion_params, declared, user_extra)
|
||||
else:
|
||||
diffusion_params.extra_args.update({k: v for k, v in user_extra.items() if v is not None})
|
||||
|
||||
init_non_diffusion = should_init_extra_args_for_non_diffusion_stages(model_class_name)
|
||||
sampling_params_list = [clone_sampling_params(p) for p in (omni.default_sampling_params_list or [])]
|
||||
if not sampling_params_list:
|
||||
sampling_params_list = [diffusion_params]
|
||||
|
||||
replaced = False
|
||||
for idx, params in enumerate(sampling_params_list):
|
||||
if isinstance(params, OmniDiffusionSamplingParams):
|
||||
sampling_params_list[idx] = diffusion_params
|
||||
replaced = True
|
||||
elif init_non_diffusion and hasattr(params, "extra_args"):
|
||||
params.extra_args = {**(params.extra_args or {}), **(diffusion_params.extra_args or {})}
|
||||
if hasattr(params, "seed"):
|
||||
params.seed = args.seed
|
||||
# MammothModa2 AR stage: one visual token per grid cell + one EOL per row + 1 look-ahead.
|
||||
info = prompt_dict.get("additional_information", {})
|
||||
if idx == 0 and info.get("omni_task") == ["t2i"]:
|
||||
ar_w = int(info.get("ar_width", [0])[0])
|
||||
ar_h = int(info.get("ar_height", [0])[0])
|
||||
if ar_w > 0 and ar_h > 0:
|
||||
params.max_tokens = ar_h * (ar_w + 1) + 1
|
||||
if not replaced and len(sampling_params_list) == 1:
|
||||
sampling_params_list = [diffusion_params]
|
||||
return prompt_dict, sampling_params_list
|
||||
|
||||
|
||||
def stage_metrics(outputs) -> dict:
|
||||
"""Pull vllm-omni's per-stage timing out of the request output when present."""
|
||||
res = {}
|
||||
for out in outputs or []:
|
||||
metrics = getattr(out, "metrics", None)
|
||||
if isinstance(metrics, dict) and "stage_metrics" in metrics:
|
||||
for sid, m in metrics["stage_metrics"].items():
|
||||
if not isinstance(m, dict):
|
||||
continue
|
||||
res[str(sid)] = {
|
||||
k: m.get(k)
|
||||
for k in ("stage_gen_time_ms", "num_tokens_in", "num_tokens_out", "vllm_ttft_ms", "vllm_tpot_ms")
|
||||
if k in m
|
||||
}
|
||||
return res
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
result = {
|
||||
"label": args.label,
|
||||
"model": args.model,
|
||||
"prompt": args.prompt,
|
||||
"negative_prompt": args.negative_prompt,
|
||||
"sample_memory": args.sample_memory,
|
||||
"model_fs": fs_type(args.model),
|
||||
"deploy_config": args.deploy_config,
|
||||
"height": args.height,
|
||||
"width": args.width,
|
||||
"seed": args.seed,
|
||||
"extra_body": args.extra_body,
|
||||
"parallel_stage_init": args.parallel_stage_init,
|
||||
"omp_num_threads": os.environ.get("OMP_NUM_THREADS"),
|
||||
"env": gpu_info(),
|
||||
"phases_s": {"imports": round(T_IMPORTS_DONE - T_PROCESS_START, 3)},
|
||||
"requests": [],
|
||||
}
|
||||
|
||||
omni_kwargs = {"model": args.model, "mode": "text-to-image", "log_stats": True}
|
||||
if args.deploy_config:
|
||||
omni_kwargs["deploy_config"] = args.deploy_config
|
||||
if args.parallel_stage_init:
|
||||
omni_kwargs["parallel_stage_init"] = True
|
||||
|
||||
sampler = PeakSampler().start() if args.sample_memory else None
|
||||
t0 = time.perf_counter()
|
||||
omni = None
|
||||
try:
|
||||
omni = Omni(**omni_kwargs)
|
||||
t1 = time.perf_counter()
|
||||
result["peak_mem_startup"] = sampler.stop() if sampler else {}
|
||||
sampler = None
|
||||
result["phases_s"]["engine_init"] = round(t1 - t0, 3)
|
||||
result["phases_s"]["process_to_engine_ready"] = round(t1 - T_PROCESS_START, 3)
|
||||
print(
|
||||
f"BENCH engine_init={t1 - t0:.2f}s process_to_engine_ready={t1 - T_PROCESS_START:.2f}s "
|
||||
f"peak_mem_startup={result['peak_mem_startup']}",
|
||||
flush=True,
|
||||
)
|
||||
sampler = PeakSampler().start() if args.sample_memory else None
|
||||
|
||||
generator = torch.Generator(device=current_omni_platform.device_type).manual_seed(args.seed)
|
||||
outputs = None
|
||||
first_outputs = None
|
||||
first_image_elapsed = 0.0
|
||||
for i in range(args.repeat):
|
||||
prompt_dict, sampling_params_list = build_request(omni, args, generator)
|
||||
tr0 = time.perf_counter()
|
||||
outputs = omni.generate(prompt_dict, sampling_params_list=sampling_params_list)
|
||||
tr1 = time.perf_counter()
|
||||
if first_outputs is None:
|
||||
first_outputs = outputs
|
||||
first_image_elapsed = tr1 - T_PROCESS_START
|
||||
rec = {
|
||||
"index": i,
|
||||
"kind": "first" if i == 0 else "steady",
|
||||
"latency_s": round(tr1 - tr0, 3),
|
||||
"stages": stage_metrics(outputs),
|
||||
}
|
||||
result["requests"].append(rec)
|
||||
print(f"BENCH request[{i}] {rec['kind']} latency={tr1 - tr0:.2f}s stages={rec['stages']}", flush=True)
|
||||
|
||||
result["peak_mem_requests"] = sampler.stop() if sampler else {}
|
||||
sampler = None
|
||||
steady = [r["latency_s"] for r in result["requests"] if r["kind"] == "steady"]
|
||||
result["summary_s"] = {
|
||||
"imports": result["phases_s"]["imports"],
|
||||
"engine_init": result["phases_s"]["engine_init"],
|
||||
"first_request": result["requests"][0]["latency_s"],
|
||||
"steady_request_avg": round(sum(steady) / len(steady), 3) if steady else None,
|
||||
"time_to_first_image_from_process_start": round(first_image_elapsed, 3),
|
||||
}
|
||||
|
||||
for key in ("host_rss_peak_gib", "gpu_used_peak_gib"):
|
||||
result["summary_s"][key] = (
|
||||
max(result["peak_mem_startup"][key], result["peak_mem_requests"][key]) if args.sample_memory else None
|
||||
)
|
||||
|
||||
from vllm_omni.diffusion.utils.image_output import extract_images_from_outputs
|
||||
|
||||
images = extract_images_from_outputs(first_outputs)
|
||||
if not images:
|
||||
raise RuntimeError("First request returned no image")
|
||||
if args.save_image:
|
||||
os.makedirs(os.path.dirname(os.path.abspath(args.save_image)), exist_ok=True)
|
||||
images[0].save(args.save_image)
|
||||
|
||||
print("BENCH_JSON " + json.dumps(result), flush=True)
|
||||
if args.output_json:
|
||||
with open(args.output_json, "w") as f:
|
||||
json.dump(result, f, indent=2)
|
||||
print(f"BENCH wrote {args.output_json}", flush=True)
|
||||
finally:
|
||||
if sampler:
|
||||
sampler.stop()
|
||||
if omni is not None:
|
||||
omni.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# MODEL_LOCAL=/local/model MODEL_NFS=/nfs/model OUT_DIR=./startup-bench bash "$0"
|
||||
# Optional SCENARIOS: local-cold local-warm nfs-cold nfs-warm.
|
||||
set -euo pipefail
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
OUT_DIR=${OUT_DIR:-./startup-bench}
|
||||
DEPLOY_CONFIG=${DEPLOY_CONFIG:-vllm_omni/deploy/mammoth_moda2.yaml}
|
||||
STEPS=${STEPS:-50}
|
||||
SIZE=${SIZE:-1024}
|
||||
REPEAT=${REPEAT:-2}
|
||||
SEED=${SEED:-42}
|
||||
EXTRA=${EXTRA:-'{"text_guidance_scale": 4.0, "cfg_range": [0.0, 1.0], "num_inference_steps": '$STEPS'}'}
|
||||
mkdir -p "$OUT_DIR/logs" "$OUT_DIR/json" "$OUT_DIR/images"
|
||||
rm -f "$OUT_DIR/summary.json" "$OUT_DIR/summary.md"
|
||||
|
||||
prepare_cache() {
|
||||
python - "$1" "$2" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
files = sorted(Path(sys.argv[1]).glob("*.safetensors"))
|
||||
if not files:
|
||||
raise SystemExit("no checkpoint shards found")
|
||||
for path in files:
|
||||
with path.open("rb") as f:
|
||||
if sys.argv[2] == "cold":
|
||||
os.posix_fadvise(f.fileno(), 0, 0, os.POSIX_FADV_DONTNEED)
|
||||
else:
|
||||
while f.read(8 * 1024 * 1024):
|
||||
pass
|
||||
print("[bench]", "requested eviction of" if sys.argv[2] == "cold" else "read", len(files), "shards")
|
||||
PY
|
||||
}
|
||||
|
||||
default_scenarios=""
|
||||
if [ -n "${MODEL_LOCAL:-}" ]; then default_scenarios="local-cold local-warm"; fi
|
||||
if [ -n "${MODEL_NFS:-}" ]; then default_scenarios="$default_scenarios nfs-cold nfs-warm"; fi
|
||||
read -r -a scenarios <<< "${SCENARIOS:-$default_scenarios}"
|
||||
if [ "${#scenarios[@]}" -eq 0 ]; then echo "Set MODEL_LOCAL or MODEL_NFS" >&2; exit 1; fi
|
||||
logs=()
|
||||
for label in "${scenarios[@]}"; do
|
||||
case "$label" in
|
||||
local-cold|local-warm) model=${MODEL_LOCAL:?Set MODEL_LOCAL} ;;
|
||||
nfs-cold|nfs-warm) model=${MODEL_NFS:?Set MODEL_NFS} ;;
|
||||
*) echo "Unknown scenario: $label" >&2; exit 1 ;;
|
||||
esac
|
||||
prepare_cache "$model" "${label##*-}"
|
||||
log="$OUT_DIR/logs/$label.log"
|
||||
# Remove previous outputs for this label so a failed rerun cannot leave stale results.
|
||||
rm -f "$OUT_DIR/json/$label.json" "$OUT_DIR/images/$label.png"
|
||||
if python "$HERE/bench_startup.py" --model "$model" --deploy-config "$DEPLOY_CONFIG" \
|
||||
--height "$SIZE" --width "$SIZE" --seed "$SEED" --extra-body "$EXTRA" --repeat "$REPEAT" \
|
||||
--label "$label" --output-json "$OUT_DIR/json/$label.json" --save-image "$OUT_DIR/images/$label.png" \
|
||||
> "$log" 2>&1; then
|
||||
logs+=("$log")
|
||||
else
|
||||
rc=$?
|
||||
echo "[bench] $label failed (exit $rc); see $log" >&2
|
||||
exit "$rc"
|
||||
fi
|
||||
done
|
||||
python "$HERE/parse_startup_log.py" "${logs[@]}" --require-complete --markdown \
|
||||
--json-out "$OUT_DIR/summary.json" | tee "$OUT_DIR/summary.md"
|
||||
echo BENCH_ALL_DONE
|
||||
@@ -0,0 +1,245 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Merge per-stage startup numbers from a bench_startup.py log with its BENCH_JSON line.
|
||||
|
||||
The stage engine cores run in subprocesses and report weight-loading / init timings
|
||||
only through log lines such as:
|
||||
|
||||
(StageEngineCoreProc_stage0_replica0 pid=1) INFO ... Loading weights took 39.66 seconds
|
||||
(StageEngineCoreProc_stage0_replica0 pid=1) INFO ... Model loading took 21.45 GiB memory and 40.59 seconds
|
||||
(StageEngineCoreProc_stage0_replica0 pid=1) INFO ... init engine (profile, create kv cache, warmup model) took 7.00 s
|
||||
(StageEngineCoreProc_stage0_replica0 pid=1) INFO ... Filesystem type for checkpoints: NFS4.
|
||||
Checkpoint size: 34.52 GiB. Available RAM: 22.76 GiB.
|
||||
INFO ... [Omni] AsyncOmniEngine initialized in 159.74 seconds
|
||||
|
||||
Usage:
|
||||
python benchmarks/mammoth_moda2/parse_startup_log.py run.log [more.log ...] [--markdown] [--json-out summary.json]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
STAGE = r"\(StageEngineCoreProc_stage(?P<stage>\d+)_replica\d+ pid=\d+\)"
|
||||
PATTERNS = {
|
||||
"weights_load_s": re.compile(STAGE + r".*Loading weights took (?P<v>[\d.]+) seconds"),
|
||||
"model_load_s": re.compile(STAGE + r".*Model loading took (?P<mem>[\d.]+) GiB memory and (?P<v>[\d.]+) seconds"),
|
||||
"engine_init_s": re.compile(
|
||||
STAGE + r".*init engine \(profile, create kv cache, warmup model\) took (?P<v>[\d.]+) s"
|
||||
),
|
||||
# Only present when the stage does not run with enforce_eager.
|
||||
"graph_capture_s": re.compile(
|
||||
STAGE + r".*Graph capturing finished in (?P<v>[\d.]+) secs, took (?P<mem>[\d.]+) GiB"
|
||||
),
|
||||
"compile_s": re.compile(STAGE + r".*torch\.compile takes (?P<v>[\d.]+) s in total"),
|
||||
"ckpt_fs": re.compile(
|
||||
STAGE
|
||||
+ r".*Filesystem type for checkpoints: (?P<fs>\w+)\. Checkpoint size: (?P<size>[\d.]+) GiB\."
|
||||
+ r" Available RAM: (?P<ram>[\d.]+) GiB"
|
||||
),
|
||||
}
|
||||
ENGINE_READY = re.compile(r"AsyncOmniEngine initialized in (?P<v>[\d.]+) seconds")
|
||||
PREFETCH_SKIP = re.compile(STAGE + r".*exceeds 90% of available RAM")
|
||||
COMPILE_UNSUPPORTED = re.compile(STAGE + r".*`torch\.compile` is turned on, but the model .* does not support it")
|
||||
|
||||
# Startup timeline: (milestone key, regex). Timestamps in vllm logs are "MM-DD HH:MM:SS" (1 s resolution).
|
||||
TS = re.compile(r"(?:INFO|WARNING|ERROR) (?P<ts>\d\d-\d\d \d\d:\d\d:\d\d)")
|
||||
MILESTONES = [
|
||||
("omni_init_start", re.compile(r"\[Omni\] Initializing with model")),
|
||||
("stage{stage}_launch", re.compile(r"Stage-(?P<stage>\d+) set runtime devices")),
|
||||
("stage{stage}_proc_up", re.compile(STAGE + r".*world_size=\d+ rank=\d+")),
|
||||
("stage{stage}_load_start", re.compile(STAGE + r".*Starting to load model")),
|
||||
("stage{stage}_load_done", re.compile(STAGE + r".*Loading weights took")),
|
||||
("stage{stage}_init_done", re.compile(STAGE + r".*init engine \(profile, create kv cache, warmup model\) took")),
|
||||
("stage{stage}_ready", re.compile(r"\[StageRuntime\] Stage (?P<stage>\d+) initialized")),
|
||||
("engine_ready", re.compile(r"AsyncOmniEngine initialized in")),
|
||||
]
|
||||
|
||||
|
||||
def _ts_seconds(ts: str) -> int:
|
||||
# Use a leap year because engine timestamps do not include the year.
|
||||
value = datetime.strptime("2000-" + ts, "%Y-%m-%d %H:%M:%S")
|
||||
return int((value - datetime(2000, 1, 1)).total_seconds())
|
||||
|
||||
|
||||
def timeline(lines: list[str]) -> dict:
|
||||
"""Return milestone timestamps (s, relative to omni_init_start) and derived per-stage phase durations."""
|
||||
marks: dict[str, int] = {}
|
||||
for line in lines:
|
||||
tsm = TS.search(line)
|
||||
if not tsm:
|
||||
continue
|
||||
for key, pat in MILESTONES:
|
||||
m = pat.search(line)
|
||||
if m:
|
||||
k = key.format(stage=m.group("stage")) if "{stage}" in key else key
|
||||
marks.setdefault(k, _ts_seconds(tsm.group("ts")))
|
||||
break
|
||||
if "omni_init_start" not in marks:
|
||||
return {}
|
||||
t0 = marks["omni_init_start"]
|
||||
rel = {k: (v - t0) % (366 * 86400) for k, v in marks.items()}
|
||||
phases: dict[str, Any] = {}
|
||||
names = ("launch", "proc_up", "load_start", "load_done", "init_done", "ready")
|
||||
for s in sorted({k.split("_")[0] for k in rel if k.startswith("stage")}):
|
||||
found = {name: rel.get(f"{s}_{name}") for name in names}
|
||||
if any(v is None for v in found.values()):
|
||||
continue
|
||||
ts = {name: int(v) for name, v in found.items() if v is not None}
|
||||
phases[s] = {
|
||||
"spawn_import_config_s": ts["proc_up"] - ts["launch"],
|
||||
"device_dist_init_s": ts["load_start"] - ts["proc_up"],
|
||||
"model_and_weights_load_s": ts["load_done"] - ts["load_start"],
|
||||
"profile_kv_warmup_s": ts["init_done"] - ts["load_done"],
|
||||
"ready_handoff_s": ts["ready"] - ts["init_done"],
|
||||
"total_s": ts["ready"] - ts["launch"],
|
||||
}
|
||||
if "engine_ready" in rel:
|
||||
last_ready = max((v for k, v in rel.items() if k.endswith("_ready") and k != "engine_ready"), default=0)
|
||||
phases["post_stages_wiring_s"] = rel["engine_ready"] - last_ready
|
||||
phases["engine_ready_s"] = rel["engine_ready"]
|
||||
return {"marks_rel_s": rel, "phases": phases}
|
||||
|
||||
|
||||
def parse(path: str) -> dict:
|
||||
stages: dict[str, dict] = {}
|
||||
rec: dict[str, Any] = {"log": path, "stages": stages}
|
||||
with open(path, errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
rec["timeline"] = timeline(lines)
|
||||
for line in lines:
|
||||
if line.startswith("BENCH_JSON "):
|
||||
rec["bench"] = json.loads(line[len("BENCH_JSON ") :])
|
||||
continue
|
||||
m = ENGINE_READY.search(line)
|
||||
if m:
|
||||
rec["engine_ready_s_reported"] = float(m.group("v"))
|
||||
continue
|
||||
m = PREFETCH_SKIP.search(line)
|
||||
if m:
|
||||
stages.setdefault(m.group("stage"), {})["page_cache_prefetch_skipped"] = True
|
||||
continue
|
||||
m = COMPILE_UNSUPPORTED.search(line)
|
||||
if m:
|
||||
stages.setdefault(m.group("stage"), {})["torch_compile_unsupported"] = True
|
||||
continue
|
||||
for key, pat in PATTERNS.items():
|
||||
m = pat.search(line)
|
||||
if not m:
|
||||
continue
|
||||
st = stages.setdefault(m.group("stage"), {})
|
||||
if key == "ckpt_fs":
|
||||
st["checkpoint_fs"] = m.group("fs")
|
||||
st["checkpoint_size_gib"] = float(m.group("size"))
|
||||
st["available_ram_gib"] = float(m.group("ram"))
|
||||
elif key == "graph_capture_s":
|
||||
st["graph_capture_s"] = float(m.group("v"))
|
||||
st["graph_capture_mem_gib"] = float(m.group("mem"))
|
||||
elif key == "model_load_s":
|
||||
st["model_load_s"] = float(m.group("v"))
|
||||
st["model_load_mem_gib"] = float(m.group("mem"))
|
||||
# "Model loading took" covers model construction + weight loading; "Loading weights took" only
|
||||
# the weights. The difference also includes loader setup/teardown overhead.
|
||||
if "weights_load_s" in st:
|
||||
st["model_setup_estimate_s"] = round(st["model_load_s"] - st["weights_load_s"], 2)
|
||||
else:
|
||||
st[key] = float(m.group("v"))
|
||||
break
|
||||
rec["status"] = "complete" if "bench" in rec else "incomplete"
|
||||
return rec
|
||||
|
||||
|
||||
def _table(columns: dict[str, str], records: list[dict]) -> str:
|
||||
def row(values):
|
||||
return "| " + " | ".join("" if v is None else str(v) for v in values) + " |"
|
||||
|
||||
lines = [row(columns.values()), row(["---"] * len(columns))]
|
||||
lines.extend(row(record.get(key) for key in columns) for record in records)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def to_markdown(recs: list[dict]) -> str:
|
||||
summary, stages, phases, warnings = [], [], [], []
|
||||
for r in recs:
|
||||
b = r.get("bench", {})
|
||||
label = b.get("label", r["log"])
|
||||
if r.get("status") == "incomplete":
|
||||
warnings.append(f"Incomplete log: {r['log']} (no BENCH_JSON).")
|
||||
summary.append({"label": label, **b.get("summary_s", {})})
|
||||
for sid, st in sorted(r["stages"].items()):
|
||||
stages.append({"label": label, "stage": sid, **st})
|
||||
if st.get("torch_compile_unsupported"):
|
||||
stages[-1]["compile_s"] = "unsupported"
|
||||
ph = r.get("timeline", {}).get("phases", {})
|
||||
for sid, values in sorted(ph.items()):
|
||||
if sid.startswith("stage"):
|
||||
phases.append({"label": label, "stage": sid, **values})
|
||||
if "post_stages_wiring_s" in ph:
|
||||
phases.append({"label": label, "stage": "after last stage", "total_s": ph["post_stages_wiring_s"]})
|
||||
tables = [
|
||||
(
|
||||
{
|
||||
"label": "label",
|
||||
"imports": "imports (s)",
|
||||
"engine_init": "engine ready (s)",
|
||||
"first_request": "first request (s)",
|
||||
"steady_request_avg": "subsequent avg (s)",
|
||||
"time_to_first_image_from_process_start": "process→first image (s)",
|
||||
"host_rss_peak_gib": "sampled host RSS (GiB)",
|
||||
"gpu_used_peak_gib": "sampled GPU used (GiB)",
|
||||
},
|
||||
summary,
|
||||
),
|
||||
(
|
||||
{
|
||||
"label": "label",
|
||||
"stage": "stage",
|
||||
"model_setup_estimate_s": "model setup estimate (s)",
|
||||
"weights_load_s": "weights load (s)",
|
||||
"compile_s": "compile (s)",
|
||||
"graph_capture_s": "graph capture (s)",
|
||||
"engine_init_s": "profile/KV/warmup reported (s)",
|
||||
},
|
||||
stages,
|
||||
),
|
||||
(
|
||||
{
|
||||
"label": "label",
|
||||
"stage": "stage",
|
||||
"spawn_import_config_s": "spawn/import (s)",
|
||||
"device_dist_init_s": "device init (s)",
|
||||
"model_and_weights_load_s": "model+weights load (s)",
|
||||
"profile_kv_warmup_s": "profile/KV/warmup interval (s)",
|
||||
"total_s": "total (s)",
|
||||
},
|
||||
phases,
|
||||
),
|
||||
]
|
||||
return "\n\n".join(warnings + [_table(columns, rows) for columns, rows in tables])
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("logs", nargs="+")
|
||||
ap.add_argument("--markdown", action="store_true")
|
||||
ap.add_argument("--json-out", default=None)
|
||||
ap.add_argument("--require-complete", action="store_true", help="fail if any log has no BENCH_JSON")
|
||||
args = ap.parse_args()
|
||||
recs = [parse(p) for p in args.logs]
|
||||
if args.require_complete and any(r["status"] != "complete" for r in recs):
|
||||
ap.error("incomplete benchmark log: missing BENCH_JSON")
|
||||
if args.json_out:
|
||||
with open(args.json_out, "w") as f:
|
||||
json.dump(recs, f, indent=2)
|
||||
if args.markdown:
|
||||
print(to_markdown(recs))
|
||||
else:
|
||||
json.dump(recs, sys.stdout, indent=2)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,127 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Raw safetensors -> GPU micro-benchmark, without vLLM's loader.
|
||||
|
||||
Compares CPU and GPU dtype conversion after materializing each mmap-backed tensor.
|
||||
Materialization includes page faults, CPU allocation and copying; it is not a disk
|
||||
bandwidth measurement. CUDA setup is excluded; transfer includes GPU allocation.
|
||||
|
||||
--mode cpu_cast materialize -> cast to bf16 on the CPU -> copy to GPU
|
||||
--mode gpu_cast materialize -> copy source dtype to GPU -> cast on the GPU
|
||||
|
||||
python benchmarks/mammoth_moda2/raw_load_bench.py --model /path/MammothModa2-Preview --shards 6,7,8
|
||||
OMP_NUM_THREADS=4 python benchmarks/mammoth_moda2/raw_load_bench.py --model /path --shards 6,7,8 --mode cpu_cast
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
|
||||
SHARD_NO = re.compile(r"-(\d+)-of-\d+\.safetensors$")
|
||||
|
||||
|
||||
def shard_files(model: str, shards: str) -> list[str]:
|
||||
"""Resolve ``--shards`` to files, refusing empty or partial matches so a typo cannot pass as a fast load."""
|
||||
files = sorted(f for f in os.listdir(model) if f.endswith(".safetensors"))
|
||||
if not files:
|
||||
raise SystemExit(f"no .safetensors files under {model}")
|
||||
if shards != "all":
|
||||
want = {int(x) for x in shards.split(",")}
|
||||
found = {int(m.group(1)): f for f in files if (m := SHARD_NO.search(f))}
|
||||
missing = sorted(want - found.keys())
|
||||
if missing:
|
||||
raise SystemExit(f"shard(s) {missing} not found under {model}; available: {sorted(found)}")
|
||||
files = [found[n] for n in sorted(want)]
|
||||
return [os.path.join(model, f) for f in files]
|
||||
|
||||
|
||||
def sync() -> None:
|
||||
if torch.cuda.is_available():
|
||||
torch.accelerator.synchronize()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--model", required=True)
|
||||
ap.add_argument("--shards", default="all", help="comma list of 1-based shard numbers, or 'all'")
|
||||
ap.add_argument("--mode", choices=("cpu_cast", "gpu_cast"), default="cpu_cast")
|
||||
ap.add_argument("--label", default="raw")
|
||||
ap.add_argument("--no-cuda", action="store_true", help="read + CPU cast only (mode must be cpu_cast)")
|
||||
a = ap.parse_args()
|
||||
if a.no_cuda and a.mode != "cpu_cast":
|
||||
ap.error("--no-cuda only makes sense with --mode cpu_cast")
|
||||
|
||||
paths = shard_files(a.model, a.shards)
|
||||
total_bytes = sum(os.path.getsize(p) for p in paths)
|
||||
|
||||
if not a.no_cuda:
|
||||
# Initialize the context and exercise both operations outside the timer.
|
||||
warmup = torch.zeros(1).to("cuda")
|
||||
warmup = warmup.to(torch.bfloat16)
|
||||
sync()
|
||||
del warmup
|
||||
|
||||
t0 = time.perf_counter()
|
||||
n_tensors, cpu_bytes = 0, 0
|
||||
dtypes: dict[str, int] = {}
|
||||
t_materialize = t_cast = t_h2d = 0.0
|
||||
for p in paths:
|
||||
with safe_open(p, framework="pt", device="cpu") as f:
|
||||
for k in f.keys():
|
||||
r0 = time.perf_counter()
|
||||
t = f.get_tensor(k).clone() # touch every page and own the CPU storage
|
||||
t_materialize += time.perf_counter() - r0
|
||||
dtypes[str(t.dtype)] = dtypes.get(str(t.dtype), 0) + 1
|
||||
cpu_bytes += t.numel() * t.element_size()
|
||||
needs_cast = t.is_floating_point() and t.dtype != torch.bfloat16
|
||||
if a.mode == "cpu_cast":
|
||||
c0 = time.perf_counter()
|
||||
if needs_cast:
|
||||
t = t.to(torch.bfloat16)
|
||||
t_cast += time.perf_counter() - c0
|
||||
if not a.no_cuda:
|
||||
h0 = time.perf_counter()
|
||||
g = t.to("cuda")
|
||||
sync()
|
||||
t_h2d += time.perf_counter() - h0
|
||||
del g
|
||||
else:
|
||||
h0 = time.perf_counter()
|
||||
g = t.to("cuda")
|
||||
sync()
|
||||
t_h2d += time.perf_counter() - h0
|
||||
c0 = time.perf_counter()
|
||||
if needs_cast:
|
||||
g = g.to(torch.bfloat16)
|
||||
sync()
|
||||
t_cast += time.perf_counter() - c0
|
||||
del g
|
||||
n_tensors += 1
|
||||
del t
|
||||
total = time.perf_counter() - t0
|
||||
res = {
|
||||
"label": a.label,
|
||||
"mode": a.mode,
|
||||
"files": [os.path.basename(p) for p in paths],
|
||||
"file_bytes_gib": round(total_bytes / 2**30, 2),
|
||||
"tensor_bytes_gib": round(cpu_bytes / 2**30, 2),
|
||||
"n_tensors": n_tensors,
|
||||
"dtypes": dtypes,
|
||||
"torch_threads": torch.get_num_threads(),
|
||||
"cpu_affinity": len(os.sched_getaffinity(0)) if hasattr(os, "sched_getaffinity") else None,
|
||||
"materialize_s": round(t_materialize, 3),
|
||||
"cast_s": round(t_cast, 3),
|
||||
"h2d_s": round(t_h2d, 3),
|
||||
"total_s": round(total, 3),
|
||||
"cuda_setup_included": False,
|
||||
}
|
||||
print("RAWLOAD_JSON " + json.dumps(res))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"date": "2026-09-10",
|
||||
"runtime_commit": "34aa1d2afa59547945fc59f88ab6f4f25f52a777",
|
||||
"environment": {"torch": "2.13.0+cu129", "cuda": "12.9", "python": "3.12.14", "vllm": "0.28.0", "vllm_omni": "0.28.1.dev81+g34aa1d2af.d20260908", "gpu": "NVIDIA A800-SXM4-80GB, 580.126.09, 81920 MiB"},
|
||||
"method": "One excluded warm-up/canary; warm page cache; three rounds of the 2x2 eager configurations with rotated order; fresh process for every run; memory polling disabled.",
|
||||
"request": {"height": 512, "width": 512, "seed": 42, "steps": 8, "guidance": 4.0, "prompt": "A stylish woman riding a motorcycle in NYC, movie poster style"},
|
||||
"gpu_memory_utilization": [0.5, 0.3],
|
||||
"cpu_quota": 16,
|
||||
"startup_columns": ["configuration", "round", "engine_init_s", "imports_s", "first_request_s", "time_to_first_image_s", "image_md5"],
|
||||
"startup_runs": [
|
||||
["default-serial", 1, 60.022, 10.158, 20.827, 91.089, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["default-serial", 2, 60.015, 10.082, 20.766, 90.909, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["default-serial", 3, 60.34, 9.911, 21.089, 91.381, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["omp-serial", 1, 55.45, 9.965, 20.711, 86.169, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["omp-serial", 2, 57.01, 9.667, 20.697, 87.418, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["omp-serial", 3, 57.044, 9.707, 20.415, 87.211, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["default-parallel", 1, 40.258, 10.112, 20.392, 70.829, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["default-parallel", 2, 42.177, 10.135, 20.692, 73.093, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["default-parallel", 3, 40.848, 9.997, 20.995, 71.887, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["omp-parallel", 1, 36.88, 9.666, 20.319, 66.932, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["omp-parallel", 2, 36.534, 9.6, 20.314, 66.517, "60c1fe4d464fba8c6ffd1b752fbbd40c"],
|
||||
["omp-parallel", 3, 36.689, 9.582, 20.467, 66.802, "60c1fe4d464fba8c6ffd1b752fbbd40c"]
|
||||
],
|
||||
"raw_loading_columns": ["configuration", "round", "torch_threads", "n_tensors", "materialize_s", "cast_s", "h2d_s", "total_s"],
|
||||
"raw_loading_runs": [
|
||||
["cpu64", 1, 64, 620, 3.174, 2.309, 1.217, 7.796],
|
||||
["cpu64", 2, 64, 620, 2.957, 2.443, 1.433, 7.846],
|
||||
["cpu64", 3, 64, 620, 2.916, 2.613, 1.067, 7.942],
|
||||
["cpu16", 1, 16, 620, 0.857, 1.137, 0.713, 3.643],
|
||||
["cpu16", 2, 16, 620, 0.871, 1.174, 0.746, 3.608],
|
||||
["cpu16", 3, 16, 620, 0.814, 1.142, 0.724, 3.487],
|
||||
["gpu16", 1, 16, 620, 0.938, 0.028, 1.41, 3.74],
|
||||
["gpu16", 2, 16, 620, 0.926, 0.029, 1.467, 3.83],
|
||||
["gpu16", 3, 16, 620, 0.87, 0.029, 1.395, 3.661]
|
||||
],
|
||||
"raw_loading_workload": {"shards": [6, 7, 8], "source_dtype": "float32", "tensor_gib": 13.3, "warmup": "CUDA context, copy, cast and synchronize before timer", "order_per_round": ["cpu64", "cpu16", "gpu16"]},
|
||||
"executed_script_sha256": {"parse_startup_log.py": "aa260de2cfcaec2fc4a678b245bb7c16fa464543aaec72b947fd4f769d547011", "raw_load_bench.py": "1f70f6b3c205402b2c2c7c52b224e1be3c04a12d12d11044a86f1b9b6c7a1d0f", "bench_startup.py": "04b1e8b9398b4d9885fbb6fe341e8a9ee98792d0be200c15c57e1fc5183c6ff0", "__init__.py": "b1d617f45f34dbe6f5f58d792d90c84af8d8d0a7e7def77c48170ca93495ca0d"},
|
||||
"notes": ["Standard deviations use ddof=1.", "Only first-request ONGs are compared.", "Raw materialization includes CPU allocation/copy; H2D includes GPU allocation; not engine-loader timings."]
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Universal TTS benchmark CLI for vllm-omni.
|
||||
|
||||
Runs ``vllm bench serve --omni`` with model-aware defaults loaded from
|
||||
@@ -87,6 +90,7 @@ def build_bench_args(
|
||||
served_model_name: str | None = None,
|
||||
num_warmups: int = 2,
|
||||
request_seed: int | None = None,
|
||||
duration_seconds: float | None = None,
|
||||
) -> list[str]:
|
||||
"""Build the ``vllm bench serve --omni`` command for one (task, concurrency) run."""
|
||||
dataset_name = _TASK_TO_DATASET[task]
|
||||
@@ -95,7 +99,10 @@ def build_bench_args(
|
||||
task_extra_body: dict[str, Any] = dict((model_cfg.get("task_extra_body") or {}).get(task) or {})
|
||||
if request_seed is not None:
|
||||
task_extra_body["seed"] = request_seed
|
||||
|
||||
if duration_seconds is not None:
|
||||
if not math.isfinite(duration_seconds) or duration_seconds <= 0:
|
||||
raise ValueError("duration_seconds must be finite and positive")
|
||||
task_extra_body["duration_seconds"] = duration_seconds
|
||||
# Resolve dataset path
|
||||
if dataset_path:
|
||||
resolved_dataset_path = dataset_path
|
||||
@@ -194,7 +201,7 @@ def print_summary_table(results: list[dict[str, Any]]) -> None:
|
||||
if not results:
|
||||
return
|
||||
header = (
|
||||
f"{'Task':<16} {'Concurrency':>11} {'RTF mean':>10} "
|
||||
f"{'Task':<16} {'Task Type':<12} {'Concurrency':>11} {'RTF mean':>10} "
|
||||
f"{'TTFP (ms)':>10} {'Throughput':>12} {'WER':>7} {'SIM':>7} {'UTMOS':>7}"
|
||||
)
|
||||
print(f"\n{'=' * len(header)}")
|
||||
@@ -204,6 +211,7 @@ def print_summary_table(results: list[dict[str, Any]]) -> None:
|
||||
print("-" * len(header))
|
||||
for r in results:
|
||||
task = r.get("_task", "?")
|
||||
task_type = r.get("_task_type", "-")
|
||||
conc = r.get("_concurrency", "?")
|
||||
rtf = r.get("mean_audio_rtf", float("nan"))
|
||||
ttfp = r.get("mean_audio_ttfp_ms", float("nan"))
|
||||
@@ -216,7 +224,7 @@ def print_summary_table(results: list[dict[str, Any]]) -> None:
|
||||
return f"{v:.{digits}f}" if not math.isnan(v) else " n/a"
|
||||
|
||||
print(
|
||||
f"{task:<16} {str(conc):>11} {fmt(rtf):>10} {fmt(ttfp, 0):>10} "
|
||||
f"{task:<16} {task_type:<12} {str(conc):>11} {fmt(rtf):>10} {fmt(ttfp, 0):>10} "
|
||||
f"{fmt(throughput):>12} {fmt(wer):>7} {fmt(sim):>7} {fmt(utmos):>7}"
|
||||
)
|
||||
print("=" * len(header))
|
||||
@@ -249,6 +257,12 @@ def main() -> None:
|
||||
help="Number of prompts per run. If one value, applied to all concurrency levels.",
|
||||
)
|
||||
parser.add_argument("--num-warmups", type=int, default=2, help="Warmup requests before each measured run")
|
||||
parser.add_argument(
|
||||
"--duration-seconds",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Target audio duration per request (AuK defaults to 5 seconds). Independent of --output-len.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--request-seed",
|
||||
type=int,
|
||||
@@ -277,6 +291,8 @@ def main() -> None:
|
||||
|
||||
if args.num_warmups < 0:
|
||||
parser.error("--num-warmups cannot be negative")
|
||||
if args.duration_seconds is not None and (not math.isfinite(args.duration_seconds) or args.duration_seconds <= 0):
|
||||
parser.error("--duration-seconds must be finite and positive")
|
||||
|
||||
model_configs = load_model_configs(Path(args.model_configs))
|
||||
if args.model not in model_configs:
|
||||
@@ -333,11 +349,16 @@ def main() -> None:
|
||||
served_model_name=args.served_model_name,
|
||||
num_warmups=args.num_warmups,
|
||||
request_seed=args.request_seed,
|
||||
duration_seconds=args.duration_seconds,
|
||||
)
|
||||
result = run_one_benchmark(cmd)
|
||||
if result is not None:
|
||||
result["_task"] = task
|
||||
result["_concurrency"] = concurrency
|
||||
task_body = model_cfg.get("task_extra_body", {}).get(task, {})
|
||||
task_type = task_body.get("task_type")
|
||||
if task_type is not None:
|
||||
result["_task_type"] = task_type
|
||||
all_results.append(result)
|
||||
# Persist the metadata so plot_results.py can pick it up.
|
||||
if args.output_dir and result_filename:
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
# is tracked here.
|
||||
|
||||
models:
|
||||
tencent/AuK: &auk
|
||||
supported_tasks: [default_voice, voice_clone]
|
||||
backend: openai-audio-speech
|
||||
endpoint: /v1/audio/speech
|
||||
trust_remote_code: true
|
||||
task_extra_body:
|
||||
default_voice:
|
||||
duration_seconds: 5.0
|
||||
voice: default
|
||||
task_type: CustomVoice
|
||||
voice_clone:
|
||||
duration_seconds: 5.0
|
||||
voice: default
|
||||
task_type: Base
|
||||
tencent/AuK-Flash: *auk
|
||||
|
||||
BreezeBlue/Breeze-TTS-2:
|
||||
supported_tasks: [default_voice, voice_design, voice_clone]
|
||||
backend: openai-audio-speech
|
||||
|
||||
+3
-1
@@ -15,7 +15,7 @@ nav:
|
||||
- Image Edit: serving/image_edit_api.md
|
||||
- Videos: serving/videos_api.md
|
||||
- Streaming and Realtime:
|
||||
- Realtime Audio: serving/realtime_api.md
|
||||
- Full Duplex OpenAI: serving/realtime_openai_duplex_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
|
||||
@@ -47,6 +47,7 @@ nav:
|
||||
- configuration/*
|
||||
- Models:
|
||||
- models/supported_models.md
|
||||
- SeedVR2: models/seedvr2.md
|
||||
- Features:
|
||||
- Overview: features/README.md
|
||||
- Runtime and Stage Execution:
|
||||
@@ -151,6 +152,7 @@ nav:
|
||||
- Hybrid Sharded Data Parallel (HSDP): design/feature/hsdp.md
|
||||
- Pipeline Parallel: design/feature/pipeline_parallel.md
|
||||
- Sequence Parallel: design/feature/sequence_parallel.md
|
||||
- Window Sequence Parallel: design/feature/window_sequence_parallel.md
|
||||
- Tensor Parallel: design/feature/tensor_parallel.md
|
||||
- VAE Patch Parallelism: design/feature/vae_parallel.md
|
||||
- Attention Optimization:
|
||||
|
||||
+3
-3
@@ -56,9 +56,9 @@ vLLM-Omni is flexible and easy to use with:
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
- Omni-modality models (e.g. Qwen3-Omni, MiniCPM-o 4.5, Cosmos3, HunyuanImage, BAGEL)
|
||||
- TTS models (e.g. Qwen3-TTS, IndexTTS 2.5, CosyVoice3)
|
||||
- Diffusion models — image, video, and audio generation (e.g. MiniMax H3, LTX-2.5, SANA-Video, Wan2.2)
|
||||
- Robot-policy and action models (e.g. π0, GR00T-N1.7, DreamZero-DROID, InternVLA-A1)
|
||||
- TTS models (e.g. Qwen3-TTS, Tencent AuK, Breeze-TTS-2, CosyVoice3)
|
||||
- Diffusion models — image, video, and audio generation (e.g. MiniMax H3, LingBot World, MAGI-2, LTX-2.5, Wan2.2)
|
||||
- Robot-policy and action models (e.g. π0.5, GR00T-N1.7, DreamZero-DROID, InternVLA-A1)
|
||||
|
||||
For more information, checkout the following:
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 248 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
This section lists the most common options for running vLLM-Omni.
|
||||
|
||||
For options within a vLLM Engine, please refer to the [vLLM 0.29 configuration guide](https://docs.vllm.ai/en/v0.29.0/configuration/index.html).
|
||||
For options within a vLLM Engine, please refer to the [vLLM 0.30 configuration guide](https://docs.vllm.ai/en/v0.30.0/configuration/index.html).
|
||||
|
||||
Each model defines fixed topology in a registered `PipelineConfig` and runtime
|
||||
overrides in a deploy YAML.
|
||||
|
||||
@@ -64,6 +64,7 @@ The lifecycle labels used below are:
|
||||
| Name | Type and default | Applies to and read time | Precedence and invalid values | Lifecycle |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `DIFFUSION_ATTENTION_BACKEND` | Backend name or `auto`; default `auto` (platform selection) | Diffusion stages; read when `OmniDiffusionConfig` is constructed | `diffusion_attention_config.default` wins. An unknown backend fails during backend resolution. | Stable fallback |
|
||||
| `DIFFUSION_ATTENTION_QUANT` | `<dtype_qk>:<dtype_vo>[:<q_block_size>:<k_block_size>]`; default unset | Diffusion stages; read when `OmniDiffusionConfig` is constructed | Used only when `DIFFUSION_ATTENTION_BACKEND` sets a non-`auto` backend and no explicit attention default exists. Quantization support varies by backend. Malformed fields raise `ValueError`. | Stable fallback |
|
||||
| `DIFFUSION_CACHE_BACKEND` | `none`, `cache_dit`, `tea_cache`, `mag_cache`, `step_cache`, `stepcache`, or `step_cache_dit`; default `none` | Diffusion runner startup | Explicit `cache_backend` in config wins. Otherwise this name wins over the deprecated alias. Unsupported values raise `ValueError` during runner setup. | Stable fallback |
|
||||
| `DIFFUSION_CACHE_ADAPTER` | Same values as `DIFFUSION_CACHE_BACKEND`; default `none` | Diffusion runner startup | Used only when neither explicit `cache_backend` nor `DIFFUSION_CACHE_BACKEND` is set. Unsupported values raise `ValueError`. | Deprecated; use `DIFFUSION_CACHE_BACKEND` |
|
||||
| `OMNI_DIFFUSION_PROMPT_EMBED_CACHE` | Boolean: `1`, `true`, `yes`, `on`, `0`, `false`, `no`, or `off`; default disabled | Each diffusion runner; resolved during model setup | A recognized environment value overrides the explicit enable setting. An unrecognized value is ignored. | Experimental |
|
||||
@@ -77,6 +78,13 @@ Backend names for `DIFFUSION_ATTENTION_BACKEND` are the members of
|
||||
`SAGE_ATTN`, `FLASHINFER_ATTN`, and `TRTLLM_ATTN`. Platform support still
|
||||
depends on the installed kernels and model path.
|
||||
|
||||
For `DIFFUSION_ATTENTION_QUANT`, the dtype fields accept `float16`,
|
||||
`bfloat16`, `int8`, or `fp8_e4m3`; the selected backend may support only a
|
||||
subset. For example, `DIFFUSION_ATTENTION_BACKEND=TRTLLM_ATTN` with
|
||||
`DIFFUSION_ATTENTION_QUANT=int8:bfloat16:1:16` selects INT8 Q/K SageAttention.
|
||||
Explicit `diffusion_attention_config.default` takes precedence over both
|
||||
environment variables.
|
||||
|
||||
### Serving and runtime
|
||||
|
||||
| Name | Type and default | Applies to and read time | Precedence and invalid values | Lifecycle |
|
||||
@@ -130,6 +138,22 @@ settings.
|
||||
Omni-owned variable, even though vLLM-Omni supplies a persistent default when
|
||||
it is unset.
|
||||
|
||||
### Torch compilation
|
||||
|
||||
| Name | Type and default | Applies to and read time | Precedence and invalid values | Lifecycle |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `VLLM_OMNI_TORCH_DYNAMO_RECOMPILE_LIMIT` | Positive integer; unset preserves the current Torch setting | Process default for Torch Dynamo; read when `vllm_omni` is imported | Sets `torch._dynamo.config.recompile_limit` to the requested value. A non-positive or non-integer value raises `ValueError`. Later backend-specific overrides still take precedence. | Diagnostic |
|
||||
|
||||
Set this variable before launching Omni, for example:
|
||||
|
||||
```bash
|
||||
export VLLM_OMNI_TORCH_DYNAMO_RECOMPILE_LIMIT=64
|
||||
```
|
||||
|
||||
The setting applies across model families and platforms. vLLM compilation
|
||||
contexts and backends can subsequently apply their own limits; this variable
|
||||
does not replace those backend-specific policies.
|
||||
|
||||
## Per-stage environment
|
||||
|
||||
Deploy configurations can set arbitrary environment keys for one stage:
|
||||
@@ -162,7 +186,7 @@ their keys only.
|
||||
## Inherited vLLM variables
|
||||
|
||||
vLLM-Omni also reads variables through its aligned vLLM dependency. Refer to
|
||||
the [vLLM 0.29 environment-variable reference](https://docs.vllm.ai/en/v0.29.0/configuration/env_vars.html)
|
||||
the [vLLM 0.30 environment-variable reference](https://docs.vllm.ai/en/v0.30.0/configuration/env_vars.html)
|
||||
for their definitions. This includes vLLM launch, cache, logging, plugin, ROCm,
|
||||
XPU, ModelScope, and FlashInfer workspace settings.
|
||||
|
||||
@@ -190,7 +214,7 @@ collection, examples, bug reports, or logs.
|
||||
|
||||
## Model-specific variables
|
||||
|
||||
The audit found 58 variables read by a single model or pipeline family. They are
|
||||
The inventory includes 66 variables read by a single model or pipeline family. They are
|
||||
not listed as public usage options here because doing so would turn implementation
|
||||
escape hatches into an accidental compatibility contract.
|
||||
|
||||
@@ -199,11 +223,11 @@ Every audited model-specific name has a migration disposition in the
|
||||
|
||||
| Disposition | Count | Required outcome |
|
||||
| --- | ---: | --- |
|
||||
| Promote | 35 | Move a stable setting into typed stage or model configuration. |
|
||||
| Request scope | 6 | Move request-varying behavior into a declared request-option schema. |
|
||||
| Promote | 38 | Move a stable setting into typed stage or model configuration. |
|
||||
| Request scope | 5 | Move request-varying behavior into a declared request-option schema. |
|
||||
| External | 0 | Retain only when a supported third-party library owns the contract. |
|
||||
| Internalize | 11 | Keep a debug or diagnostic switch out of public documentation and configuration. |
|
||||
| Deprecate/remove | 6 | Remove a compatibility escape hatch that has no continuing contract. |
|
||||
| Internalize | 16 | Keep a debug or diagnostic switch out of public documentation and configuration. |
|
||||
| Deprecate/remove | 7 | Remove a compatibility escape hatch that has no continuing contract. |
|
||||
|
||||
The disposition is a migration target, not a statement that the existing
|
||||
environment switch is stable. Promote or request-scope work should land in
|
||||
|
||||
@@ -80,6 +80,19 @@ Note: for the diffusion path, an omitted `distributed_executor_backend` selects
|
||||
segments) and `mp` when `num_gpus > 1`. Set `mp` explicitly to keep a worker
|
||||
subprocess on one GPU. `ray` / `external_launcher` are not fully supported yet.
|
||||
|
||||
### Stage-level runner selection
|
||||
|
||||
`model_runner: v1` or `v2` at the deploy level sets the default runner.
|
||||
A `model_runner` on an individual stage overrides that default, allowing
|
||||
one stage to migrate or roll back independently. This does not change
|
||||
`PipelineConfig` topology, input adapters, or the connector payload contract.
|
||||
An omitted stage override preserves the deploy-level selection.
|
||||
|
||||
MRv2 native downstream receivers currently support turn-based requests only;
|
||||
streaming sessions and resumable input require the V1 prompt-replacement path.
|
||||
Selecting a runner does not add the session capabilities it lacks. Platform
|
||||
fallbacks and stage overrides are validated after configuration resolution.
|
||||
|
||||
### Stage fields
|
||||
|
||||
Each entry under `stages:` accepts any `StageDeployConfig` field directly (no nested `engine_args:`). Only fields whose value legitimately varies across stages live here; pipeline-wide settings (trust_remote_code, distributed_executor_backend, dtype, quantization, prefix/chunked prefill, DP/PP sizes) are declared at the top level and applied to every stage. Unknown keys fall through to `engine_extras:` and are forwarded to the engine. Frequently used fields are listed below; the source-of-truth schema is `StageDeployConfig` in `vllm_omni/config/stage_config.py`.
|
||||
@@ -337,6 +350,41 @@ supply `mtp_sampling_params` and `get_mtp_seed(sampling_params)` for model-local
|
||||
(with `mtp_sample_steps` and `mtp_sample_vocab_size`) as needed. Qwen3-TTS retains
|
||||
its existing `talker_mtp` entry point for V1.
|
||||
|
||||
### Qwen3-TTS first-frame delivery and rollback
|
||||
|
||||
The standard `qwen3_tts.yaml`, `qwen3_tts_high_concurrency.yaml`,
|
||||
`qwen3_tts_mrv2.yaml` and `qwen3_tts_high_concurrency_mrv2.yaml` profiles
|
||||
enable the `talker_first_audio` connector option, as does the experimental
|
||||
single-GPU profile. This changes the default CUDA MRV2 streaming path:
|
||||
residual prediction runs eagerly after the Talker sample, the Talker loads
|
||||
an additional first-frame decoder with its weights and CUDA graphs, and the
|
||||
orchestrator orders its audio before subsequent Code2Wav chunks.
|
||||
|
||||
Code2Wav retains full-audio prefix graphs alongside the optional state-only
|
||||
graphs. Each request's delivery marker selects the graph and audio trimming;
|
||||
enabling the option alone never suppresses audio or disables prefix batching
|
||||
for requests that retain regular codec delivery.
|
||||
|
||||
The path requires asynchronous chunks, TP/PP 1, an in-process executor and
|
||||
disabled prefix caching. Requests with reference codes and unsupported
|
||||
runners or platforms retain regular Code2Wav delivery.
|
||||
|
||||
To restore regular codec delivery and avoid loading the Talker's additional
|
||||
decoder, use a deploy overlay with the relevant base profile:
|
||||
|
||||
```yaml
|
||||
base_config: qwen3_tts.yaml
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
extra:
|
||||
talker_first_audio: false
|
||||
```
|
||||
|
||||
First-packet latency measures when PCM starts arriving. Time to first audible
|
||||
audio (TTFA) also includes any leading silence in the generated audio. An
|
||||
earlier first packet therefore does not necessarily improve TTFA; measure
|
||||
both for the intended voice and workload.
|
||||
|
||||
### Included performance work
|
||||
|
||||
- Request snapshots have a fast path for immutable scalar leaves, including
|
||||
@@ -372,14 +420,34 @@ speaker similarity before adopting either batching preset for a production
|
||||
workload. Floating-point decoder outputs can differ across batch sizes; this PR
|
||||
does not claim bitwise or quality equivalence.
|
||||
|
||||
### Optional MPS deployment
|
||||
### Experimental MPS deployment
|
||||
|
||||
NVIDIA MPS is an optional operator setting for colocated CUDA processes, not a
|
||||
YAML option or a library default. This PR does not establish a throughput or
|
||||
first-packet latency benefit from MPS. Measure the exact deployment with and
|
||||
without MPS before enabling it.
|
||||
NVIDIA MPS lets colocated CUDA stage processes share GPU execution resources.
|
||||
It is disabled by default. The experimental
|
||||
`qwen3_tts_high_concurrency_mrv2_single_gpu.yaml` profile sets `cuda_mps: true`,
|
||||
alongside cached residual prediction, fused sampling, time-major codec
|
||||
convolutions, first-frame delivery, and larger graph batches.
|
||||
|
||||
Use only assigned GPUs and an independent MPS pipe directory. A private MPS
|
||||
server does not provide exclusive GPU ownership or MIG isolation. For a
|
||||
single-GPU deployment, explicitly place both stages on that GPU; the supplied
|
||||
high-concurrency profile places its two stages on different GPUs by default.
|
||||
The runtime requires `nvidia-cuda-mps-control` on `PATH` and one explicit CUDA
|
||||
GPU per local EngineCore stage, with `parallel_stage_init: false`.
|
||||
Use numeric GPU ordinals for stage placement and visibility so initialization
|
||||
locks identify the physical GPU before MPS remaps it. It starts a private MPS daemon for each selected
|
||||
GPU and stops its own daemon after the stages exit. If
|
||||
`CUDA_MPS_PIPE_DIRECTORY` already names an operator-managed daemon, the runtime
|
||||
reuses it without stopping it. Diffusion and remote stages are unsupported.
|
||||
Set `cuda_mps: false` in a deploy overlay to disable automatic MPS management.
|
||||
|
||||
Stage `env.CUDA_MPS_PIPE_DIRECTORY` overrides the parent setting when selecting
|
||||
the daemon; an explicit empty string selects a private daemon. Stages sharing
|
||||
a GPU reuse the first stage's daemon. Later stages may omit the setting or
|
||||
explicitly match that policy; a conflicting explicit setting fails before
|
||||
that stage starts. The parent environment is unchanged.
|
||||
|
||||
MPS can improve throughput under concurrent load while increasing first-packet
|
||||
or first-audible-audio latency, particularly at low request rates. For a
|
||||
latency-sensitive workload, compare the same profile with `cuda_mps: false`.
|
||||
An inherited operator-managed MPS daemon must also be disabled by its owner
|
||||
for that comparison; this switch only controls automatic MPS management.
|
||||
|
||||
MPS does not reserve a GPU. Use only assigned GPUs, explicitly place stages on
|
||||
the intended GPU, and warm the complete pipeline before measuring performance.
|
||||
@@ -0,0 +1,186 @@
|
||||
# Window-Aligned Sequence Parallelism (SeedVR2)
|
||||
|
||||
## Motivation
|
||||
|
||||
SeedVR2's diffusion transformer (`NaDiT`) has no global attention: every layer
|
||||
partitions the video tokens into 3D windows and attends only inside a window.
|
||||
The window size is normalised against a fixed 720p reference area, so raising the
|
||||
output resolution grows the *number* of windows, never their size.
|
||||
|
||||
Ulysses redistributes the sequence and head dimensions around attention.
|
||||
For SeedVR2, window-aligned SP offers a different communication tradeoff: one
|
||||
hidden-state exchange per layout boundary versus Q/K/V and output exchanges
|
||||
for Ulysses. The actual performance crossover requires measurement.
|
||||
Window-aligned SP instead assigns **whole windows** to ranks:
|
||||
|
||||
* every rank keeps **all** attention heads, so no head-count divisibility is
|
||||
required,
|
||||
* attention inside a layer needs **zero** communication,
|
||||
* regular and shifted layouts alternate at every layer, so the released
|
||||
32-layer model has 31 inter-layer transitions, plus one text reduction per
|
||||
layer and a final video gather. A transition can be a local permutation
|
||||
when ownership does not change.
|
||||
|
||||
## Window layouts
|
||||
|
||||
`nadit.py` ports the reference windowing verbatim (Apache-2.0,
|
||||
ByteDance SeedVR2). For a post-patch token grid `(T, H, W)`:
|
||||
|
||||
```text
|
||||
scale = sqrt(45 * 80 / (H * W)) # 720p reference area
|
||||
wh, ww = ceil(round(H * scale) / nh), ceil(round(W * scale) / nw)
|
||||
wt = ceil(min(T, 30) / nt) # BYTEDANCE_MAX_TEMPORAL_WINDOW
|
||||
```
|
||||
|
||||
The regular method tiles with `slice(it * wt, min((it + 1) * wt, T))`; the
|
||||
shifted method moves every cut by half a window and **clips at the boundary**
|
||||
(`max(..., 0)` / `min(..., size)`) -- it is not a cyclic `torch.roll`. Both
|
||||
methods produce an exact partition of the grid; the shifted layout is genuinely
|
||||
different, so an `A -> B -> A -> B` layer schedule needs both directions of
|
||||
re-sharding.
|
||||
|
||||
Windows are enumerated `for iw ... for ih ... for it ...` (w outermost, t
|
||||
innermost) and tokens inside a window are row-major in `(t, h, w)`. The
|
||||
canonical token id is `(t * H + h) * W + w`, which is exactly what the reference
|
||||
obtains by windowing `torch.arange`.
|
||||
|
||||
Consequences that matter (all verified against the reference):
|
||||
|
||||
* Window token counts are **ragged** (edge windows are clipped), so a shard must
|
||||
be balanced by token count, not by window count.
|
||||
* The number of layout transitions follows the actual layer schedule. For the
|
||||
released 3B checkpoint (`num_layers=32`, alternating methods) there are 31
|
||||
inter-layer transitions, not 16; `ensure_layout(current, required)` derives
|
||||
them instead of assuming `layer % 2`.
|
||||
|
||||
## Planner
|
||||
|
||||
`nadit.py` implements a deterministic LPT assignment:
|
||||
|
||||
```text
|
||||
windows sorted by (-video_token_count, original_window_id)
|
||||
each window is placed on the rank with the smallest (assigned_tokens, rank_id)
|
||||
each rank then lists its windows in ascending original order
|
||||
```
|
||||
|
||||
The cache key covers the post-patch token grid, the full window method and
|
||||
parameters (via a SHA-1 geometry fingerprint), the geometry version, the **SP
|
||||
world size** and the planner version, so a different degree can never reuse a
|
||||
stale assignment. Caches are LRU-bounded.
|
||||
|
||||
## Plan A redistribution
|
||||
|
||||
At a layout boundary the activations must move from the source assignment to the
|
||||
destination assignment. With `Z_src[r]` / `Z_dst[d]` the canonical token ids held
|
||||
by each rank and `C[r, d]` the number of rows rank `d` needs from rank `r`:
|
||||
|
||||
* `input_split_sizes` of rank `r` is row `r` of `C`, `output_split_sizes` is
|
||||
column `r`,
|
||||
* each peer segment is packed in **destination-local order**, so the receiver can
|
||||
reorder with a single `argsort` of the concatenated destination positions,
|
||||
* `network_exchange_required` is computed from the global count matrix, so every
|
||||
rank takes the same branch; a rank that happens to exchange nothing never
|
||||
skips the collective alone.
|
||||
|
||||
The runtime is a variable-split `torch.distributed.all_to_all_single` over an
|
||||
regular SP process group (`get_sp_group().device_group`), plus a local-permutation fast
|
||||
path when the whole group stays in place (which is the SP=1 case). The first
|
||||
version is deliberately synchronous: no extra streams, no per-layer
|
||||
`cuda.synchronize()` or barrier "fixups", and no hidden all-gather.
|
||||
|
||||
## Text stream
|
||||
|
||||
Text is replicated on every rank and stays identical: each layer's attention
|
||||
produces one text output per window, ranks sum their local window outputs,
|
||||
one SUM all-reduce runs on the window-SP group, and every rank divides by the
|
||||
**global** window count of the current layout.
|
||||
|
||||
```text
|
||||
T_next = (sum_r sum_{w in Omega_r} T_w) / W_global
|
||||
```
|
||||
|
||||
This is neither a token-weighted mean nor a mean of rank means; a rank with no
|
||||
window contributes a zero tensor (with the *same dtype as its peers* -- a dtype
|
||||
mismatch changes the collective and deadlocks NCCL, which is why the empty-rank
|
||||
path is covered by a GPU test).
|
||||
|
||||
## Existing SP configuration
|
||||
|
||||
Configure `ulysses_degree=N` to create the existing N-rank SP group. Pass
|
||||
`DiffusionParallelConfig` to `SeedVR2NaDiT.build_runtime(parallel_config=...)`;
|
||||
the model validates the configuration and obtains `get_sp_group().device_group`.
|
||||
This adds no framework parallel option or group-construction branch.
|
||||
|
||||
For SeedVR2 this selects **window-aligned Plan A**, not head-sharded Ulysses.
|
||||
Every rank retains all 20 attention heads, so SP=8 does not require the head
|
||||
count to divide by eight. The model has no `_sp_plan`, and its attention uses
|
||||
`skip_sequence_parallel=True`; generic sequence hooks and Ulysses attention
|
||||
exchanges are not installed. Reports name the actual execution path explicitly.
|
||||
|
||||
SeedVR2 rejects ring, allgather, `advanced_uaa`, and `ulysses_a2a_permute`.
|
||||
Other diffusion models keep their existing Ulysses/Ring/AllGather behavior.
|
||||
|
||||
## SeedVR2 integration
|
||||
|
||||
`SeedVR2WindowRuntime` owns, per request: the layer schedule, the entry
|
||||
distribution, `ensure_layout` transitions, the text reduction and the final
|
||||
reconstruction back to canonical token order. The DiT forward keeps only this
|
||||
rank's video rows from the patch embedding onwards; the patch projection,
|
||||
norms, ADA modulation, residual and MLP are all tokenwise, so they run locally.
|
||||
|
||||
The block never calls a parallel attention strategy: the shared `Attention`
|
||||
layer is created with `skip_sequence_parallel=True`, and window attention is a
|
||||
packed-varlen call over `[video window, replicated text]` segments (with a
|
||||
grouped-SDPA fallback for backends without a packed-varlen entry point).
|
||||
|
||||
## Reference quirks reproduced on purpose
|
||||
|
||||
* `vid_out_ada` is declared with `layers=["out"]`, whose embedding regrouping is
|
||||
arithmetically incompatible with its 1-D parameters. The reference never uses
|
||||
that value: it asks the forward-wide cache for `emb_repeat_0_vid`, a key the
|
||||
first block already wrote, and therefore applies the *block* attention
|
||||
modulation together with the learned `out_scale` / `out_shift`. The port
|
||||
applies that effective modulation directly; changing it would break parity.
|
||||
* `mm_layers=10` controls weight sharing only. Text is consumed and produced by
|
||||
all 32 blocks; only the last block freezes its text MLP path.
|
||||
* RoPE positions are window-local: a video token at window-local `(dt, dh, dw)`
|
||||
uses axial indices `(text_len + dt, dh, dw)`, and every window restarts at 0.
|
||||
* The rotary span is `3 * (rope_dim // 3)` (126 of 128 channels for the 3B), so
|
||||
the last two channels pass through unrotated.
|
||||
|
||||
## Checkpoint compatibility
|
||||
|
||||
The parameter layout of the port mirrors the reference, with one explicit
|
||||
exception: the released checkpoint stores the per-block RoPE table as
|
||||
`blocks.<i>.attn.rope.rope.freqs`, while this port registers it as
|
||||
`blocks.<i>.attn.rope.freqs`. The validation loader normalizes exactly that
|
||||
suffix (and only that suffix); every other key must already match.
|
||||
|
||||
> The validation loader explicitly normalizes the reference RoPE buffer suffix
|
||||
> from `.rope.rope.freqs` to `.rope.freqs`. Checkpoint validation fails on any
|
||||
> remaining missing or unexpected key after normalization.
|
||||
|
||||
Normalization is restricted and collision-checked: a key that is already
|
||||
normalized is left untouched, and two source keys mapping onto the same target
|
||||
key raise instead of silently overwriting a tensor. Loading then fails on any
|
||||
remaining missing, unexpected or shape-mismatched key, so a checkpoint that does
|
||||
not match the port cannot reach inference. Full 32-layer loading is the
|
||||
acceptance path; truncating the block list is a development fixture and requires
|
||||
an explicit flag.
|
||||
|
||||
The native serving loader applies the same suffix normalization and strict
|
||||
checkpoint check.
|
||||
|
||||
## Integration limits
|
||||
|
||||
The native serving pipeline passes its parallel configuration to the DiT runtime
|
||||
and returns restored video through the standard media path. The causal VAE can
|
||||
run replicated, tiled, or height-sharded across the window-SP group.
|
||||
|
||||
## Limitations
|
||||
|
||||
* Plan A re-shards the full activation at every layout boundary. Plan B halo
|
||||
exchange is a follow-up with a separate crossover evaluation.
|
||||
* The packed-varlen attention kernel needs a backend that accepts
|
||||
`cu_seqlens`; the grouped-SDPA path is the portable fallback and is what the
|
||||
pinned L20 environment exercises.
|
||||
@@ -1,142 +1,137 @@
|
||||
# PersonaPlex Unified Full-Duplex Design
|
||||
# PersonaPlex on the Unified Full-Duplex Framework
|
||||
|
||||
## Status and target
|
||||
|
||||
This design adapts PersonaPlex PR #4771 to the unified full-duplex runtime on
|
||||
top of:
|
||||
|
||||
- vLLM-Omni `origin/main`: `67c54777bb22e9e7e08fdf7c47a64f06b566fc47`
|
||||
- PersonaPlex PR head: `477fb7c225f0c06991bc8aa55eadbd908ba282e4`
|
||||
|
||||
The target is the engine-native path:
|
||||
PersonaPlex (`nvidia/personaplex-7b-v1`, a Moshi finetune) is served by the
|
||||
[Unified Full-Duplex Framework](fullduplex.md) through one model plugin,
|
||||
`PersonaPlexDuplexPlugin` (`vllm_omni/model_executor/models/personaplex/duplex/plugin.py`),
|
||||
selected by `PipelineConfig.duplex_plugin`. The serving path is:
|
||||
|
||||
```text
|
||||
/v1/realtime?duplex=1
|
||||
-> OpenAI Realtime session actor
|
||||
-> DuplexRequestClient
|
||||
-> AsyncOmni correlated RPC
|
||||
-> DuplexControlPlane
|
||||
-> resumable Stage 0 request
|
||||
-> PersonaPlex Talker
|
||||
-> streaming PersonaPlex Code2Wav
|
||||
-> PersonaPlex data-plane projector
|
||||
-> OmniDuplexSessionHandler (thin: websocket I/O, Realtime <-> DuplexCommand/DuplexEvent)
|
||||
-> DuplexOmni / DuplexOmniEngine
|
||||
-> DuplexSessionManager -> DuplexSessionRunner (one per session)
|
||||
plugin.plan_append: one 80 ms frame -> one resumable Stage 0 append
|
||||
-> PersonaPlex Talker (Stage 0, lockstep temporal transformer + depformer)
|
||||
-> streaming PersonaPlex Code2Wav (Stage 1, Mimi)
|
||||
-> plugin.data_plane: cumulative audio/text -> deltas
|
||||
-> response.output_audio.delta + response.output_audio_transcript.delta
|
||||
```
|
||||
|
||||
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 was invalid
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
duplex_control_enabled = True
|
||||
duplex_runtime_extension = "..."
|
||||
duplex_serving_adapter = "..."
|
||||
```
|
||||
|
||||
The adapter must supply a real scheduler data plane, not just endpoint
|
||||
capabilities.
|
||||
The pre-framework pair (engine runtime extension plus serving adapter) and
|
||||
the standalone `/api/chat` / `/v1/audio/duplex` server are gone; the plugin is
|
||||
the only integration surface, and this document describes it.
|
||||
|
||||
## Supported scope
|
||||
|
||||
The unified implementation supports:
|
||||
The integration supports:
|
||||
|
||||
- up to two engine-owned sessions on one replica;
|
||||
- 24 kHz mono float PCM input;
|
||||
- one 1920-sample, 80 ms model frame per physical append unit;
|
||||
- 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` (the wire vocabulary is catalogued in the
|
||||
[Realtime Duplex API](../serving/realtime_duplex_api.md) serving guide);
|
||||
- up to `duplex_session.max_sessions` engine-owned sessions on one replica
|
||||
(the shipped deploy sets two);
|
||||
- 24 kHz mono float PCM input, one 1920-sample (80 ms) model frame per
|
||||
physical append unit; clients may send any chunking, the session buffers
|
||||
whole frames;
|
||||
- continuous user input while assistant audio is generated or played
|
||||
(pure lockstep: the model listens while it speaks);
|
||||
- bundled `.pt` voice prompts (`voice`) and a session persona (`instructions`);
|
||||
- greedy text and depformer sampling (one temporal token per frame);
|
||||
- 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.
|
||||
`pcm_f32le` input, voice, persona) as the canonical session-config source
|
||||
for `DuplexClient` / `InlineDuplexClient` consumers;
|
||||
- engine lease close, disconnect cleanup and explicit response cancellation
|
||||
without cross-session state reuse.
|
||||
|
||||
The implementation does not claim:
|
||||
It does not claim:
|
||||
|
||||
- more than two simultaneous PersonaPlex sessions on one replica;
|
||||
- arbitrary WAV voice cloning;
|
||||
- turn-based `response.create` semantics for an otherwise continuous model;
|
||||
- scheduler migration of live codec state between replicas;
|
||||
- exact output equality with the standalone engine after different scheduling
|
||||
boundaries.
|
||||
- arbitrary WAV voice cloning (voices are bundled basenames, resolved by the
|
||||
worker under the checkpoint);
|
||||
- turn-based `response.create` semantics, client commits or external turn
|
||||
signals (`supports_client_commit=false`, `supports_external_turn_signal=false`);
|
||||
- text seeding, hence no `/v1/chat/completions` route
|
||||
(`supports_chat_completions=false`);
|
||||
- session resume across a transport drop (`supports_session_resume=false`);
|
||||
- destructive output interruption or model-state rewind at a playback cursor
|
||||
(`supports_barge_in=false`, `supports_audio_truncate=false`): overlapping
|
||||
speech is native model behaviour, not a barge-in contract.
|
||||
|
||||
The capability payload derives multi-session support from the configured
|
||||
session limit. The shipped two-session deployment reports
|
||||
`supports_multi_session=true` and `supports_multi_session_same_replica=true`.
|
||||
`duplex_session.max_sessions` is the only capacity source: config resolution
|
||||
propagates it to every stage as `duplex_max_sessions`, and both Mimi pools read
|
||||
that model-config value. Connector extras do not carry a second model-specific
|
||||
capacity knob that could drift from engine admission.
|
||||
It still reports `supports_barge_in=false`: the generic epoch fence can suppress
|
||||
stale transport output, but neither PersonaPlex nor the current MiniCPM-o 4.5
|
||||
adapter proves that model-owned streaming state can be destructively rewound or
|
||||
restarted at a playback cursor. Continuous overlapping speech is model-native
|
||||
duplex behavior, not by itself a barge-in contract.
|
||||
propagates it to every stage as `duplex_max_sessions`, and both Mimi pools
|
||||
(Stage 0 encoders, Stage 1 decoders) read that model-config value. The
|
||||
capability payload derives `supports_multi_session` /
|
||||
`supports_multi_session_same_replica` from it.
|
||||
|
||||
## Components and ownership
|
||||
|
||||
### Serving adapter
|
||||
### The plugin (engine side)
|
||||
|
||||
`PersonaPlexServingRuntimeAdapter` owns only serving-side state:
|
||||
`PersonaPlexDuplexPlugin` owns both halves of the contract.
|
||||
|
||||
- a transactional PCM append buffer;
|
||||
- validation of `voice_prompt` and `instructions`;
|
||||
- public capabilities;
|
||||
- PersonaPlex data-plane output projection.
|
||||
Engine policy:
|
||||
|
||||
It accepts 24 kHz `pcm_f32le`, groups client packets into whole 1920-sample
|
||||
frames, zero-pads only the final residual, and rolls a reservation back when an
|
||||
engine append fails. It never loads CUDA weights and never encodes user audio.
|
||||
- `configure_sampling_params`: Stage 0 greedy (`temperature=0`, `top_k=1`,
|
||||
`max_tokens=1`), other stages untouched.
|
||||
- `plan_append`: validates exactly one 1920-sample 24 kHz `pcm_f32le` frame
|
||||
(through `model_executor/common/duplex/payload.py`) and reserves one
|
||||
scheduler slot per frame plus, on the first append of an epoch (`seq == 1`),
|
||||
the `personaplex_prefill_slots` of the voice/persona prefill. The prompt
|
||||
carries the fence, `seq`, the payload and the runtime config under
|
||||
`model_intermediate_buffer["duplex"]`.
|
||||
- `decide_output`: never decides. PersonaPlex is always-clocked; what the
|
||||
client hears comes from the final-stage data plane.
|
||||
- `silence_unit_payload`: one frame of zeros at 24 kHz
|
||||
(`silence_continuation_samples=1920`,
|
||||
`silence_continuation_sample_rate_hz=24000`). The runner uses it to keep a
|
||||
model turn clocked when the client pauses; the startup warmup sends it.
|
||||
|
||||
Client input cannot provide local filesystem paths. A voice is a bundled
|
||||
basename such as `NATF2.pt`; the worker resolves it under the local model
|
||||
checkpoint. `instructions` is the persona string.
|
||||
Session policy:
|
||||
|
||||
### Runtime extension
|
||||
- `capabilities`: `personaplex_capabilities(max_sessions)` (80 ms units,
|
||||
append-only, no commits, no barge-in, no resume, no chat route).
|
||||
- `prepare_runtime_config`: `voice` must be a bundled `.pt` basename,
|
||||
`instructions` defaults to the shipped persona; the prefill slot count is
|
||||
computed once per `(model, voice, persona)` off the orchestrator loop
|
||||
(`personaplex_prefill_slots`, itself caching the tokenizer and the voice
|
||||
bundle row count). The result is server-owned runtime config
|
||||
(`personaplex_model_path`, `personaplex_voice_prompt`,
|
||||
`personaplex_persona`, `personaplex_prefill_slots`); the same keys are
|
||||
refused in a client's `extra_body`.
|
||||
- `runtime_config_for_update`: persona and voice are immutable for the
|
||||
session (`persona_update_unsupported`, `voice_update_unsupported`).
|
||||
- session state, extra-body validation and data-plane context are the
|
||||
framework defaults (`DefaultDuplexModelSessionState` with the 80 ms
|
||||
`PersonaPlexPcmAppendBuffer`, `DuplexDataPlaneContext`).
|
||||
|
||||
`PersonaPlexDuplexRuntimeExtension` is pure model policy. It:
|
||||
Because `supports_client_commit` is off, the session auto-responds without
|
||||
`extra_body.auto_response`: a stock Realtime client streams audio and hears
|
||||
the model without any vendor flag.
|
||||
|
||||
- configures greedy Stage 0 sampling and bounded segment lengths;
|
||||
- maps each accepted PCM append to a scheduler prompt;
|
||||
- places immutable session identity, append sequence, PCM payload, voice, and
|
||||
persona under `model_intermediate_buffer["duplex"]`;
|
||||
- reserves exactly one scheduler prompt slot per encoded Mimi frame, plus the
|
||||
first-append voice/persona prefill length;
|
||||
- never performs model inference or owns session state.
|
||||
### Input framing
|
||||
|
||||
The extension returns no turn/listen decision. PersonaPlex is an always-clocked
|
||||
model, so visible audio/text comes from the final stage data plane.
|
||||
`PersonaPlexPcmAppendBuffer` is the `FixedFramePcmAppendBuffer` of
|
||||
`model_executor/common/duplex/pcm_buffer.py` at 24 kHz / 1920 samples /
|
||||
80 ms. It accepts 24 kHz `pcm_f32le` only, groups client packets into whole
|
||||
frames, takes one frame out per append as a reservation (committed when the
|
||||
stage accepted the append, rolled back to the front of the buffer when it did
|
||||
not), zero-pads only a final residual on commit, and never encodes audio.
|
||||
|
||||
### Stage 0 streaming runtime
|
||||
### Stage 0 streaming runtime (worker side)
|
||||
|
||||
The Talker owns a `PersonaPlexStage0DuplexRuntime` helper, analogous to
|
||||
MiniCPM-o's Stage 0 helper but with PersonaPlex lockstep semantics.
|
||||
The Talker owns a `PersonaPlexStage0DuplexRuntime`
|
||||
(`model_executor/models/personaplex/duplex/stage0.py`), created lazily in
|
||||
`_duplex_stage0_runtime()` and released through `on_requests_finished`.
|
||||
|
||||
For each admitted session it owns:
|
||||
For each live `(session_id, epoch)` it owns:
|
||||
|
||||
- streaming Mimi encoder convolution and transformer state;
|
||||
- the selected voice embedding bundle;
|
||||
- persona tokenization and prefill embeddings;
|
||||
- the prior user code frame needed by the one-frame acoustic delay;
|
||||
- append identity used to make a retried scheduler update idempotent.
|
||||
- the streaming Mimi encoder convolution and transformer state;
|
||||
- the selected voice embedding bundle and the persona prefill embeddings;
|
||||
- the prior user code frames needed by the one- and two-frame acoustic
|
||||
delays;
|
||||
- the append identity `(epoch, seq)` that makes a retried scheduler update
|
||||
idempotent.
|
||||
|
||||
The first append builds this ordered prefill:
|
||||
The first append of an epoch builds this ordered prefill:
|
||||
|
||||
```text
|
||||
voice embeddings
|
||||
@@ -146,45 +141,47 @@ voice embeddings
|
||||
-> first live user frame
|
||||
```
|
||||
|
||||
Later appends encode only new 1920-sample frames. The helper returns the
|
||||
Later appends encode only new 1920-sample frames. The runtime returns the
|
||||
per-frame user codes and prompt embeddings through the request's
|
||||
`model_intermediate_buffer`. The normal vLLM runner remains authoritative for
|
||||
attention metadata, block tables, KV allocation, scheduling, and sampling.
|
||||
`model_intermediate_buffer`; the normal vLLM runner remains authoritative for
|
||||
attention metadata, block tables, KV allocation, scheduling and sampling.
|
||||
|
||||
The Talker must distinguish resumable prompt prefill from decode positions.
|
||||
Prompt rows consume the exact prepared embeddings; sampled decode rows continue
|
||||
to use the existing delayed agent/user frame construction. No code path may
|
||||
fall back to an all-initial user stream for a duplex request.
|
||||
**Epochs.** The framework identifies a Stage 0 request by
|
||||
`(session_id, epoch)`; `response.cancel` and `output_audio_buffer.clear`
|
||||
advance the epoch, abort the current request and start the next append at
|
||||
`seq == 1` on a fresh request with fresh KV. The Stage 0 runtime therefore
|
||||
keys its state by `(session_id, epoch)`: when an append of a newer epoch
|
||||
arrives, any older-epoch state of the same session is closed first (its Mimi
|
||||
encoder returns to the pool, so the codec budget never counts a superseded
|
||||
epoch), and the voice/persona prefill is replayed because the plan reserved
|
||||
the slots again. The user-visible consequence is that a cancel restarts the
|
||||
model's conversation context. A late `on_requests_finished` for the aborted
|
||||
request finds nothing to close.
|
||||
|
||||
Cleanup is keyed by the full `(session_id, incarnation)` identity. Every live
|
||||
session has an independent Mimi encoder instance; encoder convolution/KV state
|
||||
is never shared between asynchronously scheduled sessions. A finished or
|
||||
Every live session has an independent Mimi encoder instance; encoder state is
|
||||
never shared between asynchronously scheduled sessions. A finished or
|
||||
aborted scheduler request resets and returns only that session's encoder.
|
||||
|
||||
### Stage 1 streaming decoder
|
||||
|
||||
The current `PersonaPlexCode2Wav` calls one-shot `MimiModel.decode` and is not
|
||||
CUDA-graph safe. Unified duplex uses eager Stage 1 and maintains an independent
|
||||
streaming Mimi decoder for every active request. Decoder ownership is keyed by
|
||||
the stable Stage 1 request id and released by `on_requests_finished`; a mixed
|
||||
batch must never advance another request's convolution or transformer state.
|
||||
|
||||
Each Stage 0 segment emits de-delayed agent codebooks. Stage 1 decodes only the
|
||||
new code frames, emits only the new PCM suffix, and resets state when the
|
||||
request is closed. Connector chunk boundaries retain the final raw code frame
|
||||
needed to de-delay the next chunk.
|
||||
|
||||
The deploy default sets Stage 1 `enforce_eager: true`; a default configuration
|
||||
that fails during CUDA graph capture is not an acceptable deployment profile.
|
||||
`PersonaPlexCode2Wav` runs eager (`enforce_eager: true` in the deploy) and
|
||||
maintains an independent streaming Mimi decoder for every active request,
|
||||
keyed by the Stage 1 request id and released by `on_requests_finished`. Each
|
||||
Stage 0 segment emits de-delayed agent codebooks; Stage 1 decodes only the new
|
||||
code frames and emits only the new PCM suffix. Connector chunk boundaries
|
||||
retain the final raw code frame needed to de-delay the next chunk.
|
||||
|
||||
### Data-plane projector
|
||||
|
||||
`PersonaPlexDataPlaneSession` converts cumulative or delta Stage 1 output into
|
||||
model-neutral native results:
|
||||
`PersonaPlexDataPlaneSession` is the `CumulativeAudioTextDataPlane` of
|
||||
`model_executor/common/duplex/data_plane.py` with a 24 kHz default rate. It
|
||||
keeps one audio/text cursor per request so cumulative Stage 1 output cannot
|
||||
replay old audio, and yields one internal result per new suffix:
|
||||
|
||||
```python
|
||||
{
|
||||
"stage_role": "tts",
|
||||
"is_listen": False,
|
||||
"data_plane_request_id": request_id,
|
||||
"text": text_delta,
|
||||
"audio_data": encoded_audio_delta,
|
||||
@@ -192,82 +189,74 @@ model-neutral native results:
|
||||
"sample_rate_hz": 24000,
|
||||
"audio_duration_ms": delta_duration,
|
||||
"end_of_turn": False,
|
||||
"runtime_impl": "scheduler_data_plane",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
It owns per-request audio and text cursors so a cumulative output cannot replay
|
||||
old audio. The generic projector turns each native result into the Realtime
|
||||
`response.output_audio.delta` / `response.output_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.
|
||||
The runner turns each result into the Realtime
|
||||
`response.output_audio.delta` / `response.output_audio_transcript.delta` pair
|
||||
under one `response_id`. PersonaPlex keeps one visible response open while
|
||||
continuous output arrives; session close or cancellation terminates it through
|
||||
the generic Realtime lifecycle. A codec segment finishing is not a
|
||||
conversational turn boundary.
|
||||
|
||||
## Error and lifecycle contracts
|
||||
|
||||
- Unsupported sample rate, malformed base64, non-finite PCM, invalid voice
|
||||
basename, or changed format fails before scheduler submission.
|
||||
- Unsupported sample rate, malformed base64, non-finite PCM, a partial frame
|
||||
at the plan, an invalid voice basename or a changed format fails before
|
||||
scheduler submission, as a typed `error` event.
|
||||
- Append is prepare/submit/commit. Failure rolls back the exact reserved PCM
|
||||
bytes and does not advance the model frame cursor.
|
||||
- A repeated `operation_id` must not encode or submit the same frame twice.
|
||||
- Input iterator exceptions execute the same cleanup as explicit close.
|
||||
- Cancellation does not release the engine lease until the stage request and
|
||||
any in-flight codec operation are actually finished.
|
||||
- A bounded cleanup timeout returns a cleanup error and keeps the session in
|
||||
the closing admission set; it must not make the slot available while work
|
||||
still mutates shared state.
|
||||
- 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,
|
||||
which followed the same exception-safe rule, have been removed from the tree.)
|
||||
- A repeated `(epoch, seq)` identity does not encode or submit the same frame
|
||||
twice.
|
||||
- Cancellation advances the epoch; the aborted request's Stage 0 state is
|
||||
released by the next-epoch append or by `on_requests_finished`, whichever
|
||||
comes first.
|
||||
- Close releases the admission slot only once stage cleanup succeeded; the
|
||||
reaper retries a failed cleanup while the slot stays held.
|
||||
- New sessions cannot observe a previous voice, persona, PCM tail, Mimi
|
||||
convolution state or Talker delayed code frame.
|
||||
|
||||
## Testing and acceptance
|
||||
|
||||
### Contract tests
|
||||
### CPU contract tests
|
||||
|
||||
Tests first cover:
|
||||
- `tests/model_executor/models/personaplex/duplex/test_plugin.py`: pipeline
|
||||
binding and plugin load, honest capabilities, private keys, voice/persona
|
||||
resolution and caching, immutable updates, greedy sampling, one-slot-plus-
|
||||
prefill planning, frame validation, the 24 kHz silence unit, and the
|
||||
CPU-checked helpers of the E2E driver.
|
||||
- `tests/model_executor/models/personaplex/duplex/test_stage0_runtime.py`:
|
||||
first-append prefill, depformer teacher forcing, causal user-frame delays,
|
||||
idempotent retries, independent encoders per session, capacity, epoch
|
||||
restart and late finish of a superseded request.
|
||||
- `tests/engine/duplex/test_session_runner_personaplex.py`: the session
|
||||
runner with the real plugin -- one submission per frame, prefill on the
|
||||
first append only, half frames buffered, wrong rate refused, cumulative
|
||||
Code2Wav output projected as 24 kHz deltas, cancel restarting the epoch,
|
||||
close aborting the request.
|
||||
- `tests/model_executor/executor_common/`: the shared toolbox (PCM helpers, payload
|
||||
validation, fixed-frame buffer, cumulative data plane, request-output
|
||||
readers).
|
||||
|
||||
- pipeline registration enables the control plane and selects both PersonaPlex
|
||||
adapters;
|
||||
- the capability payload is two-session, 80 ms, append-only, and honest;
|
||||
- PCM reservation commit/rollback, partial-frame flush, invalid input, and
|
||||
operation idempotency;
|
||||
- runtime prompt fields and exact token budgeting;
|
||||
- first-append voice/persona prefill followed by live user codes;
|
||||
- later appends retain per-session Mimi state and do not replay prefill;
|
||||
- interleaved Stage 0 and Stage 1 work preserves independent codec histories;
|
||||
- output projection emits only audio/text deltas;
|
||||
- close, exception, timeout, and reconnect cleanup;
|
||||
- ordinary non-duplex imports do not load PersonaPlex modules.
|
||||
### GPU validation
|
||||
|
||||
### Remote H20 validation
|
||||
`tests/e2e/online_serving/personaplex_realtime_duplex.py` (wrapped by
|
||||
`tests/e2e/online_serving/test_personaplex_duplex.py` when
|
||||
`PERSONAPLEX_MODEL_PATH` is set) drives the default `personaplex.yaml`
|
||||
deployment and requires:
|
||||
|
||||
Validation runs in an isolated remote worktree using the ModelScope
|
||||
`nv-community/personaplex-7b-v1` checkpoint and its Mimi dependency.
|
||||
|
||||
The required evidence is:
|
||||
|
||||
1. default `personaplex.yaml` reaches ready without a local eager override;
|
||||
2. `/health` returns 200;
|
||||
3. `/v1/realtime?duplex=1` reports `model_native_duplex`, `chunk_period_ms=80`,
|
||||
and two-session admission;
|
||||
4. paced 24 kHz PCM appends produce finite, non-silent 24 kHz audio deltas and
|
||||
text deltas;
|
||||
5. microphone input continues during assistant output without cancelling the
|
||||
scheduler request;
|
||||
6. two paced sessions simultaneously produce independent non-empty audio, and
|
||||
a third session is rejected with `resource_exhausted`;
|
||||
7. closing either session frees only its scheduler request and GPU codec state,
|
||||
after which a replacement session can use a different persona without state
|
||||
leakage;
|
||||
8. malformed input returns one typed error and does not poison the next append;
|
||||
9. all focused unit tests and `git diff --check` pass.
|
||||
1. the server reaches ready without a local eager override; `/health` is 200;
|
||||
2. `/v1/realtime?duplex=1` reports `model_native_duplex`, `chunk_period_ms=80`
|
||||
and two-session admission, with server-allocated session ids;
|
||||
3. paced 24 kHz PCM appends produce finite, non-silent, whole-frame 24 kHz
|
||||
audio deltas and text deltas;
|
||||
4. two paced sessions simultaneously produce independent non-empty audio, and
|
||||
a third session is refused with `resource_exhausted`;
|
||||
5. closing one session frees only its scheduler request and codec state, after
|
||||
which a replacement session with a different persona is admitted with a
|
||||
fresh id and no state leakage.
|
||||
|
||||
Audio that is empty, all zero, non-finite, or only a protocol `listen` event is
|
||||
not a successful end-to-end result.
|
||||
|
||||
@@ -115,3 +115,5 @@ The design contracts separate selection mechanics from backend algorithms:
|
||||
The pre-#5137 pages are preserved in the
|
||||
[legacy module archive](module/archive/README.md) for historical reference and
|
||||
are not active design contracts.
|
||||
|
||||
- [MiniCPM-o 4.5 turn-mode MRv2 performance](minicpm_o45_mrv2_performance.md)
|
||||
@@ -0,0 +1,22 @@
|
||||
# MiniCPM-o 4.5 MRv2 profiles
|
||||
|
||||
Turn mode uses the stage-level MRv2 contracts from #8184. Thinker emits live
|
||||
latent metadata outside graph replay; Talker keeps codec history and EOS
|
||||
control on device. Code2Wav reuses mainline Whole-Euler Flow graphs and shared
|
||||
prompt state. The previous experimental block compilation, tiled attention,
|
||||
channels-last and merged-CFM implementations are removed.
|
||||
|
||||
`minicpmo_4_5_turn_mrv2_h200.yaml` selects Talker capacity 16 and 4 GiB KV;
|
||||
the generic MRv2 profile selects capacity 8 and 2 GiB KV. These configurations
|
||||
require new end-to-end performance measurements after the mainline integration.
|
||||
Previously reported numbers do not describe this revised codec backend.
|
||||
|
||||
The generic MRv2 profile and the opt-in native V1 duplex H200 profile enable
|
||||
TF32 matmuls within Code2Wav forward/capture only; the previous process policy
|
||||
is restored afterwards. cuDNN's TF32 policy is independent. TF32 changes rounding.
|
||||
Duplex keeps the mainline V1 session path. Turn results do not establish duplex
|
||||
performance or interruption correctness.
|
||||
|
||||
The shared asynchronous output snapshot and batched Talker preprocessing also
|
||||
apply to V1 when async chunking/scheduling is enabled. Default V1 behavior must
|
||||
therefore be included in end-to-end regression validation.
|
||||
@@ -93,13 +93,13 @@ Each optimization stacks on the previous one. The summary plots below show the c
|
||||
|
||||
**Benchmark environment:**
|
||||
|
||||
| | Qwen3-Omni | Qwen3-TTS |
|
||||
| --- |-----------------------------| --- |
|
||||
| **GPU** | A100 | H200 |
|
||||
| | Qwen3-Omni | Qwen3-TTS |
|
||||
| --- | ----------------------------- | --- |
|
||||
| **GPU** | A100 | H200 |
|
||||
| **Model** | Qwen3-Omni-30B-A3B-Instruct | Qwen3-TTS-12Hz-1.7B-CustomVoice |
|
||||
| **vLLM** | v0.17.0 | v0.18.0 |
|
||||
| **vllm-omni** | commit 199f7832 | v0.18.0rc2 |
|
||||
| **CUDA** | 12.9 | 12.8 |
|
||||
| **vLLM** | v0.17.0 | v0.18.0 |
|
||||
| **vllm-omni** | commit 199f7832 | v0.18.0rc2 |
|
||||
| **CUDA** | 12.9 | 12.8 |
|
||||
|
||||
This post walks through each optimization in the same order they are typically enabled in practice, then ends with deployment playbooks for both models.
|
||||
|
||||
@@ -310,7 +310,68 @@ self._compiled_model_fwd = torch.compile(
|
||||
|
||||
`mode="default"` is used instead of `mode="reduce-overhead"` to avoid conflicts with vLLM's own CUDA graph capture on the main Talker model. `dynamic=True` handles the growing sequence length without recompilation.
|
||||
|
||||
These optimizations are always-on in the current codebase - all Qwen3-TTS benchmark results in this post include them.
|
||||
The historical benchmark results above use re-prefill. It remains the default
|
||||
for Qwen3-TTS and the shared Qwen3-Omni predictor.
|
||||
|
||||
### Experimental Qwen3-TTS single-GPU throughput profile
|
||||
|
||||
The opt-in `qwen3_tts_high_concurrency_mrv2_single_gpu.yaml` profile uses a
|
||||
Qwen3-TTS-specific execution path on CUDA with BF16 weights:
|
||||
|
||||
- `code_predictor_kv_cache` reuses each layer's keys and values across the 15
|
||||
residual-codebook predictions within one audio frame. The first step writes
|
||||
two positions; subsequent steps write one. Scratch storage is bounded by the
|
||||
configured batch capacity, overwritten for every frame, and never shared
|
||||
between requests across frames. The worker's serialized CUDA stream owns it;
|
||||
it is separate from the Talker's paged KV cache.
|
||||
- A short causal/GQA Triton kernel masks unwritten K/V loads. Dynamic-batch
|
||||
`torch.compile` warms the predictor before the runner captures whole-MTP
|
||||
CUDA graphs. This path creates no inner CUDA graphs. Unsupported devices and
|
||||
dtypes retain re-prefill; FP16 retains its existing precision safeguards.
|
||||
- `code_predictor_fused_sampling` fuses temperature scaling, top-k filtering,
|
||||
and Gumbel argmax when the runner supplies FP32 uniforms. It preserves top-k
|
||||
ties and uses the supplied randomness. Explicit generator paths retain the
|
||||
existing sampler. Qwen3-Omni does not read either TTS option.
|
||||
- `decode_cudnn_benchmark` selects convolution algorithms during Code2Wav
|
||||
warmup and graph capture. The process's cuDNN flags are restored afterwards,
|
||||
including on capture failure. Captured graphs retain the selected algorithms.
|
||||
|
||||
- `decode_time_major_conv` keeps codec convolution activations in time-major
|
||||
layout to reduce transposes and copies between layers.
|
||||
- `talker_first_audio` completes residual prediction immediately after the
|
||||
first codebook sample and decodes the first frame in the Talker process.
|
||||
The codec advances its state without repeating the delivered frame; the
|
||||
orchestrator orders that frame before subsequent codec chunks. This path
|
||||
requires CUDA, MRV2, asynchronous chunks, a single-process executor, TP/PP 1,
|
||||
and disabled prefix caching. Reference-code requests retain codec delivery.
|
||||
Supported Qwen3-TTS profiles enable it explicitly; other models do not opt in.
|
||||
|
||||
The predictor, sampling, convolution-layout and autotuning options default to
|
||||
false outside this throughput profile. It uses fixed 1/25-frame chunks,
|
||||
72 frames of left context, Talker/codec capacities of 128/64, codec batches
|
||||
through 8, and synchronous codec scheduling. Client concurrency is independent
|
||||
of those capacities. This profile also sets `cuda_mps: true`; the runtime owns
|
||||
a private MPS daemon unless an operator supplies an existing MPS pipe directory.
|
||||
Ordinary profiles leave MPS disabled. See
|
||||
[experimental MPS deployment](../configuration/stage_configs.md#experimental-mps-deployment)
|
||||
for requirements and ownership. Startup includes compilation and graph capture.
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --omni \
|
||||
--deploy-config vllm_omni/deploy/qwen3_tts_high_concurrency_mrv2_single_gpu.yaml \
|
||||
--api-server-count 4 --stage-init-timeout 1800 --init-timeout 2400
|
||||
```
|
||||
|
||||
KV reuse changes GEMM shapes and attention reduction order; cuDNN autotuning
|
||||
can also change waveform numerics. This path is not bitwise equivalent to
|
||||
re-prefill, and aggregate WER alone cannot establish perceptual or speaker
|
||||
similarity equivalence. Validate quality on the intended workload before
|
||||
adopting it. Qwen3-Omni keeps its existing predictor and audio-delivery behavior.
|
||||
|
||||
For a controlled ablation, copy the profile and disable individual options while keeping capacities, chunking, hardware, client concurrency,
|
||||
and warmup identical. Report repeated runs, first-audio latency and failures
|
||||
alongside throughput. Warm requests measure a ready service; they do not measure
|
||||
cold-start latency.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -251,8 +251,8 @@ curl -X POST http://localhost:8000/v1/images/generations \
|
||||
|
||||
| Method | Arguments | Return Type | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **sleep** | `stage_ids: List[int], level: int` | `List[OmniACK]` | Triggers hibernation for specified stages. |
|
||||
| **wake_up** | `stage_ids: List[int]` | `List[OmniACK]` | Reloads weights and re-maps memory. |
|
||||
| **sleep** | `stage_ids: List[int], level: int` | `List[OmniACK]` | Triggers hibernation for specified stages. Raises `RuntimeError` if a stage fails; the stages stay marked as sleeping, so `wake_up` brings them back, unless a level-2 sleep already discarded weights on some replica. |
|
||||
| **wake_up** | `stage_ids: List[int]` | `List[OmniACK]` | Reloads weights and re-maps memory. Raises `RuntimeError` if a stage fails; that stage stays sleeping and the call can be retried. |
|
||||
| **pause_generation** | `mode: str, stage_ids: List[int]` | `None` | Stops admission; with `mode="keep"` also stops diffusion schedulers and returns after their ACK. |
|
||||
| **resume_generation** | `stage_ids: List[int]` | `None` | Reopens paused schedulers, then admission. |
|
||||
|
||||
@@ -261,7 +261,7 @@ curl -X POST http://localhost:8000/v1/images/generations \
|
||||
| Field | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **task_id** | `str` | Unique identifier for the operation. |
|
||||
| **status** | `str` | `SUCCESS` or `ERROR`. |
|
||||
| **status** | `str` | `SUCCESS` or `ERROR`. `sleep` and `wake_up` raise instead of returning an `ERROR` ACK, and `/v1/omni/sleep` and `/v1/omni/wakeup` return HTTP 500 with the error. |
|
||||
| **stage_id** | `int` | The ID of the stage that responded. |
|
||||
| **rank** | `int` | The rank ID within the Tensor Parallel group. |
|
||||
| **freed_bytes** | `int` | Actual amount of physical VRAM reclaimed. |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Installation
|
||||
|
||||
!!! important
|
||||
vLLM-Omni is released against the matching upstream vLLM major/minor version. The 0.30 development line requires vLLM 0.30.x; the source-install instructions pin vLLM 0.30.0. Published vLLM-Omni 0.28.0 wheels and images still require vLLM 0.28.x.
|
||||
vLLM-Omni is released against the matching upstream vLLM major/minor version. The vLLM-Omni 0.30.x release line uses vLLM 0.30.x; the source-install instructions pin vLLM 0.30.0.
|
||||
|
||||
vLLM-Omni supports the following hardware platforms:
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@ vLLM-Omni is a Python library that supports the following GPU variants. The libr
|
||||
|
||||
### Pre-built wheels
|
||||
|
||||
The source-install instructions target vLLM 0.30.0. Until vLLM-Omni 0.30.0 artifacts are published, the CUDA/ROCm pre-built examples below remain on the matching 0.28.0 release pair.
|
||||
|
||||
Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc1, 0.14.0rc1, 0.14.0, 0.16.0, 0.18.0, 0.20.0, 0.21.0, 0.22.0, 0.23.0, 0.24.0, 0.25.0, 0.26.0, and 0.28.0. If you need a newer unreleased revision, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
|
||||
Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc1, 0.14.0rc1, 0.14.0, 0.16.0, 0.18.0, 0.20.0, 0.21.0, 0.22.0, 0.23.0, 0.24.0, 0.25.0, 0.26.0, 0.28.0, and 0.30.0. If you need a newer unreleased revision, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
|
||||
|
||||
=== "NVIDIA CUDA"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# --8<-- [end:requirements]
|
||||
# --8<-- [start:set-up-using-python]
|
||||
|
||||
vLLM-Omni depends on the matching major/minor release of vLLM. The 0.30 development line uses vLLM 0.30.x. Published 0.28.0 wheels and images use vLLM 0.28.x.
|
||||
vLLM-Omni depends on the matching major/minor release of vLLM. The vLLM-Omni 0.30.x release line uses vLLM 0.30.x.
|
||||
|
||||
!!! note
|
||||
PyTorch installed via `conda` will statically link `NCCL` library, which can cause issues when vLLM tries to use `NCCL`. See <gh-issue:8420> for more details.
|
||||
@@ -18,22 +18,22 @@ Therefore, it is recommended to install vLLM and vLLM-Omni with a **fresh new**
|
||||
|
||||
#### Installation of vLLM
|
||||
|
||||
These pre-built wheel instructions install the published vLLM-Omni 0.28.0 release. For the 0.30 development line, use the source-install instructions below.
|
||||
These pre-built wheel instructions install the published vLLM-Omni 0.30.0 release.
|
||||
|
||||
vLLM-Omni is built based on vLLM. Please install it with command below.
|
||||
```bash
|
||||
uv pip install vllm==0.28.0 --torch-backend=auto
|
||||
uv pip install vllm==0.30.0 --torch-backend=auto
|
||||
```
|
||||
|
||||
#### Installation of vLLM-Omni
|
||||
|
||||
```bash
|
||||
uv pip install vllm-omni==0.28.0
|
||||
uv pip install vllm-omni==0.30.0
|
||||
```
|
||||
|
||||
To run Gradio demos, also install the optional extras:
|
||||
```bash
|
||||
uv pip install 'vllm-omni[demo]==0.28.0'
|
||||
uv pip install 'vllm-omni[demo]==0.30.0'
|
||||
```
|
||||
|
||||
# --8<-- [end:pre-built-wheels]
|
||||
@@ -111,7 +111,7 @@ docker run --runtime nvidia --gpus 2 \
|
||||
--env "HF_TOKEN=$HF_TOKEN" \
|
||||
-p 8091:8091 \
|
||||
--ipc=host \
|
||||
vllm/vllm-omni:v0.28.0 \
|
||||
vllm/vllm-omni:v0.30.0 \
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
|
||||
```
|
||||
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
|
||||
For ROCm, vLLM-Omni currently recommends the setup steps through Docker Images.
|
||||
|
||||
vLLM-Omni depends on the matching major/minor release of vLLM. The 0.30
|
||||
development line uses vLLM 0.30.x. Published 0.28.0 wheels and images use vLLM
|
||||
0.28.x.
|
||||
vLLM-Omni depends on the matching major/minor release of vLLM. The vLLM-Omni
|
||||
0.30.x release line uses vLLM 0.30.x.
|
||||
|
||||
The Dockerfile's `BASE_IMAGE` pin applies only to Docker builds. The
|
||||
`vllm-omni` package does not install vLLM as a dependency, so non-Docker source
|
||||
@@ -23,12 +22,12 @@ installing vLLM-Omni, as shown below.
|
||||
|
||||
#### Installation of vLLM
|
||||
|
||||
These pre-built wheel instructions install the published vLLM-Omni 0.28.0 release. For the 0.30 development line, use the source-install instructions below.
|
||||
These pre-built wheel instructions install the published vLLM-Omni 0.30.0 release.
|
||||
|
||||
vLLM-Omni is built based on vLLM. Please install it with command below.
|
||||
|
||||
```bash
|
||||
uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.28.0/rocm723
|
||||
uv pip install vllm==0.30.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.30.0/rocm723
|
||||
```
|
||||
|
||||
#### Installation of vLLM-Omni
|
||||
@@ -37,7 +36,7 @@ uv pip install vllm==0.28.0+rocm723 --extra-index-url https://wheels.vllm.ai/roc
|
||||
# we need to add --no-build-isolation as the torch
|
||||
# is not obtained from pypi, we have to install using the
|
||||
# torch installed in our environment
|
||||
uv pip install vllm-omni==0.28.0
|
||||
uv pip install vllm-omni==0.30.0
|
||||
|
||||
# Optional if want to run Qwen3 TTS
|
||||
uv pip uninstall onnxruntime # should be removed before we can install onnxruntime-rocm
|
||||
@@ -150,6 +149,8 @@ vllm-omni-rocm
|
||||
|
||||
vLLM-Omni offers an official docker image for deployment. These images are built on top of vLLM docker images and available on Docker Hub as [vllm/vllm-omni-rocm](https://hub.docker.com/r/vllm/vllm-omni-rocm/tags). The version of vLLM-Omni indicates which release of vLLM it is based on.
|
||||
|
||||
The prebuilt ROCm image is published separately from the release pipeline: `v0.28.0` is the latest tag currently available on Docker Hub, and newer-tag availability is tracked in [#7405](https://github.com/vllm-project/vllm-omni/issues/7405).
|
||||
|
||||
#### Launch vLLM-Omni Server
|
||||
|
||||
Here's an example deployment command that has been verified on 2 x MI300's:
|
||||
|
||||
@@ -53,7 +53,7 @@ docker run --rm \
|
||||
|
||||
# Inside the container, install vLLM-Omni from source
|
||||
cd /vllm-workspace
|
||||
git clone -b v0.30.0rc1 https://github.com/vllm-project/vllm-omni.git
|
||||
git clone -b v0.30.0 https://github.com/vllm-project/vllm-omni.git
|
||||
cd vllm-omni
|
||||
pip install -v -e . --no-build-isolation
|
||||
# or VLLM_OMNI_TARGET_DEVICE=npu pip install -v -e .
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
# SeedVR2 3B video restoration
|
||||
|
||||
The native `SeedVR2Pipeline` restores an input video using the released 3B FP16
|
||||
NaDiT and s8/c16/t4 causal VAE. It uses fixed checkpoint conditioning, CFG=1,
|
||||
and one Euler step. Text prompts do not change conditioning.
|
||||
|
||||
## Code ownership
|
||||
|
||||
The model package groups each computation stage in one file: `nadit.py` owns
|
||||
window geometry, routing, RoPE and the DiT; `vae.py` owns the causal VAE and its
|
||||
temporal/spatial tiling; `pipeline_seedvr2.py` owns input validation, decoding,
|
||||
conditioning, the Euler step and colour transfer. `config.py` owns model limits,
|
||||
and `long_video.py` owns the optional HTTP job/window lifecycle. Framework code
|
||||
only registers the model, its pre/postprocessors, metadata and HTTP routes.
|
||||
|
||||
`color_correction_method` is model-specific: online requests use the existing
|
||||
`extra_params` JSON object, and offline sampling uses
|
||||
`extra_args={"color_correction_method": "wavelet"}`. The default remains `lab`;
|
||||
invalid values are rejected by model-side validation before inference. It is
|
||||
not a top-level field in the common video API.
|
||||
|
||||
## Model directory
|
||||
|
||||
Place these files together, retaining their names:
|
||||
|
||||
| File | SHA-256 of the validated release |
|
||||
| --- | --- |
|
||||
| `seedvr2_ema_3b_fp16.safetensors` | `2fd0e03a3dad24e07086750360727ca437de4ecd456f769856e960ae93e2b304` |
|
||||
| `ema_vae_fp16.safetensors` | `20678548f420d98d26f11442d3528f8b8c94e57ee046ef93dbb7633da8612ca1` |
|
||||
| `pos_emb.pt` | `fa07a14844314772266b66c3b95deb0027696d8fe7065721263db5176f45d799` |
|
||||
|
||||
Add `model_index.json` containing:
|
||||
|
||||
```json
|
||||
{"_class_name": "SeedVR2Pipeline"}
|
||||
```
|
||||
|
||||
The loader checks every checkpoint tensor, including RoPE buffers, and rejects
|
||||
missing, duplicate, unexpected, or incompatible tensors. Install PyAV for video
|
||||
decoding. The validated environment uses PyAV 18.1, PyTorch 2.13/CUDA 13, and L20 GPUs.
|
||||
|
||||
## Serve and restore a video
|
||||
|
||||
```bash
|
||||
vllm serve "$MODEL_DIR" --omni \
|
||||
--model-class-name SeedVR2Pipeline --dtype float16 --enforce-eager \
|
||||
--num-gpus 1 --host 127.0.0.1 --port 8098
|
||||
|
||||
curl --fail-with-body http://127.0.0.1:8098/v1/videos/sync \
|
||||
-F 'prompt= ' \
|
||||
-F 'input_references=@input.mp4;type=video/mp4' \
|
||||
-F 'size=224x128' -F 'num_inference_steps=1' \
|
||||
-F 'guidance_scale=1' -F 'seed=7723' \
|
||||
--output restored.mp4
|
||||
```
|
||||
|
||||
For two-rank window sequence parallelism, expose two GPUs and replace `--num-gpus 1` with:
|
||||
|
||||
```bash
|
||||
--num-gpus 2 --distributed-executor-backend mp \
|
||||
--stage-overrides '{"0":{"ulysses_degree":2}}'
|
||||
```
|
||||
|
||||
Use degree 4 and four visible GPUs for SP4. Window-SP alone replicates the VAE.
|
||||
To shard VAE activations across the same ranks, use:
|
||||
|
||||
```bash
|
||||
--vae-use-tiling --num-gpus 2 --distributed-executor-backend mp \
|
||||
--stage-overrides '{"0":{"ulysses_degree":2,"vae_patch_parallel_size":2,"vae_parallel_mode":"spatial_shard_height"}}'
|
||||
```
|
||||
|
||||
The VAE patch degree must match the window-SP degree.
|
||||
For a 16-frame 1280×720 input restored at its original size, use four GPUs,
|
||||
VAE tiling, and height sharding with both degrees set to four:
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 vllm serve "$MODEL_DIR" --omni \
|
||||
--model-class-name SeedVR2Pipeline --dtype float16 --enforce-eager \
|
||||
--vae-use-tiling --num-gpus 4 --distributed-executor-backend mp \
|
||||
--stage-overrides '{"0":{"ulysses_degree":4,"vae_patch_parallel_size":4,"vae_parallel_mode":"spatial_shard_height"}}' \
|
||||
--host 127.0.0.1 --port 8098
|
||||
```
|
||||
|
||||
Request `size=1280x720` for this landscape input or `size=720x1280` for a
|
||||
portrait input of the same dimensions. Keep the requested output size equal to
|
||||
the input size when testing restoration without upscaling.
|
||||
|
||||
The multipart API requires a prompt field; a single space supplies a blank
|
||||
prompt. Nonblank text is rejected. Omit `fps` to retain the source frame rate;
|
||||
an explicit rate must match the source. Output dimensions are explicit multiples
|
||||
of 16. Input frames are resized with antialiased bicubic interpolation. The final
|
||||
frame is repeated internally to reach 4n+1, and the decoded output is cropped back
|
||||
to the original frame count. Five frames remain five; six frames are internally
|
||||
padded to nine and return six.
|
||||
|
||||
## Colour correction
|
||||
|
||||
Restoration reproduces detail faithfully but shifts global colour, so the
|
||||
resized input is used to transfer colour back onto the restored frames. This is
|
||||
on by default; pass `extra_params={"color_correction_method":"wavelet"}` to choose how:
|
||||
|
||||
| Value | Behavior |
|
||||
| --- | --- |
|
||||
| `lab` (default) | Swaps the lowest frequency band, then histogram-matches CIELAB chroma. Most faithful colour. |
|
||||
| `wavelet` | Swaps only the lowest frequency band, keeping every higher band from the restoration. |
|
||||
| `adain` | Matches per-channel mean and standard deviation. Cheapest, and corrects only a global tint. |
|
||||
| `none` | Skips the transfer and returns raw restored colour. |
|
||||
|
||||
```bash
|
||||
curl --fail-with-body http://127.0.0.1:8098/v1/videos/sync \
|
||||
-F 'prompt= ' -F 'input_references=@input.mp4;type=video/mp4' \
|
||||
-F 'size=224x128' -F 'num_inference_steps=1' -F 'guidance_scale=1' \
|
||||
-F 'seed=7723' -F 'extra_params={"color_correction_method":"wavelet"}' \
|
||||
--output restored.mp4
|
||||
```
|
||||
|
||||
`lab` and `wavelet` retain the restored high-frequency detail; `adain` measurably
|
||||
softens it because it rescales every frequency. The transfer runs in FP32 after
|
||||
the VAE decode and costs a fraction of a second per frame. Unrecognized values
|
||||
are rejected with 400 before the clip is admitted. Correction cannot recover
|
||||
colour lost to 4:2:0 chroma subsampling in the returned MP4, which dominates the
|
||||
remaining deviation once the transfer is applied.
|
||||
|
||||
## Request admission
|
||||
|
||||
The default 3B limit is 848×480 pixels per input or output frame and 2,035,200
|
||||
pixels across the requested output clip. With VAE tiling and window SP of at
|
||||
least four ranks whose `vae_patch_parallel_size` matches `ulysses_degree`, the
|
||||
per-frame limit rises to 2560×1472 and the clip budget to 18,841,600 pixels.
|
||||
Raising the degree never lowers the budget. Both budgets count temporal
|
||||
padding to 4n+1 frames: the default admits up to five 848×480 frames or 93
|
||||
192×112 frames; the sharded profile admits up to 45 848×480 frames or five
|
||||
2560×1472 frames. A 16-frame 1280×720 or 720×1280 clip pads to 17 frames and
|
||||
uses 15,667,200 of the sharded clip's 18,841,600 pixels. Both orientations passed
|
||||
original-size HTTP restoration on four RTX 5090 GPUs with VAE tiling and height
|
||||
sharding. An independent 257-frame cap bounds per-frame decoder work for tiny
|
||||
inputs. The other longer combinations above are admission bounds, not completed
|
||||
GPU validation. The decoder checks
|
||||
declared duration, frame count, and input dimensions when available, then
|
||||
enforces the limits as frames arrive. Requests outside these budgets return 400
|
||||
before building the resized whole-clip tensor.
|
||||
|
||||
The per-frame cap is calibrated for the smallest qualified device. The clip cap
|
||||
scales with the smallest visible device's memory: it stays at the calibrated
|
||||
five 2560×1472 frames on a 32 GB device and grows on larger ones from a
|
||||
per-rank memory model measured at SP4 (see `sharded_budget` in
|
||||
`vllm_omni/diffusion/models/seedvr2/pipeline_seedvr2.py`), which keeps 15% of the device
|
||||
free and leaves room for allocator fragmentation. It stops at 2,118,057,984
|
||||
padded pixels (513 frames at 1536×2688), the largest clip validated on four
|
||||
B300 ranks; 80 GB devices get about 1.08 billion and 141 GB or larger devices
|
||||
reach the cap. Clips at the cap in both memory regimes (561 frames at 2560×1472
|
||||
and 2,049 frames at 768×1344) peaked at 54 GiB of activations per rank. Set
|
||||
these before starting the server to override the caps; each must be a positive
|
||||
integer, and validating an override on the target hardware is the operator's
|
||||
responsibility:
|
||||
|
||||
| Variable | Default | Bounds |
|
||||
| --- | --- | --- |
|
||||
| `VLLM_OMNI_SEEDVR2_SHARDED_FRAME_PIXELS` | 3,768,320 | Pixels per frame on the sharded profile |
|
||||
| `VLLM_OMNI_SEEDVR2_SHARDED_CLIP_PIXELS` | 18,841,600, scaled up with device memory | Padded pixels per clip on the sharded profile |
|
||||
| `VLLM_OMNI_SEEDVR2_MAX_FRAMES` | 257 | Decoder-work frame cap, all profiles |
|
||||
|
||||
The sharded profile applies to any `ulysses_degree` of four or more, so eight
|
||||
ranks inherit a budget calibrated on four. Raising these caps trades a 400 for a
|
||||
CUDA OOM: the admission check passes and the request fails later inside the DiT
|
||||
forward or the VAE decode, which can abort every in-flight request on the engine
|
||||
rather than only the oversized one. Before serving with raised caps, restore the
|
||||
largest clip they admit once and confirm it completes.
|
||||
|
||||
A 362-frame 1536×2688 2x upscale was restored on eight ranks with
|
||||
`ulysses_degree=8`, VAE tiling and height sharding, using 4,300,000 for the
|
||||
per-frame cap, the memory-scaled clip cap and 2,000 for the frame cap. The long-video
|
||||
route sizes its windows from these caps, so the clip cap sets how many frames
|
||||
each window covers.
|
||||
|
||||
## Long-video restoration
|
||||
|
||||
`POST /v1/seedvr2/restore-long` accepts one uploaded 24 FPS video, a blank
|
||||
prompt, `size`, `num_frames` (up to 7,200), and optional `loop_input=true` and
|
||||
`extra_params={"color_correction_method":"wavelet"}`, which it applies to every window.
|
||||
An output frame must fit the sharded per-frame pixel cap. It returns a job ID; poll
|
||||
`GET /v1/seedvr2/restore-long/{id}` and download the completed MP4 from
|
||||
`GET /v1/seedvr2/restore-long/{id}/content`. `DELETE
|
||||
/v1/seedvr2/restore-long/{id}` asks a running job to stop; it settles as
|
||||
`cancelled` at the next window boundary. A job whose owning process is gone,
|
||||
such as after a server restart, reports `failed` on the next poll.
|
||||
|
||||
Jobs live on the API server's local disk, so the route requires
|
||||
`--api-server-count 1` and runs one job at a time. Storage is bounded by an
|
||||
upload cap and by deleting settled jobs once they age out:
|
||||
|
||||
| Variable | Default | Bounds |
|
||||
| --- | --- | --- |
|
||||
| `VLLM_OMNI_SEEDVR2_LONG_OUTPUT_DIR` | system temp dir | Parent of the job directories |
|
||||
| `VLLM_OMNI_SEEDVR2_LONG_MAX_UPLOAD_BYTES` | 8 GiB | Largest accepted upload |
|
||||
| `VLLM_OMNI_SEEDVR2_LONG_JOB_TTL_SECONDS` | 3,600 | Age at which a settled job is deleted |
|
||||
| `VLLM_OMNI_SEEDVR2_LONG_MAX_WINDOW` | 121 | Longest model window, in frames |
|
||||
|
||||
Download a completed job before its TTL expires; the sweep runs on each new
|
||||
submission and removes the output with the job directory.
|
||||
|
||||
The service sends windows through the existing SeedVR2 endpoint, blends four
|
||||
frames at each boundary, and writes one continuous MP4 encoder. Each window is
|
||||
the longest 4n+1-frame clip that the sharded clip-pixel cap, the frame cap and
|
||||
`VLLM_OMNI_SEEDVR2_LONG_MAX_WINDOW` admit at the output size. Every window pays
|
||||
a fixed round trip through the serving stack and recomputes its overlap, and a
|
||||
window of four latent frames gets no temporal attention in the DiT, so raise
|
||||
the clip cap as far as the device allows. Windows carry source pixels, which
|
||||
the model resizes on the device, and cross the endpoint losslessly; only the
|
||||
final MP4 is compressed. The next window is restored while the previous one is
|
||||
blended and encoded. It repeats source frames and audio only when
|
||||
`loop_input=true`. The sharded serving profile above (VAE tiling, VAE height
|
||||
sharding, Ulysses degree of at least four) is required. This route keeps a
|
||||
window within the whole-clip pixel budget; it does not raise the
|
||||
`/v1/videos/sync` 257-frame or pixel limits. The long route
|
||||
uses fixed one-step, CFG=1 conditioning and requires `imageio[ffmpeg]` for
|
||||
audio muxing. The 7,200-frame 768×1344 case is still undergoing full GPU E2E.
|
||||
|
||||
## Temporal and spatial VAE tiling
|
||||
|
||||
Add `--vae-use-tiling` to bound VAE intermediate activations along time. The
|
||||
encoder processes nine frames first, then eight per chunk; the decoder processes
|
||||
two latent frames per chunk. Each causal convolution carries its past inputs
|
||||
within that encode/decode call, with temporal stride and first-frame upsampling
|
||||
alignment preserved. GroupNorm and attention still see the entire spatial frame.
|
||||
There is no overlap blend or independent restart at chunk boundaries.
|
||||
|
||||
Five-frame clips use one temporal chunk. On a single GPU, convolutions with
|
||||
more than 256 input rows additionally process tiles of 128 output rows with
|
||||
exact halos. GroupNorm and bottleneck attention retain full-frame statistics.
|
||||
This bounds convolution workspace; full-frame activations and the DiT's
|
||||
whole-clip activations still consume memory.
|
||||
|
||||
With VAE patch parallelism, each rank owns a band of latent rows and the
|
||||
corresponding encoder/decoder rows. Neighbor exchanges supply convolution halos;
|
||||
all-reduced centered statistics preserve full-frame GroupNorm. Bottleneck
|
||||
attention gathers the low-resolution frame before splitting it again. Encoder
|
||||
parameters are gathered before latent sampling, and decoded pixels are gathered
|
||||
before returning the video. Unequal bands are supported; fewer latent rows than
|
||||
ranks fall back to replicated execution. Width sharding and batch slicing are
|
||||
unsupported. FP16 reduction and convolution order can change rounding, so
|
||||
parallel outputs are numerically close rather than bitwise identical.
|
||||
|
||||
## Current integration scope
|
||||
|
||||
| Property | Contract |
|
||||
| --- | --- |
|
||||
| Weights / dtype | Released 3B DiT and VAE, FP16 |
|
||||
| Reference semantics | C0 whole clip, colour correction on by default |
|
||||
| Video input | One uploaded file, or offline TCHW RGB floats / PIL frames |
|
||||
| Timing | Constant frame rate, increasing PTS; normalize the video origin to zero |
|
||||
| Audio | First mono/stereo track, aligned by source PTS, cropped to the video interval, re-encoded as AAC |
|
||||
| Randomness | Per-request generator; preserve reference latent strides when sampling noise |
|
||||
| Sequence parallelism | Native engine SP1/2/4 correctness verified on five-frame L20 cases |
|
||||
| VAE placement | Replicated by default; optional height sharding on the window-SP group |
|
||||
| Unsupported | VFR, multichannel audio, 7B, other sampling schedules, quantization, cache acceleration, VAE width sharding / batch slicing, CPU offload, CFG/TP/PP parallelism, compiled execution, LoRA |
|
||||
|
||||
Unsupported engine modes are rejected before process hooks and worker creation.
|
||||
`ulysses_degree` selects the model-owned SP group. This branch assigns whole
|
||||
windows to ranks; the specialized head-sharded path is a dependent change.
|
||||
Ring and AllGather-KV are unsupported.
|
||||
|
||||
The practical P0 reference's five-frame batching, overlap, and CPU swapping are
|
||||
separate execution semantics. Temporal tiling and VAE patch
|
||||
parallelism reduce activation peaks but do not bound the whole-clip DiT memory.
|
||||
Large output frames may still exceed device capacity.
|
||||
|
||||
Invalid inputs return 400. A single-GPU model OOM fails that request; a
|
||||
subsequent short request succeeds. With VAE patch parallelism, restart the
|
||||
service after an OOM: another rank may still be waiting in a collective. Cancelling an in-progress video job removes it through the
|
||||
existing DELETE endpoint; in-flight GPU work may drain before memory is reusable.
|
||||
A lost SP worker fails the request and makes health return 503. Restart the
|
||||
service to restore availability; automatic rank recovery is not provided.
|
||||
|
||||
## Reproducible deployment
|
||||
|
||||
See the [RTX 5090 recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/ByteDance/SeedVR2-RTX-5090.md) for the
|
||||
input/output contract, complete serving command, and media checks. The local
|
||||
tests in `tests/diffusion/models/seedvr2/test_seedvr2_e2e.py` exercise the full
|
||||
3B pipeline, all colour modes, and HTTP restoration with USP 1 and 8.
|
||||
@@ -19,9 +19,9 @@ th {
|
||||
|
||||
| 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) |
|
||||
| `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` | ✅︎ | | | | — |
|
||||
| `MingFlashOmniForConditionalGeneration` + `MingImagePipeline` | Ming-flash-omni-2.0 (omni-speech + imagegen<sup>1</sup>) | `Jonathan1909/Ming-flash-omni-2.0` | ✅︎ | | | ✅︎ | — |
|
||||
| `BagelForConditionalGeneration` | BAGEL (DiT-only) | `ByteDance-Seed/BAGEL-7B-MoT` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `InternVLAA1Pipeline` | InternVLA-A1 | `InternRobotics/InternVLA-A1-3B` | ✅︎ | ✅︎ | | | — |
|
||||
| `Gr00tN1d7Pipeline` | GR00T N1.7 | `nvidia/GR00T-N1.7-3B` | ✅︎ | | | | — |
|
||||
@@ -33,9 +33,9 @@ th {
|
||||
| `QwenImageLayeredPipeline` | Qwen-Image-Layered | `Qwen/Qwen-Image-Layered` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `QwenImageEditPlusPipeline` | Qwen-Image-Edit-2511 | `Qwen/Qwen-Image-Edit-2511` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `GlmImagePipeline` | GLM-Image | `zai-org/GLM-Image` | ✅︎ | ✅︎ | | | — |
|
||||
| `ZImagePipeline` | Z-Image | `Tongyi-MAI/Z-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Krea2Pipeline` | Krea 2 (Raw + Turbo) | `krea/Krea-2-Raw`, `krea/Krea-2-Turbo` | ✅︎ | | | ✅︎ | — |
|
||||
| `WanPipeline` | Wan2.1-T2V, Wan2.2-T2V, Wan2.2-TI2V | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers`, `Wan-AI/Wan2.1-T2V-14B-Diffusers`, `Wan-AI/Wan2.2-T2V-A14B-Diffusers`, `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `ZImagePipeline` | Z-Image | `Tongyi-MAI/Z-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Tongyi-MAI/Z-Image-Turbo.md) |
|
||||
| `Krea2Pipeline` | Krea 2 (Raw + Turbo) | `krea/Krea-2-Raw`, `krea/Krea-2-Turbo` | ✅︎ | | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/krea/Krea-2.md) |
|
||||
| `WanPipeline` | Wan2.1-T2V, Wan2.2-T2V, Wan2.2-TI2V | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers`, `Wan-AI/Wan2.1-T2V-14B-Diffusers`, `Wan-AI/Wan2.2-T2V-A14B-Diffusers`, `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Wan-AI/Wan2.2-T2V.md) |
|
||||
| `WanImageToVideoPipeline` | Wan2.2-I2V | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `LingBotWorldCausalDMDPipeline` | LingBot-World 2.0 v1 (experimental) | `robbyant/lingbot-world-v2-14b-causal-fast-diffusers` | Experimental | | | | — |
|
||||
| `Cosmos3OmniDiffusersPipeline` | Cosmos3 T2I, T2V, I2V, V2V, T2V with sound, action policy | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
@@ -43,17 +43,18 @@ th {
|
||||
| `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` | ✅︎ | | | ✅︎ | — |
|
||||
| `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` | ✅︎ | ✅︎ | | | — |
|
||||
| `LTX2DistilledTwoStagePipeline` | LTX-2 / LTX-2.3 merged-distilled two-stage T2V and I2V | `rootonchair/LTX-2-19b-distilled`, `diffusers/LTX-2.3-Distilled-Diffusers` + matching Lightricks upsampler | ✅︎ | ✅︎ | | | — |
|
||||
| `LTX2Pipeline` | LTX-2.5 Full/SFT one-stage T2V and I2V | `Lightricks/LTX-2.5-Diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.5.md) |
|
||||
| `LTX2Pipeline` | LTX-2 / LTX-2.3 one-stage T2V and I2V | `Lightricks/LTX-2`, `diffusers/LTX-2.3-Diffusers` | ✅︎ | ✅︎ | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.md) |
|
||||
| `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 | ✅︎ | ✅︎ | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.md) |
|
||||
| `LTX2DistilledOneStagePipeline` | LTX-2 / LTX-2.3 merged-distilled one-stage T2V and I2V | `rootonchair/LTX-2-19b-distilled`, `diffusers/LTX-2.3-Distilled-Diffusers` | ✅︎ | ✅︎ | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.md) |
|
||||
| `LTX2DistilledTwoStagePipeline` | LTX-2 / LTX-2.3 merged-distilled two-stage T2V and I2V | `rootonchair/LTX-2-19b-distilled`, `diffusers/LTX-2.3-Distilled-Diffusers` + matching Lightricks upsampler | ✅︎ | ✅︎ | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.md) |
|
||||
| `LTX2Pipeline` | LTX-2.5 Full/SFT one-stage T2V and I2V | `Lightricks/LTX-2.5-Diffusers` | ✅︎ | | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.5.md) |
|
||||
| `LTX2TwoStagePipeline` | LTX-2.5 Full/SFT two-stage T2V and I2V | `Lightricks/LTX-2.5-Diffusers` + `Lightricks/LTX-2.5` LoRA | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.5.md) |
|
||||
| `LTX2DistilledOneStagePipeline` | LTX-2.5 merged-distilled one-stage T2V and I2V | `Lightricks/LTX-2.5-Diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.5.md) |
|
||||
| `LTX2DistilledTwoStagePipeline` | LTX-2.5 merged-distilled two-stage T2V and I2V | `Lightricks/LTX-2.5-Diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/LTX/LTX-2.5.md) |
|
||||
| `LingBotVideoPipeline` | LingBot-Video dense and MoE T2I, T2V, TI2V | `robbyant/lingbot-video-dense-1.3b`, `robbyant/lingbot-video-moe-30b-a3b` | ✅︎ | | | | — |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `SeedVR2Pipeline` | [SeedVR2 3B](seedvr2.md) video restoration | `ByteDance-Seed/SeedVR2-3B` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/ByteDance/SeedVR2-RTX-5090.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` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
@@ -65,23 +66,24 @@ th {
|
||||
| `LongCatImageEditPipeline` | LongCat-Image-Edit | `meituan-longcat/LongCat-Image-Edit` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `LongCatVideoAvatarPipeline` | LongCat-Video-Avatar-1.5 A2V/AI2V (native single-speaker and multi-speaker AI2V/AVC) | `meituan-longcat/LongCat-Video-Avatar-1.5` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/meituan-longcat/LongCat-Video-Avatar-1.5.md) |
|
||||
| `BooguImagePipeline` | Boogu-Image | `Boogu/Boogu-Image-0.1-Base`, `Boogu/Boogu-Image-0.1-Edit`, `Boogu/Boogu-Image-0.1-Edit-Turbo` | ✅︎ | | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Boogu/Boogu-Image.md) |
|
||||
| `BooguImageTurboPipeline` | Boogu-Image Turbo distilled T2I | `Boogu/Boogu-Image-0.1-Turbo` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Boogu/Boogu-Image.md) |
|
||||
| `BooguImageTurboPipeline` | Boogu-Image Turbo distilled T2I | `Boogu/Boogu-Image-0.1-Turbo` | ✅︎ | | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Boogu/Boogu-Image.md) |
|
||||
| `StableDiffusionXLPipeline` | Stable-Diffusion-XL | `stabilityai/stable-diffusion-xl-base-1.0` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `StableDiffusion3Pipeline` | Stable-Diffusion-3 | `stabilityai/stable-diffusion-3.5-medium` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `StableDiffusion3Pipeline` | Stable-Diffusion-3 | `stabilityai/stable-diffusion-3.5-medium` | ✅︎ | ✅︎ | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/StabilityAI/Stable-Diffusion-3.5.md) |
|
||||
| `CosyVoice3Model` | CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `AuKForConditionalGeneration` | AuK / AuK-Flash (instruction-driven speech generation and editing) | `tencent/AuK`, `tencent/AuK-Flash` (with `Qwen/Qwen2.5-Omni-3B`; assemble with `tools/prepare_auk_checkpoint.py`) | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Tencent/AuK-H100.md) |
|
||||
| `OmniVoiceModel` | OmniVoice | `k2-fsa/OmniVoice` | ✅︎ | | | ✅︎ | — |
|
||||
| `VoxCPM2TalkerForConditionalGeneration` | VoxCPM2 | `openbmb/VoxCPM2` | ✅︎ | | | ✅︎ | — |
|
||||
| `BreezeForConditionalGeneration` | Breeze-TTS-2 (voice design, voice clone, voice direction, CFG) | `BreezeBlue/Breeze-TTS-2` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/BreezeBlue/Breeze-TTS-2.md) |
|
||||
| `MammothModa2ForConditionalGeneration` | MammothModa2-Preview | `bytedance-research/MammothModa2-Preview` | ✅︎ | ✅︎ | | | — |
|
||||
| `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` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `FluxKontextPipeline` | FLUX.1-Kontext-dev | `black-forest-labs/FLUX.1-Kontext-dev` | ✅︎ | ✅︎ | | | — |
|
||||
| `Flux2KleinPipeline` | FLUX.2-klein | `black-forest-labs/FLUX.2-klein-4B`, `black-forest-labs/FLUX.2-klein-9B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/black-forest-labs/FLUX.2-klein.md) |
|
||||
| `FluxKontextPipeline` | FLUX.1-Kontext-dev | `black-forest-labs/FLUX.1-Kontext-dev` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `FluxPipeline` | FLUX.1-dev | `black-forest-labs/FLUX.1-dev` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `FluxPipeline` | FLUX.1-schnell | `black-forest-labs/FLUX.1-schnell` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `OmniGen2Pipeline` | OmniGen2 | `OmniGen2/OmniGen2` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `StableAudioPipeline` | Stable-Audio-Open | `stabilityai/stable-audio-open-1.0` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `MiniMaxMusic3ForConditionalGeneration` | MiniMax Music 3 (text-to-music) | `MiniMaxAI/MiniMax-Music3` | ✅︎ | | | | — |
|
||||
| `Yue2ForCausalLM` | YuE2-3B (text-to-music with ABC plan) | `m-a-p/YuE2-3B` | ✅︎ | | | | — |
|
||||
| `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-CustomVoice | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` | ✅︎ | | | ✅︎ | [Published](https://recipes.vllm.ai/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice) |
|
||||
| `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-VoiceDesign | `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-Base | `Qwen/Qwen3-TTS-12Hz-1.7B-Base` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
@@ -97,16 +99,18 @@ th {
|
||||
| `NextStep11Pipeline` | NextStep-1.1 | `stepfun-ai/NextStep-1.1` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `MiMoAudioModel` | MiMo-Audio-7B-Instruct | `XiaomiMiMo/MiMo-Audio-7B-Instruct` | ✅︎ | ✅︎ | | | — |
|
||||
| `MiMoV2ASRForCausalLM` | MiMo-V2.5-ASR | `XiaomiMiMo/MiMo-V2.5-ASR` | ✅︎ | ✅︎ | | | — |
|
||||
| `Flux2Pipeline` | FLUX.2-dev | `black-forest-labs/FLUX.2-dev` | ✅︎ | ✅︎ | | | — |
|
||||
| `Flux2Pipeline` | FLUX.2-dev | `black-forest-labs/FLUX.2-dev` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `FishSpeechSlowARForConditionalGeneration` | Fish Speech S2 Pro | `fishaudio/s2-pro` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `Audio8TTSSlowARForConditionalGeneration` | Audio8 TTS Preview | `Audio8/Audio8-TTS-Preview-0.6b` | ✅︎ | | | | — |
|
||||
| `SenseNovaU1Pipeline` | SenseNova-U1 (DiT-only), SenseNova-U1.5 | `SenseNova/SenseNova-U1-8B-MoT`, `sensenova/SenseNova-U1.5-8B-MoT` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/SenseNova/SenseNova-U1.5.md) |
|
||||
| `Audio8TTSSlowARForConditionalGeneration` | Audio8 TTS Preview | `Audio8/Audio8-TTS-Preview-0.6b` | ✅︎ | | | ✅︎ | — |
|
||||
| `SenseNovaU1Pipeline` | SenseNova-U1 (DiT-only), SenseNova-U1.5 | `SenseNova/SenseNova-U1-8B-MoT`, `sensenova/SenseNova-U1.5-8B-MoT` | ✅︎ | | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/SenseNova/SenseNova-U1.5.md) |
|
||||
| `SenseNovaU1Pipeline` | SenseNova-U1-A3B (MoE) | `sensenova/SenseNova-U1-A3B-MoT` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/SenseNova/SenseNova-U1.md#a3b-moe-variant) |
|
||||
| `LancePipeline` | Lance | `bytedance-research/Lance` | ✅︎ | | | | — |
|
||||
| `HunyuanVideo15Pipeline` | HunyuanVideo-1.5-T2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | ✅︎ | ✅︎ | | | — |
|
||||
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | | — |
|
||||
| `HunyuanVideo15Pipeline` | HunyuanVideo-1.5-T2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | ✅︎ | ✅︎ | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Tencent/HunyuanVideo-1.5.md) |
|
||||
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | ✅︎ | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Tencent/HunyuanVideo-1.5.md) |
|
||||
| `VoxtralTTSForConditionalGeneration` | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✅︎ | ✅︎ | | | — |
|
||||
| `CovoAudioForConditionalGeneration` | Covo-Audio-Chat | `tencent/Covo-Audio-Chat` | ✅︎ | | | | — |
|
||||
| `MiniCPMO45OmniForConditionalGeneration` | MiniCPM-o 4.5 | `openbmb/MiniCPM-o-4_5` | ✅︎ | | ✅︎ | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/OpenBMB/MiniCPM-o-4_5.md) |
|
||||
| `PersonaPlexTalkerForConditionalGeneration` | PersonaPlex (full duplex, 24 kHz speech in/out over `/v1/realtime?duplex=1`) | `nvidia/personaplex-7b-v1` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/NVIDIA/PersonaPlex.md) |
|
||||
| `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` | ✅︎ | ✅︎ | | | — |
|
||||
|
||||
@@ -144,7 +144,7 @@ interchangeably.
|
||||
| ---------- | ---------- | ------------------- | --------- |
|
||||
| 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) |
|
||||
| Turn-based realtime audio | `WS /v1/realtime` | Stream one audio input and receive transcript/audio events | [Realtime Audio](realtime_openai_duplex_api.md) |
|
||||
| Continuous speech-to-speech interaction | `WS /v1/realtime?duplex=1` (alias `WS /v1/duplex`) | Listen and speak concurrently with session control | [Full Duplex](full_duplex_api.md) |
|
||||
| Generated video chunks | `WS /v1/realtime/video` | Start a diffusion request and receive fragmented MP4 | [Streaming Video Output](streaming_video_output_api.md) |
|
||||
| Robot policy inference | `WS /v1/realtime/robot/openpi` | Send MessagePack observations and receive action arrays | [OpenPI Robot Policy](openpi_api.md) |
|
||||
|
||||
@@ -5,7 +5,7 @@ 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).
|
||||
Full duplex is distinct from the turn-based [Realtime Audio API](realtime_openai_duplex_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
|
||||
|
||||
@@ -54,7 +54,7 @@ vllm-omni serve openbmb/MiniCPM-o-4_5 \
|
||||
```
|
||||
|
||||
`vllm_omni/deploy/minicpmo_4_5.yaml` declares `session_mode: duplex` and
|
||||
`duplex_session.max_sessions: 4`. Because the MiniCPM-o 4.5 pipeline declares a
|
||||
`duplex_session.max_sessions: 16`. Because the MiniCPM-o 4.5 pipeline declares a
|
||||
`duplex_plugin`, `vllm-omni serve` runs it through `DuplexOmni`: the server
|
||||
mounts `ws://<host>:8099/v1/realtime?duplex=1` (this page; `ws://<host>:8099/v1/duplex`
|
||||
is an alias of the same route), `POST /v1/chat/completions`, `/v1/models` and
|
||||
@@ -384,9 +384,9 @@ with no OpenAI counterpart.
|
||||
|
||||
The event vocabulary is uniform, but several surfaces are gated by the
|
||||
`capabilities` object the server returns in `session.created`; a client must
|
||||
branch on those flags rather than on the model name. MiniCPM-o 4.5 is the
|
||||
only model on the plugin contract today; the other two columns record what
|
||||
their integrations advertise once the follow-up PRs port them:
|
||||
branch on those flags rather than on the model name. MiniCPM-o 4.5 and
|
||||
PersonaPlex are on the plugin contract today; the Nemotron VoiceChat column
|
||||
records what its integration advertises once the follow-up PR ports it:
|
||||
|
||||
| Capability | MiniCPM-o 4.5 | PersonaPlex | Nemotron VoiceChat | Gated surface |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -401,7 +401,11 @@ their integrations advertise once the follow-up PRs port them:
|
||||
Everything else in the catalogue — session lifecycle, heartbeat and event
|
||||
acknowledgement, append/commit/clear, the response envelope, playback
|
||||
acknowledgement, and the error envelope — behaves identically for every
|
||||
model.
|
||||
model. Two PersonaPlex specifics follow from its capabilities rather than from
|
||||
special-casing: a model with `supports_client_commit=false` auto-responds
|
||||
without `extra_body.auto_response`, and `response.cancel` /
|
||||
`output_audio_buffer.clear` restart its conversation context (a new Stage 0
|
||||
request replays the voice/persona prefill).
|
||||
|
||||
### Compatibility with the OpenAI Realtime protocol
|
||||
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
# Realtime Audio WebSocket API
|
||||
|
||||
Use `WS /v1/realtime` to stream PCM audio into a compatible omni model and
|
||||
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
|
||||
[Full-Duplex API](full_duplex_api.md) instead.
|
||||
As a companion to the vLLM-Omni-proprietary [fullduplex `/v1/realtime`](full_duplex_api.md),
|
||||
supported models also provide a `/v1/realtime` handler supporting
|
||||
a substantial subset of the OpenAI `/v1/realtime` API supporting
|
||||
audio streaming, client-driven VAD, and tool calling
|
||||
|
||||
## Quick Start
|
||||
|
||||
Start a compatible model. The currently documented example uses Qwen3-Omni:
|
||||
The endpoint is currently only supported for Qwen3-Omni. To start:
|
||||
|
||||
Start vLLM-Omni with port `8000` bound to an address reachable from Docker:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8000 --host 0.0.0.0
|
||||
```
|
||||
|
||||
Run the provided client with a mono, 16-bit PCM, 16 kHz WAV file:
|
||||
Then start the docker compose:
|
||||
|
||||
```bash
|
||||
python examples/online_serving/qwen3_omni/openai_realtime_client.py \
|
||||
--url ws://localhost:8091/v1/realtime \
|
||||
--model Qwen/Qwen3-Omni-30B-A3B-Instruct \
|
||||
--input-wav input_16k_mono.wav \
|
||||
--output-wav response.wav \
|
||||
--output-text response.txt
|
||||
cd examples/online_serving/qwen3_omni/fullduplex-livekit-frontend
|
||||
VLLM_OMNI_HOST=$VLLM_OMNI_HOST docker compose up --build
|
||||
```
|
||||
|
||||
The client dependency is `websockets`.
|
||||
Where `VLLM_OMNI_HOST` is a network address. If vLLM-Omni is running on localhost, use
|
||||
`host.docker.internal`
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) and begin chatting.
|
||||
|
||||
## Protocol
|
||||
|
||||
@@ -37,10 +35,9 @@ 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 |
|
||||
| Client to server | `session.update` | Selects and validates the served model. Configures system prompt and tools |
|
||||
| Client to server | `input_audio_buffer.append` | Appends base64 PCM16 audio |
|
||||
| Client to server | `input_audio_buffer.commit` with `final: true` | Marks the end of input |
|
||||
| Client to server | `input_audio_buffer.commit` | Starts generation over the incoming stream |
|
||||
| Server to client | `transcription.delta` | Carries incremental response text |
|
||||
| Server to client | `transcription.done` | Carries final text and token usage |
|
||||
| Server to client | `response.output_audio.delta` | Carries incremental PCM16 response audio |
|
||||
@@ -50,15 +47,15 @@ event is base64-encoded raw PCM16.
|
||||
A minimal client sends events in this order:
|
||||
|
||||
```json
|
||||
{"type":"session.update","model":"Qwen/Qwen3-Omni-30B-A3B-Instruct"}
|
||||
{"type":"input_audio_buffer.commit","final":false}
|
||||
{"type":"input_audio_buffer.append","audio":"<base64-pcm16>"}
|
||||
{"type":"input_audio_buffer.commit","final":true}
|
||||
{"type":"input_audio_buffer.append","audio":"<base64-pcm16>"},
|
||||
{"type":"input_audio_buffer.commit"}
|
||||
```
|
||||
|
||||
The initial non-final commit intentionally starts generation before all audio
|
||||
has arrived. Continue sending `input_audio_buffer.append` events while the
|
||||
engine is consuming the stream.
|
||||
This OpenAI-compatible streaming path is distinct from the vLLM-Omni-proprietary
|
||||
lane and as such is reached via different configuration. The vLLM-Omni-proprietary
|
||||
path must be specified explicitly via "duplex=1|true|on" as the WS query parameter
|
||||
or via an explicit configuration in the `deploy_config`. The OpenAI path simply replaces
|
||||
the default `/v1/realtime` handler in all supported models.
|
||||
|
||||
## Audio Handling
|
||||
|
||||
@@ -67,8 +64,6 @@ engine is consuming the stream.
|
||||
- Read `sample_rate_hz` from each audio event instead of assuming an output
|
||||
rate. Qwen3-Omni output is typically 24 kHz.
|
||||
- Concatenate audio deltas in receive order to construct the output waveform.
|
||||
- Set `input_audio_buffer.commit.final` to `true` after the last input chunk so
|
||||
the server can terminate the streaming request.
|
||||
|
||||
## Availability and Limitations
|
||||
|
||||
@@ -76,7 +71,8 @@ The path is registered on the unified API server, but it is usable only when
|
||||
the loaded pipeline implements realtime audio input and produces compatible
|
||||
audio output. Unsupported deployments return an `error` event. The endpoint
|
||||
does not provide duplex session resume, playback acknowledgement, overlap
|
||||
policy, or barge-in controls.
|
||||
policy, or barge-in controls internally. Those must be driven by the client application
|
||||
via the relevant WS events (e.g. `response.cancel`, `conversation.item.truncate`)
|
||||
|
||||
See the [Qwen3-Omni online serving example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/qwen3_omni)
|
||||
for concurrency options, chunk pacing, and per-delta audio debugging.
|
||||
@@ -1029,3 +1029,64 @@ vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
|
||||
--enforce-eager \
|
||||
--uvicorn-log-level debug
|
||||
```
|
||||
|
||||
### Reusing reference audio
|
||||
|
||||
The voice upload API and cache settings below are shared serving features.
|
||||
For repeated voices, upload the reference once and reuse its name. The example
|
||||
uses MOSS-TTS Local: uploading stores the audio, and the first speech request
|
||||
encodes it. MOSS-TTS Local can then reuse cached reference codes without resolving
|
||||
the waveform while the speaker cache entry remains resident. After a server
|
||||
restart or speaker-cache eviction, another speech request warms the codes again.
|
||||
Reference processing and cache-hit behavior for other models depend on their
|
||||
serving adapters.
|
||||
|
||||
```bash
|
||||
curl --fail http://localhost:8123/v1/audio/voices \
|
||||
-F 'name=my-speaker' \
|
||||
-F 'consent=user_consent_id' \
|
||||
-F 'audio_sample=@/path/to/reference.wav'
|
||||
|
||||
# Run once to warm the reference codes, then reuse the same voice.
|
||||
curl --fail http://localhost:8123/v1/audio/speech \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"model":"OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5","voice":"my-speaker","input":"Hello, world.","response_format":"wav"}' \
|
||||
--output speech.wav
|
||||
```
|
||||
|
||||
If you have the reference transcript, upload it with `-F 'ref_text=...'`.
|
||||
MOSS-TTS Local uses a reference transcript for continuation conditioning; omitting
|
||||
it uses reference-audio conditioning instead. Keep this choice consistent when
|
||||
comparing benchmarks. Supplying inline `ref_audio` overrides the uploaded voice
|
||||
and does not use the named-voice cache shortcut.
|
||||
|
||||
The separate decoded-waveform resolve cache defaults to **4 GiB and 2048 entries
|
||||
per speech server instance**. These are LRU limits, not preallocated memory or a
|
||||
limit on total server RSS. Waveforms retain their source sampling rate and are
|
||||
stored as owned, contiguous float32 arrays in **CPU memory**. The byte limit
|
||||
counts numeric buffers (four bytes per mono sample), not the encoded file size
|
||||
or GPU memory.
|
||||
Either limit can cause eviction. Configure the API-process caches in the deploy
|
||||
YAML passed to `--deploy-config` (alongside `stages`, not inside a stage):
|
||||
|
||||
```yaml
|
||||
speech_cache:
|
||||
resolve_max_bytes: 4294967296
|
||||
resolve_max_entries: 2048
|
||||
speaker_max_bytes: 536870912
|
||||
```
|
||||
|
||||
All values must be non-negative integers. Either resolve limit set to `0`
|
||||
disables waveform storage; `speaker_max_bytes: 0` disables speaker artifact
|
||||
storage. Invalid values fail configuration loading. `base_config` inheritance
|
||||
merges this section by field, preserving unspecified values from the base.
|
||||
Restart the service to apply changes. The effective settings are logged at startup.
|
||||
|
||||
The speaker budget defaults to **512 MiB** and counts cached tensor bytes,
|
||||
not Python metadata or total process RSS. It configures the API process's
|
||||
speaker cache. MOSS reference codes are cached as **CPU int32 tensors**; this
|
||||
budget does not reserve GPU memory or include the reference encoder's GPU
|
||||
weights and working memory. The shared cache itself does not move tensors
|
||||
between devices; other adapters determine the device of their cached artifacts.
|
||||
Separate model-worker caches are not controlled by this section. The process-wide singleton rejects a
|
||||
conflicting explicit budget instead of silently ignoring it.
|
||||
@@ -335,6 +335,13 @@ window and output frame count stay unchanged. Wan S2V keeps its existing
|
||||
per-clip behavior by default. Zero, negative, fractional, boolean, string, and
|
||||
null values are rejected when pre-encoding is enabled.
|
||||
|
||||
The transfer ring applies backpressure before another D2H copy when both of
|
||||
its two slots are occupied or the combined pending uint8 payload would exceed
|
||||
256 MiB. This byte bound keeps large resolutions and uneven final chunks from
|
||||
turning a fixed item count into unexpectedly large memory growth. A native VAE
|
||||
chunk larger than 256 MiB is admitted only when the ring is otherwise empty,
|
||||
so supported chunk shapes cannot deadlock.
|
||||
|
||||
`preencode_mp4` applies to the complete-MP4 response paths only. The
|
||||
`/v1/realtime/video` WebSocket endpoint rejects it, because that path already
|
||||
overlaps encoding through its own incremental fragmented-MP4 encoder. Wan also
|
||||
|
||||
@@ -70,3 +70,42 @@ as safety checks or audio/video packaging.
|
||||
|
||||
See [RFC #6541](https://github.com/vllm-project/vllm-omni/issues/6541) for the
|
||||
contract, lifecycle, batching rules, and migration plan.
|
||||
|
||||
## Experimental registered-SHM video transport
|
||||
|
||||
`video_output_transport.enable_registered_shm` is a separate, default-off option
|
||||
for CUDA request-mode workers. It copies eligible video tensors directly into
|
||||
CUDA-registered shared memory and returns borrowed CPU views, removing the
|
||||
intermediate pinned-to-SHM and SHM-to-consumer host copies. The D2H transfer still
|
||||
occurs.
|
||||
|
||||
`video_output_transport.enable_borrowed_frames` independently enables borrowed
|
||||
RGB AVFrames in the Videos API MP4 response encoder, removing the third host
|
||||
copy into PyAV frame buffers. It also defaults to false. Eligible uint8 RGB
|
||||
frames are wrapped with `VideoFrame.from_numpy_buffer`; other dtypes/layouts use
|
||||
the existing encoder path. Color conversion, compression and muxing still occur.
|
||||
|
||||
Enable it through the same CLI, Python or deploy-stage configuration:
|
||||
|
||||
```bash
|
||||
--video-output-transport '{"enable_registered_shm": true, "enable_borrowed_frames": true}'
|
||||
```
|
||||
|
||||
Registered SHM applies only to prepared typed video media (currently WAN2.2) and
|
||||
explicitly marked legacy video outputs (currently MiniMax-H3). H3 marks the video
|
||||
entry of its `(video, audio)` output; audio, trajectory data, images and unmarked
|
||||
legacy outputs keep their existing transport. Device-side postprocessing is
|
||||
independent and does not need to be enabled for registered SHM.
|
||||
Pre-encoded MP4 outputs bypass raw-frame transport and API-side encoding.
|
||||
|
||||
The existing SHM routing threshold is unchanged: the tensor or its backing
|
||||
storage must exceed 1,000,000 bytes. This is not a measured performance crossover.
|
||||
SHM is allocated and registered on every transfer, without pooling; small outputs
|
||||
can regress and registration latency can vary. Benchmark representative output
|
||||
sizes before opting in. CPU outputs, non-CUDA backends, and synchronous/step-mode
|
||||
transfers keep their existing path.
|
||||
|
||||
Borrowed views keep the unlinked mapping alive until their last user releases
|
||||
it. CUDA registration/copy failures fail the request; if CUDA cleanup cannot be
|
||||
confirmed, the worker retains the mapping for safety and must be restarted before
|
||||
further registered-SHM transfers.
|
||||
@@ -163,7 +163,9 @@ The following tables show which models support each feature:
|
||||
| **Qwen-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||
| **Qwen-Image-Edit-2509** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ (decode) | ✅ | ❌ | ❌ |
|
||||
| **Qwen-Image-Layered** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||
| **SenseNova-U1 / U1.5** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **SenseNova-U1** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| **SenseNova-U1.5** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ |
|
||||
| **SenseNova-U1-A3B** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| **Stable-Diffusion-XL** | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||
| **Stable-Diffusion3.5** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ (decode) | ❌ | ❌ |
|
||||
| **Z-Image** | ✅ | ✅ | ✅ | ❓ | ✅ (TP=2 only) | ❌ | ✅ | ❌ | ✅ (decode) | ✅ | ❌ |
|
||||
@@ -179,6 +181,7 @@ The following tables show which models support each feature:
|
||||
> 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).
|
||||
> 7. MammothModa2 runs its DiT stage on the diffusion runner (`StageExecutionType.DIFFUSION`); Cache-DiT is enabled through the standard diffusion-stage knobs on the stage entry of the deploy YAML (`cache_backend: cache_dit`, plus optional `cache_config` / `enable_cache_dit_summary`). The runner installs the backend at startup and the pipeline adopts it per request. Only the repeated main-layer stack is cached; requests with `text_guidance_scale = 1.0` bypass the cache hooks.
|
||||
> 8. SenseNova-U1-A3B: `--cfg-parallel-size 2` and TP=2 both complete. CFG-P covers t2i 2-branch (`cond`+`uncond`), img2img 2-branch (`cond`+`img_cond` when `img_cfg_scale=1`), and img2img 3-branch (`cond`+`img_cond`+`uncond` when `img_cfg_scale` is neither 1 nor equal to `cfg_scale`; 3 branches on 2 ranks dispatch as `[[0, 2], [1]]`). Cache-DiT does not: the adapter is declared on `SenseNovaU1Model`, but discovery walks `_dit_modules` (`language_model` / `SenseNovaU1ForCausalLM`) and raises. Layerwise CPU offload is a no-op for the same reason — that class has no `_layerwise_offload_blocks_attrs`, so the 72.2 GiB expert weights stay resident.
|
||||
|
||||
### VideoGen
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ For the full list of supported architectures across all modalities, see
|
||||
|
||||
| Model | HuggingFace repo | Voice cloning | Streaming | Voice presets / upload | Gradio demo |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| AuK / AuK-Flash | assembled local bundle | ✓ (`ref_audio`) | HTTP audio after full generation | — | — |
|
||||
| CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | — |
|
||||
| Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ |
|
||||
| Gepard-1.0 | `nineninesix/gepard-1.0` | — (zero-shot default voice) | ✓ (PCM / WAV stream) | `default` only | — |
|
||||
@@ -99,6 +100,41 @@ For full request-shape documentation (all parameters, response formats, error co
|
||||
|
||||
---
|
||||
|
||||
## AuK / AuK-Flash
|
||||
|
||||
AuK serves speech generation and reference-voice synthesis at 24 kHz. Assemble a local bundle before serving:
|
||||
|
||||
```bash
|
||||
python tools/prepare_auk_checkpoint.py \
|
||||
--auk-dir ckpts/AuK --qwen-dir ckpts/Qwen2.5-Omni-3B --out ckpts/auk-omni
|
||||
MODEL=ckpts/auk-omni bash examples/online_serving/text_to_speech/auk/run_server.sh
|
||||
```
|
||||
|
||||
AuK accepts the complete instruction after you fill in the [local recipe template](../../../../recipes/Tencent/AuK-H100.md#supported-model-contract) or the [official AuK cookbook](https://github.com/Tencent-Hunyuan/AuK/blob/main/docs/COOKBOOK.md). Pass it in `instructions` and leave `input` empty. For instruct TTS, fill in `Generate speech based on the following description: "{voice description}". The content to speak is: "{text}".`; for zero-shot TTS, editing, enhancement and separation, use the corresponding complete instruction from the cookbook. Set `duration_seconds` for text-only generation; with `ref_audio`, omitting it keeps the source length.
|
||||
|
||||
For compatibility with clients that can only send `input`, AuK treats an input-only request as the complete instruction and logs a recommendation to move it to `instructions` with `input=""`.
|
||||
|
||||
`extra_params` accepts `num_inference_steps`, `guidance_scale`, `sway`, `t_grid`, and `vae_sample`; `seed` is a top-level field.
|
||||
|
||||
```bash
|
||||
python examples/online_serving/text_to_speech/auk/speech_client.py \
|
||||
--model ckpts/auk-omni \
|
||||
--instructions 'Generate speech based on the following description: "A calm young woman speaking warmly and slowly.". The content to speak is: "Welcome back, how was your day?".' \
|
||||
--duration-seconds 3 --seed 7
|
||||
```
|
||||
|
||||
Complete-instruction editing also does not need a separate text argument:
|
||||
|
||||
```bash
|
||||
python examples/online_serving/text_to_speech/auk/speech_client.py \
|
||||
--model ckpts/auk-omni --instructions "Keep pure speech voice, remove noise and reverberation." \
|
||||
--ref-audio noisy.wav --seed 7 --output clean.wav
|
||||
```
|
||||
|
||||
The client accepts local reference files, URLs, and data URLs. `--stream` writes signed 16-bit 24 kHz PCM after the complete waveform is ready. See `speech_client.py --help` for editing and sampling options.
|
||||
|
||||
---
|
||||
|
||||
## CosyVoice3
|
||||
|
||||
2-stage TTS (`talker` + flow-matching `code2wav`) at 24 kHz. Voice cloning only — every request needs `ref_audio` + `ref_text`; there are no built-in voice presets.
|
||||
|
||||
@@ -106,6 +106,7 @@ warmup_quack_fp8([(14040, 2048, 6144), (14040, 2048, 2048)])
|
||||
| HunyuanVideo-1.5 | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `720p_t2v`, `480p_i2v` | Yes | Yes | All layers | None | |
|
||||
| Cosmos3 | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | Yes | Not validated | All layers | None | |
|
||||
| MiniMax-H3 | `MiniMaxAI/MiniMax-H3` (`FL2VA` / `Ref2VA`) | Yes | Not validated | `quantization="fp8"` quantizes eligible DiT and text-encoder linears; mixed-precision input/output heads stay FP32 | None | ✅︎ |
|
||||
| SenseNova-U1.5 | `sensenova/SenseNova-U1.5-8B-MoT` | Yes | Not validated | UND/GEN language-model linears only | None | |
|
||||
|
||||
### Multi-Stage Omni/TTS Model (Qwen3-Omni, Qwen3-TTS)
|
||||
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
"""Offline Qwen3-TTS example with word-level timestamps.
|
||||
|
||||
This demo runs Qwen3-TTS offline, aligns the synthesized audio in memory with
|
||||
the same shared forced aligner used by the streaming server path, then writes
|
||||
the WAV and JSON sidecar:
|
||||
|
||||
python examples/offline_inference/text_to_speech/qwen3_tts/word_timestamps.py \
|
||||
--model Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
|
||||
--forced-aligner Qwen/Qwen3-ForcedAligner-0.6B
|
||||
|
||||
On machines without a local CUDA toolkit, set ``VLLM_USE_FLASHINFER_SAMPLER=0``
|
||||
to avoid FlashInfer sampler JIT during warmup.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
||||
# The in-process offline aligner consumes token_classify outputs. Keeping V1
|
||||
# multiprocessing off avoids msgspec serialization issues for token-wise logits.
|
||||
os.environ.setdefault("VLLM_ENABLE_V1_MULTIPROCESSING", "0")
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
import torch
|
||||
from end2end import _estimate_prompt_len
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.utils.forced_aligner import align, build_forced_aligner_config
|
||||
|
||||
|
||||
def _default_deploy_config() -> str:
|
||||
repo_root = Path(__file__).resolve().parents[4]
|
||||
return str(repo_root / "vllm_omni" / "deploy" / "qwen3_tts.yaml")
|
||||
|
||||
|
||||
def _build_custom_voice_input(args: Any) -> dict[str, Any]:
|
||||
additional_information = {
|
||||
"task_type": ["CustomVoice"],
|
||||
"text": [args.text],
|
||||
"language": [args.language],
|
||||
"speaker": [args.speaker],
|
||||
"instruct": [args.instructions],
|
||||
"max_new_tokens": [args.max_new_tokens],
|
||||
}
|
||||
return {
|
||||
"prompt_token_ids": [0] * _estimate_prompt_len(additional_information, args.model),
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
|
||||
|
||||
def _audio_tensor_and_sample_rate(mm: dict[str, Any]) -> tuple[torch.Tensor, int]:
|
||||
audio_data = mm["audio"]
|
||||
sr_raw = mm["sr"]
|
||||
sr_val = sr_raw[-1] if isinstance(sr_raw, list) and sr_raw else sr_raw
|
||||
sample_rate = int(sr_val.item()) if hasattr(sr_val, "item") else int(sr_val)
|
||||
audio_tensor = torch.cat(audio_data, dim=-1) if isinstance(audio_data, list) else audio_data
|
||||
return audio_tensor.float().detach().cpu().flatten(), sample_rate
|
||||
|
||||
|
||||
def _float_audio_to_pcm16_bytes(audio: torch.Tensor) -> bytes:
|
||||
samples = audio.numpy()
|
||||
pcm = (np.clip(samples, -1.0, 1.0) * 32767.0).astype("<i2")
|
||||
return pcm.tobytes()
|
||||
|
||||
|
||||
async def _run_alignment(args: Any, audio: torch.Tensor, sample_rate: int) -> list[dict[str, Any]] | None:
|
||||
aligner_config = build_forced_aligner_config(args)
|
||||
if aligner_config is None:
|
||||
raise ValueError("--forced-aligner or --forced-aligner-config is required")
|
||||
timestamps = await align(
|
||||
audio=_float_audio_to_pcm16_bytes(audio),
|
||||
text=args.text,
|
||||
sample_rate=sample_rate,
|
||||
config=aligner_config,
|
||||
language=args.language,
|
||||
)
|
||||
if timestamps is None:
|
||||
return None
|
||||
return [
|
||||
{
|
||||
"word": item.word,
|
||||
"start_ms": item.start_ms,
|
||||
"end_ms": item.end_ms,
|
||||
}
|
||||
for item in timestamps
|
||||
]
|
||||
|
||||
|
||||
def main(args: Any) -> None:
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
omni_kwargs = vars(args).copy()
|
||||
for key in (
|
||||
"forced_aligner",
|
||||
"text",
|
||||
"language",
|
||||
"speaker",
|
||||
"instructions",
|
||||
"max_new_tokens",
|
||||
"output_dir",
|
||||
):
|
||||
omni_kwargs.pop(key, None)
|
||||
omni_kwargs["deploy_config"] = args.deploy_config or _default_deploy_config()
|
||||
omni_kwargs["log_stats"] = args.log_stats
|
||||
omni = Omni(**omni_kwargs)
|
||||
|
||||
prompt = _build_custom_voice_input(args)
|
||||
final_output = None
|
||||
for stage_outputs in omni.generate([prompt]):
|
||||
final_output = stage_outputs
|
||||
if final_output is None:
|
||||
raise RuntimeError("Qwen3-TTS did not produce an output.")
|
||||
|
||||
mm = final_output.outputs[0].multimodal_output
|
||||
audio, sample_rate = _audio_tensor_and_sample_rate(mm)
|
||||
|
||||
wav_path = output_dir / "qwen3_tts_word_timestamps.wav"
|
||||
# Align the in-memory tensor, not the saved WAV. The WAV is only a demo
|
||||
# artifact so users can listen to the same audio referenced by the sidecar.
|
||||
timestamps = asyncio.run(_run_alignment(args, audio, sample_rate))
|
||||
sf.write(wav_path, audio.numpy(), samplerate=sample_rate, format="WAV")
|
||||
|
||||
sidecar = {
|
||||
"text": args.text,
|
||||
"sample_rate": sample_rate,
|
||||
"audio_path": str(wav_path),
|
||||
"timestamps": timestamps,
|
||||
}
|
||||
json_path = output_dir / "qwen3_tts_word_timestamps.json"
|
||||
json_path.write_text(json.dumps(sidecar, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
print(f"Saved audio: {wav_path}")
|
||||
print(f"Saved timestamps: {json_path}")
|
||||
print(json.dumps(sidecar["timestamps"], ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def parse_args() -> Any:
|
||||
parser = FlexibleArgumentParser(description="Offline Qwen3-TTS word timestamp example")
|
||||
parser.add_argument("--model", default="Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice", help="Qwen3-TTS model path/name")
|
||||
parser.add_argument(
|
||||
"--forced-aligner",
|
||||
default=None,
|
||||
help="Qwen3 forced aligner model path/name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--forced-aligner-config",
|
||||
default=None,
|
||||
help="Optional YAML file for forced aligner settings (incl. gpu_memory_utilization)",
|
||||
)
|
||||
parser.add_argument("--deploy-config", default=None, help="Qwen3-TTS deploy YAML")
|
||||
parser.add_argument("--text", default="Hello world.", help="Text to synthesize and align")
|
||||
parser.add_argument("--language", default="English", help="Qwen3-TTS language field")
|
||||
parser.add_argument("--speaker", default="Vivian", help="CustomVoice speaker name")
|
||||
parser.add_argument("--instructions", default="", help="Optional speaking style instruction")
|
||||
parser.add_argument("--max-new-tokens", type=int, default=2048, help="TTS max_new_tokens")
|
||||
parser.add_argument("--output-dir", default="output_audio", help="Directory for WAV and JSON sidecar")
|
||||
parser.add_argument("--log-stats", action="store_true", default=False, help="Enable vLLM-Omni stats logging")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(parse_args())
|
||||
@@ -0,0 +1,177 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""YuE2-3B offline end-to-end text-to-music.
|
||||
|
||||
One song is one or two engine requests against the single-stage pipeline:
|
||||
``cot=full|melody`` first asks the model to write an ABC score (the abc phase,
|
||||
tokens only), then the semantic phase generates codec tokens and the model
|
||||
solves the acoustic ODE + decodes 48 kHz stereo as the request finishes.
|
||||
``cot=off`` and a user-supplied ``--abc-file`` skip the first request.
|
||||
Prefix caching lets the second request reuse the first one's blocks.
|
||||
|
||||
Example:
|
||||
python end2end.py --model /models/YuE2-3B --vae /models/YuE2-Vae \
|
||||
--style "gentle lo-fi hip-hop, 85 BPM, F minor" \
|
||||
--lyrics "[Verse]\nHumming on a quiet street\n[Chorus]\nStay a while" \
|
||||
--cot full --seed 831001 --max-frames 200 --output song.wav
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
from pathlib import Path
|
||||
|
||||
import soundfile as sf
|
||||
import torch
|
||||
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.model_executor.models.yue2.yue2 import (
|
||||
ABC_SAMPLING,
|
||||
KEY_MAX_AUDIO_FRAMES,
|
||||
KEY_MIN_TOKENS,
|
||||
KEY_PENALTY_WINDOW,
|
||||
KEY_PHASE,
|
||||
KEY_PREFIX_IDS,
|
||||
KEY_REPETITION_PENALTY,
|
||||
KEY_SEED,
|
||||
KEY_SKIP_SYNTHESIS,
|
||||
KEY_TEMPERATURE,
|
||||
KEY_TOP_K,
|
||||
KEY_TOP_P,
|
||||
SEMANTIC_SAMPLING,
|
||||
STOP_TOKEN_IDS,
|
||||
)
|
||||
from vllm_omni.tokenizers.yue2_prompt import (
|
||||
abc_ids_from_generated,
|
||||
abc_prefix_ids,
|
||||
semantic_prefix_ids,
|
||||
)
|
||||
from vllm_omni.tokenizers.yue2_tokenizer import YuE2TextTokenizer
|
||||
|
||||
|
||||
def sampling_params(engine, *, phase, seed, max_frames, prompt_ids):
|
||||
"""Deep-copy the stage defaults, then set the yue2_* keys for one phase."""
|
||||
params = copy.deepcopy(engine.resolve_sampling_params_list(None))[0]
|
||||
preset = ABC_SAMPLING if phase == "abc" else SEMANTIC_SAMPLING
|
||||
params.extra_args = {
|
||||
**(params.extra_args or {}),
|
||||
KEY_PHASE: phase,
|
||||
KEY_SEED: seed,
|
||||
KEY_TEMPERATURE: preset["temperature"],
|
||||
KEY_TOP_P: preset["top_p"],
|
||||
KEY_TOP_K: preset["top_k"],
|
||||
KEY_REPETITION_PENALTY: preset["repetition_penalty"],
|
||||
KEY_PENALTY_WINDOW: preset["penalty_window"],
|
||||
KEY_MIN_TOKENS: preset["min_tokens"],
|
||||
KEY_MAX_AUDIO_FRAMES: max_frames,
|
||||
KEY_SKIP_SYNTHESIS: phase == "abc",
|
||||
# Full prompt ids: under a KV prefix-cache hit the engine schedules
|
||||
# only the uncached tail, so the model cannot rebuild its NAR
|
||||
# conditioning prefix from the scheduled tokens alone.
|
||||
KEY_PREFIX_IDS: list(prompt_ids),
|
||||
}
|
||||
params.max_tokens = preset["max_tokens"] if phase == "abc" else max_frames + 1
|
||||
params.stop_token_ids = list(STOP_TOKEN_IDS)
|
||||
params.detokenize = False
|
||||
return params
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--model", required=True, help="m-a-p/YuE2-3B checkpoint dir or repo id")
|
||||
parser.add_argument("--vae", default=None, help="m-a-p/YuE2-Vae dir or repo id (or $YUE2_VAE)")
|
||||
parser.add_argument("--style", required=True)
|
||||
parser.add_argument("--lyrics", required=True)
|
||||
parser.add_argument("--cot", choices=["off", "melody", "full"], default="full")
|
||||
parser.add_argument("--seed", type=int, default=831001)
|
||||
parser.add_argument("--max-frames", type=int, default=200, help="semantic frame budget (25 frames = 1 s)")
|
||||
parser.add_argument("--abc-file", default=None, help="external ABC score (requires cot=melody/full)")
|
||||
parser.add_argument("--output", default="yue2_song.wav")
|
||||
parser.add_argument(
|
||||
"--dump-tokens",
|
||||
default=None,
|
||||
metavar="PATH",
|
||||
help="write prompt + generated token ids to this json (default: <output>.tokens.json)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gpu-memory-utilization",
|
||||
type=float,
|
||||
default=None,
|
||||
help="override the deploy yaml's per-stage gpu_memory_utilization "
|
||||
"(necessary on cards shared with other tenants: the yaml fraction is "
|
||||
"of the WHOLE card, not of the free remainder)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.vae:
|
||||
import os
|
||||
|
||||
os.environ["YUE2_VAE"] = args.vae
|
||||
|
||||
tokenizer = YuE2TextTokenizer(Path(args.model) / "qwen.tiktoken")
|
||||
engine_kwargs: dict = {}
|
||||
if args.gpu_memory_utilization is not None:
|
||||
engine_kwargs["gpu_memory_utilization"] = args.gpu_memory_utilization
|
||||
engine = Omni(model=args.model, **engine_kwargs)
|
||||
|
||||
abc_ids = None
|
||||
if args.abc_file is not None:
|
||||
if args.cot == "off":
|
||||
raise SystemExit("--abc-file requires --cot melody|full")
|
||||
abc_ids = tokenizer.encode(Path(args.abc_file).read_text())
|
||||
print(f"Using supplied ABC score ({len(abc_ids)} tokens)")
|
||||
elif args.cot != "off":
|
||||
prompt_ids = abc_prefix_ids(tokenizer.encode, args.style, args.lyrics, args.cot)
|
||||
prompt = {"prompt_token_ids": prompt_ids}
|
||||
params = sampling_params(engine, phase="abc", seed=args.seed, max_frames=args.max_frames, prompt_ids=prompt_ids)
|
||||
outputs = engine.generate([prompt], [params])
|
||||
generated = list(outputs[0].outputs[0].token_ids)
|
||||
abc_ids = abc_ids_from_generated(generated)
|
||||
print(f"Generated ABC score ({len(abc_ids)} tokens):\n{tokenizer.decode(abc_ids)}\n")
|
||||
|
||||
prompt_ids = semantic_prefix_ids(tokenizer.encode, args.style, args.lyrics, args.cot, abc_ids=abc_ids)
|
||||
prompt = {"prompt_token_ids": prompt_ids}
|
||||
params = sampling_params(
|
||||
engine, phase="semantic", seed=args.seed, max_frames=args.max_frames, prompt_ids=prompt_ids
|
||||
)
|
||||
outputs = engine.generate([prompt], [params])
|
||||
output = outputs[0].outputs[0]
|
||||
generated_ids = list(output.token_ids)
|
||||
if args.dump_tokens:
|
||||
import json
|
||||
|
||||
payload = {
|
||||
"prompt_token_ids": prompt_ids,
|
||||
"generated_token_ids": generated_ids,
|
||||
"seed": args.seed,
|
||||
"cot": args.cot,
|
||||
"max_frames": args.max_frames,
|
||||
}
|
||||
dump_path = args.dump_tokens if args.dump_tokens.endswith(".json") else args.output + ".tokens.json"
|
||||
Path(dump_path).write_text(json.dumps(payload))
|
||||
print(f"Dumped {len(generated_ids)} generated token ids to {dump_path}")
|
||||
mm = output.multimodal_output or {}
|
||||
audio = mm.get("audio")
|
||||
if audio is None and "model_outputs" in mm:
|
||||
audio = mm["model_outputs"]
|
||||
if audio is None:
|
||||
raise SystemExit("No audio in the model output; check server logs")
|
||||
sr = int(mm.get("sr").item()) if mm.get("sr") is not None else 48000
|
||||
truncated = False
|
||||
meta = mm.get("meta") or {}
|
||||
if "truncated" in meta:
|
||||
truncated = bool(torch.as_tensor(meta["truncated"]).reshape(-1)[-1].item())
|
||||
|
||||
waveform = audio.float()
|
||||
# soundfile wants [frames, channels] float in [-1, 1]; torchaudio has no
|
||||
# wheel matching vllm 0.29.0's torch pin, so the driver uses libsndfile.
|
||||
sf.write(args.output, waveform.T.contiguous(), sr)
|
||||
print(
|
||||
f"Saved {args.output}: {waveform.shape[-1] / sr:.1f}s @ {sr} Hz stereo, "
|
||||
f"truncated={truncated}, generated {len(output.token_ids)} semantic tokens"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
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
|
||||
through the unified duplex serving stack (`/v1/duplex` and `/v1/realtime?duplex=1`).
|
||||
through the unified full-duplex framework (`/v1/realtime?duplex=1`, alias `/v1/duplex`).
|
||||
|
||||
> Requires a GPU and Hugging Face access to the gated repo
|
||||
> (`HF_TOKEN` with access to `nvidia/personaplex-7b-v1`).
|
||||
|
||||
## Start the server
|
||||
|
||||
The default `vllm_omni/deploy/personaplex.yaml` enables the engine-owned
|
||||
full-duplex control plane (`session_mode: duplex`):
|
||||
The default `vllm_omni/deploy/personaplex.yaml` is a duplex deployment
|
||||
(`session_mode: duplex`, two sessions per replica):
|
||||
|
||||
```bash
|
||||
HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 python -m vllm_omni.entrypoints.cli.main serve \
|
||||
@@ -19,18 +19,32 @@ HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 python -m vllm_omni.entrypoints.cli.main ser
|
||||
--deploy-config vllm_omni/deploy/personaplex.yaml
|
||||
```
|
||||
|
||||
This exposes:
|
||||
This exposes `WS /v1/realtime?duplex=1` (alias `WS /v1/duplex`): the OpenAI
|
||||
Realtime session protocol projected onto vLLM-Omni duplex sessions (client API and
|
||||
wire protocol: [`docs/serving/realtime_duplex_api.md`](../../../docs/serving/realtime_duplex_api.md)).
|
||||
There is no `/v1/chat/completions` route: PersonaPlex answers speech only.
|
||||
|
||||
- `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 is native duplex, audio flows
|
||||
continuously in both directions in 80 ms frames, the model decides when to speak,
|
||||
and there are no client commits or external turn signals
|
||||
(`supports_client_commit=false`, `supports_external_turn_signal=false`). A session
|
||||
therefore auto-responds without any vendor flag.
|
||||
|
||||
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`).
|
||||
## Talk to it
|
||||
|
||||
With the client library and the PersonaPlex preset (24 kHz `pcm_f32le` in, bundled
|
||||
voice prompt, persona text):
|
||||
|
||||
```python
|
||||
from vllm_omni.clients.duplex import DuplexClient
|
||||
from vllm_omni.clients.personaplex import create_duplex_session_config
|
||||
|
||||
cfg = create_duplex_session_config(voice="NATF2.pt", persona="You are a concise assistant.")
|
||||
async with DuplexClient("ws://127.0.0.1:8000/v1/realtime?duplex=1", model="/path/to/personaplex-7b-v1", config=cfg) as c:
|
||||
await c.stream_pcm(pcm_f32le_24k) # keep streaming; the model speaks while it listens
|
||||
```
|
||||
|
||||
Voice and persona are fixed for the session (`session.update` cannot change them).
|
||||
|
||||
## Validate the serving path
|
||||
|
||||
@@ -45,9 +59,11 @@ python tests/e2e/online_serving/personaplex_realtime_duplex.py \
|
||||
--output-dir /tmp/personaplex-realtime-duplex
|
||||
```
|
||||
|
||||
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.
|
||||
The endpoint advertises `supports_barge_in=false`: overlapping speech is native
|
||||
model behaviour, but destructive output interruption and model-state rewind have
|
||||
not been validated for PersonaPlex. `response.cancel` and
|
||||
`output_audio_buffer.clear` restart the model's conversation context (a fresh
|
||||
Stage 0 request replays the voice/persona prefill).
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -56,8 +72,8 @@ rewind have not been validated for PersonaPlex.
|
||||
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`.
|
||||
and has been removed; use the unified endpoint above.
|
||||
- The model plugin, worker-side lockstep runtime and input framing live in
|
||||
`vllm_omni/model_executor/models/personaplex/duplex/`; design notes in
|
||||
[`docs/design/fullduplex-personaplex.md`](../../../docs/design/fullduplex-personaplex.md).
|
||||
- Full runbook: [`recipes/NVIDIA/PersonaPlex.md`](../../../recipes/NVIDIA/PersonaPlex.md).
|
||||
@@ -0,0 +1,6 @@
|
||||
agent-starter-react/
|
||||
.git/
|
||||
.claude/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.log
|
||||
@@ -0,0 +1,25 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y ca-certificates ffmpeg libsndfile1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install \
|
||||
"livekit-agents[openai,turn-detector,silero]>=1.6.10"
|
||||
|
||||
RUN useradd --create-home --uid 10001 agent
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=agent:agent agent.py ./agent.py
|
||||
|
||||
USER agent
|
||||
|
||||
ENTRYPOINT ["python", "agent.py", "start"]
|
||||
@@ -0,0 +1,36 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
FROM node:24-bookworm-slim AS dependencies
|
||||
|
||||
ARG FRONTEND_REF=main
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y ca-certificates git \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& npm install --global pnpm@9.15.9
|
||||
|
||||
WORKDIR /app
|
||||
RUN git clone --depth 1 --branch "${FRONTEND_REF}" \
|
||||
https://github.com/livekit-examples/agent-starter-react.git . \
|
||||
&& pnpm install --frozen-lockfile \
|
||||
&& rm -rf .git
|
||||
|
||||
FROM node:24-bookworm-slim
|
||||
|
||||
ENV NODE_ENV=development \
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& npm install --global pnpm@9.15.9
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=dependencies --chown=node:node /app /app
|
||||
|
||||
USER node
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["pnpm", "dev", "--hostname", "0.0.0.0"]
|
||||
@@ -0,0 +1,28 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
ARG LIVEKIT_REF=v1.10.1
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk add --no-cache git ca-certificates
|
||||
|
||||
WORKDIR /src
|
||||
RUN git clone --depth 1 --branch "${LIVEKIT_REF}" \
|
||||
https://github.com/livekit/livekit.git . \
|
||||
&& go mod download \
|
||||
&& CGO_ENABLED=0 GOOS=linux GOARCH="${TARGETARCH}" \
|
||||
go build -trimpath -ldflags="-s -w" -o /out/livekit-server ./cmd/server
|
||||
|
||||
FROM alpine:3.22
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
&& addgroup -S livekit \
|
||||
&& adduser -S -G livekit livekit
|
||||
|
||||
COPY --from=builder /out/livekit-server /usr/local/bin/livekit-server
|
||||
|
||||
USER livekit
|
||||
ENTRYPOINT ["/usr/local/bin/livekit-server"]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Qwen3-Omni LiveKit demo
|
||||
|
||||
This directory contains a Docker Compose version of the local LiveKit demo.
|
||||
It builds the LiveKit server, the Next.js frontend, and the Python agent as
|
||||
separate images. The vLLM-Omni server runs outside Compose and is configured
|
||||
with `VLLM_OMNI_HOST`.
|
||||
|
||||
Start vLLM-Omni with port `8000` bound to an address reachable from Docker,
|
||||
for example with `--host 0.0.0.0`, then run:
|
||||
|
||||
```bash
|
||||
cd examples/online_serving/qwen3_omni/fullduplex-livekit-frontend
|
||||
VLLM_OMNI_HOST=host.docker.internal docker compose up --build
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000). The LiveKit server is
|
||||
available at `ws://localhost:7880`; its development credentials are `devkey`
|
||||
and `secret`.
|
||||
|
||||
For a machine other than the local host, set both the vLLM address and the
|
||||
address LiveKit advertises to WebRTC clients:
|
||||
|
||||
```bash
|
||||
VLLM_OMNI_HOST=192.168.1.20 \
|
||||
LIVEKIT_NODE_IP=192.168.1.10 \
|
||||
LIVEKIT_PUBLIC_URL=ws://192.168.1.10:7880 \
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Use `docker compose up --build --abort-on-container-exit` when the Compose
|
||||
process should stop the remaining services after one component exits, matching
|
||||
the behavior of `run-livekit-stack.sh`.
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "livekit-agents[openai,turn-detector,silero]>=1.6.10",
|
||||
# ]
|
||||
# ///
|
||||
import logging
|
||||
import os
|
||||
|
||||
from livekit.agents import (
|
||||
Agent,
|
||||
AgentServer,
|
||||
AgentSession,
|
||||
JobContext,
|
||||
RunContext,
|
||||
TurnHandlingOptions,
|
||||
cli,
|
||||
function_tool,
|
||||
inference,
|
||||
)
|
||||
from livekit.plugins.openai.realtime import RealtimeModel
|
||||
|
||||
logger = logging.getLogger("voice-assistant")
|
||||
|
||||
VLLM_BASE_URL = f"http://{os.environ['VLLM_OMNI_HOST']}:8000/v1"
|
||||
LIVEKIT_URL = os.environ.get("LIVEKIT_URL", "ws://localhost:7880")
|
||||
|
||||
server = AgentServer(
|
||||
ws_url=LIVEKIT_URL,
|
||||
api_key=os.environ.get("LIVEKIT_API_KEY", "devkey"),
|
||||
api_secret=os.environ.get("LIVEKIT_API_SECRET", "secret"),
|
||||
)
|
||||
|
||||
|
||||
class VoiceAssistant(Agent):
|
||||
@function_tool()
|
||||
async def lookup_weather(
|
||||
self,
|
||||
context: RunContext,
|
||||
location: str,
|
||||
) -> dict[str, str | int]:
|
||||
"""Return deterministic sample data for the demo tool."""
|
||||
return {
|
||||
"location": location,
|
||||
"weather": "clear skies",
|
||||
"temperature_f": 72,
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
instructions="You are a helpful voice assistant. Respond naturally and concisely.",
|
||||
)
|
||||
|
||||
|
||||
@server.rtc_session()
|
||||
async def entrypoint(ctx: JobContext):
|
||||
# Qwen3-Omni only supports turn_detection=null (manual mode) -- it does
|
||||
# not perform voice activity detection itself, so turn-taking must be
|
||||
# decided client-side. semantic_vad (MiniCPM-o only) is rejected by
|
||||
# vLLM-Omni's OpenAI-compatible realtime endpoint for Qwen3-Omni.
|
||||
# Omni serves one model, so an empty model lets the server select it.
|
||||
model = RealtimeModel(
|
||||
base_url=VLLM_BASE_URL,
|
||||
model="",
|
||||
api_key="unused",
|
||||
)
|
||||
|
||||
# turn_handling's client-side TurnDetector means the server isn't asked
|
||||
# to decide turns, so turn_detection is left NOT_GIVEN on RealtimeModel.
|
||||
session = AgentSession(
|
||||
llm=model,
|
||||
turn_handling=TurnHandlingOptions(turn_detection=inference.TurnDetector()),
|
||||
)
|
||||
|
||||
await session.start(
|
||||
agent=VoiceAssistant(),
|
||||
room=ctx.room,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Voice assistant started, connected to vLLM-Omni at %s",
|
||||
VLLM_BASE_URL,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli.run_app(server)
|
||||
@@ -0,0 +1,55 @@
|
||||
name: vllm-omni-livekit
|
||||
|
||||
services:
|
||||
livekit:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.livekit
|
||||
args:
|
||||
LIVEKIT_REF: ${LIVEKIT_REF:-v1.10.1}
|
||||
command:
|
||||
- --config
|
||||
- /etc/livekit.yaml
|
||||
- --dev
|
||||
- --node-ip
|
||||
- ${LIVEKIT_NODE_IP:-127.0.0.1}
|
||||
volumes:
|
||||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||||
ports:
|
||||
- "${LIVEKIT_HTTP_PORT:-7880}:7880"
|
||||
- "${LIVEKIT_TCP_PORT:-7881}:7881/tcp"
|
||||
- "${LIVEKIT_UDP_PORT:-7882}:7882/udp"
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.frontend
|
||||
args:
|
||||
FRONTEND_REF: ${FRONTEND_REF:-main}
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
|
||||
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-secret}
|
||||
LIVEKIT_URL: ${LIVEKIT_PUBLIC_URL:-ws://localhost:7880}
|
||||
AGENT_NAME: ${AGENT_NAME:-}
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-3000}:3000"
|
||||
depends_on:
|
||||
- livekit
|
||||
restart: unless-stopped
|
||||
|
||||
agent:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.agent
|
||||
environment:
|
||||
VLLM_OMNI_HOST: ${VLLM_OMNI_HOST:-host.docker.internal}
|
||||
LIVEKIT_URL: ws://livekit:7880
|
||||
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
|
||||
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-secret}
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
depends_on:
|
||||
- livekit
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,10 @@
|
||||
# Local development configuration for the Compose stack.
|
||||
port: 7880
|
||||
bind_addresses:
|
||||
- 0.0.0.0
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
udp_port: 7882
|
||||
use_external_ip: false
|
||||
keys:
|
||||
devkey: secret
|
||||
@@ -1,398 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Realtime client for vLLM-Omni /v1/realtime (audio + text events).
|
||||
|
||||
This client:
|
||||
1) Reads a local WAV file (must be mono, 16-bit PCM, 16kHz),
|
||||
2) Streams PCM16 chunks to /v1/realtime with OpenAI-style events,
|
||||
3) Receives response.output_audio.*, response.output_text.*, and transcript events,
|
||||
4) Saves synthesized audio to an output WAV file and optional text file.
|
||||
|
||||
By default each ``response.output_audio.delta`` is treated as an **incremental PCM**
|
||||
chunk and all chunks are concatenated into the final ``--output-wav``.
|
||||
|
||||
Optional debugging: pass ``--delta-dump-dir DIR`` to write every
|
||||
``response.output_audio.delta`` payload as ``delta_000001.wav``, ``delta_000002.wav``, …
|
||||
|
||||
Usage:
|
||||
python openai_realtime_client.py \
|
||||
--url ws://localhost:8091/v1/realtime \
|
||||
--model Qwen/Qwen3-Omni-30B-A3B-Instruct \
|
||||
--input-wav input_16k_mono.wav \
|
||||
--output-wav realtime_output.wav \
|
||||
--server-vad \
|
||||
--delta-dump-dir ./rt_delta_wavs
|
||||
|
||||
Dependencies:
|
||||
pip install websockets
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import wave
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||
|
||||
try:
|
||||
import websockets
|
||||
except ImportError:
|
||||
print("Please install websockets: pip install websockets")
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def _read_wav_pcm16(path: Path) -> bytes:
|
||||
with wave.open(str(path), "rb") as wf:
|
||||
nchannels = wf.getnchannels()
|
||||
sampwidth = wf.getsampwidth()
|
||||
framerate = wf.getframerate()
|
||||
comptype = wf.getcomptype()
|
||||
nframes = wf.getnframes()
|
||||
|
||||
if nchannels != 1:
|
||||
raise ValueError(f"Input WAV must be mono (got {nchannels} channels).")
|
||||
if sampwidth != 2:
|
||||
raise ValueError(f"Input WAV must be 16-bit PCM (got sample width={sampwidth}).")
|
||||
if framerate != 16000:
|
||||
raise ValueError(f"Input WAV must be 16kHz (got {framerate} Hz).")
|
||||
if comptype != "NONE":
|
||||
raise ValueError(f"Input WAV must be uncompressed PCM (got comptype={comptype}).")
|
||||
if nframes <= 0:
|
||||
raise ValueError("Input WAV has no audio frames.")
|
||||
|
||||
return wf.readframes(nframes)
|
||||
|
||||
|
||||
def _write_wav_pcm16(path: Path, pcm16_bytes: bytes, sample_rate_hz: int) -> None:
|
||||
with wave.open(str(path), "wb") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2)
|
||||
wf.setframerate(sample_rate_hz)
|
||||
wf.writeframes(pcm16_bytes)
|
||||
|
||||
|
||||
def _with_realtime_route(url: str, *, server_vad: bool) -> str:
|
||||
parts = urlsplit(url)
|
||||
query = [(key, value) for key, value in parse_qsl(parts.query, keep_blank_values=True) if key != "duplex"]
|
||||
if not server_vad:
|
||||
query.append(("duplex", "0"))
|
||||
return urlunsplit(parts._replace(query=urlencode(query)))
|
||||
|
||||
|
||||
async def run_client(
|
||||
url: str,
|
||||
model: str,
|
||||
input_wav: Path,
|
||||
output_wav: Path,
|
||||
output_text: Path | None,
|
||||
chunk_ms: int,
|
||||
send_delay_ms: int,
|
||||
delta_dump_dir: Path | None,
|
||||
server_vad: bool,
|
||||
request_idx: int = 1,
|
||||
total_requests: int = 1,
|
||||
) -> None:
|
||||
log_prefix = f"[req {request_idx:02d}/{total_requests:02d}] " if total_requests > 1 else ""
|
||||
pcm16 = _read_wav_pcm16(input_wav)
|
||||
bytes_per_ms = 16000 * 2 // 1000 # mono PCM16 at 16kHz
|
||||
chunk_bytes = max(bytes_per_ms * chunk_ms, 2)
|
||||
|
||||
incremental_pcm_parts: list[bytes] = []
|
||||
output_sample_rate = 24000
|
||||
delta_index = 0
|
||||
text_chunks: list[str] = []
|
||||
final_text: str = ""
|
||||
final_audio_transcript: str = ""
|
||||
|
||||
if delta_dump_dir is not None:
|
||||
delta_dump_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Keep the CLI mode authoritative even if the input URL already contains
|
||||
# an explicit duplex query parameter.
|
||||
url = _with_realtime_route(url, server_vad=server_vad)
|
||||
async with websockets.connect(url, max_size=64 * 1024 * 1024) as ws:
|
||||
# 1) Validate model.
|
||||
if server_vad:
|
||||
session_update = {
|
||||
"type": "session.update",
|
||||
"session": {
|
||||
"model": model,
|
||||
"audio": {
|
||||
"input": {
|
||||
"format": {"type": "audio/pcm", "rate": 16_000},
|
||||
"turn_detection": {
|
||||
"type": "server_vad",
|
||||
"threshold": 0.5,
|
||||
"prefix_padding_ms": 300,
|
||||
"silence_duration_ms": 500,
|
||||
"create_response": True,
|
||||
"interrupt_response": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
else:
|
||||
# Preserve the existing non-duplex wire format.
|
||||
session_update = {"type": "session.update", "model": model}
|
||||
await ws.send(json.dumps(session_update))
|
||||
|
||||
# 2) Preserve the legacy explicit-commit flow unless Server VAD owns endpointing.
|
||||
if not server_vad:
|
||||
await ws.send(json.dumps({"type": "input_audio_buffer.commit", "final": False}))
|
||||
|
||||
# 3) Stream audio chunks.
|
||||
for i in range(0, len(pcm16), chunk_bytes):
|
||||
chunk = pcm16[i : i + chunk_bytes]
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "input_audio_buffer.append",
|
||||
"audio": base64.b64encode(chunk).decode("utf-8"),
|
||||
}
|
||||
)
|
||||
)
|
||||
if send_delay_ms > 0:
|
||||
await asyncio.sleep(send_delay_ms / 1000.0)
|
||||
|
||||
# 4) Finalize the turn explicitly, or append silence for Server VAD endpointing.
|
||||
if server_vad:
|
||||
trailing_silence = bytes(16_000 * 2)
|
||||
for i in range(0, len(trailing_silence), chunk_bytes):
|
||||
await ws.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "input_audio_buffer.append",
|
||||
"audio": base64.b64encode(trailing_silence[i : i + chunk_bytes]).decode("utf-8"),
|
||||
}
|
||||
)
|
||||
)
|
||||
else:
|
||||
await ws.send(json.dumps({"type": "input_audio_buffer.commit", "final": True}))
|
||||
|
||||
# 5) Receive server events until audio done.
|
||||
while True:
|
||||
message = await ws.recv()
|
||||
if isinstance(message, bytes):
|
||||
# We only expect JSON text frames.
|
||||
continue
|
||||
|
||||
event = json.loads(message)
|
||||
event_type = event.get("type")
|
||||
|
||||
if event_type == "session.created":
|
||||
continue
|
||||
|
||||
if event_type == "response.output_audio.delta":
|
||||
sr = event.get("sample_rate_hz")
|
||||
if isinstance(sr, int) and sr > 0:
|
||||
output_sample_rate = sr
|
||||
audio_b64 = event.get("delta" if server_vad else "audio", "")
|
||||
if audio_b64:
|
||||
pcm_delta = base64.b64decode(audio_b64)
|
||||
incremental_pcm_parts.append(pcm_delta)
|
||||
if delta_dump_dir is not None and pcm_delta:
|
||||
delta_index += 1
|
||||
dump_path = delta_dump_dir / f"delta_{delta_index:06d}.wav"
|
||||
_write_wav_pcm16(dump_path, pcm_delta, output_sample_rate)
|
||||
print(
|
||||
f"{log_prefix}delta dump #{delta_index}: {dump_path} "
|
||||
f"(pcm bytes={len(pcm_delta)}, sr={output_sample_rate})"
|
||||
)
|
||||
continue
|
||||
|
||||
if event_type in {"response.output_text.delta", "transcription.delta"}:
|
||||
delta = event.get("delta", "")
|
||||
if delta:
|
||||
text_chunks.append(delta)
|
||||
print(delta, end="", flush=True)
|
||||
continue
|
||||
|
||||
if event_type in {"response.output_text.done", "transcription.done"}:
|
||||
final_text = event.get("text", "") or "".join(text_chunks)
|
||||
usage = event.get("usage")
|
||||
final_text_with_tag = f"Final transcription: {final_text}"
|
||||
if text_chunks:
|
||||
print()
|
||||
print(f"{log_prefix}{final_text_with_tag}")
|
||||
if usage:
|
||||
print(f"{log_prefix}text usage: {usage}")
|
||||
continue
|
||||
|
||||
if event_type == "response.output_audio_transcript.done":
|
||||
final_audio_transcript = event.get("transcript", "")
|
||||
continue
|
||||
|
||||
if event_type == "response.output_audio.done":
|
||||
if not server_vad:
|
||||
break
|
||||
continue
|
||||
|
||||
if event_type == "response.done" and server_vad:
|
||||
await ws.send(json.dumps({"type": "session.close"}))
|
||||
break
|
||||
|
||||
if event_type == "error":
|
||||
raise RuntimeError(f"Server error: {event}")
|
||||
|
||||
all_pcm16 = b"".join(incremental_pcm_parts)
|
||||
if not all_pcm16:
|
||||
raise RuntimeError("No audio received from server.")
|
||||
|
||||
output_wav.parent.mkdir(parents=True, exist_ok=True)
|
||||
_write_wav_pcm16(output_wav, all_pcm16, output_sample_rate)
|
||||
print(f"{log_prefix}Saved realtime audio to: {output_wav} (incremental chunks joined)")
|
||||
|
||||
if output_text is not None:
|
||||
text_to_save = final_text or "".join(text_chunks) or final_audio_transcript
|
||||
output_text.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_text.write_text(text_to_save, encoding="utf-8")
|
||||
print(f"{log_prefix}Saved realtime text to: {output_text}")
|
||||
|
||||
|
||||
def _indexed_output_path(path: Path | None, index: int, total: int) -> Path | None:
|
||||
if path is None or total <= 1:
|
||||
return path
|
||||
return path.with_name(f"{path.stem}_{index:02d}{path.suffix}")
|
||||
|
||||
|
||||
async def run_clients_concurrent(
|
||||
*,
|
||||
url: str,
|
||||
model: str,
|
||||
input_wav: Path,
|
||||
output_wav: Path,
|
||||
output_text: Path | None,
|
||||
chunk_ms: int,
|
||||
send_delay_ms: int,
|
||||
delta_dump_dir: Path | None,
|
||||
server_vad: bool,
|
||||
num_requests: int,
|
||||
concurrency: int,
|
||||
) -> None:
|
||||
sem = asyncio.Semaphore(concurrency)
|
||||
|
||||
async def _run_one(index: int) -> tuple[int, bool, str | None]:
|
||||
per_output_wav = _indexed_output_path(output_wav, index, num_requests)
|
||||
per_output_text = _indexed_output_path(output_text, index, num_requests)
|
||||
per_delta_dir = None
|
||||
if delta_dump_dir is not None:
|
||||
per_delta_dir = delta_dump_dir / f"req_{index:02d}"
|
||||
async with sem:
|
||||
try:
|
||||
await run_client(
|
||||
url=url,
|
||||
model=model,
|
||||
input_wav=input_wav,
|
||||
output_wav=per_output_wav,
|
||||
output_text=per_output_text,
|
||||
chunk_ms=chunk_ms,
|
||||
send_delay_ms=send_delay_ms,
|
||||
delta_dump_dir=per_delta_dir,
|
||||
server_vad=server_vad,
|
||||
request_idx=index,
|
||||
total_requests=num_requests,
|
||||
)
|
||||
return index, True, None
|
||||
except Exception as exc:
|
||||
return index, False, str(exc)
|
||||
|
||||
tasks = [asyncio.create_task(_run_one(i), name=f"rt-client-{i}") for i in range(1, num_requests + 1)]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
failed = [(idx, err) for idx, ok, err in results if not ok]
|
||||
succeeded = num_requests - len(failed)
|
||||
print(f"[summary] succeeded={succeeded}, failed={len(failed)}, total={num_requests}")
|
||||
if failed:
|
||||
for idx, err in failed:
|
||||
print(f"[summary] req {idx:02d} failed: {err}")
|
||||
raise RuntimeError(f"{len(failed)} concurrent request(s) failed")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Realtime audio/text client for vLLM-Omni")
|
||||
parser.add_argument("--url", default="ws://localhost:8091/v1/realtime", help="WebSocket URL")
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="Qwen/Qwen3-Omni-30B-A3B-Instruct",
|
||||
help="Model name for session.update",
|
||||
)
|
||||
parser.add_argument("--input-wav", required=True, type=Path, help="Input WAV (mono, PCM16, 16kHz)")
|
||||
parser.add_argument("--output-wav", default=Path("realtime_output.wav"), type=Path, help="Output WAV path")
|
||||
parser.add_argument(
|
||||
"--output-text",
|
||||
default=None,
|
||||
type=Path,
|
||||
help="Optional output text path for final transcription",
|
||||
)
|
||||
parser.add_argument("--chunk-ms", type=int, default=200, help="Input chunk size in milliseconds")
|
||||
parser.add_argument(
|
||||
"--send-delay-ms",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Delay between chunk sends; set >0 to simulate realtime upload",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delta-dump-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="If set, each response.output_audio.delta is saved as delta_NNNNNN.wav under this directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--server-vad",
|
||||
action="store_true",
|
||||
help="Let the server detect speech endpoints instead of sending input_audio_buffer.commit",
|
||||
)
|
||||
parser.add_argument("--num-requests", type=int, default=1, help="Total number of requests to send")
|
||||
parser.add_argument(
|
||||
"--concurrency",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Maximum number of concurrent websocket requests",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.num_requests <= 0:
|
||||
raise ValueError("--num-requests must be >= 1")
|
||||
if args.concurrency <= 0:
|
||||
raise ValueError("--concurrency must be >= 1")
|
||||
concurrency = min(args.concurrency, args.num_requests)
|
||||
|
||||
if args.num_requests == 1:
|
||||
asyncio.run(
|
||||
run_client(
|
||||
url=args.url,
|
||||
model=args.model,
|
||||
input_wav=args.input_wav,
|
||||
output_wav=args.output_wav,
|
||||
output_text=args.output_text,
|
||||
chunk_ms=args.chunk_ms,
|
||||
send_delay_ms=args.send_delay_ms,
|
||||
delta_dump_dir=args.delta_dump_dir,
|
||||
server_vad=args.server_vad,
|
||||
)
|
||||
)
|
||||
else:
|
||||
asyncio.run(
|
||||
run_clients_concurrent(
|
||||
url=args.url,
|
||||
model=args.model,
|
||||
input_wav=args.input_wav,
|
||||
output_wav=args.output_wav,
|
||||
output_text=args.output_text,
|
||||
chunk_ms=args.chunk_ms,
|
||||
send_delay_ms=args.send_delay_ms,
|
||||
delta_dump_dir=args.delta_dump_dir,
|
||||
server_vad=args.server_vad,
|
||||
num_requests=args.num_requests,
|
||||
concurrency=concurrency,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from examples.online_serving.realtime_web.server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(default_profile="qwen3-turn")
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
# MODEL=/models/auk-omni bash examples/online_serving/text_to_speech/auk/run_server.sh
|
||||
set -euo pipefail
|
||||
|
||||
: "${MODEL:?Set MODEL to the assembled AuK or AuK-Flash checkpoint directory}"
|
||||
exec vllm serve "$MODEL" --omni --host "${HOST:-127.0.0.1}" --port "${PORT:-8091}" "$@"
|
||||
@@ -0,0 +1,103 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""AuK speech generation and audio editing through /v1/audio/speech.
|
||||
|
||||
See the online TTS user guide for checkpoint preparation and serving. Local
|
||||
reference audio is sent as a data URL; it need not exist on the server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import mimetypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def reference_audio(value: str) -> str:
|
||||
if value.startswith(("http://", "https://", "data:")):
|
||||
return value
|
||||
path = Path(value).expanduser()
|
||||
mime = mimetypes.guess_type(path.name)[0] or "audio/wav"
|
||||
return f"data:{mime};base64,{base64.b64encode(path.read_bytes()).decode('ascii')}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--api-base", default="http://localhost:8091")
|
||||
parser.add_argument("--api-key", default=os.environ.get("OPENAI_API_KEY", "EMPTY"))
|
||||
parser.add_argument("--model", required=True, help="Model name or assembled bundle path used by the server")
|
||||
parser.add_argument(
|
||||
"--instructions",
|
||||
required=True,
|
||||
help=(
|
||||
"Complete AuK instruction after filling the cookbook template "
|
||||
"(https://github.com/Tencent-Hunyuan/AuK/blob/main/docs/COOKBOOK.md)"
|
||||
),
|
||||
)
|
||||
parser.add_argument("--ref-audio", help="Reference/source audio: local file, HTTP URL or data URL")
|
||||
parser.add_argument("--duration-seconds", type=float, help="Target duration; defaults to source length with audio")
|
||||
parser.add_argument("--seed", type=int)
|
||||
parser.add_argument("--num-inference-steps", type=int)
|
||||
parser.add_argument("--guidance-scale", type=float)
|
||||
parser.add_argument("--sway", type=float)
|
||||
parser.add_argument("--t-grid", type=float, nargs="+", help="Explicit increasing Euler time grid")
|
||||
parser.add_argument("--vae-sample", action="store_true", help="Sample the VAE posterior instead of taking its mean")
|
||||
parser.add_argument("--stream", action="store_true", help="Receive raw PCM over HTTP after full generation")
|
||||
parser.add_argument("--response-format", choices=["wav", "pcm", "flac", "mp3", "opus"], default="wav")
|
||||
parser.add_argument("--output", type=Path, help="Output file; defaults to output.wav or output.pcm")
|
||||
parser.add_argument("--timeout", type=float, default=300)
|
||||
args = parser.parse_args()
|
||||
if args.duration_seconds is None and args.ref_audio is None:
|
||||
parser.error("--duration-seconds is required without --ref-audio")
|
||||
|
||||
response_format = "pcm" if args.stream else args.response_format
|
||||
payload = {
|
||||
"model": args.model,
|
||||
# OpenAI's shared Speech schema still declares input, while AuK's
|
||||
# canonical request carries all content in the complete instruction.
|
||||
"input": "",
|
||||
"voice": "default",
|
||||
"response_format": response_format,
|
||||
}
|
||||
for name in ("duration_seconds", "seed"):
|
||||
value = getattr(args, name)
|
||||
if value is not None:
|
||||
payload[name] = value
|
||||
payload["instructions"] = args.instructions
|
||||
if args.ref_audio:
|
||||
payload["ref_audio"] = reference_audio(args.ref_audio)
|
||||
extra = {
|
||||
name: getattr(args, name)
|
||||
for name in ("num_inference_steps", "guidance_scale", "sway", "t_grid")
|
||||
if getattr(args, name) is not None
|
||||
}
|
||||
if args.vae_sample:
|
||||
extra["vae_sample"] = True
|
||||
if extra:
|
||||
payload["extra_params"] = extra
|
||||
if args.stream:
|
||||
payload.update(stream=True, stream_format="audio")
|
||||
|
||||
output = args.output or Path(f"output.{response_format}")
|
||||
headers = {"Authorization": f"Bearer {args.api_key}"}
|
||||
with httpx.Client(timeout=args.timeout) as client:
|
||||
with client.stream(
|
||||
"POST", f"{args.api_base.rstrip('/')}/v1/audio/speech", json=payload, headers=headers
|
||||
) as response:
|
||||
if response.is_error:
|
||||
response.read()
|
||||
raise RuntimeError(f"Speech API returned {response.status_code}: {response.text}")
|
||||
with output.open("wb") as handle:
|
||||
for chunk in response.iter_bytes():
|
||||
handle.write(chunk)
|
||||
print(f"Saved {output} ({output.stat().st_size} bytes). Native audio: 24000 Hz, mono.")
|
||||
if response_format == "pcm":
|
||||
print("PCM format: signed 16-bit little-endian, 24000 Hz, mono.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+19
-2
@@ -56,7 +56,9 @@ dev = [
|
||||
# 0.3.12 includes acknowledged TCP writes (Mooncake #2850): completion
|
||||
# must mean the receive buffer is ready before zero-copy consumers read it.
|
||||
"mooncake-transfer-engine-cuda13==0.3.12.post1",
|
||||
"av==18.0.0", # for ComfyUI tests
|
||||
"av==18.0.0; python_version >= '3.11'", # for ComfyUI tests
|
||||
"av==17.1.0; python_version < '3.11'", # last av release that supports Python 3.10
|
||||
"tomli==2.4.1; python_version < '3.11'", # tomllib backport used by tests/helpers/mark.py
|
||||
"openpyxl==3.1.5", # for nightly CI
|
||||
"pyttsx3==2.99",
|
||||
"opencc==1.4.1",
|
||||
@@ -159,12 +161,25 @@ vllm-omni = "vllm_omni.entrypoints.cli.main:main"
|
||||
vllm_omni_register_models = "vllm_omni.engine.arg_utils:register_omni_models_to_vllm"
|
||||
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = false
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["vllm_omni*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"vllm_omni" = ["_version.py", "py.typed", "transformers_utils/chat_templates/*.jinja"]
|
||||
"vllm_omni" = [
|
||||
"_version.py",
|
||||
"py.typed",
|
||||
"deploy/*.md",
|
||||
"deploy/*.yaml",
|
||||
"diffusion/distributed/csrc/*.cu",
|
||||
"diffusion/distributed/csrc/*.hpp",
|
||||
"model_executor/models/covo_audio/speaker_prompt/*.npy",
|
||||
"model_executor/models/step_audio2/assets/*.wav",
|
||||
"transformers_utils/chat_templates/*.jinja",
|
||||
]
|
||||
|
||||
[tool.setuptools_scm]
|
||||
# no extra settings needed, presence enables setuptools-scm
|
||||
@@ -325,6 +340,8 @@ skip_covered = true
|
||||
|
||||
[tool.typos.default]
|
||||
extend-ignore-identifiers-re = [
|
||||
# Canonical PyTorch CUDA device property.
|
||||
"^shared_memory_per_block_optin$",
|
||||
".*_thw",
|
||||
".*thw",
|
||||
"ein",
|
||||
|
||||
@@ -286,7 +286,6 @@ curl -X POST http://localhost:8091/v1/images/generations \
|
||||
"size": "1024x1024",
|
||||
"seed": 42
|
||||
}' | jq -r '.data[0].b64_json' | base64 -d > output-turbo.png
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
@@ -418,6 +417,45 @@ curl -s http://localhost:8091/v1/chat/completions \
|
||||
supports Boogu-Image-Edit directly
|
||||
(`--model Boogu/Boogu-Image-0.1-Edit --guidance-scale 5.0`, optional
|
||||
`--guidance-scale-2`).
|
||||
- **Request batching (`--max-num-seqs > 1`) is not bit-exact vs. serial.**
|
||||
Compatible TI2I requests (same task type, same `guidance_scale_2`
|
||||
explicitness) can share one forward pass. The instruction encoder batches
|
||||
with `padding="longest"`, so a request's *padded batch shape* depends on
|
||||
whichever partner request(s) happen to co-arrive, even though production
|
||||
RoPE derives each row's own effective position count from that row's own
|
||||
`attention_mask.sum()` rather than from the batch's padded width. In the
|
||||
measurements below, `--max-num-seqs 1` and `--max-num-seqs > 1` do not
|
||||
produce pixel-identical output for the same seed; the exact mechanism
|
||||
(padding-shape-dependent kernel/reduction-order numerics vs. some other
|
||||
batch-shape effect) has not been isolated, so treat this as an observed
|
||||
drift under the specific configs below, not a diagnosed root cause, and
|
||||
do not extrapolate the numbers below as a general upper bound on other
|
||||
configs. Two distinct regimes have been measured so far, and they are
|
||||
**not** interchangeable tolerances:
|
||||
- **Fixed-shape co-batch** (partner instructions encode to the same
|
||||
length): `Boogu-Image-0.1-Edit`, 512x512, 20 steps, guidance 5/2, one
|
||||
fixed reference image and instruction pair, seeds 42-73, B=2 vs. serial
|
||||
-- mean pixel MAE **1.70/255**, max per-image MAE **2.68/255**, min PSNR
|
||||
**34.35 dB** (2/32 image pairs were pixel-exact).
|
||||
- **Heterogeneous partner length** (co-batched instructions encode to
|
||||
different lengths): MAD **2.6-6.0** in the fixed-length partner-mixing
|
||||
check on this PR's test head -- larger than the fixed-shape case above
|
||||
in that check. A targeted current-head A/B run observed a comparable
|
||||
range: `Boogu-Image-0.1-Edit`, 512x512, 20 steps, guidance 5/2, one
|
||||
fixed A request (5-token instruction) co-batched in turn with a
|
||||
2-token and a 49-token partner instruction (both also differing in
|
||||
reference image and content, not length alone; `max_num_seqs=2`,
|
||||
co-batching verified via request tracing, not assumed) --
|
||||
solo vs. `A`+2-token partner MAD **4.06/255** (PSNR 29.9 dB),
|
||||
solo vs. `A`+49-token partner MAD **3.87/255** (PSNR 29.7 dB),
|
||||
2-token vs. 49-token partner MAD **4.05/255** (PSNR 30.2 dB). A
|
||||
controlled ablation (same partner content/reference, varying only
|
||||
instruction length) would be needed to isolate a pure length effect,
|
||||
and has not been run.
|
||||
Pin `--max-num-seqs 1` for any workload that requires strict
|
||||
serial-reproducibility (e.g. golden-image regression tests); use
|
||||
`--max-num-seqs > 1` only when this drift is acceptable for the throughput
|
||||
gain.
|
||||
|
||||
## Fast image editing (Boogu-Image-0.1-Edit-Turbo)
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# SeedVR2 video restoration — RTX 5090
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: ByteDance
|
||||
- Model: SeedVR2 3B
|
||||
- Task: restore or upscale an uploaded video
|
||||
- Mode: native offline/HTTP pipeline, one Euler step
|
||||
- Hardware: NVIDIA GeForce RTX 5090, 32 GiB per device
|
||||
- Maintainer: 0z5a
|
||||
|
||||
## When to use this recipe
|
||||
|
||||
Run short, constant-frame-rate videos with the native SeedVR2 pipeline. This
|
||||
recipe covers the whole-clip path and the separate long-video route. Color
|
||||
correction from external applications is a separate execution semantic.
|
||||
|
||||
## Supported model contract
|
||||
|
||||
| Property | Contract |
|
||||
| --- | --- |
|
||||
| Model directory | See [checkpoint files and hashes](../../docs/models/seedvr2.md#model-directory) |
|
||||
| Input | One uploaded video; blank prompt; positive dimensions divisible by 16 |
|
||||
| Output | MP4 at requested geometry, original frame count and source frame rate |
|
||||
| Audio | First mono/stereo track, aligned by PTS; re-encoded as AAC |
|
||||
| Sampling | One Euler step, CFG=1, per-request seed |
|
||||
| Precision | 3B DiT and VAE FP16 |
|
||||
| Parallelism | Whole-window SP via `ulysses_degree`; replicated model weights |
|
||||
| Frame padding | Internal 4n+1 padding, cropped back; five returns five, six returns six |
|
||||
|
||||
## References
|
||||
|
||||
- [Canonical SeedVR](https://github.com/ByteDance-Seed/SeedVR)
|
||||
- [Model guide](../../docs/models/seedvr2.md)
|
||||
- [Integration RFC](https://github.com/vllm-project/vllm-omni/issues/7723)
|
||||
|
||||
## Hardware
|
||||
|
||||
RTX 5090 with 32 GiB per GPU, PCIe, one GPU for the default command or two/four
|
||||
for SP. Each GPU must fit a complete model copy and its activations. Host RAM
|
||||
must accommodate checkpoint staging per rank. Admission uses a padded
|
||||
five-frame 848×480 output pixel budget by default, allowing longer clips at
|
||||
smaller resolutions, plus a 257-frame decoder-work cap. The validated SP4
|
||||
configuration with VAE tiling and `vae_patch_parallel_size=4` uses a padded
|
||||
five-frame 2560×1472 budget, as does any higher matched degree. On devices
|
||||
with more memory the clip budget grows automatically; the other caps are raised
|
||||
through `VLLM_OMNI_SEEDVR2_SHARDED_FRAME_PIXELS` and
|
||||
`VLLM_OMNI_SEEDVR2_MAX_FRAMES`, and `VLLM_OMNI_SEEDVR2_SHARDED_CLIP_PIXELS`
|
||||
overrides the clip budget. The model guide lists
|
||||
exact bounds and which profiles have completed GPU validation.
|
||||
|
||||
## Software environment
|
||||
|
||||
Linux x86-64, Python 3.12, PyTorch 2.13.0+cu130, CUDA 13.0, vLLM 0.29.0, and
|
||||
PyAV in the existing environment. Use the vLLM-Omni branch containing this recipe.
|
||||
|
||||
## Command
|
||||
|
||||
Set `MODEL_DIR` to the authorized checkpoint directory documented in the model
|
||||
guide; it must also contain `model_index.json` selecting `SeedVR2Pipeline`.
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 vllm serve "$MODEL_DIR" --omni \
|
||||
--model-class-name SeedVR2Pipeline --dtype float16 --enforce-eager \
|
||||
--num-gpus 1 --host 127.0.0.1 --port 8098
|
||||
```
|
||||
|
||||
For SP=2, expose two GPUs and replace the GPU count with:
|
||||
|
||||
```bash
|
||||
--num-gpus 2 --distributed-executor-backend mp \
|
||||
--stage-overrides '{"0":{"ulysses_degree":2}}'
|
||||
```
|
||||
|
||||
Use degree 4 and four visible GPUs for SP=4. Combine any feature-specific
|
||||
settings below into the same stage-overrides object; do not repeat that flag.
|
||||
The validated 16-frame 720p original-size profile is:
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0,1,2,3 vllm serve "$MODEL_DIR" --omni \
|
||||
--model-class-name SeedVR2Pipeline --dtype float16 --enforce-eager \
|
||||
--vae-use-tiling --num-gpus 4 --distributed-executor-backend mp \
|
||||
--stage-overrides '{"0":{"ulysses_degree":4,"vae_patch_parallel_size":4,"vae_parallel_mode":"spatial_shard_height"}}' \
|
||||
--host 127.0.0.1 --port 8098
|
||||
```
|
||||
|
||||
Use `size=1280x720` for a landscape 1280×720 input or `size=720x1280` for a
|
||||
portrait 720×1280 input. Both require 16 decoded source frames and retain the
|
||||
source frame rate; no upscaling is requested.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
curl --fail-with-body http://127.0.0.1:8098/v1/videos/sync \
|
||||
-F 'prompt= ' -F 'input_references=@input.mp4;type=video/mp4' \
|
||||
-F 'size=224x128' -F 'num_inference_steps=1' \
|
||||
-F 'guidance_scale=1' -F 'seed=7723' --output restored.mp4
|
||||
python - <<'PY'
|
||||
import av
|
||||
with av.open('restored.mp4') as video:
|
||||
stream = video.streams.video[0]
|
||||
print(stream.width, stream.height, stream.average_rate,
|
||||
sum(1 for _ in video.decode(video=0)), len(video.streams.audio))
|
||||
PY
|
||||
```
|
||||
|
||||
Expect HTTP 200 and a completely decodable 224×128 video. Check the frame count,
|
||||
FPS, timestamps, and audio against the input. Repeat the same seed and compare
|
||||
decoded frames; changing the seed should change the restored video. Inspect
|
||||
matching first/middle/last frames at the same display scale. AAC is re-encoded,
|
||||
so input/output audio packet hashes need not be identical.
|
||||
|
||||
The released 3B checkpoint also passed these four-GPU SP4 HTTP cases with the
|
||||
configuration above. Each input had 16 frames at 24 FPS; the output retained
|
||||
the original size, 16 decoded frames, timestamps, and decodable audio. All four
|
||||
workers and the server exited normally after each run.
|
||||
|
||||
| 1× input → output | Earlier six-frame admission | Revised HTTP result | Request time |
|
||||
| --- | --- | --- | ---: |
|
||||
| 1280×720, 16 frames → 1280×720 | Rejected | HTTP 200; media checks passed | 3.553 s |
|
||||
| 720×1280, 16 frames → 720×1280 | Rejected | HTTP 200; media checks passed | 3.388 s |
|
||||
|
||||
These are single requests on a shared host, not performance comparisons.
|
||||
|
||||
### One long-video request
|
||||
|
||||
For a 24 FPS source, the long route can restore up to 7,200 frames at
|
||||
768×1344. Use the SP4/VAE height-sharding command above, serve with
|
||||
`--api-server-count 1`, and set a persistent
|
||||
`VLLM_OMNI_SEEDVR2_LONG_OUTPUT_DIR` before starting. If the source is shorter
|
||||
than 300 seconds, `loop_input=true` repeats its frames and audio. The service
|
||||
runs the longest model windows the clip budget admits, with four-frame overlap,
|
||||
and writes one MP4;
|
||||
it does not concatenate separate video files. Download the result within
|
||||
`VLLM_OMNI_SEEDVR2_LONG_JOB_TTL_SECONDS`, after which the job directory is
|
||||
swept; `DELETE` on the job URL stops a run at the next window boundary.
|
||||
|
||||
```bash
|
||||
curl --fail-with-body http://127.0.0.1:8098/v1/seedvr2/restore-long \
|
||||
-F 'input_references=@input.mp4;type=video/mp4' \
|
||||
-F 'prompt= ' -F 'size=768x1344' -F 'num_frames=7200' \
|
||||
-F 'loop_input=true' -F 'seed=7723' -F 'extra_params={"color_correction_method":"lab"}'
|
||||
# Use the returned id to poll /v1/seedvr2/restore-long/{id};
|
||||
# download /v1/seedvr2/restore-long/{id}/content when completed.
|
||||
```
|
||||
|
||||
The input must have constant 24 FPS timestamps starting at zero. Only one job
|
||||
can run per API server, and `--api-server-count` must be one. The existing
|
||||
whole-clip `/v1/videos/sync` limits remain in force. Full 7,200-frame GPU
|
||||
validation is in progress; use this route only with a memory-checked SP4
|
||||
deployment.
|
||||
|
||||
Set `VLLM_TEST_SEEDVR2_MODEL_DIR` to the directory containing all model files:
|
||||
|
||||
```bash
|
||||
python -m pytest -o addopts='' -v tests/diffusion/models/seedvr2/test_seedvr2_e2e.py
|
||||
```
|
||||
|
||||
The tests run the complete native pipeline for every colour mode and the HTTP
|
||||
long-video route at USP 1 and 8 (eight GPUs required for the latter). The HTTP
|
||||
cases force a 13-frame window, restore 20 frames across a seam, preserve audio
|
||||
and timestamps, reject invalid `extra_params`, and cancel a longer job. They
|
||||
use the released model rather than mocking its forward pass.
|
||||
|
||||
## Supported features
|
||||
|
||||
| Feature | Status |
|
||||
| --- | --- |
|
||||
| [Window SP](../../docs/models/seedvr2.md) | Model-local regular/shifted window attention |
|
||||
| CPU offload, LoRA, compiled execution, CFG/TP/PP | Unsupported |
|
||||
| VFR or multichannel audio | Unsupported |
|
||||
| VAE temporal/spatial tiling | Available with `--vae-use-tiling` |
|
||||
| [Colour correction](../../docs/models/seedvr2.md) | On by default; `color_correction_method=lab\|wavelet\|adain\|none` |
|
||||
| Quantization | Separate opt-in feature |
|
||||
|
||||
## Measurement scope
|
||||
|
||||
Compare identical inputs, seeds, dimensions, GPU counts, and warmups. Separate
|
||||
correctness from performance: use repeated interleaved HTTP measurements before
|
||||
claiming a stable gain. Keep raw logs, tensors, and generated media outside the
|
||||
source diff; link selected visuals from the PR's test results.
|
||||
|
||||
## Temporal tiling and VAE patch parallelism
|
||||
|
||||
Use `--vae-use-tiling` for causal temporal chunks. On a single GPU this also
|
||||
bounds large convolution workspace with exact spatial halos. To shard VAE
|
||||
activations across the same two SP ranks, use:
|
||||
|
||||
```bash
|
||||
--vae-use-tiling --num-gpus 2 --distributed-executor-backend mp \
|
||||
--stage-overrides '{"0":{"ulysses_degree":2,"vae_patch_parallel_size":2,"vae_parallel_mode":"spatial_shard_height"}}'
|
||||
```
|
||||
|
||||
The VAE patch degree must equal the SP degree. Height sharding is supported;
|
||||
width sharding and batch slicing are not. Compare clips that cross a temporal
|
||||
chunk boundary as well as five/six-frame clips, and inspect frames adjacent to
|
||||
chunk boundaries. Reduced peak memory does not by itself establish lower latency.
|
||||
High-resolution clips can still exceed device capacity; validate the intended
|
||||
frame count and output size on the target GPUs.
|
||||
@@ -89,6 +89,7 @@ Install matching vLLM and vLLM-Omni versions, and ensure `ffmpeg` and
|
||||
| NVIDIA B200 or H200 | Capacity-based recommendation; not yet verified | All four canonical pipelines |
|
||||
| NVIDIA GB200 or GB300 | Capacity-based recommendation; not yet verified | All four canonical pipelines |
|
||||
| NVIDIA H100 80 GB | FP8 recipe | Distilled one-stage at 960x544 |
|
||||
| Intel Arc Pro B70 32 GB | Verified | Offline Full/SFT one-stage T2V at 768x512 |
|
||||
|
||||
The 1920x1088 two-stage examples require about 114 GB of peak GPU memory.
|
||||
80 GB GPUs do not have enough safety margin for the canonical two-stage
|
||||
@@ -133,6 +134,33 @@ SP attention head counts must be divisible by `ulysses_degree`. Use each
|
||||
phase's `H, W` (half/full resolution for two-stage); non-divisible shapes fail.
|
||||
Each GPU must still fit the resident weights. Warm up with the production shape.
|
||||
|
||||
### 1x Intel Arc Pro B70 32 GB (XPU)
|
||||
|
||||
Offline Full/SFT one-stage T2V at 768x512, 17 frames. Select the legacy
|
||||
convolutional VAE with `ltx2_use_conv_vae`; the default DiffVAE decoder loads a
|
||||
NATTEN kernel, which has no XPU build. FP8 with layerwise offload streams both
|
||||
the transformer and the Gemma-4 text encoder.
|
||||
|
||||
Environment: torch 2.14.0+xpu, vLLM 0.29.0, vLLM-Omni `main` at `d8d162d8`.
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_video/text_to_video.py \
|
||||
--model Lightricks/LTX-2.5-Diffusers \
|
||||
--model-class-name LTX2Pipeline \
|
||||
--stage-overrides '{"0":{"extras":{"ltx2_use_conv_vae":true}}}' \
|
||||
--prompt "A serene lakeside sunrise with mist over the water." \
|
||||
--num-frames 17 \
|
||||
--quantization fp8 \
|
||||
--enable-layerwise-offload \
|
||||
--vae-use-tiling \
|
||||
--vae-use-slicing \
|
||||
--enforce-eager \
|
||||
--output ltx25_output.mp4
|
||||
```
|
||||
|
||||
Peaks at 16.3 GiB in 163 s. Two-stage, distilled, and I2V paths were not
|
||||
exercised on this hardware.
|
||||
|
||||
## Offline inference
|
||||
|
||||
Choose values from the pipeline table. For example, the distilled two-stage
|
||||
|
||||
@@ -165,6 +165,63 @@ curl -X POST http://localhost:8000/v1/videos/sync \
|
||||
Use `image_reference` for a URL or JSON-safe image reference. Do not provide it
|
||||
together with `input_reference`.
|
||||
|
||||
## Hardware Support
|
||||
|
||||
## XPU
|
||||
|
||||
### 1x Intel Arc Pro B70 (32 GB)
|
||||
|
||||
Offline one-stage T2V at 768x512, 17 frames. FP8 with layerwise offload streams
|
||||
both the transformer and the Gemma text encoder, which otherwise stays resident
|
||||
beside it.
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- torch: 2.14.0+xpu
|
||||
- vLLM: 0.29.0
|
||||
- vLLM-Omni: `main` at `d8d162d8`
|
||||
|
||||
#### Command
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_video/text_to_video.py \
|
||||
--model diffusers/LTX-2.3-Diffusers \
|
||||
--prompt "A serene lakeside sunrise with mist over the water." \
|
||||
--num-frames 17 \
|
||||
--quantization fp8 \
|
||||
--enable-layerwise-offload \
|
||||
--vae-use-tiling \
|
||||
--vae-use-slicing \
|
||||
--enforce-eager \
|
||||
--output ltx23_output.mp4
|
||||
```
|
||||
|
||||
The same flags cover every one-stage checkpoint; only `--model` changes. The
|
||||
distilled checkpoints additionally need their class and an explicit size,
|
||||
because their preset defaults to 1536x1024:
|
||||
|
||||
```bash
|
||||
--model diffusers/LTX-2.3-Distilled-Diffusers \
|
||||
--model-class-name LTX2DistilledOneStagePipeline \
|
||||
--height 512 --width 768
|
||||
```
|
||||
|
||||
| Checkpoint | Peak | Time |
|
||||
| --- | ---: | ---: |
|
||||
| `Lightricks/LTX-2` | 14.8 GiB | 160 s |
|
||||
| `diffusers/LTX-2.3-Diffusers` | 16.4 GiB | 167 s |
|
||||
| `dg845/LTX-2.3-Diffusers` | 16.4 GiB | 155 s |
|
||||
| `diffusers/LTX-2.3-Distilled-Diffusers` | 14.9 GiB | 128 s |
|
||||
|
||||
#### Verification
|
||||
|
||||
Each command writes its `--output` MP4. Confirm it decodes and that the sampled
|
||||
frames match the prompt.
|
||||
|
||||
|
||||
|
||||
## Guidance
|
||||
|
||||
One-stage and ordinary Stage 1 support independent video/audio CFG,
|
||||
|
||||
@@ -289,6 +289,15 @@ The first request took 85.224 seconds. The AR stage generated 4,161 visual token
|
||||
|
||||
The output was a valid 1024 by 1024 RGB PNG.
|
||||
|
||||
### Startup benchmark
|
||||
|
||||
The [startup benchmark](../../benchmarks/mammoth_moda2/README.md) provides
|
||||
commands, measurement boundaries and a single-A800 eager baseline. On that
|
||||
16-CPU-quota machine, parallel stage initialization and 16 CPU threads reduced
|
||||
`Omni()` initialization from about 60 s to 37 s. See the benchmark for the
|
||||
configuration and limitations; CUDA-graph mode needs separate memory and
|
||||
quality validation.
|
||||
|
||||
## MammothModa2-Dev unified inference
|
||||
|
||||
MammothModa2-Dev uses a Qwen3-VL AR backbone, while MammothModa2-Preview uses
|
||||
|
||||
@@ -281,6 +281,186 @@ curl -sS -X POST "${API_URL}" \
|
||||
-o t2va.mp4
|
||||
```
|
||||
|
||||
This requires the FLASH_ATTN backend and MindIE-SD. H3 automatically applies
|
||||
exact power-of-two input pre-scaling (`laser_input_scale=256`) so the
|
||||
kernel's fp16 workspace cannot overflow on outlier activations. Measured on
|
||||
the FastH3 four-step Dense configuration below, T2VA 15 s at 1344x768, this
|
||||
kernel reduced end-to-end latency from ~73 s to ~57 s (about 22%).
|
||||
|
||||
|
||||
## FastH3 four-step on A3
|
||||
|
||||
[FastH3](https://haoailab.com/blogs/fasth3-preview/) is FastVideo's four-step
|
||||
DMD2 student of H3-Base; see
|
||||
[MiniMax-H3.md § FastH3 adapter](MiniMax-H3.md#fasth3-adapter) for the adapter
|
||||
contract. On A3 the adapter cannot be fused the GPU way. The GPU recipe fuses
|
||||
it at load time from `--lora-path`, but that path replicates the full model per
|
||||
rank and does not fit in 64 GB HBM. A3 needs distributed layerwise offload, and
|
||||
offload is refused with `--lora-path` because it streams weights in without
|
||||
going through the fusion (see the GPU recipe's note on why
|
||||
`--enable-distributed-layerwise-offload` fails fast with a FastH3 adapter).
|
||||
|
||||
The A3 path therefore fuses the adapter **offline**, once, into a native-layout
|
||||
checkpoint that distributed layerwise offload can memory-map directly. This
|
||||
also lets the offloaded server start without `--lora-path`, so the fusion
|
||||
contract check that rejects offload never fires.
|
||||
|
||||
### Prepare the fused checkpoint (one-time, offline)
|
||||
|
||||
The fusion runs in the vLLM-Omni native namespace
|
||||
(`blocks.N.attn.qkv_proj.weight`), matching the base H3 `FL2VA` transformer
|
||||
layout. It reads the base transformer and the Dense adapter, adds the adapter's
|
||||
low-rank and full-rank deltas per shard, symlinks the unchanged components
|
||||
(text encoder, VAEs, tokenizer, processor), and writes the four-step sigma
|
||||
ladder into `model_index.json` so the pipeline samples on the release's rungs
|
||||
rather than a uniform schedule.
|
||||
|
||||
Download the base checkpoint's `FL2VA` partition (native layout) and the Dense
|
||||
adapter:
|
||||
|
||||
```bash
|
||||
export BASE_DIR=/path/to/MiniMax-H3
|
||||
hf download MiniMaxAI/MiniMax-H3 --include "FL2VA/*" --local-dir "${BASE_DIR}"
|
||||
|
||||
export FASTH3_DIR=/path/to/fasth3
|
||||
hf download FastVideo/FastVideo-FastH3-4-step-Preview-v1-LoRA \
|
||||
dense-datafree/adapter_model.safetensors --local-dir "${FASTH3_DIR}"
|
||||
```
|
||||
|
||||
> The published full checkpoint
|
||||
> `FastVideo/FastVideo-FastH3-4-step-Preview-v1-Dense-DataFree` is stored in the
|
||||
> diffusers layout (`transformer_blocks.N.attn.to_q/to_k/to_v`, split QKV,
|
||||
> value-first MLP). Distributed layerwise offload memory-maps by exact runtime
|
||||
> parameter name and has no diffusers-to-native remap for H3, so that artifact
|
||||
> cannot be served directly. Fuse from the base `FL2VA` partition instead — its
|
||||
> keys already match the runtime.
|
||||
|
||||
Run the fusion:
|
||||
|
||||
```python
|
||||
# prepare_fasth3_dense.py — fuse base H3 FL2VA + FastH3 Dense adapter (native layout)
|
||||
import json, os, shutil, sys, time
|
||||
import torch
|
||||
from safetensors.torch import load_file, save_file
|
||||
from vllm_omni.diffusion.models.minimax_h3.fasth3 import FastH3WeightFusion
|
||||
|
||||
BASE = os.environ["BASE_DIR"] + "/FL2VA" # base H3 FL2VA partition (native layout)
|
||||
ADAPTER = os.environ["FASTH3_DIR"] + "/dense-datafree/adapter_model.safetensors"
|
||||
OUT = os.environ.get("FUSED_DIR", "/path/to/FastH3-Dense-Fused/FL2VA")
|
||||
|
||||
# MiniMax-H3 architecture
|
||||
HEAD_DIM, NUM_BLOCKS, NUM_REFINER_BLOCKS = 128, 50, 2
|
||||
# FastH3 four-step sigma positions; the pipeline adds per-modality shift on top.
|
||||
BASE_SCHEDULE = [0.999, 0.749, 0.5, 0.25, 0.0]
|
||||
COMPONENTS = ["audio_vae", "video_vae", "text_encoder", "tokenizer", "processor"]
|
||||
|
||||
fusion = FastH3WeightFusion.from_path(
|
||||
ADAPTER, head_dim=HEAD_DIM, num_blocks=NUM_BLOCKS,
|
||||
num_refiner_blocks=NUM_REFINER_BLOCKS)
|
||||
assert fusion is not None, "adapter not recognized as FastH3"
|
||||
|
||||
os.makedirs(OUT + "/transformer", exist_ok=True)
|
||||
src_t = BASE + "/transformer"
|
||||
for shard in sorted(f for f in os.listdir(src_t)
|
||||
if f.endswith(".safetensors") and "index" not in f):
|
||||
data = load_file(os.path.join(src_t, shard), device="cpu")
|
||||
fused = {}
|
||||
for k, v in data.items():
|
||||
fv = fusion.fuse(k, v)
|
||||
fused[k] = fv.to("cpu").to(torch.bfloat16) if fv is not v else v
|
||||
save_file(fused, os.path.join(OUT, "transformer", shard))
|
||||
fusion.validate_fully_applied() # every delta must have met its parameter
|
||||
|
||||
for j in os.listdir(src_t): # copy transformer index/config
|
||||
if j.endswith(".json"):
|
||||
shutil.copy2(os.path.join(src_t, j), os.path.join(OUT, "transformer", j))
|
||||
|
||||
for c in COMPONENTS: # symlink unchanged components
|
||||
s = os.path.join(BASE, c)
|
||||
if os.path.exists(s):
|
||||
os.symlink(os.path.realpath(s), os.path.join(OUT, c))
|
||||
|
||||
idx = json.loads(open(BASE + "/model_index.json").read()) # inject sigma ladder
|
||||
idx.setdefault("_minimax_h3", {})["base_schedule"] = BASE_SCHEDULE
|
||||
open(OUT + "/model_index.json", "w").write(json.dumps(idx, indent=4) + "\n")
|
||||
print("fused ->", OUT)
|
||||
```
|
||||
|
||||
```bash
|
||||
export FUSED_DIR=/path/to/FastH3-Dense-Fused/FL2VA
|
||||
python prepare_fasth3_dense.py
|
||||
```
|
||||
|
||||
### Serve the fused checkpoint
|
||||
|
||||
Serve the fused directory with the same multi-NPU flags as the base recipe, but
|
||||
**without** `--lora-path` — the adapter is already in the weights. Keep
|
||||
distributed layerwise offload and, optionally, LaserAttention:
|
||||
|
||||
```bash
|
||||
export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
|
||||
export PORT=9098
|
||||
export MODEL=/path/to/FastH3-Dense-Fused/FL2VA
|
||||
export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
export VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800
|
||||
export MINDIE_SD_FA_TYPE="ascend_laser_attention" # optional
|
||||
|
||||
vllm serve "${MODEL}" \
|
||||
--omni \
|
||||
--host 0.0.0.0 \
|
||||
--port "${PORT}" \
|
||||
--trust-remote-code \
|
||||
--task-type t2va \
|
||||
--init-timeout 1800 \
|
||||
--stage-init-timeout 1800 \
|
||||
--num-gpus 8 \
|
||||
--usp 8 \
|
||||
--ring 1 \
|
||||
--text-encoder-tp-size 8 \
|
||||
--enable-distributed-layerwise-offload \
|
||||
--vae-parallel-mode tile \
|
||||
--vae-use-tiling \
|
||||
--vae-patch-parallel-size 8 \
|
||||
--diffusion-attention-backend FLASH_ATTN
|
||||
```
|
||||
|
||||
Requests must ask for `num_inference_steps=4` and `task=t2va`; FastH3 preview v1
|
||||
distills the T2VA path only. The first request includes regional compilation
|
||||
(~50 s warmup); exclude it from steady-state numbers.
|
||||
|
||||
```bash
|
||||
curl -s -X POST "http://localhost:${PORT}/v1/videos/sync" \
|
||||
-F 'prompt=A golden retriever running through a sunflower field at sunset' \
|
||||
-F 'seconds=5' \
|
||||
-F 'aspect_ratio=16:9' \
|
||||
-F 'num_inference_steps=4' \
|
||||
-o out.mp4
|
||||
```
|
||||
|
||||
### Measured FastH3 four-step evidence
|
||||
|
||||
Measured on an Atlas 800I A3 server (8x NPU) with CANN 9.0.1, PyTorch
|
||||
2.10.0+cpu, torch_npu 2.10.0.post2, the multi-NPU configuration above, T2VA at
|
||||
1344x768, one warmup excluded:
|
||||
|
||||
| Duration | LaserAttention | End-to-end |
|
||||
| ---: | --- | ---: |
|
||||
| 4 s | on | ~12 s (768x768), ~15 s (1344x768) |
|
||||
| 8 s | on | ~26 s |
|
||||
| 15 s | on | ~57 s |
|
||||
| 5 s | off | ~19 s |
|
||||
| 10 s | off | ~41 s |
|
||||
| 15 s | off | ~73 s |
|
||||
|
||||
These describe the validated shapes rather than a general throughput guarantee.
|
||||
|
||||
> A native `--lora-path + --enable-distributed-layerwise-offload` path that
|
||||
> fuses during the offload memory-map (avoiding the offline step) is possible
|
||||
> through the offload backend's per-tensor transform hook, but is left as future
|
||||
> work pending upstream design discussion.
|
||||
|
||||
|
||||
## HTTP API examples
|
||||
Switch tasks via `extra_params.task` (`t2va` / `fl2va` / `ref2va`); FL2VA and
|
||||
Ref2VA additionally pass `input_references` (first-frame image, reference
|
||||
video/audio). The full parameter table lives in the GPU recipe.
|
||||
|
||||
@@ -725,6 +725,9 @@ curl -sS -X POST "${API_URL}" \
|
||||
### 3. Ref2VA: image-only, image/audio, or mixed references
|
||||
|
||||
Run these requests against the combined service or a Ref2VA-only service.
|
||||
Reference images keep their original resolution, with dimensions aligned to
|
||||
32 pixels. No additional request or server setting is needed.
|
||||
|
||||
Image-only Ref2VA omits `audio_reference`; adding one or more audio references
|
||||
is optional. The typed fields accept one object or an ordered JSON list.
|
||||
`audio_reference` accepts an HTTP(S) URL or a `data:` URL. In one terminal,
|
||||
@@ -782,6 +785,9 @@ Run this request against the combined service. Repeat the
|
||||
`input_references` multipart field once per source video. H3 consumes the
|
||||
videos in form order and preserves their original soundtracks during
|
||||
conditioning.
|
||||
Small reference videos retain their original size; larger videos are scaled
|
||||
down to the reference canvas. Dimensions are aligned to 32 pixels. No
|
||||
additional request or server setting is needed.
|
||||
|
||||
```bash
|
||||
export SUBJECT_VIDEO=/path/to/green_screen_subject.mp4
|
||||
@@ -1509,9 +1515,184 @@ vllm serve "${MODEL_ROOT}/FL2VA" \
|
||||
--cache-config '{"rel_l1_thresh":0.17}'
|
||||
```
|
||||
|
||||
## Latent super-resolution
|
||||
|
||||
H3's video VAE is a ~5B-parameter model, so the usual way to raise output
|
||||
resolution -- decode, upscale in pixel space, re-encode -- pays for that VAE
|
||||
twice. The community
|
||||
[Minimax H3 latent upscaler](https://huggingface.co/LBH-123-AI/Minimax_h3_latent_Upscaler)
|
||||
is a learned 3D-convolutional resizer trained on H3's 24-channel latents: it
|
||||
runs between the denoise loop and the VAE, so the clip is decoded once, at the
|
||||
larger size. Unlike bilinear or bicubic interpolation of a latent, it does not
|
||||
introduce ghosting or double-image artifacts.
|
||||
|
||||
The network runs one normalization below the pipeline latent: an H3 latent in
|
||||
vLLM-Omni is already normalized, and the upscaler was trained on that tensor
|
||||
normalized again by the VAE's `latents_mean` / `latents_std`. The stage applies
|
||||
and undoes that itself, so nothing about a request changes -- but a port that
|
||||
skips it inflates the latent about 5x and decodes as a magenta grid at one tile
|
||||
per latent cell.
|
||||
|
||||
The checkpoint is optional and is not part of the H3 release. Download one file
|
||||
from that repository (`minimax_h3_latent_upscaler_3d_bf16.safetensors`, 691 MB)
|
||||
and name it at startup:
|
||||
|
||||
```bash
|
||||
vllm serve "${MODEL_ROOT}/FL2VA" \
|
||||
--omni \
|
||||
--trust-remote-code \
|
||||
--additional-config '{"latent_upscaler_path":"/models/minimax_h3_latent_upscaler_3d_bf16.safetensors"}'
|
||||
```
|
||||
|
||||
A request then asks for a size. The three modes are mutually exclusive and all
|
||||
resolve to the same 16-pixel latent grid, so the three requests below are the
|
||||
same 960x544 -> 1920x1088 upscale:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "${API_URL}" \
|
||||
-F 'prompt=...' \
|
||||
-F 'width=960' -F 'height=544' -F 'fps=24' \
|
||||
-F 'extra_params={"task":"t2va","duration":8.7,"latent_upscale":2.0}' \
|
||||
-o t2va_1080p.mp4
|
||||
|
||||
# or name the output size in pixels
|
||||
# "latent_upscale":{"width":1920,"height":1088}
|
||||
# or name a pixel budget and keep the aspect ratio
|
||||
# "latent_upscale":{"megapixels":2.0}
|
||||
```
|
||||
|
||||
Over HTTP the field is `extra_params`: the videos request model declares that
|
||||
name and nothing else, so an `extra_args` form field is dropped without an
|
||||
error and the request silently returns the un-upscaled size. Offline, the same
|
||||
value goes in `extra_args`:
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model=os.path.join(os.environ["MODEL_ROOT"], "FL2VA"),
|
||||
trust_remote_code=True,
|
||||
additional_config={"latent_upscaler_path": "/models/minimax_h3_latent_upscaler_3d_bf16.safetensors"},
|
||||
)
|
||||
outputs = omni.generate(
|
||||
"A quiet cinematic night scene with matching ambient sound.",
|
||||
OmniDiffusionSamplingParams(
|
||||
height=544,
|
||||
width=960,
|
||||
fps=24,
|
||||
num_inference_steps=50,
|
||||
seed=42,
|
||||
extra_args={"task": "t2va", "duration": 8.7, "latent_upscale": 2.0},
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### The hi-res route: generate small, refine large
|
||||
|
||||
Upscaling alone decodes the enlarged latent directly, which is fast but can
|
||||
only interpolate detail the first pass never generated. Adding `latent_refine`
|
||||
resumes the denoise schedule at the new size, so the DiT puts real detail back:
|
||||
|
||||
1. generate at a cheap size -- far fewer DiT tokens,
|
||||
2. upscale the latent,
|
||||
3. re-noise it and run the tail of the schedule at the target size.
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "${API_URL}" \
|
||||
-F 'prompt=...' \
|
||||
-F 'width=960' -F 'height=544' -F 'fps=24' \
|
||||
-F 'num_inference_steps=50' \
|
||||
-F 'extra_params={"task":"t2va","duration":8.7,"latent_upscale":2.0,"latent_refine":0.4}' \
|
||||
-o t2va_1080p.mp4
|
||||
```
|
||||
|
||||
`latent_refine` is the fraction of the request's steps the second pass re-runs,
|
||||
the img2img convention, so it sets both how much the pass may change and what
|
||||
it costs: `0.4` of `num_inference_steps=50` is 50 cheap steps followed by 20
|
||||
expensive ones. Low values stay close to the first pass and mostly sharpen;
|
||||
high values re-generate and may drift from it. `1.0` re-runs the whole schedule
|
||||
from noise, which discards the first pass entirely.
|
||||
|
||||
Detail does not keep climbing with strength. Measured on a 15s 2688x1536 clip,
|
||||
six frames apart, against the same seed and prompt: 0.5 returned 0.98x the
|
||||
high-frequency energy of 0.35 and 0.65 returned 0.92x, while the mean per-pixel
|
||||
drift from the 0.35 result rose from 6.3 to 12.6 (0-255). What caps detail is
|
||||
what the DiT can resolve at the target size, not how noisy a start it is given;
|
||||
past that, a higher strength only erases more of the first pass and re-imagines
|
||||
it from less. On this checkpoint 0.35 was the best of the three -- treat higher
|
||||
values as "re-generate", not as "sharpen".
|
||||
|
||||
Why this is faster than generating at the target size: the DiT cost follows the
|
||||
video token count, which is quadratic in the latent area for attention. The
|
||||
example above is 31,620 video tokens at 960x544 against 126,480 at 1920x1088 --
|
||||
4x the tokens, so roughly 4x the linear cost and up to 16x the attention cost
|
||||
per step. Paying that on 20 steps instead of 50 is where the saving comes from.
|
||||
The gain therefore depends on the resolutions, the strength, and how
|
||||
attention-bound the shape is; measure it on your own workload.
|
||||
|
||||
Audio is re-noised and refined alongside the video at the same schedule
|
||||
position. The two modalities are shifted apart (`flow_shift` 12.0 against
|
||||
`audio_flow_shift` 3.0), so each resumes at its own sigma for that position --
|
||||
the state the first pass actually passed through, which is what keeps the pass
|
||||
in distribution.
|
||||
|
||||
`latent_refine` works without `latent_upscale` too, as a plain detail pass at
|
||||
the generated size. For FL2VA, the keyframe condition is pinned to the output
|
||||
latent size, so a refine after an upscale re-encodes the keyframes from the
|
||||
originals at the new size; REF2VA references carry their own shapes and are
|
||||
reused as they are.
|
||||
|
||||
### Options
|
||||
|
||||
| `--additional-config` key | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `latent_upscaler_path` | unset | Checkpoint file, or a directory holding exactly one. Unset disables the stage and loads no extra weights. |
|
||||
| `latent_upscaler_dtype` | the engine dtype | `bf16`, `fp16` or `fp32`. |
|
||||
| `latent_upscaler_chunk_frames` | `0` | `0` runs the whole clip in one pass; a positive value bounds activation memory with approximate temporal chunking. |
|
||||
| `latent_upscaler_resident` | `false` | Keep the ~700 MB of weights on the GPU between requests instead of in host memory. |
|
||||
| `latent_refine_max_tokens_per_rank` | `65536` | Reject larger refine layouts before denoising. Set `0` only after validating a larger deployment. |
|
||||
| `latent_upscale` | unset | A default target applied to every request, in the request's own format. A request overrides it, and `false` opts out. |
|
||||
| `latent_refine` | unset | A default refine strength applied to every request. A request overrides it, and `false` opts out. |
|
||||
|
||||
The `latent_upscale` request value is a multiplier (`2.0`), `false`, or an
|
||||
object naming one mode: `{"scale": 2.0}`, `{"width": …, "height": …}` or
|
||||
`{"megapixels": …}`, each accepting an `align` (default `32` pixels). The
|
||||
checkpoint was trained between 1.0x and 4.0x and only upscales; a smaller
|
||||
target is rejected, and `scale` 1.0 is a no-op that skips the stage. The
|
||||
`latent_refine` value is a strength in `(0, 1]`, `false`, or
|
||||
`{"strength": 0.4}`; it needs no checkpoint of its own.
|
||||
|
||||
When `latent_refine` is enabled, both resolved output dimensions must be
|
||||
divisible by 32 pixels for the DiT's spatial patches. A custom `align` that
|
||||
produces an incompatible target is rejected before denoising; upscale-only
|
||||
requests can still use the VAE's 16-pixel grid. Numeric upscale fields must be
|
||||
positive and finite, and `width`, `height`, and `align` must be integers.
|
||||
|
||||
Temporal chunking can bound activation memory on long clips -- a 15-second clip is
|
||||
102 latent frames, more than three 32-frame chunks. It is an approximation rather than a
|
||||
partition: the network's GroupNorms pool statistics over whatever clip they are
|
||||
handed, so a chunked pass can differ from a single pass across the whole output
|
||||
and not only at the chunk seams. How much that costs in practice was not
|
||||
measured here; the default single pass buys the exact result for the
|
||||
memory a full-length activation volume takes (10.1 GiB against 5.6 GiB at
|
||||
2688x1536 for 15 seconds, measured on one B300). Set
|
||||
`latent_upscaler_chunk_frames: 32` explicitly if that memory cost is too high.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- TeaCache is calibrated for FL2VA only; Ref2VA requests run uncached.
|
||||
- The latent upscaler is a community checkpoint, not part of the H3 release,
|
||||
and is unavailable unless `latent_upscaler_path` names one. It runs
|
||||
replicated on every DiT rank rather than sharded.
|
||||
- `latent_refine` is request-mode only. It is a second denoise loop with its
|
||||
own resolution and its own truncated schedule, and the step contract carries
|
||||
one schedule per request, so step execution rejects it. `latent_upscale`
|
||||
without `latent_refine` works in both modes.
|
||||
- Large refine layouts can abort workers with `CUDA error: an illegal memory
|
||||
access was encountered`. On B300, observed video token counts of 54,144 per
|
||||
rank complete while 108,288 and 109,360 abort. The preflight limit of 65,536
|
||||
counts the full padded layout, including text, audio, and visual references,
|
||||
per Ulysses rank. It rejects the observed failing sizes before the base pass.
|
||||
This is an empirical B300 guard, not a proven kernel limit on every device;
|
||||
deployments with different hardware can adjust the limit after validation.
|
||||
- Combined serving requires sibling `FL2VA` and `Ref2VA` directories, loads
|
||||
both task-specific DiTs, and loads shared components once from `FL2VA`.
|
||||
- Request mode executes one generation request per diffusion batch. Use
|
||||
|
||||
@@ -25,10 +25,10 @@ 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 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/`.
|
||||
unified full-duplex framework: its `pipeline.py` declares one
|
||||
`duplex_plugin` (`PersonaPlexDuplexPlugin`), and the model-specific package at
|
||||
`vllm_omni/model_executor/models/personaplex/duplex/` holds the plugin, the
|
||||
worker-side lockstep Stage 0 runtime and the 80 ms input framing.
|
||||
|
||||
## References
|
||||
|
||||
@@ -114,16 +114,18 @@ HF_TOKEN=... CUDA_VISIBLE_DEVICES=0 python -m vllm_omni.entrypoints.cli.main ser
|
||||
--deploy-config vllm_omni/deploy/personaplex.yaml
|
||||
```
|
||||
|
||||
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`.
|
||||
This exposes `WS /v1/realtime?duplex=1` (alias `WS /v1/duplex`): the OpenAI
|
||||
Realtime session protocol, client API and wire vocabulary in
|
||||
[`docs/serving/realtime_duplex_api.md`](../../docs/serving/realtime_duplex_api.md).
|
||||
Voice (`voice`, a bundled `.pt` basename) and persona (`instructions`) are set
|
||||
in `session.update`; the model takes no client commits and serves no
|
||||
`/v1/chat/completions` route.
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
# GPU-free contract tests (stage0 runtime + unified serving adapter)
|
||||
pytest tests/model_executor/models/personaplex/duplex/ -q
|
||||
# GPU-free contract tests (plugin, stage0 runtime, runner scenario)
|
||||
pytest tests/model_executor/models/personaplex/duplex/ tests/engine/duplex/test_session_runner_personaplex.py -q
|
||||
|
||||
# GPU e2e: paced 24 kHz PCM over /v1/realtime?duplex=1, two concurrent
|
||||
# sessions, overflow admission, slot recycling, non-silent output
|
||||
|
||||
@@ -141,6 +141,12 @@ The deploy config at
|
||||
is loaded automatically by the model registry (HF `model_type=voxcpm2`).
|
||||
Pass `--deploy-config <path>` to override.
|
||||
|
||||
The `engine_extras.hf_overrides.voxcpm2_runtime_config` section controls
|
||||
LocDiT CFM generation. `inference_timesteps`, `cfg_value`, and
|
||||
`cfg_cutoff_ratio` default to `10`, `2.0`, and `1.0`, respectively.
|
||||
`inference_timesteps` is clamped to at least `2`: with CFG-Zero*, a single
|
||||
step would skip all estimator calls and return the initial noise.
|
||||
|
||||
#### Verification
|
||||
|
||||
**Server cold-start**: ~60 s from `vllm serve` to `Application startup
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
- Vendor: Qwen
|
||||
- Model: `Qwen/Qwen-Image`
|
||||
- Task: Text-to-image generation
|
||||
- Mode: Online serving with optional step-wise continuous batching
|
||||
- Mode: Online serving with optional step-wise continuous batching; offline
|
||||
inference
|
||||
- Maintainer: Community
|
||||
|
||||
## When to use this recipe
|
||||
@@ -25,11 +26,14 @@ the benchmark assets already bundled in this repository.
|
||||
[`examples/online_serving/text_to_image/README.md`](../../examples/online_serving/text_to_image/README.md)
|
||||
- Related benchmark:
|
||||
[`benchmarks/diffusion/diffusion_benchmark_serving.py`](../../benchmarks/diffusion/diffusion_benchmark_serving.py)
|
||||
- Offline inference example used by the XPU section:
|
||||
[`examples/offline_inference/text_to_image/text_to_image.py`](../../examples/offline_inference/text_to_image/text_to_image.py)
|
||||
|
||||
## Hardware Support
|
||||
|
||||
This recipe currently documents one CUDA GPU serving configuration. Extend it
|
||||
with more hardware sections as community validation lands.
|
||||
This recipe documents CUDA GPU serving configurations and one Intel XPU
|
||||
offline-inference configuration. Extend it with more hardware sections as
|
||||
community validation lands.
|
||||
|
||||
## GPU
|
||||
|
||||
@@ -235,3 +239,59 @@ CUTLASS was the fastest validated backend for this checkpoint.
|
||||
- The benchmark table intentionally reports Qwen-Image-2512 as
|
||||
concurrency-1 request-level data; do not compare it directly with an online
|
||||
HTTP concurrency benchmark.
|
||||
|
||||
## XPU
|
||||
|
||||
### 1x Intel Arc Pro B70 (32 GB)
|
||||
|
||||
Offline text-to-image at 1024x1024, with layerwise offload streaming the
|
||||
transformer blocks so the 20B model fits one card.
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- torch: 2.13.0+xpu
|
||||
- vLLM: 0.29.0 (`98dff2a8`)
|
||||
- vLLM-Omni: `main` at `4c7a98c2`
|
||||
|
||||
#### Command
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Qwen/Qwen-Image \
|
||||
--prompt "a cup of coffee on the table" \
|
||||
--num-inference-steps 50 \
|
||||
--enable-layerwise-offload \
|
||||
--vae-use-tiling \
|
||||
--vae-use-slicing \
|
||||
--enforce-eager \
|
||||
--output qwen_image_output.png
|
||||
```
|
||||
|
||||
Given spare cards, `--enable-cpu-offload` shards the transformer instead of
|
||||
streaming it. It shortens the denoise loop but not the run, because the
|
||||
per-request model swap costs more than the loop saves.
|
||||
|
||||
```bash
|
||||
--tensor-parallel-size 4 \
|
||||
--enable-cpu-offload
|
||||
```
|
||||
|
||||
| Offload | Cards | Peak VRAM | Time |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `--enable-layerwise-offload` | 1 | 16.6 GiB | 50 s |
|
||||
| `--enable-cpu-offload` | 2 | 26.4 GiB | 85 s |
|
||||
| `--enable-cpu-offload` | 4 | 19.8 GiB | 70 s |
|
||||
| `--enable-cpu-offload` | 8 | 16.9 GiB | 87 s |
|
||||
|
||||
#### Verification
|
||||
|
||||
Confirm `qwen_image_output.png` is written and looks coherent for the prompt.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Memory usage: peak 16.6 GiB, about 55 s per image.
|
||||
- Known limitations: only offline generation was qualified. Step-wise continuous
|
||||
batching, ModelOpt quantization, and online serving are out of scope for this
|
||||
profile.
|
||||
@@ -53,7 +53,7 @@ vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \
|
||||
--deploy-config /path/to/your_qwen3_omni_overrides.yaml
|
||||
```
|
||||
|
||||
#### Runtime tuning
|
||||
### Runtime tuning
|
||||
|
||||
Prefer CLI overrides for day-to-day tuning:
|
||||
|
||||
@@ -75,7 +75,7 @@ vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \
|
||||
}'
|
||||
```
|
||||
|
||||
#### Stage-based launch (one stage per process)
|
||||
### Stage-based launch (one stage per process)
|
||||
|
||||
Use three terminals (one per stage). Start with the default commands below, then
|
||||
add `--max-num-seqs` only if you need explicit per-stage concurrency control.
|
||||
@@ -135,7 +135,7 @@ CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
|
||||
If you use custom deploy YAML, add `--deploy-config` to each stage command.
|
||||
|
||||
#### Verification
|
||||
### Verification
|
||||
|
||||
After server startup, run a multimodal example client:
|
||||
|
||||
@@ -185,7 +185,7 @@ python examples/online_serving/qwen3_omni/openai_realtime_client.py \
|
||||
--output-wav realtime_output.wav
|
||||
```
|
||||
|
||||
#### Benchmark with `vllm bench`
|
||||
### Benchmark with `vllm bench`
|
||||
|
||||
After the server is up, you can run online serving benchmarks with
|
||||
`vllm bench serve --omni`.
|
||||
@@ -240,8 +240,54 @@ vllm bench serve \
|
||||
--percentile-metrics ttft,tpot,itl,e2el
|
||||
```
|
||||
|
||||
#### Notes
|
||||
### Notes
|
||||
|
||||
- `/v1/realtime` is unsupported while `async_chunk` is enabled.
|
||||
- The default deploy uses `SharedMemoryConnector`; this is for single-host
|
||||
stage wiring.
|
||||
|
||||
## XPU
|
||||
|
||||
### 5x Intel Arc Pro B70 (32 GB)
|
||||
|
||||
Offline end-to-end text query returning both text and speech. The 62 GiB of
|
||||
BF16 thinker weights need four cards, so the `xpu:` block of
|
||||
`vllm_omni/deploy/qwen3_omni_moe.yaml` runs the thinker at TP=4 on cards 0-3
|
||||
and the talker and code2wav on card 4. It is the registry default here, so
|
||||
`--deploy-config` is not needed.
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- torch: 2.13.0+xpu
|
||||
- vLLM: 0.29.0 (`98dff2a8`)
|
||||
- vLLM-Omni: `main` at `4c7a98c2`
|
||||
|
||||
#### Command
|
||||
|
||||
The raised timeouts absorb the first decode step, which JIT-compiles the
|
||||
Triton MoE kernels.
|
||||
|
||||
```bash
|
||||
VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS=1800 \
|
||||
python examples/offline_inference/qwen3_omni/end2end.py \
|
||||
--model Qwen/Qwen3-Omni-30B-A3B-Instruct \
|
||||
--query-type text \
|
||||
--stage-init-timeout 1800 \
|
||||
--init-timeout 1800 \
|
||||
--output-wav qwen3omni_output
|
||||
```
|
||||
|
||||
#### Verification
|
||||
|
||||
`qwen3omni_output/` holds one `.txt` answer and the matching `.wav`. Confirm
|
||||
the transcript of the audio tracks the text response.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Runtime: about 260 s for a single text query.
|
||||
- The thinker MoE layers have no tuned Triton config for this shape and log
|
||||
`Using default MoE config. Performance might be sub-optimal!`.
|
||||
- Known limitations: only `--query-type text` was qualified. The audio, image,
|
||||
and video query types and online serving are out of scope for this profile.
|
||||
+10
-7
@@ -45,6 +45,7 @@ recipes/
|
||||
| [`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 |
|
||||
| [`black-forest-labs/FLUX.1-schnell.md`](./black-forest-labs/FLUX.1-schnell.md) | Offline text-to-image generation with CPU offload | 1x NVIDIA GeForce RTX 5090 32GB |
|
||||
| [`black-forest-labs/FLUX.2-klein.md`](./black-forest-labs/FLUX.2-klein.md) | Offline image editing (FLUX.2-klein-4B) | 1x Intel Arc Pro B70 32GB |
|
||||
| [`Boogu/Boogu-Image.md`](./Boogu/Boogu-Image.md) | Text-to-image online serving (Boogu-Image-0.1-Base) | 1x A100/H100 40GB+ |
|
||||
| [`BosonAI/Higgs-Audio-V3-TTS.md`](./BosonAI/Higgs-Audio-V3-TTS.md) | Online + offline multilingual TTS with voice cloning | 1x H100 80GB |
|
||||
| [`BreezeBlue/Breeze-TTS-2.md`](./BreezeBlue/Breeze-TTS-2.md) | Online + offline TTS (plain / voice design / voice clone / voice direction, streaming PCM) | 1x NVIDIA L20 48GB |
|
||||
@@ -62,9 +63,9 @@ recipes/
|
||||
| [`MiniMaxAI/MiniMax-H3.md`](./MiniMaxAI/MiniMax-H3.md) | T2VA, FL2VA, image+audio Ref2VA, and multi-video Ref2VA serving | 1x GPU with CPU offload / 4x B300 |
|
||||
| [`MiniMaxAI/MiniMax-H3-Spark-GB10.md`](./MiniMaxAI/MiniMax-H3-Spark-GB10.md) | MiniMax H3 T2VA and Ref2VA on DGX Spark | 1x or 2x DGX Spark (GB10) |
|
||||
| [`MiniMaxAI/MiniMax-H3-MUSA.md`](./MiniMaxAI/MiniMax-H3-MUSA.md) | MiniMax H3 T2VA, FL2VA, and Ref2VA on MUSA | 4x MTT S5000 (TP4 Ref2VA profile) |
|
||||
| [`krea/Krea-2.md`](./krea/Krea-2.md) | Text-to-image (Turbo + Raw), offline + online, with LoRA | 1x H100 80GB |
|
||||
| [`LTX/LTX-2.md`](./LTX/LTX-2.md) | LTX-2/LTX-2.3 text-to-video and image-to-video with synchronized audio | H200 141GB / 96GB-class GPU |
|
||||
| [`LTX/LTX-2.5.md`](./LTX/LTX-2.5.md) | LTX-2.5-Diffusers: Full/SFT one-stage and distilled two-stage T2V/I2V with synchronized audio | NVIDIA B300; cuDNN-qualified |
|
||||
| [`krea/Krea-2.md`](./krea/Krea-2.md) | Text-to-image (Turbo + Raw), offline + online, with LoRA | 1x H100 80GB / 1x Intel Arc Pro B70 32GB |
|
||||
| [`LTX/LTX-2.md`](./LTX/LTX-2.md) | LTX-2/LTX-2.3 text-to-video and image-to-video with synchronized audio | H200 141GB / 96GB-class GPU / 1x Intel Arc Pro B70 32GB |
|
||||
| [`LTX/LTX-2.5.md`](./LTX/LTX-2.5.md) | LTX-2.5-Diffusers: Full/SFT one-stage and distilled two-stage T2V/I2V with synchronized audio | NVIDIA B300; cuDNN-qualified / 1x Intel Arc Pro B70 32GB |
|
||||
| [`MammothModa2/MammothModa2.md`](./MammothModa2/MammothModa2.md) | Preview and Dev text-to-image (AR → DiT); Dev text/image understanding | Preview: 1x L40S 48GB / 1x ≥40GB GPU; Dev: 1x NVIDIA GPU with sufficient cache headroom |
|
||||
| [`NVIDIA/SANA-Video-2B.md`](./NVIDIA/SANA-Video-2B.md) | Native and Diffusers-adapter 480p/720p T2V+I2V | 1x RTX 5090 32GB |
|
||||
| [`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 |
|
||||
@@ -82,10 +83,10 @@ recipes/
|
||||
| [`NVIDIA/Nemotron-Labs-Audex.md`](./NVIDIA/Nemotron-Labs-Audex.md) | TTS / text-to-audio / audio understanding / cascaded S2S (Audex 2B + 30B-A3B) | 1x H100 80GB |
|
||||
| [`NVIDIA/NemotronLabs-VoiceChat.md`](./NVIDIA/NemotronLabs-VoiceChat.md) | Offline speech-to-speech voice chat (11B, frame-locked 12.5 Hz, 3-stage thinker/talker/code2wav) | 1x H100 80GB |
|
||||
| [`NVIDIA/SANA-WM.md`](./NVIDIA/SANA-WM.md) | First-frame image-to-video serving with camera control | 1x 24GB+ CUDA GPU; ~40GB disk |
|
||||
| [`Qwen/Qwen-Image.md`](./Qwen/Qwen-Image.md) | Text-to-image serving with step-wise continuous batching replay and ModelOpt mixed FP8/NVFP4 | 1x A100 80GB / 2x B200 |
|
||||
| [`Qwen/Qwen-Image.md`](./Qwen/Qwen-Image.md) | Text-to-image serving with step-wise continuous batching replay and ModelOpt mixed FP8/NVFP4; offline text-to-image on XPU | 1x A100 80GB / 2x B200 / 1x Intel Arc Pro B70 32GB |
|
||||
| [`Qwen/Qwen-Image-2512.md`](./Qwen/Qwen-Image-2512.md) | Text-to-image serving with step-wise continuous batching replay and ModelOpt FP8 / mixed FP8/NVFP4 | 1x A800 80GB / 2x B200 |
|
||||
| [`Qwen/Qwen-Image-Edit.md`](./Qwen/Qwen-Image-Edit.md) | Text-guided single-image editing | 1x or 2x H200 141GB |
|
||||
| [`Qwen/Qwen3-Omni.md`](./Qwen/Qwen3-Omni.md) | Online serving for multimodal chat | 1x A100 80GB |
|
||||
| [`Qwen/Qwen3-Omni.md`](./Qwen/Qwen3-Omni.md) | Online serving for multimodal chat; offline end-to-end text-to-speech on XPU | 1x A100 80GB / 5x Intel Arc Pro B70 32GB |
|
||||
| [`Qwen/Qwen3-TTS.md`](./Qwen/Qwen3-TTS.md) | Text-to-speech serving (CustomVoice / VoiceDesign / Base) | 1x H100/A100 80GB / 1x AMD MI300X |
|
||||
| [`SenseNova/SenseNova-U1.md`](./SenseNova/SenseNova-U1.md) | Unified image generation and understanding | 1x or 2x H200 / 1x AMD MI300X |
|
||||
| [`SenseNova/SenseNova-U1.5.md`](./SenseNova/SenseNova-U1.5.md) | Unified image generation and understanding, with an 8-step distilled LoRA | 1x A800 80GB |
|
||||
@@ -93,13 +94,15 @@ recipes/
|
||||
| [`Tencent/AuK-H100.md`](./Tencent/AuK-H100.md) | Offline instruction-driven TTS, speech editing, enhancement and separation (AuK base and Flash) | 1x H100 80GB |
|
||||
| [`Tencent/Covo-Audio-Chat.md`](./Tencent/Covo-Audio-Chat.md) | Online serving for audio chat | 1x A100 80GB |
|
||||
| [`Tencent/HunyuanImage-3.0-Instruct.md`](./Tencent/HunyuanImage-3.0-Instruct.md) | DiT-only text-to-image serving and benchmark, including ModelOpt mixed FP8/NVFP4 | 4x H100/H800 80GB / 2x B200 |
|
||||
| [`Wan-AI/Wan2.2-T2V.md`](./Wan-AI/Wan2.2-T2V.md) | Text-to-video serving (Wan2.2 14B) with Skip-Softmax sparse attention | 1x datacenter Blackwell (B200/B300) |
|
||||
| [`Tencent/HunyuanVideo-1.5.md`](./Tencent/HunyuanVideo-1.5.md) | Offline text-to-video (HunyuanVideo-1.5 Diffusers) | 1x Intel Arc Pro B70 32GB |
|
||||
| [`Tongyi-MAI/Z-Image-Turbo.md`](./Tongyi-MAI/Z-Image-Turbo.md) | Offline few-step text-to-image (Z-Image-Turbo) | 1x Intel Arc Pro B70 32GB |
|
||||
| [`Wan-AI/Wan2.2-T2V.md`](./Wan-AI/Wan2.2-T2V.md) | Text-to-video serving (Wan2.2 14B) with Skip-Softmax sparse attention | 8x H20/H100/A100 / 1x datacenter Blackwell (B200/B300) / 1x Intel Arc Pro B70 32GB |
|
||||
| [`Wan-AI/Wan2.2-TI2V-5B-A100.md`](./Wan-AI/Wan2.2-TI2V-5B-A100.md) | Unified 720p text-to-video and image-to-video serving (Wan2.2 5B) | 1x A100-SXM4-80GB |
|
||||
| [`Wan-AI/Wan2.2-I2V.md`](./Wan-AI/Wan2.2-I2V.md) | Image-to-video serving (Wan2.2 14B) | 8x Ascend NPU (A2/A3) |
|
||||
| [`Wan-AI/Wan2.2-S2V.md`](./Wan-AI/Wan2.2-S2V.md) | Speech-to-video serving (Wan2.2 14B) | 2x A100/H100 80GB |
|
||||
| [`Wan-AI/Wan2.1-VACE.md`](./Wan-AI/Wan2.1-VACE.md) | Unified T2V, I2V, V2LF, FLF2V, inpaint, and R2V | 1x RTX 5090 (1.3B) / 1x L40S 48GB with layerwise offload (14B) |
|
||||
| [`XiaomiMiMo/MiMo-Audio.md`](./XiaomiMiMo/MiMo-Audio.md) | Offline + online omni audio (TTS, ASR, dialogue) | 1x RTX 5090 / 5090D 32GB |
|
||||
| [`StabilityAI/Stable-Diffusion-3.5.md`](./StabilityAI/Stable-Diffusion-3.5.md) | Text-to-image serving (SD 3.5-medium and SD 3.5-large) | 1x RTX A6000 48GB |
|
||||
| [`StabilityAI/Stable-Diffusion-3.5.md`](./StabilityAI/Stable-Diffusion-3.5.md) | Text-to-image serving (SD 3.5-medium and SD 3.5-large) | 1x RTX A6000 48GB / 1x Intel Arc Pro B70 32GB |
|
||||
| [`zai-org/GLM-TTS.md`](./zai-org/GLM-TTS.md) | Online serving for Chinese/English zero-shot voice-cloned TTS | 1x A40 48GB / 2x non-standard RTX 4090 48GB (49,140 MiB each) |
|
||||
| [`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+ |
|
||||
|
||||
@@ -79,6 +79,9 @@ python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
classifier-free guidance; the default `4.0` applies guidance twice and produces a
|
||||
blown-out, posterised image.
|
||||
|
||||
Online FP8 does not support the 8-step distilled LoRA.
|
||||
Use BF16 without `--quantization fp8` for this LoRA.
|
||||
|
||||
##### Online serving
|
||||
|
||||
```bash
|
||||
@@ -90,7 +93,39 @@ python examples/online_serving/sensenova_u1/openai_chat_client.py \
|
||||
|
||||
`-s` takes the base URL; the client appends `/v1` itself.
|
||||
|
||||
#### Measured latency (1x A800 80GB, 25 steps, median of 3 after a warmup)
|
||||
##### Online FP8 quantization
|
||||
|
||||
`--quantization fp8` quantizes only eligible attention and MLP linears in the
|
||||
`language_model` understanding and generation branches at load time. Other layers
|
||||
retain their existing precision.
|
||||
|
||||
On the tested A800 (SM80) setup with vLLM 0.29.0,
|
||||
`VLLM_DISABLED_KERNELS=CutlassFP8ScaledMMLinearKernel` is **required** for online
|
||||
FP8 to start successfully. Without it, the selected CUTLASS kernel fails during
|
||||
the startup dummy run with `RuntimeError: cutlass_scaled_mm_sm80_epilogue`.
|
||||
Disabling this kernel selects Marlin W8A16 (FP8 weights with BF16 activations).
|
||||
|
||||
```bash
|
||||
VLLM_DISABLED_KERNELS=CutlassFP8ScaledMMLinearKernel \
|
||||
vllm serve sensenova/SenseNova-U1.5-8B-MoT --omni \
|
||||
--quantization fp8 --port 8091
|
||||
```
|
||||
|
||||
#### Measured BF16 vs FP8 performance (1x A800 80GB, 28 steps, P95 over 10 prompts)
|
||||
|
||||
vLLM 0.29.0, BF16 base dtype, seed 42, TORCH_SDPA, eager execution, TP=1, batch size 1,
|
||||
paged decode off, no LoRA. Each resolution uses the same 10 prompts for BF16 and FP8.
|
||||
Both runs set `VLLM_DISABLED_KERNELS=CutlassFP8ScaledMMLinearKernel`.
|
||||
|
||||
| Resolution | Peak reserved memory (BF16 → FP8) | Step latency (BF16 → FP8) |
|
||||
| --- | --- | --- |
|
||||
| 512x512 | 33.166 → 18.697 GiB | 63.674 → 81.136 ms |
|
||||
| 1024x1024 | 33.555 → 18.561 GiB | 187.867 → 229.115 ms |
|
||||
|
||||
Step latency is the P95 of per-request mean step times, excluding warmup requests; FP8 reduced
|
||||
memory usage but increased step latency.
|
||||
|
||||
#### Measured BF16 latency (1x A800 80GB, 25 steps, median of 3 after a warmup)
|
||||
|
||||
| Resolution | Step latency | Total |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -34,6 +34,126 @@ text-to-text chat.
|
||||
- HuggingFace model page:
|
||||
[SenseNova/SenseNova-U1-8B-MoT](https://huggingface.co/SenseNova/SenseNova-U1-8B-MoT)
|
||||
|
||||
## A3B (MoE) variant
|
||||
|
||||
[sensenova/SenseNova-U1-A3B-MoT](https://huggingface.co/sensenova/SenseNova-U1-A3B-MoT)
|
||||
runs on this same recipe and the same `SenseNovaU1Pipeline` — the checkpoint keeps
|
||||
`model_type: neo_chat`, so it resolves without `--model-class-name`. Only the `--model`
|
||||
id changes.
|
||||
|
||||
The backbone is Qwen3-MoE instead of dense Qwen3, and both MoT branches are sparse:
|
||||
`mlp` routes over 128 experts and `mlp_mot_gen` over 32, top-8 each, with per-expert
|
||||
width `moe_intermediate_size=768`. `SenseNovaU1Config` picks `SenseNovaU1MoELLMConfig`
|
||||
whenever `llm_config` carries `num_experts`, so the dense 8B path is untouched. The
|
||||
gen-path knobs (`gen_num_experts`, `gen_num_experts_per_tok`, `gen_moe_intermediate_size`)
|
||||
fall back to their understanding-path counterparts when absent.
|
||||
|
||||
Experts run through vLLM's `FusedMoE`, whose `moe_forward` op resolves its layer through
|
||||
`vllm.forward_context` rather than the diffusion runner's own context, so
|
||||
`SenseNovaU1ForCausalLM.forward` enters that context when the model has MoE layers.
|
||||
|
||||
### Memory
|
||||
|
||||
The weights dominate, and every expert stays resident even though only the top-8 of each
|
||||
tower run per token -- the "A3B" in the name is the ~4B activated, not the 38.7B stored.
|
||||
Counted from the checkpoint's safetensors headers:
|
||||
|
||||
| Component | Params | BF16 |
|
||||
| --- | --- | --- |
|
||||
| Understanding-path experts (128/layer) | 28.99 B | 54.0 GiB |
|
||||
| Generation-path experts (32/layer) | 7.25 B | 13.5 GiB |
|
||||
| Attention (dual tower) | 1.81 B | 3.4 GiB |
|
||||
| Embedding + LM head | 0.62 B | 1.2 GiB |
|
||||
| Routers, norms, vision and flow-matching heads | 0.06 B | 0.1 GiB |
|
||||
| Total | 38.74 B | 72.2 GiB |
|
||||
|
||||
At 1024x1024 the 72.9 GiB peak is those 72.2 GiB of weights plus about 0.7 GiB of
|
||||
activations and workspace. The 2048x2048 edit, the largest activation case below, peaks at
|
||||
74.6 GiB, 2.4 GiB above the weights. Text-to-image only routes through the generation tower,
|
||||
leaving the 54 GiB of understanding-path experts idle; that tower is reached by the text
|
||||
prefix, think mode, `img2text` and `text2text`. One 96 GB card fits TP=1, and TP=2 halves
|
||||
the per-GPU footprint.
|
||||
|
||||
### Measured (1x H20 96GB, BF16)
|
||||
|
||||
Fixed case used for the stage split and the torch.profiler kernel table:
|
||||
|
||||
- Prompt: `Close portrait of an elderly woman by a farmhouse window, warm natural light.`
|
||||
- 1024x1024, 50 Euler steps, think off, seed 42, `cfg_scale` 4.0, `cfg_norm` none,
|
||||
`timestep_shift` 3.0, `t_eps` 0.02
|
||||
- Python 3.12, vLLM 0.30.0, torch 2.13.0+cu130
|
||||
- Peak GPU memory is the reserved high-water mark the runner records (`peak_memory_mb`)
|
||||
|
||||
| | Time | Memory |
|
||||
| --- | --- | --- |
|
||||
| Weights after load | ~13 s | 72.2 GiB |
|
||||
| Pipeline `forward` | 7.68 s | 72.9 GiB peak |
|
||||
|
||||
`--enable-diffusion-pipeline-profiler` on that request:
|
||||
|
||||
| Stage | Time | Share |
|
||||
| --- | --- | --- |
|
||||
| Text prefix (`_t2i_prefix_forward`, understanding tower) | 0.06 s | 0.7% |
|
||||
| Denoising loop (`_run_denoising_loop`, generation tower, 50 steps) | 7.56 s | 98.5% |
|
||||
| Pipeline `forward` | 7.68 s | 100% |
|
||||
|
||||
There is no VAE. The loop ends in unpatchify / denorm / PIL, which is inside the denoise
|
||||
row and is a few tens of milliseconds at 1024x1024.
|
||||
|
||||
A warm 8-step generate at the same resolution, wrapped in `torch.profiler` after one
|
||||
unprofiled warmup request, spent 1.19 s of self-CUDA time as:
|
||||
|
||||
| Kernel family | Self CUDA | Share | What it is |
|
||||
| --- | --- | --- | --- |
|
||||
| `fused_moe_kernel` | 576 ms | 48% | top-8 expert GEMMs on the 32-expert generation tower |
|
||||
| FlashAttention `flash_fwd_kernel` | 222 ms | 19% | dual-tower attention |
|
||||
| `nvjet` GEMMs | ~233 ms | 20% | `qkv_proj` / `o_proj` (and the small dense leftovers) |
|
||||
| `qk_norm_rope_kernel` | 58 ms | 5% | fused RMSNorm + RoPE |
|
||||
| MoE routing / `act_and_mul` / elementwise | ~40 ms | 3% | top-k, SwiGLU, residual |
|
||||
|
||||
So a denoise step is about half FusedMoE and about two-fifths attention + dense GEMM.
|
||||
`--cfg-parallel-size 2` on the same 8-step t2i request (2-branch `cond`+`uncond`)
|
||||
is 0.95 s vs 1.48 s on one GPU (~1.56x). Img2img CFG-P=2 also completes for both
|
||||
branch counts at 8-step 1024x1024: 2-way (`img_cfg_scale=1`, `cond`+`img_cond`)
|
||||
1.09 s; 3-way (`cfg_scale=4`, `img_cfg_scale=2`, `cond`+`img_cond`+`uncond`)
|
||||
1.74 s (3 branches on 2 ranks dispatch as `[[0, 2], [1]]`). Cache-DiT and
|
||||
layerwise CPU offload do not take effect on A3B; see
|
||||
[diffusion features](../../docs/user_guide/diffusion_features.md) note 8.
|
||||
|
||||
Other configurations on the same machine (second run, warm kernels):
|
||||
|
||||
| Case | Total | Peak GPU memory |
|
||||
| --- | --- | --- |
|
||||
| text2img 1024x1024, 50 steps, think on | 9.16 s | 73.0 GiB |
|
||||
| text2img 1024x1024, 50 steps, think off, TP=2 | 5.64 s | 36.7 GiB per GPU |
|
||||
| img2img 2048x2048 output, 25 steps, think off | 20.93 s | 74.6 GiB |
|
||||
|
||||
Think-on spends 1.37 s in `_generate_think` (215 tokens). At the same seed TP=2 differs
|
||||
from TP=1 only numerically (MAE 2.97/255, identical composition) from the changed
|
||||
reduction order.
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model sensenova/SenseNova-U1-A3B-MoT \
|
||||
--prompt "Close portrait of an elderly woman by a farmhouse window, warm natural light." \
|
||||
--width 1024 --height 1024 \
|
||||
--seed 42 --num-inference-steps 50 --cfg-scale 4.0 \
|
||||
--extra-body '{"think": false, "cfg_norm": "none", "timestep_shift": 3.0, "t_eps": 0.02}' \
|
||||
--output sensenova_a3b_t2i.png
|
||||
```
|
||||
|
||||
`img2text` and `text2text` go through the server, as for the dense checkpoint:
|
||||
|
||||
```bash
|
||||
vllm serve sensenova/SenseNova-U1-A3B-MoT --omni --port 8091
|
||||
|
||||
python examples/online_serving/sensenova_u1/openai_chat_client.py \
|
||||
-s http://127.0.0.1:8091 -m img2text -i input.png -p "Describe this image."
|
||||
```
|
||||
|
||||
All four modalities were verified on A3B: text2img (with and without think), img2img
|
||||
editing, img2text and text2text.
|
||||
|
||||
## Hardware Support
|
||||
|
||||
## GPU
|
||||
|
||||
@@ -137,3 +137,39 @@ print('saved output.png')
|
||||
- Image quality is noticeably better than medium, with sharper details.
|
||||
- `negative_prompt` requires `guidance_scale > 1` to take effect; SD3 defaults to 1.0 (no CFG).
|
||||
- 2048×2048 fits within 48 GB but leaves little headroom (~45.5 GiB used out of 49 GiB).
|
||||
|
||||
## XPU
|
||||
|
||||
### 1x Intel Arc Pro B70 (32 GB) — stable-diffusion-3.5-medium
|
||||
|
||||
Offline text-to-image at 1024x1024 with BF16 weights fully resident.
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- torch: 2.13.0+xpu
|
||||
- vLLM: 0.29.0 (`98dff2a8`)
|
||||
- vLLM-Omni: `main` at `4c7a98c2`
|
||||
|
||||
#### Command
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model stabilityai/stable-diffusion-3.5-medium \
|
||||
--prompt "a sunset over mountains, photorealistic" \
|
||||
--num-inference-steps 28 \
|
||||
--vae-use-tiling \
|
||||
--output sd35_medium_output.png
|
||||
```
|
||||
|
||||
#### Verification
|
||||
|
||||
Confirm `sd35_medium_output.png` is written and matches the prompt.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Memory usage: peak 19.5 GiB against 29.8 GiB free.
|
||||
- Known limitations: only `stable-diffusion-3.5-medium` offline generation was
|
||||
qualified. `stable-diffusion-3.5-large` and online serving are out of scope
|
||||
for this profile.
|
||||
@@ -12,7 +12,7 @@
|
||||
both with `Qwen/Qwen2.5-Omni-3B` as the frozen encoder
|
||||
- Task: zero-shot TTS, instruct TTS, content and acoustic editing,
|
||||
paralinguistic editing, speech enhancement, speaker and music separation
|
||||
- Mode: offline inference (online chat serving is not yet qualified)
|
||||
- Mode: offline inference and online `/v1/audio/speech` (online chat serving is not yet qualified)
|
||||
- Hardware: 1x H100 80GB
|
||||
- Maintainer: Community
|
||||
|
||||
@@ -130,17 +130,35 @@ Measured on one H100 against the upstream implementation with the same seeds
|
||||
and mean VAE latents (four cookbook cases, base checkpoint): identical
|
||||
transcripts, speaker similarity to the upstream output 0.993 to 0.999, log-mel
|
||||
L1 0.11 to 0.47 (the upstream VAE-resampling noise floor is 0.39). Flash:
|
||||
0.989 to 0.999 and 0.24 to 0.51. Wall per request on the second call: base
|
||||
1.0 to 2.0 s for 3.5 to 10.9 s of audio, Flash 0.17 to 0.30 s; engine start
|
||||
about 47 s with a warm page cache.
|
||||
0.989 to 0.999 and 0.24 to 0.51.
|
||||
|
||||
Latency with the DiT and codec CUDA graphs, measured end to end on one
|
||||
H200-class GPU with both stages on it, concurrency 1, median of 10 warm
|
||||
requests: base (32 steps, CFG 2) zero-shot 0.25 / 0.31 / 0.40 s for 3 / 6 /
|
||||
12 s of audio, instruct TTS 0.17 / 0.23 / 0.32 s; Flash zero-shot 0.08 /
|
||||
0.10 / 0.14 s. The first request after start is 1.2 to 1.6 s, because the
|
||||
common shapes are compiled at startup. Engine start adds the codec decode
|
||||
compile and the DiT warmup to model loading (a few minutes on a cold
|
||||
Inductor cache, about 30 s less warm).
|
||||
|
||||
## Notes
|
||||
|
||||
- Memory usage: about 25 GB peak on the device for a 12 s generation with
|
||||
both stages resident (deploy defaults: encoder 0.45, diffusion stage 0.35
|
||||
of device memory).
|
||||
- Key flags: `enforce_eager` on both stages (the encoder walks the decoder
|
||||
layers itself for the layer fusion). `enable_prefix_caching` must stay off
|
||||
of device memory), plus about 1 GB of CUDA graph pools for the compiled
|
||||
codec decode buckets.
|
||||
- Key flags: `enforce_eager` on the encoder stage (it walks the decoder
|
||||
layers itself for the layer fusion); the diffusion stage runs with
|
||||
`enforce_eager: false` so the codec decode is compiled into bucketed CUDA
|
||||
graphs at startup (the deploy config sets 160/320/640 latent frames, i.e.
|
||||
up to 12.8 s; longer clips are decoded in overlapping 640-frame tiles of
|
||||
the same graph;
|
||||
override with `model_config.auk_vae_compile_shapes` and
|
||||
`auk_vae_tile_frames`). Each DiT denoise step replays a per-shape CUDA
|
||||
graph of the regionally compiled double- and single-stream blocks; the
|
||||
per-request conditioning (text projection, reference embedding, padding
|
||||
biases, rotary tables) is prepared once per request outside the graph.
|
||||
`enable_prefix_caching` must stay off
|
||||
for the encoder: a cache hit skips prompt positions that the fused
|
||||
condition needs. `enable_chunked_prefill` is off by default: forcing it
|
||||
(128-token chunks, so two to three chunks per prompt) reproduces the
|
||||
@@ -161,7 +179,7 @@ about 47 s with a warm page cache.
|
||||
| --- | --- | --- |
|
||||
| Offline `Omni.generate` | supported (base and Flash) | `docs/getting_started/quickstart.md` |
|
||||
| Online `/v1/chat/completions` with audio | not yet qualified | `docs/serving/` |
|
||||
| `/v1/audio/speech` | not supported (needs a TTS adapter) | `docs/contributing/model/adding_tts_model.md` |
|
||||
| `/v1/audio/speech` | supported (base and Flash, `instructions` or `task_type`) | `docs/user_guide/examples/online_serving/text_to_speech.md` |
|
||||
| Streaming / async chunk | not supported | `docs/design/feature/async_chunk.md` |
|
||||
| Batching across requests | one request per DiT forward | `docs/user_guide/diffusion/` |
|
||||
| Tensor / sequence parallelism | not supported | `docs/configuration/composable_parallel.md` |
|
||||
@@ -0,0 +1,72 @@
|
||||
# HunyuanVideo 1.5
|
||||
|
||||
> Text-to-video with the HunyuanVideo-1.5 Diffusers checkpoints
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: Tencent (community Diffusers ports)
|
||||
- Model: `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v`
|
||||
- Task: Text-to-video
|
||||
- Mode: Offline inference
|
||||
- Maintainer: Community
|
||||
|
||||
## When to use this recipe
|
||||
|
||||
Use this recipe to generate a short clip with HunyuanVideo-1.5 on a memory-
|
||||
constrained card, trading output size for capacity.
|
||||
|
||||
## References
|
||||
|
||||
- Model card:
|
||||
<https://huggingface.co/hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v>
|
||||
- Shared runnable example:
|
||||
[`examples/offline_inference/text_to_video/text_to_video.py`](../../examples/offline_inference/text_to_video/text_to_video.py)
|
||||
- CPU offload guide:
|
||||
[`docs/user_guide/diffusion/cpu_offload.md`](../../docs/user_guide/diffusion/cpu_offload.md)
|
||||
|
||||
## Hardware Support
|
||||
|
||||
This recipe documents one validated Intel XPU configuration. Extend it with
|
||||
more hardware sections as community validation lands.
|
||||
|
||||
## XPU
|
||||
|
||||
### 1x Intel Arc Pro B70 (32 GB)
|
||||
|
||||
BF16 weights with model-level CPU offload at a reduced 320x240, 17-frame
|
||||
shape. The 480p and 720p T2V and I2V checkpoints all use the same command; only
|
||||
`--model` changes.
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- torch: 2.13.0+xpu
|
||||
- vLLM: 0.29.0 (`98dff2a8`)
|
||||
- vLLM-Omni: `main` at `4c7a98c2`
|
||||
|
||||
#### Command
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_video/text_to_video.py \
|
||||
--model hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v \
|
||||
--prompt "A serene lakeside sunrise with mist over the water." \
|
||||
--height 240 --width 320 --num-frames 17 \
|
||||
--num-inference-steps 50 \
|
||||
--guidance-scale 6.0 \
|
||||
--enable-cpu-offload \
|
||||
--vae-use-tiling \
|
||||
--vae-use-slicing \
|
||||
--output hunyuan_video_15_output.mp4
|
||||
```
|
||||
|
||||
#### Verification
|
||||
|
||||
Confirm `hunyuan_video_15_output.mp4` decodes and that the sampled frames match
|
||||
the prompt.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Memory usage: 18.3 GiB loaded, 25.7 GiB peak, about 39 s per clip.
|
||||
- Known limitations: the native 720p geometry does not fit a 32 GB card with
|
||||
CPU offload. Online serving is out of scope for this profile.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Z-Image Turbo
|
||||
|
||||
> Few-step text-to-image with `Tongyi-MAI/Z-Image-Turbo`
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: Tongyi-MAI
|
||||
- Model: `Tongyi-MAI/Z-Image-Turbo`
|
||||
- Task: Text-to-image
|
||||
- Mode: Offline inference
|
||||
- Maintainer: Community
|
||||
|
||||
## When to use this recipe
|
||||
|
||||
Use this recipe to generate 1024x1024 images with the distilled Z-Image Turbo
|
||||
checkpoint through the shared offline text-to-image example.
|
||||
|
||||
## References
|
||||
|
||||
- Model card: <https://huggingface.co/Tongyi-MAI/Z-Image-Turbo>
|
||||
- Shared runnable example:
|
||||
[`examples/offline_inference/text_to_image/text_to_image.py`](../../examples/offline_inference/text_to_image/text_to_image.py)
|
||||
- CPU offload guide:
|
||||
[`docs/user_guide/diffusion/cpu_offload.md`](../../docs/user_guide/diffusion/cpu_offload.md)
|
||||
|
||||
## Hardware Support
|
||||
|
||||
This recipe documents one validated Intel XPU configuration. Extend it with
|
||||
more hardware sections as community validation lands.
|
||||
|
||||
## XPU
|
||||
|
||||
### 1x Intel Arc Pro B70 (32 GB)
|
||||
|
||||
BF16 weights with model-level CPU offload and VAE tiling/slicing, 25 steps.
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- torch: 2.13.0+xpu
|
||||
- vLLM: 0.29.0 (`98dff2a8`)
|
||||
- vLLM-Omni: `main` at `4c7a98c2`
|
||||
|
||||
#### Command
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_image/text_to_image.py \
|
||||
--model Tongyi-MAI/Z-Image-Turbo \
|
||||
--prompt "a cup of coffee on the table" \
|
||||
--num-inference-steps 25 \
|
||||
--enable-cpu-offload \
|
||||
--vae-use-tiling \
|
||||
--vae-use-slicing \
|
||||
--enforce-eager \
|
||||
--output z_image_turbo_output.png
|
||||
```
|
||||
|
||||
#### Verification
|
||||
|
||||
Confirm `z_image_turbo_output.png` is written as a 1024x1024 PNG matching the
|
||||
prompt.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Memory usage: peak 15.6 GiB, about 58 s per image.
|
||||
- Known limitations: only offline generation was qualified. Online serving is
|
||||
out of scope for this profile.
|
||||
@@ -85,14 +85,78 @@ For online serving client examples and request formats, see
|
||||
#### Notes
|
||||
|
||||
- **Key flags:**
|
||||
- `--omni` — enables vLLM-Omni diffusion serving.
|
||||
- `--use-hsdp` — Hybrid Sharded Data Parallelism for the 14B DiT weights.
|
||||
- `--usp <N>` — Unified (Ulysses) Sequence Parallelism degree.
|
||||
- `--cfg-parallel-size <N>` — CFG parallelism; set to 2 for the official model,
|
||||
- `--omni` — enables vLLM-Omni diffusion serving.
|
||||
- `--use-hsdp` — Hybrid Sharded Data Parallelism for the 14B DiT weights.
|
||||
- `--usp <N>` — Unified (Ulysses) Sequence Parallelism degree.
|
||||
- `--cfg-parallel-size <N>` — CFG parallelism; set to 2 for the official model,
|
||||
omit for distilled checkpoints.
|
||||
- `--vae-patch-parallel-size 8` / `--vae-use-tiling` — parallel + tiled VAE
|
||||
- `--vae-patch-parallel-size 8` / `--vae-use-tiling` — parallel + tiled VAE
|
||||
decoding; disabling patch parallelism can significantly increase VAE latency.
|
||||
|
||||
## XPU
|
||||
|
||||
### 1x Intel Arc Pro B70 (32 GB)
|
||||
|
||||
Offline text-to-video at a reduced 576x320, 17-frame shape. Layerwise offload
|
||||
streams the two 14B expert transformers; VAE slicing bounds the decode
|
||||
activations.
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- torch: 2.13.0+xpu
|
||||
- vLLM: 0.29.0 (`98dff2a8`)
|
||||
- vLLM-Omni: `main` at `4c7a98c2`
|
||||
|
||||
#### Command
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_video/text_to_video.py \
|
||||
--model Wan-AI/Wan2.2-T2V-A14B-Diffusers \
|
||||
--prompt "A serene lakeside sunrise with mist over the water." \
|
||||
--negative-prompt "blurry, noisy, artifacts, distorted, low quality, streaks, sparkles, blocky, pixelated" \
|
||||
--height 320 --width 576 --num-frames 17 \
|
||||
--num-inference-steps 60 \
|
||||
--guidance-scale 4.0 \
|
||||
--flow-shift 5.0 \
|
||||
--boundary-ratio 0.875 \
|
||||
--enable-layerwise-offload \
|
||||
--vae-use-slicing \
|
||||
--enforce-eager \
|
||||
--seed 12345 \
|
||||
--output wan22_t2v_output.mp4
|
||||
```
|
||||
|
||||
Given spare cards, `--enable-cpu-offload` shards the two expert transformers
|
||||
instead of streaming them, which nearly halves the denoise loop. Four is the
|
||||
fastest count; eight is slower.
|
||||
|
||||
```bash
|
||||
--tensor-parallel-size 4 \
|
||||
--enable-cpu-offload
|
||||
```
|
||||
|
||||
| Offload | Cards | Peak VRAM | Denoise |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `--enable-layerwise-offload` | 1 | 9.9 GiB | 160 s |
|
||||
| `--enable-cpu-offload` | 2 | 18.9 GiB | 113 s |
|
||||
| `--enable-cpu-offload` | 4 | 12.4 GiB | 86 s |
|
||||
|
||||
#### Verification
|
||||
|
||||
Confirm `wan22_t2v_output.mp4` decodes and that frame 0 is clean.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Key flags: `--boundary-ratio 0.875` sets the high-noise to low-noise expert
|
||||
switch. `--flow-shift 5.0` with `--guidance-scale 4.0` over 60 steps clears
|
||||
the frame-0 VAE corruption seen at the stock 50-step, `flow_shift=12.0`
|
||||
settings.
|
||||
- Known limitations: only offline T2V at 576x320 was qualified. Larger shapes,
|
||||
TI2V, HSDP, CFG parallelism, and online serving are out of scope for this
|
||||
profile.
|
||||
|
||||
## Accelerating with TRTLLM_ATTN + Skip-Softmax (datacenter Blackwell)
|
||||
|
||||
On **datacenter Blackwell** (sm_100 / sm_103), the mask-free Wan pipeline
|
||||
@@ -153,7 +217,7 @@ instead of `target_sparsity`):
|
||||
### Skip-Softmax controls
|
||||
|
||||
| Key | Valid values | Meaning |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| `target_sparsity` | finite, `[0, 1]` | Operating point on the checkpoint's calibrated curve (`a·exp(b·s)`). |
|
||||
| `threshold` | finite, `≥ 0` | Direct skip threshold, no calibration needed. Mutually exclusive with `target_sparsity`. |
|
||||
| `disabled_until_timestep` | finite, `[0, 1]` | Keeps early, high-noise steps dense; skip turns on once the normalized timestep `t ≤ D`. |
|
||||
@@ -169,7 +233,7 @@ Measured on B300 / SM103, 1280×720 / 81f / 50 steps, torch.compile, official
|
||||
ModelOpt calibration (speedup and divergence relative to dense):
|
||||
|
||||
| config | speedup | LPIPS (divergence vs dense) |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| dense | 1.000x | - |
|
||||
| `s=0.65, D=0.86` | 1.062x | 0.041 |
|
||||
| `s=0.75, D=1.00` | 1.181x | 0.377 |
|
||||
@@ -181,6 +245,6 @@ ModelOpt calibration (speedup and divergence relative to dense):
|
||||
`flashinfer`. Elsewhere, or without FlashInfer, selecting `TRTLLM_ATTN` raises
|
||||
rather than silently degrading.
|
||||
- **Known limitations:**
|
||||
- Skip-Softmax is not supported under ring sequence parallelism; use Ulysses SP
|
||||
- Skip-Softmax is not supported under ring sequence parallelism; use Ulysses SP
|
||||
(`--usp`), or run Skip-Softmax without ring.
|
||||
- `TRTLLM_ATTN` is BF16-only (no attention-level quantization).
|
||||
- `TRTLLM_ATTN` is BF16-only (no attention-level quantization).
|
||||
@@ -0,0 +1,69 @@
|
||||
# FLUX.2-klein
|
||||
|
||||
> Text-guided image editing with `black-forest-labs/FLUX.2-klein-4B`
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: Black Forest Labs
|
||||
- Model: `black-forest-labs/FLUX.2-klein-4B`
|
||||
- Task: Image editing (image-to-image)
|
||||
- Mode: Offline inference
|
||||
- Maintainer: Community
|
||||
|
||||
## When to use this recipe
|
||||
|
||||
Use this recipe to run single-image edits with FLUX.2-klein-4B through the
|
||||
shared offline image-edit example.
|
||||
|
||||
## References
|
||||
|
||||
- Model card: <https://huggingface.co/black-forest-labs/FLUX.2-klein-4B>
|
||||
- Shared runnable example:
|
||||
[`examples/offline_inference/image_to_image/image_edit.py`](../../examples/offline_inference/image_to_image/image_edit.py)
|
||||
- CPU offload guide:
|
||||
[`docs/user_guide/diffusion/cpu_offload.md`](../../docs/user_guide/diffusion/cpu_offload.md)
|
||||
|
||||
## Hardware Support
|
||||
|
||||
This recipe documents one validated Intel XPU configuration. Extend it with
|
||||
more hardware sections as community validation lands.
|
||||
|
||||
## XPU
|
||||
|
||||
### 1x Intel Arc Pro B70 (32 GB)
|
||||
|
||||
BF16 weights with model-level CPU offload. `--image` takes any RGB image.
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- torch: 2.13.0+xpu
|
||||
- vLLM: 0.29.0 (`98dff2a8`)
|
||||
- vLLM-Omni: `main` at `4c7a98c2`
|
||||
|
||||
#### Command
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/image_to_image/image_edit.py \
|
||||
--model black-forest-labs/FLUX.2-klein-4B \
|
||||
--image test_input.png \
|
||||
--prompt "Add a sunset sky with orange and purple clouds" \
|
||||
--num-inference-steps 28 \
|
||||
--guidance-scale 3.5 \
|
||||
--enable-cpu-offload \
|
||||
--enforce-eager \
|
||||
--seed 42 \
|
||||
--output image_edit_output.png
|
||||
```
|
||||
|
||||
#### Verification
|
||||
|
||||
Confirm `image_edit_output.png` follows the prompt while preserving the rest
|
||||
of the input image.
|
||||
|
||||
#### Notes
|
||||
|
||||
- Memory usage: 7.7 GiB loaded in 8.4 s, about 18 s per edit.
|
||||
- Known limitations: only offline editing of the 4B checkpoint was qualified.
|
||||
`FLUX.2-klein-9B` and online serving are out of scope for this profile.
|
||||
Loaded 100 of 555 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user