Compare commits

...
Author SHA1 Message Date
Hongsheng Liu 2a47d901a0 docs: align quantization overview
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-12 08:56:19 +08:00
Hongsheng Liu 8b0a6779d4 [Misc] Build canonical image task prompts (#6049)
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-12 08:30:34 +08:00
Sy03andYueqian Lin 58007c0084 [New Model] Support IndexTTS 2.5 (#5957)
Signed-off-by: Sysy <1370724210@qq.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-08-11 23:00:35 +00:00
Hongsheng Liu 1ff2753469 docs: align User Guide feature taxonomy (#6045)
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-12 00:20:56 +08:00
SYLAR 4697bd1be0 [Docs] Add MiniMax-H3 recipe for RTX PRO 5000 (#5857)
Signed-off-by: lishunyang12 <lishunyang12@163.com>
2026-08-11 23:53:58 +08:00
Zhou TaichangandClaude Opus 5 2ce15ab7a0 [Rebase] Rebase to vllm 0.27.0 (#5976)
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 15:23:46 +00:00
Nick Cao 584d78c673 [Test] Add tiny model builder for FluxKontextPipeline (#5823)
Signed-off-by: Nick Cao <ncao@redhat.com>
2026-08-11 10:59:09 -04:00
Jerry Song 89015d5cad [Feature]: Adds distilled LoRA support for diffusion models (#2783)
Signed-off-by: Songrui625 <songrui625@gmail.com>
2026-08-11 14:23:43 +00:00
amy-why-3459 348cf95a89 [CI][MiniCPM-o] Align MiniCPM-o 4.5 online serving tests with minicpm… (#6056)
Signed-off-by: amy-why-3459 <wuhaiyan17@huawei.com>
2026-08-11 21:11:39 +08:00
R0CKSTAR 22dd96ec5c [Hardware][MUSA] Complete Omni platform interfaces (#6058)
Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
2026-08-11 12:46:42 +00:00
e04210d690 [Hardware][MUSA] Enable Qwen3-Omni ModelOpt FP8 inference (#5671)
Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
2026-08-11 17:19:09 +08:00
4ab1b4ff9c [Perf][Flux2][HunyuanVideo1.5] Skip attention-mask to avoid varlen path (#4645)
Signed-off-by: kTorp <Kristoffer.Torp@amd.com>
Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-11 15:31:02 +08:00
06e733d3cd [Model] Add LongCat-Video-Avatar-1.5 ai2v, at2v support (#4099)
Signed-off-by: weiyan <weiyanlin117@gmail.com>
Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-11 15:17:17 +08:00
Wang fuyinandHongsheng Liu af3faa6c1e [Diffusion] Add Worker contracts and RPC plumbing for Scheduler-managed paged KV cache (#5550)
Signed-off-by: Acerak01-fy <wfy2003324@163.com>
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-11 14:55:42 +08:00
322600bfb2 [CI][MiniCPM-o] Add MiniCPM-o 4.5 accuracy and performance coverage (#5524)
Signed-off-by: R2-Y <ruiruyang2@gmail.com>
Signed-off-by: psv666 <2693925048@qq.com>
Co-authored-by: psv666 <2693925048@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
2026-08-11 14:53:49 +08:00
Hongsheng Liu c4a14091c9 [Doc] Explain repository skills for agentic contributions (#6029)
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-11 14:18:41 +08:00
yiminghub2024 6d4e4ffc7c Add MiniMax-H3 recipe for RTX 4090 setup (#5850)
Signed-off-by: yiminghub2024 <482890@qq.com>
2026-08-11 14:08:51 +08:00
Zhou248 f97af75d1d [Model] Batch MiniCPM-o Talker codec sampling (#5792)
Signed-off-by: Zhou248 <62542847+Zhou248@users.noreply.github.com>
2026-08-11 14:05:02 +08:00
Hongsheng Liu 73a312d5b6 [Bugfix] Normalize NumPy image outputs before saving (#6031)
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-11 13:48:03 +08:00
Zijun HuangandWeiQing Chen 1a56953d7a [NPU][Model] Add distilled 4-step sigma schedule support for MiniMax H3 t2va (#5991)
Signed-off-by: Zijun Huang <zijunhua@usc.edu>
Co-authored-by: WeiQing Chen <40507679+david6666666@users.noreply.github.com>
2026-08-11 13:46:10 +08:00
WeiQing Chen 1f65814fab [CI] Align MiniMax H3 Ref2VA inputs and add I2VA/Ref2VA accuracy coverage (#5978)
Signed-off-by: david6666666 <530634352@qq.com>
2026-08-11 13:36:37 +08:00
boatmanandYueqian Lin 4f9b65074d [Perf][Qwen3-TTS]Cached Incremental Decode (#5202)
Signed-off-by: boatman <109857087+sphinxkkkbc@users.noreply.github.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-08-11 05:24:04 +00:00
Juan Pablo ZuluagaandYueqian Lin 12a5f6fbf3 [Perf][TTS] step_audio2: keep streaming tokens on-device and batch audio-feature length syncs (#5067)
Signed-off-by: JuanPZuluaga <juanz9312@gmail.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-08-10 21:24:25 -07:00
Juan Pablo ZuluagaandYueqian Lin 53afbf3a0c [Perf][TTS] voxtral_tts hot path: cudagraph opt-out, host-sync removal, kernel caches (#5175)
Signed-off-by: JuanPZuluaga <juanz9312@gmail.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-08-10 21:03:32 -07:00
ee118ab492 [Bugfix] Per-replica fault isolation: keep API server alive on single-stage death (#4285) (#4583)
Signed-off-by: Shenglei Fu <sfu@confluent.io>
Signed-off-by: Shenglei Fu <117230642+ShengleiFu@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-08-10 23:00:18 -04:00
fan2956andFan c27623c29c [Bugfix] rainfusion support all shape video (#6000)
Signed-off-by: Fan <fan@FandeMacBook-Air.local>
Co-authored-by: Fan <fan@FandeMacBook-Air.local>
2026-08-11 05:10:50 +08:00
Yueqian Lin f8fa86fb1f [BugFix] Don't fail the TTS ratchet when the branch count goes down (#6008)
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
2026-08-11 05:08:31 +08:00
ooooooyeandHongsheng Liu 422d8fd8f9 [Bugfix][Diffusion] Restore supports_packed_mask_free on teacache FakeBackend (#5997)
Signed-off-by: brandneway <gyuan4892@gmail.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-10 22:20:37 +08:00
Hongsheng Liu 62e239bf1e [Docs] Fix broken attention backend link (#5998)
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-10 21:55:01 +08:00
ooooooye 740f45abcd [Perf][Diffusion] MiniMax-H3: opt into packed varlen attention on NPU to eliminate quadratic mask materialization (#5891)
Signed-off-by: brandneway <gyuan4892@gmail.com>
2026-08-10 19:47:03 +08:00
5e4b9f8c6e [Misc] Align CODEOWNERS with module/feature design docs (#5958)
Signed-off-by: hsliu_ustc <hsliu_ustc@noreply.gitcode.com>
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: hsliu_ustc <hsliu_ustc@noreply.gitcode.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-10 19:25:03 +08:00
Hongsheng Liu ae811043e9 docs: keep shared task examples in navigation (#5987)
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-08-10 19:13:52 +08:00
317 changed files with 25145 additions and 3031 deletions

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
# NPU naming: {queue}_{resource_class}
# a2b3_npu_1 — ascend-a2b3, resource_class npu-1 (A2/B3 CI image)
# a2b3_npu_4 — ascend-a2b3, resource_class npu-4
# a3_npu_2 — ascend-a3, resource_class npu-2 (A3 CI image tag …:$BUILDKITE_COMMIT-a3)
# a3_npu_2 — ascend-a3, resource_class npu-2
x-npu-k8s-plugins: &npu_k8s_plugins
- kubernetes:
+8 -3
View File
@@ -139,6 +139,11 @@ steps:
timeout_in_minutes: 50
commands:
- pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5.py -m 'advanced_model and cuda' --run-level 'advanced_model'
# h100_2, not h100_1: advanced_model asserts transcribe every audio
# response with Whisper, and tests/helpers/media.py only uses an
# accelerator for Whisper when a spare GPU exists. On a single card it
# grades on CPU: ~8-10 min per clip vs ~30 s, which alone overruns the
# 50-minute job timeout (build 2956).
mirror_hardwares: h100_2
- label: "Omni · MiniCPM-o 4.5 Duplex Test"
@@ -148,10 +153,10 @@ steps:
- tests/e2e/online_serving/test_minicpmo_4_5_duplex.py
- tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
- tests/e2e/online_serving/test_minicpmo_realtime_duplex_drivers.py
- tests/e2e/online_serving/minicpmo_realtime_duplex_scenarios.py
- tests/e2e/online_serving/helpers/minicpmo_realtime_duplex_scenarios.py
- tests/e2e/online_serving/run_minicpmo_realtime_duplex_multi_session.py
- tests/e2e/online_serving/run_minicpmo_realtime_duplex_soft_interrupt.py
- tests/helpers/minicpmo_4_5_duplex.py
- tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py
- vllm_omni/deploy/minicpmo_4_5_duplex.yaml
- vllm_omni/experimental/fullduplex/
- vllm_omni/model_executor/models/minicpmo_4_5/
@@ -164,7 +169,7 @@ steps:
timeout_in_minutes: 50
commands:
- pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py -m 'advanced_model and cuda' --run-level 'advanced_model'
mirror_hardwares: h100_2
mirror_hardwares: h100_1
- label: "TTS · VoxCPM2 Test"
source_file_dependencies:
+42 -3
View File
@@ -22,7 +22,7 @@ steps:
timeout_in_minutes: 50
commands:
- pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py -m "full_model and cuda and H100 and omni" --run-level "full_model"
mirror_hardwares: h100_2
mirror_hardwares: h100_1
- label: ":full_moon: Omni · Doc Test with L4"
timeout_in_minutes: 90
@@ -50,6 +50,19 @@ steps:
exit $$EXIT
mirror_hardwares: h100_2
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Accuracy Test"
timeout_in_minutes: 180
commands:
- export SEED_TTS_WER_EVAL=1
- export SEED_TTS_EVAL_DEVICE=cuda:1
- |
set +e
pytest -s -v tests/e2e/accuracy/minicpmo_4_5/test_minicpmo_4_5.py -m "full_model and cuda" --run-level full_model
EXIT=$$?
buildkite-agent artifact upload "tests/e2e/accuracy/minicpmo_4_5/results/*.json"
exit $$EXIT
mirror_hardwares: h100_2
- label: ":full_moon: Omni · Perf Test · No Async Chunk"
key: nightly-omni-performance-no-async-chunk
timeout_in_minutes: 300
@@ -102,6 +115,32 @@ steps:
exit $$EXIT
mirror_hardwares: h100_3
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Perf Test"
key: nightly-omni-performance-minicpmo-4-5
timeout_in_minutes: 180
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minicpmo_4_5.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
mirror_hardwares: h100_1
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Duplex Seed-TTS Perf Test"
key: nightly-omni-performance-minicpmo-4-5-duplex-seed-tts
timeout_in_minutes: 180
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minicpmo_4_5_duplex_seed_tts.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
mirror_hardwares: h100_1
- label: ":full_moon: Omni · Multi-Replica Startup Test with 4x H100"
timeout_in_minutes: 45
commands:
@@ -427,10 +466,10 @@ steps:
- pytest -s -v tests/e2e/accuracy/test_diffusers_backend_similarity.py -k '2v_matches_diffusers' -m full_model --run-level full_model
mirror_hardwares: h100_2
- label: ":full_moon: Diffusion X2V · MiniMax H3 T2VA Accuracy Test"
- label: ":full_moon: Diffusion X2V · MiniMax H3 I2VA/Ref2VA Accuracy Test"
timeout_in_minutes: 180
commands:
- pytest -s -v tests/e2e/accuracy/minimax_h3/test_minimax_h3_t2va_similarity.py -m full_model --run-level full_model
- pytest -s -v tests/e2e/accuracy/minimax_h3/test_minimax_h3_i2va_ref2va_similarity.py -m full_model --run-level full_model
mirror_hardwares: h100_4
- label: ":full_moon: Diffusion X2V · Other Accuracy Test"
+20 -5
View File
@@ -128,6 +128,21 @@ steps:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
# vLLM 0.27 added a pre-flight capacity check in
# distributed/device_communicators/shm_broadcast.py. Without this
# the container gets Docker's 64 MiB default and every test in
# this step dies at engine start with
# RuntimeError: Insufficient space in /dev/shm:
# 240 MiB required, 64 MiB available
# (9 failed in 8.7s on build #2949). vLLM 0.26 had no such check
# and allocated regardless, so this step passed on main while
# being under-provisioned the whole time — 0.27 turned a silent
# risk into an explicit error.
#
# This is the only inline docker plugin block in .buildkite/cuda/;
# every other step inherits common/ci_mirror_hardwares.yml, which
# already sets 8gb. Matching that rather than inventing a value.
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
volumes:
@@ -172,7 +187,7 @@ steps:
timeout 20m bash -c "
pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py -m 'core_model' --run-level 'core_model'
"
mirror_hardwares: h100_2
mirror_hardwares: h100_1
- label: "Omni · MiniCPM-o 4.5 Online Test"
source_file_dependencies:
@@ -183,7 +198,7 @@ steps:
timeout 20m bash -c "
pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5.py -m 'core_model' --run-level 'core_model'
"
mirror_hardwares: h100_2
mirror_hardwares: h100_1
- label: "Omni · MiniCPM-o 4.5 Duplex Test"
source_file_dependencies:
@@ -192,10 +207,10 @@ steps:
- tests/e2e/online_serving/test_minicpmo_4_5_duplex.py
- tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
- tests/e2e/online_serving/test_minicpmo_realtime_duplex_drivers.py
- tests/e2e/online_serving/minicpmo_realtime_duplex_scenarios.py
- tests/e2e/online_serving/helpers/minicpmo_realtime_duplex_scenarios.py
- tests/e2e/online_serving/run_minicpmo_realtime_duplex_multi_session.py
- tests/e2e/online_serving/run_minicpmo_realtime_duplex_soft_interrupt.py
- tests/helpers/minicpmo_4_5_duplex.py
- tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py
- vllm_omni/deploy/minicpmo_4_5_duplex.yaml
- vllm_omni/experimental/fullduplex/
- vllm_omni/model_executor/models/minicpmo_4_5/
@@ -210,7 +225,7 @@ steps:
timeout 20m bash -c "
pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py -m 'core_model and cuda' --run-level 'core_model'
"
mirror_hardwares: h100_2
mirror_hardwares: h100_1
- label: "TTS · VoxCPM2 Test"
source_file_dependencies:
+1 -1
View File
@@ -10,7 +10,7 @@ steps:
DOCKER_BUILDKIT: "1"
# Buildkite will automatically replace this with the actual commit hash
VLLM_IMAGE_TAG: "${BUILDKITE_COMMIT}"
VLLM_VERSION: "v0.26.0"
VLLM_VERSION: "v0.27.0"
priority: 100
timeout_in_minutes: 60
soft_fail: false
+16 -1
View File
@@ -34,8 +34,23 @@ docker_build() {
docker build "${out[@]}" "$@" -f docker/Dockerfile.xpu .
}
build_vllm_base() (
local vllm_source_dir
vllm_source_dir=$(mktemp -d)
trap 'rm -rf "${vllm_source_dir}"' EXIT
git clone --depth 1 --branch "${VLLM_VERSION}" \
https://github.com/vllm-project/vllm "${vllm_source_dir}"
local out=("${export_args[@]/'{{IMAGE}}'/${base_image_name}}")
docker build "${out[@]}" \
--target vllm-openai \
-f "${vllm_source_dir}/docker/Dockerfile.xpu" \
"${vllm_source_dir}"
)
if [ -z "$(docker images -q "${base_image_name}")" ]; then
docker_build "${base_image_name}" --target vllm-base --build-arg "VLLM_VERSION=${VLLM_VERSION}"
build_vllm_base
fi
# Try building the docker image
+29 -1
View File
@@ -39,7 +39,7 @@ steps:
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- label: ":robot: Omni · MiniCPM-o 4.5 Test"
- label: ":full_moon: Omni · MiniCPM-o 4.5 Test"
mirror_hardwares: a3_npu_2
env:
VLLM_WORKER_MULTIPROC_METHOD: spawn
@@ -47,6 +47,34 @@ steps:
HF_TOKEN: "${HF_TOKEN}"
commands:
- pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5_expansion.py -m 'full_model' --run-level 'full_model'
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Duplex Test"
timeout_in_minutes: 180
mirror_hardwares: a3_npu_2
env:
VLLM_WORKER_MULTIPROC_METHOD: spawn
VLLM_ALLOW_LONG_MAX_MODEL_LEN: "1"
HF_TOKEN: "${HF_TOKEN}"
commands:
- >-
pytest -s -v
tests/e2e/online_serving/test_minicpmo_4_5_duplex.py
tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
-m "npu and A3 and omni"
--run-level full_model
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Accuracy Test"
timeout_in_minutes: 180
mirror_hardwares: a3_npu_2
env:
VLLM_WORKER_MULTIPROC_METHOD: spawn
VLLM_ALLOW_LONG_MAX_MODEL_LEN: "1"
HF_TOKEN: "${HF_TOKEN}"
commands:
- |
set +e
pytest -s -v tests/e2e/accuracy/minicpmo_4_5/test_minicpmo_4_5.py -m "full_model and npu and A3" --run-level full_model
EXIT=$$?
buildkite-agent artifact upload "tests/e2e/accuracy/minicpmo_4_5/results/*.json"
exit $$EXIT
- group: ":card_index_dividers: TTS Model Test"
key: nightly-tts-test-group
depends_on: upload-nightly-pipeline
+10 -12
View File
@@ -3,22 +3,20 @@ steps:
key: ready-omni-group
depends_on: upload-ready-pipeline
steps:
- label: ":robot: Omni · MiniCPM-o 4.5 Test"
agents:
queue: "ascend-a3"
resource_class: "npu-2"
image: "swr.cn-southwest-2.myhuaweicloud.com/modelfoundry/vllm-omni-ci-npu:${BUILDKITE_COMMIT}-a3"
plugins:
- kubernetes:
podSpecPatch:
imagePullSecrets:
- name: swr-secret
- label: "Omni · MiniCPM-o 4.5 Test"
mirror_hardwares: a3_npu_2
env:
VLLM_WORKER_MULTIPROC_METHOD: spawn
VLLM_ALLOW_LONG_MAX_MODEL_LEN: "1"
HF_TOKEN: "${HF_TOKEN}"
commands:
- pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5.py -m 'core_model' --run-level 'core_model'
- pytest -s -v tests/e2e/offline_inference/test_minicpmo_4_5.py tests/e2e/online_serving/test_minicpmo_4_5.py -m 'core_model and npu and A3 and omni' --run-level 'core_model'
- label: "Omni · MiniCPM-o 4.5 Duplex Test"
mirror_hardwares: a3_npu_2
env:
VLLM_WORKER_MULTIPROC_METHOD: spawn
VLLM_ALLOW_LONG_MAX_MODEL_LEN: "1"
commands:
- pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex.py -m 'core_model and npu and A3 and omni' --run-level 'core_model'
- group: ":card_index_dividers: TTS Model Test"
key: ready-tts-group
+38 -28
View File
@@ -1,9 +1,9 @@
# Claude Skills for vLLM-Omni
# Repository Skills for vLLM-Omni
This directory contains Claude Code skills maintained for the `vllm-omni`
repository. These skills capture repeatable workflows for common contributor
tasks such as model integration, pull request review, and release note
generation.
This directory contains repository-scale skills maintained for `vllm-omni`.
They capture repeatable workflows for common contributor and maintainer tasks
such as model integration, CI-aligned testing, performance optimization, and
pull request review.
## Directory Structure
@@ -14,31 +14,41 @@ include:
- `references/`: focused reference material used by the skill
- `scripts/`: small helper scripts used by the skill
## Using the Skills
Coding agents that discover repository skills can invoke the relevant skill by
name. For other agents, point them to the skill's `SKILL.md` and ask them to
read it before changing code. Combine a domain skill with `vllm-omni-test` when
tests or CI coverage change, then use `precheck-pr` for the contributor's final
self-check.
## Available Skills
- `add-diffusion-model`: guides integration of a new diffusion model into
`vllm-omni`
- `diffusion-perf-opt`: guides diffusion model performance optimization,
including profiling traces, parallel strategies, stage timing analysis, and
benchmark-driven tuning
- `quantization`: guides quantization method selection, model integration,
checkpoint loading, and quality/performance validation for vLLM-Omni
- `add-omni-model`: covers addition of new omni-modality model support
- `add-tts-model`: covers integration of new TTS models and related serving
workflows
- `generate-release-note`: helps prepare release notes for repository changes
- `precheck-pr`: self-check a branch before creating a PR — validates PR title
format, catches dead code, verifies accuracy/perf claims, and confirms merge
readiness
- `vllm-omni-test`: guides generation and execution of CI-aligned tests (L1–L4),
pytest marker selection (`core_model` / `advanced_model` / `full_model`,
`omni` / `tts` / `diffusion`), Buildkite wiring (`test-ready.yml`,
`test-merge.yml`, `test-nightly.yml`, `test-weekly.yml`), and copy-paste
local plus CI-like `pytest` commands; see `references/test-routing.md` for
level-to-command mapping
- `review-pr`: provides a structured workflow for reviewing pull requests
- `vllm-omni-npu-model-runner-upgrade`: upgrades NPU model runners to align with the
latest vllm-ascend NPUModelRunner
- [`add-diffusion-model`](add-diffusion-model/SKILL.md): guides integration of
a new diffusion model into `vllm-omni`
- [`add-tts-model`](add-tts-model/SKILL.md): covers integration of new TTS
models and related serving workflows
- [`diffusion-perf-opt`](diffusion-perf-opt/SKILL.md): guides diffusion model
performance optimization, including profiling traces, parallel strategies,
stage timing analysis, and benchmark-driven tuning
- [`precheck-pr`](precheck-pr/SKILL.md): self-checks a branch before creating a
PR by validating title format, dead code, accuracy and performance claims,
and merge readiness
- [`quantization`](quantization/SKILL.md): guides quantization method selection,
model integration, checkpoint loading, and quality/performance validation
for vLLM-Omni
- [`review-pr`](review-pr/SKILL.md): provides a frozen-snapshot, contract-aware
workflow for maintainers and reviewers; contributors should use
`precheck-pr` for self-review
- [`vllm-omni-npu-model-runner-upgrade`](vllm-omni-npu-upgrade/SKILL.md):
upgrades NPU model runners to align with the latest vllm-ascend
`NPUModelRunner`
- [`vllm-omni-test`](vllm-omni-test/SKILL.md): guides generation and execution
of CI-aligned tests (L1–L4), pytest marker selection (`core_model` /
`advanced_model` / `full_model`, `omni` / `tts` / `diffusion`), Buildkite
wiring (`test-ready.yml`, `test-merge.yml`, `test-nightly.yml`,
`test-weekly.yml`), and copy-paste local plus CI-like `pytest` commands; see
`references/test-routing.md` for level-to-command mapping
## Maintenance Guidelines
+110 -28
View File
@@ -1,6 +1,11 @@
# CODEOWNERS is used to automatically assign PR reviewers
# Format: path pattern @username or @team
# More info: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
#
# The vllm_omni/** source-tree rules below are derived from the `owners` and
# `primary_code_paths` front matter in docs/design/module/*.md. When updating
# ownership, update the design doc first, then regenerate the matching rule here.
# Last-match-wins: more specific rules must come AFTER broader ones.
# Default owners (fallback reviewers for all files)
* @Gaohan123 @hsliuustc0106 @ywang96
@@ -21,25 +26,10 @@
# CI
/.buildkite/ @yenuo26 @congw729 @NickCao
# Engine and model_executor
/vllm_omni/core/ @tzhouam @yinpeiqi @fake0fan @Sy0307
/vllm_omni/engine/ @tzhouam @yinpeiqi @fake0fan @Sy0307
/vllm_omni/entrypoints/ @tzhouam @yinpeiqi @fake0fan @alex-jw-brooks @NickCao
/vllm_omni/model_executor/ @tzhouam @gcanlin
/vllm_omni/worker/ @tzhouam @gcanlin
# ComfyUI / external apps
/apps/ @fhfuih @alex-jw-brooks
# Diffusion
/vllm_omni/diffusion/ @Isotr0py @princepride @SamitHuang @wtomin @ZJY0516 @RuixiangMa @david6666666 @xuechendi
/vllm_omni/lora/ @princepride @SamitHuang @wtomin @ZJY0516 @RuixiangMa @yuanheng-zhao
# Diffusion CPU offloading
/vllm_omni/diffusion/offloader/ @Isotr0py @princepride @SamitHuang @wtomin @ZJY0516 @RuixiangMa @yuanheng-zhao @xuechendi
# Omni and TTS support
/vllm_omni/transformers_utils/ @tzhouam @yuanheng-zhao
/vllm_omni/model_executor/models/ @linyueqian @ZeldaHuang @princepride @yuanheng-zhao @amy-why-3459 @Bounty-hunter @Sy0307 @gcanlin @NickCao
# Benchmarks
# Benchmarks (out-of-tree)
/benchmarks/accuracy/ @david6666666
/benchmarks/build_dataset/ @linyueqian @yuanheng-zhao
/benchmarks/diffusion/ @Isotr0py @princepride @SamitHuang @wtomin @ZJY0516 @RuixiangMa @xuechendi
@@ -47,20 +37,112 @@
/benchmarks/glm_image/ @princepride @JaredforReal
/benchmarks/tts/ @linyueqian @ZeldaHuang @princepride @Sy0307 @gcanlin
# Usability and Observability
/vllm_omni/deploy/ @tzhouam @lishunyang12
/vllm_omni/metrics/ @lishunyang12
/vllm_omni/profiler/ @gcanlin
# =====================================================================
# Module design ownership (docs/design/module/*.md)
# Each section header cites the source design doc.
# =====================================================================
# ComfyUI
/apps/ @fhfuih @alex-jw-brooks
# Autoregressive runtime — docs/design/module/ar_runtime.md
/vllm_omni/core/ @tzhouam @yinpeiqi @fake0fan @Sy0307 @Gaohan123
/vllm_omni/worker/ @tzhouam @yinpeiqi @fake0fan @Sy0307 @Gaohan123
# Large Scale Serving
# Engine orchestration & stage runtime — docs/design/module/engine_orchestration.md, stage_runtime.md
/vllm_omni/engine/ @tzhouam @fake0fan
# File-level overrides: input/output modality contracts own these engine files
# — docs/design/module/input_output_modality_contracts.md
/vllm_omni/engine/messages.py @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
/vllm_omni/engine/serialization.py @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
/vllm_omni/engine/mm_outputs.py @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
/vllm_omni/engine/output_modality.py @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
/vllm_omni/engine/output_processor.py @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
# Input/output modality contracts — docs/design/module/input_output_modality_contracts.md
/vllm_omni/inputs/ @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
/vllm_omni/outputs/ @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
/vllm_omni/request.py @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
/vllm_omni/data_entry_keys.py @Sy0307 @amy-why-3459 @Gaohan123 @alex-jw-brooks
# Entrypoints and serving boundaries — docs/design/module/entrypoints.md
/vllm_omni/entrypoints/ @alex-jw-brooks @linyueqian @NickCao
# File-level overrides within entrypoints/
/vllm_omni/entrypoints/openai/errors.py @alex-jw-brooks @NickCao
/vllm_omni/entrypoints/cli/benchmark/ @alex-jw-brooks @Bounty-hunter
# Error contracts — docs/design/module/error_contracts.md
/vllm_omni/errors.py @alex-jw-brooks @NickCao
# OmniConnector (inter-stage transport) — docs/design/module/omni_connector.md
/vllm_omni/distributed/ @princepride @yuanheng-zhao @xuechendi @natureofnature @fake0fan
# Quantization and configuration
# Model integration — docs/design/module/model_integration.md
/vllm_omni/model_executor/ @tzhouam @gcanlin
/vllm_omni/model_executor/models/ @tzhouam @gcanlin @Sy0307 @amy-why-3459 @linyueqian
/vllm_omni/model_extras/ @tzhouam @gcanlin @princepride
/vllm_omni/plugins/ @gcanlin @tjtanaa @xuechendi
# Config override within model_executor — docs/design/module/vllm_omni_config.md
/vllm_omni/model_executor/stage_configs/ @lishunyang12 @alex-jw-brooks
# Execution platforms (hardware backends) — docs/design/module/execution_platforms.md
/vllm_omni/platforms/ @gcanlin @tjtanaa @xuechendi
/vllm_omni/attention/ @gcanlin @tjtanaa @xuechendi @david6666666 @lishunyang12
# Configuration & deploy — docs/design/module/vllm_omni_config.md
/vllm_omni/config/ @lishunyang12 @alex-jw-brooks
/vllm_omni/deploy/ @lishunyang12 @alex-jw-brooks
# Quantization — docs/design/module/quantization.md
/vllm_omni/quantization/ @david6666666 @Isotr0py @lishunyang12
# Hardware plugins and accelerators
/vllm_omni/platforms/ @gcanlin @tjtanaa @xuechendi
# Observability — docs/design/module/observability.md
/vllm_omni/metrics/ @lishunyang12 @vraiti
# Profiling — docs/design/module/profiling.md
/vllm_omni/profiler/ @gcanlin
# Benchmarking (in-tree) — docs/design/module/benchmarking.md
/vllm_omni/benchmarks/ @alex-jw-brooks @Bounty-hunter
# LoRA (no dedicated design doc; ownership intentionally narrowed)
/vllm_omni/lora/ @SamitHuang @wtomin
# Transformers utils (related path only in design docs; preserves prior ownership)
/vllm_omni/transformers_utils/ @tzhouam @yuanheng-zhao
# =====================================================================
# Diffusion module family — docs/design/module/diffusion/
# Umbrella rule first; sub-module rules after it win via last-match.
# =====================================================================
# Diffusion umbrella (top-level files: data.py, forward_context.py, etc.)
# — docs/design/module/diffusion/index.md
/vllm_omni/diffusion/ @Isotr0py @princepride @SamitHuang @wtomin @ZJY0516 @RuixiangMa @david6666666 @xuechendi
# Cache management — docs/design/module/cache_management.md
/vllm_omni/diffusion/cache/ @Isotr0py @princepride @SamitHuang
# Diffusion quantization — docs/design/module/quantization.md
/vllm_omni/diffusion/quantization/ @david6666666 @Isotr0py @lishunyang12
# Diffusion profiling — docs/design/module/profiling.md
/vllm_omni/diffusion/profiler/ @gcanlin
# Diffusion runtime (scheduler, executor, worker)
# — docs/design/module/diffusion/diffusion_runtime.md, continuous_batching.md
/vllm_omni/diffusion/sched/ @Isotr0py @princepride @SamitHuang @fhfuih
/vllm_omni/diffusion/executor/ @Isotr0py @princepride @SamitHuang @fhfuih
/vllm_omni/diffusion/worker/ @Isotr0py @princepride @SamitHuang @fhfuih
# Diffusion model integration (pipelines, registration, checkpoint loading)
# — docs/design/module/diffusion/diffusion_model_integration.md
/vllm_omni/diffusion/models/ @fhfuih @Bounty-hunter @wtomin
/vllm_omni/diffusion/model_loader/ @fhfuih @Bounty-hunter @wtomin @RuixiangMa
/vllm_omni/diffusion/lora/ @fhfuih @Bounty-hunter @wtomin @RuixiangMa
# Diffusion parallelism (rank topology, TP/SP/PP/VAE patch)
# — docs/design/module/diffusion/parallelism.md
/vllm_omni/diffusion/distributed/ @princepride @yuanheng-zhao @xuechendi @Bounty-hunter
/vllm_omni/diffusion/attention/parallel/ @princepride @yuanheng-zhao @xuechendi @Bounty-hunter
# Diffusion offloader (CPU offloading, DLO)
# — docs/design/module/diffusion/offloader.md
/vllm_omni/diffusion/offloader/ @yuanheng-zhao @david6666666 @lishunyang12
+4 -1
View File
@@ -66,7 +66,10 @@ repos:
entry: python tools/pre_commit/check_tts_adapter.py
language: python
pass_filenames: false
files: ^vllm_omni/entrypoints/openai/(serving_speech\.py|tts_adapters/__init__\.py)$
files: ^(vllm_omni/entrypoints/openai/(serving_speech\.py|tts_adapters/__init__\.py)|tools/pre_commit/check_tts_adapter\.py)$
# The budget-is-now-slack reminder is printed on a PASSING run, and
# pre-commit hides output from passing hooks unless the hook is verbose.
verbose: true
# Keep `suggestion` last
- id: suggestion
+28
View File
@@ -139,6 +139,34 @@ python benchmarks/tts/bench_tts.py \
--output-dir ./results
```
#### Fixed IndexTTS 2.5 performance sweep
IndexTTS 2.5 is registered in `model_configs.yaml` like the other TTS models.
Use `--served-model-name` when the server was launched from a local native
bundle instead of the registry key. The command below reproduces the acceptance
workload: Seed-TTS Eval EN, `n=500`, concurrency `4/8/16/32`, five warmups, and
dataset seed 0.
```bash
python benchmarks/tts/bench_tts.py \
--model IndexTeam/IndexTTS-2.5 \
--served-model-name /path/to/indextts-2.5 \
--task voice_clone --locale en \
--dataset-path /path/to/seedtts_testset \
--host 127.0.0.1 --port 8092 \
--concurrency 4 8 16 32 \
--num-prompts 500 --num-warmups 5 \
--output-dir ./results/indextts2_5 \
-- \
--tokenizer /path/to/indextts-2.5/qwen0.6bemo4-merge \
--seed 0 --metric-percentiles 50,95,99 --disable-tqdm --save-detailed
```
The wrapper benchmarks an already-running server. For reproducible quality
comparisons, add `--request-seed 42`; production-performance runs should omit
it. The trailing `--seed 0` controls Seed-TTS row selection rather than model
sampling.
### 4. Plot a sweep
```bash
+29 -4
View File
@@ -84,12 +84,17 @@ def build_bench_args(
result_filename: str | None,
extra_cli_args: list[str],
output_len: int | None = None,
served_model_name: str | None = None,
num_warmups: int = 2,
request_seed: int | None = None,
) -> list[str]:
"""Build the ``vllm bench serve --omni`` command for one (task, concurrency) run."""
dataset_name = _TASK_TO_DATASET[task]
backend: str = model_cfg["backend"]
endpoint: str = model_cfg["endpoint"]
task_extra_body: dict[str, Any] = (model_cfg.get("task_extra_body") or {}).get(task) or {}
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
# Resolve dataset path
if dataset_path:
@@ -109,7 +114,7 @@ def build_bench_args(
"--port",
str(port),
"--model",
model,
served_model_name or model,
"--backend",
backend,
"--endpoint",
@@ -119,7 +124,7 @@ def build_bench_args(
"--num-prompts",
str(num_prompts),
"--num-warmups",
"2",
str(num_warmups),
"--percentile-metrics",
"ttft,e2el,audio_rtf,audio_ttfp,audio_duration,audio_underrun",
]
@@ -152,7 +157,8 @@ def build_bench_args(
if result_filename:
cmd += ["--result-filename", result_filename]
cmd += extra_cli_args
passthrough = extra_cli_args[1:] if extra_cli_args[:1] == ["--"] else extra_cli_args
cmd += passthrough
return cmd
@@ -225,6 +231,12 @@ def main() -> None:
parser.add_argument(
"--model", required=True, help="HuggingFace model ID (e.g. Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice)"
)
parser.add_argument(
"--served-model-name",
default=None,
help="Model name/path accepted by the running server. Use this when --model selects a registry entry "
"but the server was launched from a local bundle path.",
)
parser.add_argument("--task", default="all", help="Task type: voice_clone | default_voice | voice_design | all")
parser.add_argument("--locale", default="en", choices=["en", "zh"])
parser.add_argument("--concurrency", type=int, nargs="+", default=[1, 4], metavar="N")
@@ -236,6 +248,13 @@ def main() -> None:
metavar="N",
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(
"--request-seed",
type=int,
default=None,
help="Optional model sampling seed added to every request body for reproducible quality runs",
)
parser.add_argument(
"--dataset-path", default=None, help="Root of seed-tts-eval dataset (required for voice_clone/default_voice)"
)
@@ -256,6 +275,9 @@ def main() -> None:
parser.add_argument("extra", nargs=argparse.REMAINDER, help="Extra args passed directly to vllm bench serve")
args = parser.parse_args()
if args.num_warmups < 0:
parser.error("--num-warmups cannot be negative")
model_configs = load_model_configs(Path(args.model_configs))
if args.model not in model_configs:
known = "\n ".join(model_configs.keys())
@@ -308,6 +330,9 @@ def main() -> None:
result_filename=result_filename,
extra_cli_args=args.extra or [],
output_len=args.output_len,
served_model_name=args.served_model_name,
num_warmups=args.num_warmups,
request_seed=args.request_seed,
)
result = run_one_benchmark(cmd)
if result is not None:
+11
View File
@@ -7,6 +7,17 @@
# is tracked here.
models:
IndexTeam/IndexTTS-2.5:
supported_tasks: [voice_clone]
backend: openai-audio-speech
endpoint: /v1/audio/speech
trust_remote_code: true
task_extra_body:
voice_clone:
extra_params:
lang: en
text_normalization: true
# -CustomVoice checkpoints lack speaker_encoder weights, so voice_clone is
# NOT supported (an attempt raises ValueError from _extract_speaker_embedding
# at model runtime). Use -Base for voice_clone.
+19 -2
View File
@@ -1,7 +1,24 @@
# Target vllm version: v0.26.0 (releases/v0.26.0 branch).
# Target vllm version: v0.27.0 (releases/v0.27.0 branch).
# Keep VLLM_BASE_TAG aligned with the target vLLM release on every rebase.
#
# Release-boundary form: there is no precompiled-wheel pin here, because the
# base image already ships exactly the target vLLM. Everything the runtime
# needs — torch, flashinfer, nvJitLink, numpy — comes from that image as one
# self-consistent set.
#
# This block reappears only when the rebase targets a vLLM commit that is NOT a
# released tag. During the 0.27 rebase, targeting v0.27.0rc1 on top of the
# v0.26.0 base image required reinstalling vLLM from wheels.vllm.ai and then
# hand-repairing the fallout: pinning torch ahead of flashinfer (installing it
# after bound flashinfer's TVM FFI runtime to the old torch and aborted engine
# core init), registering libnvJitLink.so.13 with ldconfig, and re-pinning numpy
# under numba's ceiling. All of it was compensation for a base image and a vLLM
# that disagreed. At a real release boundary they agree, so none of it is
# needed. If you pin an untagged commit again, restore the reinstall block —
# agent/tasks/20_update_wheel_url.sh fails the run rather than letting the
# Dockerfile silently ship the wrong vLLM.
ARG VLLM_BASE_IMAGE=vllm/vllm-openai
ARG VLLM_BASE_TAG=v0.26.0
ARG VLLM_BASE_TAG=v0.27.0
# We use registry-based layer caching for the dependencies,
# where the tag is determined based on the hash of the contents
+1
View File
@@ -45,6 +45,7 @@ RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
"hyperpyyaml>=1.2.0" \
"onnx>=1.16.0" \
"librosa>=0.10.0" \
"datasets>=3.0.0" \
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
--default-timeout=60 && \
+1
View File
@@ -46,6 +46,7 @@ RUN source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
"onnx>=1.16.0" \
"librosa>=0.10.0" \
"torchmetrics>=1.9.0" \
"datasets>=3.0.0" \
--index-url http://cache-service.nginx-pypi-cache.svc.cluster.local/pypi/simple \
--trusted-host cache-service.nginx-pypi-cache.svc.cluster.local \
--default-timeout=60 && \
+5 -4
View File
@@ -88,7 +88,7 @@ ENV UV_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL}
ENV UV_INDEX_STRATEGY="unsafe-best-match"
ENV UV_LINK_MODE="copy"
ARG VLLM_VERSION=v0.26.0
ARG VLLM_VERSION=v0.27.0
RUN git clone -b ${VLLM_VERSION} https://github.com/vllm-project/vllm
WORKDIR /workspace/vllm
@@ -108,7 +108,7 @@ ENV VLLM_WORKER_MULTIPROC_METHOD=spawn
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip uninstall triton triton-xpu && \
uv pip install triton-xpu==3.7.1 && \
uv pip install triton-xpu==3.7.2 && \
uv pip install --no-build-isolation --no-deps .
CMD ["/bin/bash"]
@@ -144,12 +144,13 @@ WORKDIR /workspace/vllm-omni
COPY . .
ENV VLLM_OMNI_TARGET_DEVICE=xpu
# Keep the upstream oneCCL packages paired with the XPU torch runtime.
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --no-build-isolation \
--extra-index-url https://mirrors.ustc.edu.cn/pypi/simple/ \
".[dev]" && \
uv pip uninstall triton oneccl oneccl-devel && \
uv pip install triton-xpu==3.7.1 --reinstall
uv pip uninstall triton && \
uv pip install triton-xpu==3.7.2 --reinstall
FROM vllm-omni AS vllm-omni-openai
+36 -40
View File
@@ -16,31 +16,21 @@ nav:
- Examples:
- examples/README.md
- Offline Inference:
- GLM-Image Multistage End-to-End Inference: user_guide/examples/offline_inference/glm_image.md
- HunyuanImage-3.0 Image-to-Text Inference: user_guide/examples/offline_inference/hunyuan_image3.md
- Image-To-Image: user_guide/examples/offline_inference/image_to_image.md
- Image-To-Video: user_guide/examples/offline_inference/image_to_video.md
- InternVLA-A1: user_guide/examples/offline_inference/internvla_a1.md
- MiMo-Audio: user_guide/examples/offline_inference/mimo_audio.md
- Qwen2.5-Omni: user_guide/examples/offline_inference/qwen2_5_omni.md
- Qwen3-Omni: user_guide/examples/offline_inference/qwen3_omni.md
- Speech-To-Video: user_guide/examples/offline_inference/speech_to_video.md
- Text-To-Audio: user_guide/examples/offline_inference/text_to_audio.md
- Text-To-Image: user_guide/examples/offline_inference/text_to_image.md
- Text-To-Speech: user_guide/examples/offline_inference/text_to_speech.md
- Text-To-Video: user_guide/examples/offline_inference/text_to_video.md
- X-To-Text: user_guide/examples/offline_inference/x_to_text.md
- X-To-Video-Audio: user_guide/examples/offline_inference/x_to_video_audio.md
- Online Serving:
- BAGEL-7B-MoT: user_guide/examples/online_serving/bagel.md
- vLLM-Omni Helm Chart: user_guide/examples/online_serving/chart-helm.md
- Diffusers Backend Adapter: user_guide/examples/online_serving/diffusers_pipeline_adapter.md
- GLM-Image Online Serving: user_guide/examples/online_serving/glm_image.md
- Image-To-Image: user_guide/examples/online_serving/image_to_image.md
- Image-To-Video: user_guide/examples/online_serving/image_to_video.md
- MiMo-Audio: user_guide/examples/online_serving/mimo_audio.md
- Qwen2.5-Omni: user_guide/examples/online_serving/qwen2_5_omni.md
- Qwen3-Omni: user_guide/examples/online_serving/qwen3_omni.md
- Text-To-Speech: user_guide/examples/online_serving/text_to_speech.md
- Speech-To-Video: user_guide/examples/online_serving/speech_to_video.md
- Text-To-Image: user_guide/examples/online_serving/text_to_image.md
- Text-To-Speech: user_guide/examples/online_serving/text_to_speech.md
- Text-To-Video: user_guide/examples/online_serving/text_to_video.md
- General:
- usage/*
@@ -50,30 +40,12 @@ nav:
- Models:
- models/supported_models.md
- Features:
- Sleep Mode: features/sleep_mode.md
- Diffusion Features:
- Overview: user_guide/diffusion_features.md
- Feature Compatibility: user_guide/feature_compatibility.md
- Regional Compilation: user_guide/diffusion/regional_compilation.md
- Cache Acceleration:
- TeaCache: user_guide/diffusion/cache_acceleration/teacache.md
- Cache-DiT: user_guide/diffusion/cache_acceleration/cache_dit.md
- Attention Backends: user_guide/diffusion/attention_backends.md
- Frame Interpolation: user_guide/diffusion/frame_interpolation.md
- Parallelism:
- Overview: user_guide/diffusion/parallelism/overview.md
- CFG Parallel: user_guide/diffusion/parallelism/cfg_parallel.md
- Expert Parallel: user_guide/diffusion/parallelism/expert_parallel.md
- Hybrid Sharded Data Parallel: user_guide/diffusion/parallelism/hsdp.md
- Sequence Parallel: user_guide/diffusion/parallelism/sequence_parallel.md
- Tensor Parallel: user_guide/diffusion/parallelism/tensor_parallel.md
- VAE Parallelism: user_guide/diffusion/parallelism/vae_parallelism.md
- CPU Offloading: user_guide/diffusion/cpu_offload.md
- LoRA: user_guide/diffusion/lora.md
- Custom Pipeline: features/custom_pipeline.md
- Execution Modes: user_guide/diffusion/execution_modes.md
- Overview: features/README.md
- Runtime and Stage Execution:
- Execution Modes and Streaming: user_guide/diffusion/execution_modes.md
- Sleep Mode: features/sleep_mode.md
- Quantization:
- Quantization: user_guide/quantization/overview.md
- Overview: user_guide/quantization/overview.md
- Online Quantization: user_guide/quantization/online.md
- Quantized KV Cache: user_guide/quantization/quantized_kvcache.md
- FP8 W8A8: user_guide/quantization/fp8.md
@@ -82,9 +54,33 @@ nav:
- MXFP8 W8A8: user_guide/quantization/mxfp8.md
- MXFP4 W4A4: user_guide/quantization/mxfp4.md
- ModelOpt: user_guide/quantization/modelopt.md
- GGUF: user_guide/quantization/gguf.md
- AutoRound: user_guide/quantization/autoround.md
- msModelSlim: user_guide/quantization/msmodelslim.md
- Diffusion Acceleration:
- Overview and Compatibility:
- Overview: user_guide/diffusion_features.md
- Feature Compatibility: user_guide/feature_compatibility.md
- CPU Offloading: user_guide/diffusion/cpu_offload.md
- Cache Acceleration:
- TeaCache: user_guide/diffusion/cache_acceleration/teacache.md
- Cache-DiT: user_guide/diffusion/cache_acceleration/cache_dit.md
- Parallelism:
- Overview: user_guide/diffusion/parallelism/overview.md
- CFG Parallel: user_guide/diffusion/parallelism/cfg_parallel.md
- Expert Parallel: user_guide/diffusion/parallelism/expert_parallel.md
- Hybrid Sharded Data Parallel: user_guide/diffusion/parallelism/hsdp.md
- Sequence Parallel: user_guide/diffusion/parallelism/sequence_parallel.md
- Tensor Parallel: user_guide/diffusion/parallelism/tensor_parallel.md
- VAE Parallelism: user_guide/diffusion/parallelism/vae_parallelism.md
- Attention Backends: user_guide/diffusion/attention_backends.md
- Regional Compilation: user_guide/diffusion/regional_compilation.md
- Frame Interpolation: user_guide/diffusion/frame_interpolation.md
- Startup and Loading: user_guide/diffusion/startup_and_loading.md
- LoRA: user_guide/diffusion/lora.md
- Experimental:
- Session State Manager: features/session_state_manager.md
- Integrations:
- Custom Diffusion Pipeline: features/custom_pipeline.md
- ComfyUI: features/comfyui.md
- Fault Tolerance:
- Failure Modes: user_guide/fault_injection_reliability_matrix.md
@@ -118,6 +114,7 @@ nav:
- Shared Memory Connector: design/feature/omni_connectors/shared_memory_connector.md
- Yuanrong Store Connector: design/feature/omni_connectors/yuanrong_connector.md
- Yuanrong Transfer Engine Connector: design/feature/omni_connectors/yuanrong_transfer_engine_connector.md
- Quantization: design/feature/quantization.md
- Diffusion Acceleration:
- Parallelism:
- CFG-Parallel: design/feature/cfg_parallel.md
@@ -127,9 +124,8 @@ nav:
- Sequence Parallel: design/feature/sequence_parallel.md
- Tensor Parallel: design/feature/tensor_parallel.md
- VAE Patch Parallelism: design/feature/vae_parallel.md
- Attention Backends:
- Attention Optimization:
- Skip-Softmax: design/feature/skip_softmax.md
- Quantization: design/feature/quantization.md
- Cache-DiT: design/feature/cache_dit.md
- TeaCache: design/feature/teacache.md
- Diffusion Continuous Batching: design/feature/diffusion_continuous_batching.md
+41
View File
@@ -76,6 +76,47 @@ Please refer to the [test instructions](./ci/test_execution_guide.md) for detail
!!! warning
Currently, not all unit tests pass when run on CPU platforms. If you don't have access to a GPU platform to run unit tests locally, rely on the continuous integration system to run the tests for now.
### Using repository skills with coding agents
vLLM-Omni maintains [repository-scale skills](https://github.com/vllm-project/vllm-omni/tree/main/.claude/skills) for common coding, testing, performance, and review workflows. These skills capture repository-specific structure, validation requirements, and evidence standards that a general coding agent may not know.
If your coding agent discovers repository skills automatically, ask it to use the relevant skill by name. Otherwise, point it to the linked `SKILL.md` and ask it to read the file before changing code. Skills can be combined: use a domain skill for the implementation, `vllm-omni-test` for test and CI decisions, and `precheck-pr` before opening the pull request.
| Task | Repository skill |
| --- | --- |
| Add or extend a diffusion model | [`add-diffusion-model`](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/add-diffusion-model/SKILL.md) |
| Add or extend a text-to-speech model | [`add-tts-model`](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/add-tts-model/SKILL.md) |
| Add, select, or debug quantization | [`quantization`](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/quantization/SKILL.md) |
| Diagnose or optimize diffusion performance | [`diffusion-perf-opt`](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/diffusion-perf-opt/SKILL.md) |
| Upgrade an NPU model runner | [`vllm-omni-npu-model-runner-upgrade`](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/vllm-omni-npu-upgrade/SKILL.md) |
| Add a regression test, choose L1-L4 coverage, or wire Buildkite | [`vllm-omni-test`](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/vllm-omni-test/SKILL.md) |
| Self-check a branch before opening a pull request | [`precheck-pr`](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/precheck-pr/SKILL.md) |
| Perform a maintainer or reviewer review of an existing pull request or local branch | [`review-pr`](https://github.com/vllm-project/vllm-omni/blob/main/.claude/skills/review-pr/SKILL.md) |
Use the following workflow when collaborating with a coding agent:
1. **Ground the task.** Provide the issue, RFC, or design document that defines the expected behavior. Ask the agent to state its assumptions and identify the affected module or feature contracts before editing code.
2. **Select the narrowest applicable skills.** Start with the domain skill that owns the change. Add `vllm-omni-test` whenever production behavior or tests change. For a bug fix, ask for the smallest reproducer and a regression test tied to the original symptom.
3. **Require executable evidence.** Ask the agent to run the narrowest relevant checks and report the exact commands and results. It must identify tests it could not run because of unavailable hardware, model weights, credentials, or dependencies; an unrun check is not a pass.
4. **Inspect the resulting diff.** Confirm that the implementation stays within the issue or RFC, does not overwrite unrelated work, and includes the required tests, documentation, and benchmark or accuracy evidence.
5. **Run the pre-submit workflow.** Use `precheck-pr`, then run the applicable local tests and pre-commit hooks before opening the pull request.
For example:
```text
Use add-diffusion-model and vllm-omni-test to implement the linked issue.
Read the relevant design documents first, keep the change within the issue,
and report every test command, result, and hardware validation gap.
```
```text
Use vllm-omni-test to turn this bug reproducer into the smallest deterministic
regression test. Prefer L1 CPU coverage; if the failure requires real weights
or serving, explain the required L2/L3 environment and provide the exact command.
```
Repository skills guide the agent, but they do not replace contributor judgment, the accepted issue or RFC, required test evidence, or maintainer review. Review all generated changes before submitting them.
## Issues
If you encounter a bug or have a feature request, please search existing issues first to see if it has already been reported. If not, please file a new issue, providing as much relevant information as possible.
+13 -5
View File
@@ -11,6 +11,10 @@ in the active navigation.
## Feature Design Documents
For user-facing configuration and current compatibility, see the
[Features overview](../features/README.md). A design document defines an
implementation contract; it is not, by itself, a general support claim.
### Runtime and stage execution
- [Disaggregated Inference](feature/disaggregated_inference.md)
@@ -30,6 +34,10 @@ in the active navigation.
- [Yuanrong Store Connector](feature/omni_connectors/yuanrong_connector.md)
- [Yuanrong Transfer Engine Connector](feature/omni_connectors/yuanrong_transfer_engine_connector.md)
### Quantization
- [Quantization](feature/quantization.md)
### Diffusion acceleration
#### Parallelism
@@ -42,14 +50,14 @@ in the active navigation.
- [Tensor Parallel](feature/tensor_parallel.md)
- [VAE Patch Parallelism](feature/vae_parallel.md)
#### Attention Backends
#### Attention optimization
The [Diffusion Attention Backends](../user_guide/diffusion/attention_backends.md)
guide lists all selectable backends and their platform defaults. Skip-Softmax
is the backend-related optimization with a standalone feature-design contract:
- [Skip-Softmax](feature/skip_softmax.md)
#### Quantization
- [Quantization](feature/quantization.md)
- [Cache-DiT](feature/cache_dit.md)
- [TeaCache](feature/teacache.md)
- [Diffusion Continuous Batching](feature/diffusion_continuous_batching.md)
+1
View File
@@ -7,6 +7,7 @@ owners:
- "@yinpeiqi"
- "@fake0fan"
- "@Sy0307"
- "@Gaohan123"
primary_code_paths:
- vllm_omni/core/**
- vllm_omni/worker/**
+1
View File
@@ -4,6 +4,7 @@ kind: module
status: draft
owners:
- "@alex-jw-brooks"
- "@Bounty-hunter"
primary_code_paths:
- vllm_omni/benchmarks/**
- vllm_omni/entrypoints/cli/benchmark/**
+1
View File
@@ -5,6 +5,7 @@ status: draft
owners:
- "@Isotr0py"
- "@princepride"
- "@SamitHuang"
primary_code_paths:
- vllm_omni/diffusion/cache/**
related_code_paths:
@@ -3,9 +3,10 @@ title: Diffusion Model Integration
kind: module
status: draft
owners:
- "@Isotr0py"
- "@fhfuih"
- "@Bounty-hunter"
- "@wtomin"
- "@ZJY0516"
- "@RuixiangMa"
primary_code_paths:
- vllm_omni/diffusion/models/**
- vllm_omni/diffusion/model_loader/**
@@ -6,6 +6,7 @@ owners:
- "@Isotr0py"
- "@princepride"
- "@SamitHuang"
- "@fhfuih"
primary_code_paths:
- vllm_omni/diffusion/sched/**
- vllm_omni/diffusion/executor/**
+2 -1
View File
@@ -4,7 +4,8 @@ kind: module
status: draft
owners:
- "@yuanheng-zhao"
- "@Isotr0py"
- "@david6666666"
- "@lishunyang12"
primary_code_paths:
- vllm_omni/diffusion/offloader/**
related_code_paths:
@@ -6,6 +6,7 @@ owners:
- "@princepride"
- "@yuanheng-zhao"
- "@xuechendi"
- "@Bounty-hunter"
primary_code_paths:
- vllm_omni/diffusion/distributed/**
- vllm_omni/diffusion/attention/parallel/**
+1
View File
@@ -6,6 +6,7 @@ architecture_state: current-plus-in-flight
owners:
- "@alex-jw-brooks"
- "@linyueqian"
- "@NickCao"
document_stewards:
- "@hsliuustc0106"
- "@Gaohan123"
+8 -1
View File
@@ -8,8 +8,15 @@ owners:
- "@xuechendi"
primary_code_paths:
- vllm_omni/platforms/**
related_code_paths:
- vllm_omni/attention/**
primary_code_path_owners:
# Additional owners scoped to attention kernels (quantized/custom attention)
- paths:
- vllm_omni/attention/**
owners:
- "@david6666666"
- "@lishunyang12"
related_code_paths: []
depends_on:
- ar_runtime.md
- diffusion/index.md
@@ -6,6 +6,8 @@ architecture_state: current
owners:
- "@Sy0307"
- "@amy-why-3459"
- "@Gaohan123"
- "@alex-jw-brooks"
document_stewards:
- "@hsliuustc0106"
- "@Gaohan123"
+8
View File
@@ -9,6 +9,14 @@ primary_code_paths:
- vllm_omni/model_executor/**
- vllm_omni/model_extras/**
- vllm_omni/plugins/**
primary_code_path_owners:
# Additional owners scoped to a sub-path of model_executor/**
- paths:
- vllm_omni/model_executor/models/**
owners:
- "@Sy0307"
- "@amy-why-3459"
- "@linyueqian"
related_code_paths:
- vllm_omni/transformers_utils/**
- vllm_omni/tokenizers/**
+1
View File
@@ -4,6 +4,7 @@ kind: module
status: draft
owners:
- "@lishunyang12"
- "@vraiti"
primary_code_paths:
- vllm_omni/metrics/**
related_code_paths:
+67
View File
@@ -0,0 +1,67 @@
# Features
Use this section to choose and configure vLLM-Omni runtime and optimization
features. The navigation follows the active
[Feature Design](../design/index.md#feature-design-documents) taxonomy whenever
an implementation contract has a user-facing workflow.
!!! note "User guides, design documents, and recipes"
User guides explain how to enable a feature and where it is supported.
Design documents define internal contracts and do not, by themselves,
imply general or production support. For model-specific launch flags and
hardware requirements, use the linked examples and validated recipes from
[Supported Models](../models/supported_models.md).
## Runtime and Stage Execution
| Goal | User guide | Related design contract |
| --- | --- | --- |
| Choose serial, batched, step-wise, or streaming diffusion execution | [Execution Modes and Streaming](../user_guide/diffusion/execution_modes.md) | [Diffusion Continuous Batching](../design/feature/diffusion_continuous_batching.md), [Async Diffusion Output](../design/feature/async_diffusion_output.md) |
| Reclaim stage memory without restarting the server | [Sleep Mode](sleep_mode.md) | Runtime lifecycle behavior is documented in the user guide |
Some runtime designs are deliberately not promoted as standalone User Guide
features yet:
- [Disaggregated Inference](../design/feature/disaggregated_inference.md) and
[OmniConnector implementations](../design/index.md#communication) describe
topology and transport contracts. Practical deployment configuration remains
under [Pipeline and deploy configurations](../configuration/stage_configs.md).
- [Async Chunk](../design/feature/async_chunk.md) and
[Async Omni Output Materialization](../design/feature/omni_async_output_materialization.md)
are model- and pipeline-dependent. Use the selected model's deploy
configuration and recipe for the supported settings.
- [Automatic Prefix Caching](../design/feature/prefix_caching.md) remains a
design-level contract until its user-facing configuration and compatibility
surface is consolidated.
## Quantization
Quantization is a cross-model feature rather than a diffusion-only
optimization. The unified [`quantization_config` guide](../user_guide/quantization/overview.md)
covers diffusion-only models, multi-stage omni/TTS models, and multi-stage
diffusion models. Its [design contract](../design/feature/quantization.md)
defines the shared configuration and backend extension points.
## Diffusion Acceleration
| Goal | User guide | Related design contract |
| --- | --- | --- |
| Compare acceleration methods and supported combinations | [Overview](../user_guide/diffusion_features.md), [Feature Compatibility](../user_guide/feature_compatibility.md) | [Diffusion acceleration designs](../design/index.md#diffusion-acceleration) |
| Move weights between host and device memory | [CPU Offloading](../user_guide/diffusion/cpu_offload.md) | [Distributed Layerwise Offload](../design/feature/distributed_layerwise_offload.md) |
| Reuse denoising computation | [Cache Acceleration](../user_guide/diffusion/cache_acceleration/cache_dit.md) | [Cache-DiT](../design/feature/cache_dit.md), [TeaCache](../design/feature/teacache.md) |
| Distribute diffusion work across devices | [Parallelism](../user_guide/diffusion/parallelism/overview.md) | [Parallelism designs](../design/index.md#parallelism) |
| Select dense, sparse, or quantized attention paths | [Attention Backends](../user_guide/diffusion/attention_backends.md) | [Skip-Softmax](../design/feature/skip_softmax.md) |
| Compile repeated diffusion regions | [Regional Compilation](../user_guide/diffusion/regional_compilation.md) | User-facing optimization guide |
| Add generated video frames | [Frame Interpolation](../user_guide/diffusion/frame_interpolation.md) | User-facing extension guide |
| Reduce diffusion model startup time | [Startup and Loading](../user_guide/diffusion/startup_and_loading.md) | User-facing loading guide |
| Apply diffusion adapters | [LoRA](../user_guide/diffusion/lora.md) | User-facing extension guide |
The [Pipeline Parallelism guide](../user_guide/diffusion/parallelism/pipeline_parallel.md)
remains available by direct link, but it is not promoted in the primary User
Guide navigation until its user-facing support and placement are settled.
## Experimental
[Session State Manager](session_state_manager.md) is opt-in and experimental.
Its APIs and compatibility may change without notice.
+26 -43
View File
@@ -24,15 +24,22 @@ SERVING_MODE_TITLES = {
"online_serving": "Online serving",
}
# Generated example pages keep their source-derived URLs even when their
# navigation category changes. Quantization helpers are feature documentation,
# not user-serving examples, so keep them under the existing Features /
# Quantization section instead of creating a second top-level Quantization
# category under Examples.
EXAMPLE_NAV_TARGETS = {
"quantization": "quantization",
}
GENERATED_QUANTIZATION_EXAMPLES_TITLE = "Generated Examples"
# Keep the Examples navigation focused on reusable modality/task entry points.
# Model-specific scripts remain available under ``examples/`` and are surfaced
# from these shared pages instead of getting one page per model.
GENERAL_EXAMPLE_SLUGS = frozenset(
{
"image_to_image",
"image_to_video",
"speech_to_video",
"text_to_audio",
"text_to_image",
"text_to_speech",
"text_to_video",
"x_to_text",
"x_to_video_audio",
}
)
def load_model_display_names() -> dict[str, str]:
@@ -56,6 +63,13 @@ def load_model_display_names() -> dict[str, str]:
MODEL_DISPLAY_NAMES = load_model_display_names()
def is_general_example(example: "Example") -> bool:
"""Return whether an example should get a generated serving page."""
if example.category not in SERVING_MODE_TITLES:
return True
return example.path.stem in GENERAL_EXAMPLE_SLUGS
def fix_case(text: str) -> str:
subs = {
"api": "API",
@@ -358,12 +372,9 @@ def update_nav_file(examples: list[Example]):
items.append({example.nav_title: rel_path.as_posix()})
return items
# Build the new Examples section - start with preserved items. Categories
# not explicitly routed elsewhere retain the historical Examples behavior.
# Build the new Examples section - start with preserved items.
examples_section = preserved_items.copy()
for category in sorted(examples_by_category.keys()):
if EXAMPLE_NAV_TARGETS.get(category, "examples") != "examples":
continue
items = category_items(category)
if items:
category_title = fix_case(category.replace("_", " ").title())
@@ -372,36 +383,6 @@ def update_nav_file(examples: list[Example]):
# Update the nav structure
nav_list[user_guide_idx]["User Guide"][examples_idx]["Examples"] = examples_section
features_content = next(
(item["Features"] for item in user_guide_content if isinstance(item, dict) and "Features" in item),
None,
)
quantization_content = None
if isinstance(features_content, list):
quantization_content = next(
(item["Quantization"] for item in features_content if isinstance(item, dict) and "Quantization" in item),
None,
)
if isinstance(quantization_content, list):
# Remove only the group owned by this hook so repeated builds are
# idempotent and hand-authored Quantization navigation is retained.
quantization_content[:] = [
item
for item in quantization_content
if not (isinstance(item, dict) and GENERATED_QUANTIZATION_EXAMPLES_TITLE in item)
]
quantization_examples = examples_by_category.get("quantization")
if quantization_examples:
if not isinstance(quantization_content, list):
logger.warning(
"Could not find 'User Guide' -> 'Features' -> 'Quantization' section; "
"omitting generated quantization examples from navigation"
)
else:
quantization_content.append({GENERATED_QUANTIZATION_EXAMPLES_TITLE: category_items("quantization")})
# Write back to file
nav_data["nav"] = nav_list
with open(NAV_FILE, "w", encoding="utf-8") as f:
@@ -432,6 +413,8 @@ def on_startup(command: Literal["build", "gh-deploy", "serve"], dirty: bool):
for path in category.glob("*/*.md"):
examples.append(Example(path.parent, category.stem))
examples = [example for example in examples if is_general_example(example)]
# Generate the example documentation
for example in sorted(examples, key=lambda e: e.path.stem):
example_name = f"{example.path.stem}.md"
+2
View File
@@ -51,6 +51,7 @@ th {
| `OvisImagePipeline` | Ovis-Image | `OvisAI/Ovis-Image` | ✅︎ | ✅︎ | | ✅︎ | — |
| `LongcatImagePipeline` | LongCat-Image | `meituan-longcat/LongCat-Image` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
| `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` | ✅︎ | | | | — |
| `StableDiffusionXLPipeline` | Stable-Diffusion-XL | `stabilityai/stable-diffusion-xl-base-1.0` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
| `StableDiffusion3Pipeline` | Stable-Diffusion-3 | `stabilityai/stable-diffusion-3.5-medium` | ✅︎ | ✅︎ | | ✅︎ | — |
@@ -79,6 +80,7 @@ th {
| `HiggsAudioV2ForConditionalGeneration` | Higgs-Audio v2 | `bosonai/higgs-audio-v2-generation-3B-base` | ✅︎ | | | | — |
| `HiggsMultimodalQwen3ForConditionalGeneration` | Higgs-Audio v3 (TTS) | `bosonai/higgs-audio-v3-tts-4b` | ✅︎ | | | | [Published](https://recipes.vllm.ai/bosonai/higgs-audio-v3-tts-4b) |
| `IndexTTS2TalkerForConditionalGeneration` | IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✅︎ | | | | — |
| `IndexTTS25TalkerForConditionalGeneration` | IndexTTS-2.5 | native `checkpoints/` bundle | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/IndexTeam/IndexTTS-2_5.md) |
| `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` | ✅︎ | ✅︎ | | | — |
+12 -15
View File
@@ -283,23 +283,20 @@ blocks — use Ulysses SP (`ring_degree=1`).
### Which geometries run sparse
Sparsity is only applied when the video segment is a **multiple of 128 rows**, where the row count
is `latent_t × (height / 32) × (width / 32)`. Otherwise `rf_v2`'s block mask and the kernel's own
tiling disagree on the block count, and the block straddling the seam mixes video and prefix rows
that selection may then drop. Any resolution still runs — an unaligned geometry falls back to dense
attention and logs `RAINFUSION_ATTN staying dense` with the row count it computed — but it gets no
speedup, so pick an aligned geometry when you want one.
MindIE-SD rf_v2 handles arbitrary video grids by spatially rearranging video first and, when
necessary, promoting a real-video suffix to its always-kept prefix segment. It never pads: rf_v2
does not consume an attention mask for padded keys. The remaining video rows stay sparse, while the
promoted rows remain visible to every query and key.
Alignment is necessary, not sufficient, for good quality. `rf_v2` groups video positions into 8x8
spatial tiles (two tiles fill one 128-row block), which is what makes a selected block a compact
patch of the frame. When the latent `h` or `w` is not a multiple of 8, the leftover rows or columns
are peeled off and appended as a flat run instead of being tiled, so they get pooled with spatially
distant positions and selection can no longer rank them meaningfully. This is silent, and invisible
to a `sparsity=0` check, because a fully populated mask does not care how blocks are grouped.
For example, MiniMax-H3 at 1344x768 has grid `(62, 24, 42)`: its 62496 video rows leave a 32-row
128-block residue, and its two non-8-wide latent columns form a 2928-row rearranged suffix.
MindIE-SD promotes 2976 rows, leaving 59520 sparse rows (`465 × 128`). This keeps the block mask
and kernel tiling aligned, at the cost of retaining more blocks.
"Multiple of 8" on the latent grid means **width and height that are multiples of 256**. Aligned
resolutions off that grid still run sparse; they just lose more fidelity at the same `sparsity`,
which you can buy back with `start_step`.
8x8-aligned spatial grids remain preferable for performance. A latent `h` or `w` not divisible by 8
creates an always-kept residual suffix and lowers realized sparsity. "Multiple of 8" on the latent
grid means **width and height that are multiples of 256**. This path requires a MindIE-SD release
that implements protected video tails for rf_v2.
## End-to-End Benchmark (BF16, sm_120 RTX Pro 6000 Blackwell)
+105 -7
View File
@@ -1,14 +1,18 @@
# LoRA (Low-Rank Adaptation) Guide
LoRA (Low-Rank Adaptation) enables fine-tuning diffusion models by adding trainable low-rank matrices to existing model weights. vLLM-Omni currently supports PEFT-style LoRA adapters, allowing you to customize model behavior without modifying the base model weights.
LoRA (Low-Rank Adaptation) enables fine-tuning diffusion models by adding trainable low-rank matrices to existing model weights. vLLM-Omni supports two LoRA backends: **PEFT** for PEFT-style adapters, and **Distill** usually for few-steps inference. **PEFT** backend allows you to customize model behavior without modifying the base model weights. **Distill** backend fuses LoRA weights into the base model at initialization.
## Overview
LoRA adapters are lightweight, model-specific fine-tuning weights that can be dynamically loaded and applied to diffusion models. vLLM-Omni uses a unified LoRA handling mechanism similar to vLLM with LRU cache management.
LoRA adapters are lightweight, model-specific fine-tuning weights that can be applied to diffusion models in two ways:
- **PEFT backend** (`--lora-backend peft`, default): Loads a PEFT-format adapter folder via `DiffusionLoRAManager`. Adapters are cached (LRU) and activated per request via `LoRARequest`. It uses a unified LoRA handling mechanisms similar to vLLM with LRU cache management.
- **Distill backend** (`--lora-backend distill`): Calls `pipeline.load_lora_weights` once at initialization to fuse one or more concrete checkpoint files directly into the base weights. Typically used for distilled few-step LoRAs (e.g. Lightning, LightX2V).
## LoRA Adapter Format
LoRA adapters must be in **PEFT (Parameter-Efficient Fine-Tuning)** format. A typical LoRA adapter directory structure:
### PEFT (Parameter-Efficient Fine-Tuning) format (default)
A typical PEFT-format LoRA adapter directory structure:
```
lora_adapter/
@@ -25,19 +29,20 @@ The `adapter_config.json` file contains metadata about the LoRA adapter, includi
### Offline Inference
#### Pre-loaded LoRA
#### PEFT backend: pre-loaded LoRA
Load a LoRA adapter at initialization. This adapter is pre-loaded into the cache and can be activated by requests:
Load a PEFT-format LoRA adapter at initialization. The adapter is pre-loaded into the cache and can be activated per request:
```python
from vllm_omni import Omni
from vllm_omni.lora.request import LoRARequest
lora_path="/path/to/lora_adapter"
lora_path = "/path/to/lora_adapter"
omni = Omni(
model="stabilityai/stable-diffusion-3.5-medium",
lora_path=lora_path
lora_path=lora_path,
lora_backend="peft", # default, can be omitted
)
lora_request = LoRARequest(
@@ -53,6 +58,98 @@ outputs = omni.generate(
)
```
#### Distill backend: fuse distilled LoRA at init
For distilled few-step LoRAs, pass `lora_backend="distill"` together with one or more concrete `.safetensors` files. The weights are fused into the base model once at init; subsequent `generate()` calls do not need a `LoRARequest`.
##### Supported pipelines
For Qwen-Image and Wan pipelines, the distill backend calls `pipeline.load_lora_weights(...)` during worker initialization.
| Pipeline | Supported distilled LoRA repo | Notes |
|----------|--------------------------------|-------|
| `QwenImagePipeline` | `lightx2v/Qwen-Image-2512-Lightning` | Used with Qwen-Image-2512 Lightning-style few-step inference. |
| `Wan22Pipeline` | `lightx2v/Wan2.1-Distill-Loras`, `lightx2v/Wan2.2-Distill-Loras` | Wan2.1 uses one LoRA file. For dual-transformer Wan2.2 MoE, pass high-noise then low-noise LoRA files. |
| `Wan22I2VPipeline` | `lightx2v/Wan2.2-Distill-Loras` | For dual-transformer Wan2.2 MoE, pass high-noise then low-noise LoRA files. |
Other diffusion pipelines are not currently listed as supporting distilled LoRA. Use the PEFT backend for request-time adapters, or bake converted weights into a local Diffusers directory before serving.
Single-file example (Qwen-Image-Lightning):
```python
from vllm_omni import Omni
omni = Omni(
model="Qwen/Qwen-Image-2512",
lora_path="/path/to/Qwen-Image-2512-Lightning.safetensors",
lora_backend="distill",
)
outputs = omni.generate(prompt="A piece of cheesecake")
```
Multi-file example (Wan2.2 MoE, high + low noise):
```python
from vllm_omni import Omni
omni = Omni(
model="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
lora_path=[
"/path/to/wan2.2_high_noise_lora.safetensors", # -> transformer
"/path/to/wan2.2_low_noise_lora.safetensors", # -> transformer_2
],
lora_backend="distill",
)
```
The CLI examples under `examples/offline_inference/` accept the same flags, e.g.:
```bash
python examples/offline_inference/text_to_video/text_to_video.py \
--model Wan-AI/Wan2.2-T2V-A14B-Diffusers \
--lora-backend distill \
--lora-path /path/to/high.safetensors /path/to/low.safetensors \
--prompt "A cat playing with yarn"
```
##### Online serving
Distilled LoRAs can also be fused when an online diffusion server starts. The
adapter remains active for the lifetime of that server, so requests do not pass
a per-request `lora` field.
```bash
vllm serve Qwen/Qwen-Image-2512 \
--omni \
--port 8091 \
--lora-backend distill \
--lora-path /path/to/Qwen-Image-2512-Lightning-4steps.safetensors
```
Then send a normal generation request with the sampling settings expected by
the distilled checkpoint:
```bash
curl -s http://localhost:8091/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "A Chinese female college student, around 20 years old, with a very short haircut that conveys a gentle, artistic vibe. Her hair naturally falls to partially cover her cheeks, projecting a tomboyish yet charming demeanor. She has cool-toned fair skin and delicate features, with a slightly shy yet subtly confident expression—her mouth crooked in a playful, youthful smirk. She wears an off-shoulder top, revealing one shoulder, with a well-proportioned figure. The image is framed as a close-up selfie: she dominates the foreground, while the background clearly shows her dormitory—a neatly made bed with white linens on the top bunk, a tidy study desk with organized stationery, and wooden cabinets and drawers. The photo is captured on a smartphone under soft, even ambient lighting, with natural tones, high clarity, and a bright, lively atmosphere full of youthful, everyday energy."}
],
"extra_body": {
"height": 1024,
"width": 1024,
"num_inference_steps": 4,
"true_cfg_scale": 1.0,
"seed": 42
}
}' | jq -r '.choices[0].message.content[0].image_url.url' | cut -d',' -f2 | base64 -d > qwen_image_distill_lora_output.png
```
For Wan2.2 MoE serving, pass the high-noise checkpoint first and the low-noise
checkpoint second to `--lora-path`.
!!! note "Server-side Path Requirement"
The LoRA adapter path (`local_path`) must be readable on the **server** machine. If your client and server are on different machines, ensure the LoRA adapter is accessible via a shared mount or copied to the server.
@@ -141,6 +238,7 @@ Notes:
- This route avoids runtime LoRA loading changes in vLLM-Omni when you choose to bake converted weights into a local Diffusers directory.
- Output quality and speed depend on the replacement checkpoints and sampling params you choose.
- If you only need to fuse distilled LoRAs into a Wan2.2 checkpoint at load time (without the full LightX2V convert + assemble pipeline), you can instead pass them directly via `--lora-backend distill --lora-path <high>.safetensors <low>.safetensors`. See the [Distill backend](#distill-backend-fuse-distilled-lora-at-init) section above.
## See Also
@@ -164,10 +164,12 @@ In `DiffusionParallelConfig`:
| `ulysses_degree` | int | 1 | Number of GPUs for Ulysses-SP. Uses all-to-all communication. |
| `ring_degree` | int | 1 | Number of GPUs for Ring-Attention. Uses P2P ring communication. |
| `ulysses_mode` | str | `"default"` | Ulysses attention mode. Set to `"advanced_uaa"` to handle arbitrary sequence lengths and head counts without padding. |
| `mask_sp_padding` | bool | `False` | When the sequence length is not divisible by the SP size, tokens are auto-padded with zeros. Set to `True` to mask those padding tokens (strict, but uses the slower varlen attention path); the default `False` leaves them unmasked, keeping the fast path with negligible numerical impact. |
**Notes:**
- Total sequence parallel size equals to `ulysses_degree × ring_degree`
- Degrees must evenly divide the sequence length for optimal performance (or use `ulysses_mode="advanced_uaa"` for Ulysses-SP)
- `mask_sp_padding` is an experimental feature, currently only supported by `Wan2.2`, `Wan2.2 Vace`, `Qwen-Image`, `Flux 2`, and `HunyuanVideo 1.5`
## Best Practices
@@ -0,0 +1,60 @@
# Diffusion Startup and Loading
Large diffusion models can take several minutes to load at startup. vLLM-Omni
loads safetensors shards in parallel to reduce this initialization time.
Multi-thread weight loading is enabled by default with four threads. No
configuration is needed for the default behavior.
## Configuration
| Parameter | CLI flag | Default | Description |
| --- | --- | --- | --- |
| `enable_multithread_weight_load` | `--disable-multithread-weight-load` | `True` | Pass the flag to disable multi-thread loading |
| `num_weight_load_threads` | `--num-weight-load-threads` | `4` | Number of parallel weight-loading threads |
!!! tip
The default balances startup speed and disk I/O contention. Fast NVMe
storage may benefit from more threads, while network storage or hard disks
may not.
## Online Serving
```bash
# Default: multi-thread loading with four threads
vllm serve Qwen/Qwen-Image --omni --port 8091
# Increase the thread count
vllm serve Wan-AI/Wan2.2-I2V-A14B-Diffusers --omni \
--num-weight-load-threads 8
# Disable multi-thread loading
vllm serve Qwen/Qwen-Image --omni --disable-multithread-weight-load
```
## Offline Inference
```python
from vllm_omni import Omni
# Default: multi-thread loading with four threads
omni = Omni(model="Qwen/Qwen-Image")
# Increase the thread count
omni = Omni(
model="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
num_weight_load_threads=8,
)
```
## Reference Benchmarks
The following measurements were collected on NVIDIA H800 hardware. Treat them
as reference results rather than a guarantee for other storage or hardware
configurations.
| Model | Sequential loading | Multi-thread loading | Speedup |
| --- | ---: | ---: | ---: |
| **Qwen/Qwen-Image** (53.7 GiB) | 168 s | 27 s | **6.2x** |
| **Wan-AI/Wan2.2-I2V-A14B-Diffusers** (64.5 GiB) | 283 s | 56 s | **5.1x** |
+38 -85
View File
@@ -49,7 +49,7 @@ Parallelism methods distribute computation across GPUs without quality loss (mat
| Method | Description | Best For |
|--------|-------------|----------|
| **[Multi-Thread Weight Loading](#multi-thread-weight-loading)** | Loads safetensors shards in parallel using a thread pool | All diffusion models; reduces startup from minutes to seconds |
| **[Multi-Thread Weight Loading](diffusion/startup_and_loading.md)** | Loads safetensors shards in parallel using a thread pool | All diffusion models; reduces startup from minutes to seconds |
**Note:** Some acceleration methods can be combined together for optimized performance. See [Feature Compatibility Table](#feature-compatibility) and [Feature Compatibility Tutorial](feature_compatibility.md) for detailed configuration examples.
@@ -100,7 +100,9 @@ The following tables show which models support each feature:
- **🔀SP (Ulysses & Ring)**: Includes both Ulysses-SP and Ring-Attention methods
- ✅ = Fully supported
- ✅* = Supported with the constraint listed below the table
- ❌ = Not supported
- ❓ = Not verified; not recommended
> Notes:
@@ -119,7 +121,7 @@ The following tables show which models support each feature:
| **FLUX.2-dev** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **Hidream-I1-Full** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅* |
| **Krea 2** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **LongCat-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **LongCat-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
@@ -145,6 +147,7 @@ The following tables show which models support each feature:
> 2. `Tongyi-MAI/Z-Image-Turbo` and `SII-GAIR/daVinci-MagiHuman-Base-1080p` are distilled models with minimal NFEs; CFG-Parallel is not necessary.
> 3. Cosmos3 T2I uses `Cosmos3OmniDiffusersPipeline` with `modalities=["image"]`. Model-level CPU offload swaps the nested UND reasoner and GEN generator pathways; layerwise offload remains available for blockwise GEN/UND offload.
> 4. Krea 2 currently supports single-GPU inference plus LoRA, Cache-DiT, HSDP, CPU/layerwise offload, and VAE-patch-parallel (decode). TP/SP/CFG-Parallel are not yet wired. The few-step distilled (Turbo) checkpoint uses `is_distilled=true` (fixed timestep shift `mu=1.15`); generate at 2048x2048 by default with `num_inference_steps≈8` and `guidance_scale=0`. The Raw checkpoint uses 1024x1024, `num_inference_steps=28`, and `guidance_scale=4.5`.
> 5. HunyuanImage3 supports step execution. Multi-request step execution requires `TORCH_SDPA`; see [Diffusion Execution Modes](diffusion/execution_modes.md#step-execution).
### VideoGen
@@ -155,12 +158,16 @@ The following tables show which models support each feature:
| **Wan2.1-VACE** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **LTX-2.3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **Helios** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Helios** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅* |
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
| **DreamID-Omni** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Cosmos3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
| **LongCat-Video-Avatar-1.5** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **MiniMax-H3** | ✅ (FL2VA) | ✅ | ✅ | ❌ | ✅ (DiT/TE) | ❌ | ✅ | ✅ | ✅ (tile) | ✅ (DiT) | ❌ |
> **Step execution note:** Helios supports single-request step execution only;
> use `max_num_seqs=1`.
**Frame Interpolation Support**
- **Supported**: Wan2.2 text-to-video, image-to-video, and TI2V pipelines
@@ -189,7 +196,7 @@ The following tables show which models support each feature:
| **🔀Ring-Attn** | ✅ | ✅ | ✅ | | | | | | | | | | | |
| **🔀CFG-Parallel** | ✅ | ✅ | ✅ | ✅ | | | | | | | | | | |
| **🔀Tensor Parallel** | ✅ | ✅ | ✅ | ✅ | ✅ | | | | | | | | | |
| **🔀HSDP** | ❓ | ❓ | ❓ | ❓ | ❓ | ❌ | | | | | | | | |
| **🔀HSDP** | ❓ | ❓ | ✅ | ❓ | ✅ | ❌ | | | | | | | | |
| **🔀Expert Parallel** | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | | | | | | |
| **💾CPU Offloading (Layerwise)** | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | | | | | | |
| **💾CPU Offloading (Module-wise)** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | ❌ | | | | | |
@@ -200,97 +207,43 @@ The following tables show which models support each feature:
!!! info
1. Tensor Parallel and HSDP are not compatible.
1. HSDP can be combined with Ulysses-SP or CFG-Parallel. Tensor Parallel
and HSDP are not compatible; other HSDP combinations in the table
remain unverified.
2. TeaCache and Cache-DiT are not compatible.
3. CPU Offloading (Layerwise) and CPU Offloading (Module-wise) are not compatible.
4. CPU Offloading (Layerwise) supports single-card for now.
5. Using FP8-Quant as an example of qunatization methods.
4. The CPU Offloading (Layerwise) row describes local layerwise offload.
Multi-device Distributed Layerwise Offload has a separate topology and
compatibility matrix in the [CPU Offloading guide](diffusion/cpu_offload.md#distributed-layerwise-offloading).
5. The compatibility matrix uses FP8 as the representative quantization method.
6. Step Execution is not compatible with any diffusion cache backend. LoRA is supported, but each scheduled batch must use a single adapter (requests with different `lora_request` or `lora_scale` are kept in separate batches).
## Multi-Thread Weight Loading
Large diffusion models can take several minutes to load weights at startup (e.g., ~3 min for Qwen-Image, ~5 min for Wan2.2 I2V 14B). Multi-thread weight loading speeds up this process by loading safetensors shards in parallel using a thread pool instead of sequentially.
This optimization is **enabled by default** with 4 threads. No configuration is needed for the default behavior.
### Configuration
| Parameter | CLI Flag | Default | Description |
|-----------|----------|---------|-------------|
| `enable_multithread_weight_load` | `--disable-multithread-weight-load` | `True` (enabled) | Pass the flag to disable multi-thread loading |
| `num_weight_load_threads` | `--num-weight-load-threads` | `4` | Number of threads for parallel weight loading |
!!! tip
The default of 4 threads balances speed and disk I/O contention. On fast NVMe storage you may benefit from more threads (e.g., 8). On HDD or network storage, the default of 4 avoids saturating I/O bandwidth.
### Online Serving
```bash
# Default (multi-thread enabled, 4 threads)
vllm serve Qwen/Qwen-Image --omni --port 8091
# Custom thread count
vllm serve Wan-AI/Wan2.2-I2V-A14B-Diffusers --omni --num-weight-load-threads 8
# Disable multi-thread loading
vllm serve Qwen/Qwen-Image --omni --disable-multithread-weight-load
```
### Offline Inference
```python
from vllm_omni import Omni
# Default (multi-thread enabled, 4 threads)
omni = Omni(model="Qwen/Qwen-Image")
# Custom thread count
omni = Omni(
model="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
num_weight_load_threads=8,
)
```
### Benchmarks
Measured on NVIDIA H800:
| Model | Before | After | Speedup |
|-------|--------|-------|---------|
| **Qwen/Qwen-Image** (53.7 GiB) | 168s | 27s | **6.2x** |
| **Wan-AI/Wan2.2-I2V-A14B-Diffusers** (64.5 GiB) | 283s | 56s | **5.1x** |
The loading guide now lives at [Diffusion Startup and
Loading](diffusion/startup_and_loading.md). This heading remains so existing
links to this section continue to work.
## Learn More
**Cache Acceleration:**
The Diffusion Acceleration navigation groups the remaining guides as follows:
- **[TeaCache Configuration Guide](diffusion/cache_acceleration/teacache.md)** - Parameter tuning, performance tips, troubleshooting
- **[Cache-DiT Advanced Guide](diffusion/cache_acceleration/cache_dit.md)** - DBCache, TaylorSeer, SCM techniques and optimization
| Area | Guide |
| --- | --- |
| Compatibility | [Feature Compatibility](feature_compatibility.md) |
| CPU offloading | [CPU Offloading](diffusion/cpu_offload.md) |
| Cache acceleration | [TeaCache](diffusion/cache_acceleration/teacache.md), [Cache-DiT](diffusion/cache_acceleration/cache_dit.md) |
| Parallelism | [Parallelism Overview](diffusion/parallelism/overview.md) |
| Attention | [Attention Backends](diffusion/attention_backends.md) |
| Compilation | [Regional Compilation](diffusion/regional_compilation.md) |
| Video extension | [Frame Interpolation](diffusion/frame_interpolation.md) |
| Startup | [Startup and Loading](diffusion/startup_and_loading.md) |
| Adapters | [LoRA](diffusion/lora.md) |
**Parallelism Methods:**
Related cross-model and runtime features are documented separately:
- **[Parallelism Overview](diffusion/parallelism/overview.md)** - Tensor Parallelism, Sequence Parallelism, CFG Parallelism, Pipeline Parallelism, HSDP, and Expert Parallelism
**Memory Optimization:**
- **[CPU Offload Guide](diffusion/cpu_offload.md)** - Offload model components to CPU, reduce GPU memory usage
- **[VAE Parallelism Guide](diffusion/parallelism/vae_parallelism.md)** - Distribute VAE decode work across GPUs for high-resolution images and videos
- **[Quantization](quantization/overview.md)** - Quantization methods for diffusion, multi-stage omni/TTS, and multi-stage diffusion models
**Extensions:**
- **[LoRA Inference Guide](diffusion/lora.md)** - Low-Rank Adaptation for style customization and fine-tuning
- **[Frame Interpolation Guide](diffusion/frame_interpolation.md)** - Worker-side post-generation video frame interpolation for smoother motion
**Execution Modes:**
- **[Diffusion Execution Modes](diffusion/execution_modes.md)** - Configure request batching, step execution, continuous batching, and streaming output
**Startup Optimization:**
- **[Multi-Thread Weight Loading](#multi-thread-weight-loading)** - Speed up model startup by loading safetensors shards in parallel
**Advanced Topics:**
- **[Feature Compatibility](feature_compatibility.md)** - How to combine multiple features for maximum performance
- [Quantization](quantization/overview.md) covers diffusion-only models,
multi-stage omni/TTS models, and multi-stage diffusion models.
- [Execution Modes and Streaming](diffusion/execution_modes.md) covers the
diffusion runtime, including batching, step execution, and streaming output.
@@ -88,8 +88,9 @@ python text_to_image.py \
| `--vae-use-tiling` | flag | off | Enable VAE tiling for memory optimization |
| `--cfg-parallel-size` | int | `1` | Set to `2` to enable CFG Parallel |
| `--enable-cpu-offload` | flag | off | Enable CPU offloading for diffusion models |
| `--lora-path` | str | — | Path to PEFT LoRA adapter folder |
| `--lora-path` | str | — | Path to PEFT LoRA adapter folder or checkpoint file |
| `--lora-scale` | float | `1.0` | Scale factor for LoRA weights |
| `--lora-backend` | str |`"peft"`| LoRA backend for loading LoRA adapters. Default: peft. Available options: peft, distill |
**NextStep-1.1 specific arguments:**
@@ -125,6 +125,9 @@ python text_to_video.py \
- `--audio-sample-rate`: fallback audio sample rate when the pipeline returns audio.
- `--quantization`: quantization method (`fp8` for FP8, `gguf` for GGUF).
- `--flow-shift`: scheduler flow_shift parameter.
- `--lora-path`: path to PEFT LoRA adapter folder or checkpoint file.
- `--lora-scale`: scale factor for LoRA weights.
- `--lora-backend`: backend for loading LoRA adapters. Default: peft. Available options: peft, distill.
### Wan2.2-specific
@@ -99,7 +99,7 @@ a native vLLM-Omni pipeline for those cases.
### Attention Backends
The diffusers backend converts
[vLLM-Omni standard of attention backend setting](../../../docs/user_guide/diffusion/attention_backends.md)
[vLLM-Omni standard of attention backend setting](../../diffusion/attention_backends.md)
to [diffusers standard](https://huggingface.co/docs/diffusers/optimization/attention_backends#available-backends).
Specifically for `FLASH_ATTN`, it will first attempt to use FlashAttention-3 and then FlashAttention-2.
+15 -15
View File
@@ -12,20 +12,20 @@ For the internal architecture and backend extension points, see the
| Mode | Guide | Description | Methods |
|------|-------|-------------|---------|
| Online quantization | [Online Quantization](online.md) | vLLM-Omni computes quantized weights and scales while loading the model. | FP8 W8A8, Int8 W8A8, MXFP8 W8A8, MXFP4 W4A4 |
| Online quantization | [Online Quantization](online.md) | vLLM-Omni computes quantized weights and scales while loading the model. | FP8 W8A8, Int8 W8A8, BitsAndBytes W4, MXFP8 W8A8, MXFP4 W4A4 |
| Runtime attention quantization | [Quantized KV Cache](quantized_kvcache.md) | vLLM-Omni dynamically quantizes eligible diffusion Flash Attention tensors during inference. | FP8 FA |
| Pre-quantized checkpoints | Method-specific guides | The checkpoint or an offline quantizer provides quantized weights and scales before serving. | ModelOpt, GGUF, AutoRound, msModelSlim, serialized Int8, offline MXFP8, offline MXFP4 DualScale |
| Pre-quantized checkpoints | Method-specific guides | The checkpoint or an offline quantizer provides quantized weights and scales before serving. | ModelOpt, AutoRound, msModelSlim, serialized Int8, offline MXFP8, offline MXFP4 DualScale |
## Hardware Support
| Device | FP8 W8A8 | Int8 W8A8 | ModelOpt | MXFP8 W8A8 | MXFP4 W4A4 | GGUF | AutoRound | msModelSlim |
|--------|----------|-----------|----------|------------|------------|------|-----------|-------------|
| NVIDIA Blackwell GPU (SM 100+) | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ✅ | ❌ |
| NVIDIA Ada/Hopper GPU (SM 89+) | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ✅ | ❌ |
| NVIDIA Ampere GPU (SM 80+) | ✅ | ✅ | ⭕ | ⭕ | ⭕ | ✅ | ✅ | ❌ |
| AMD ROCm | ⭕ | ⭕ | ⭕ | ⭕ | ✅ | ⭕ | ⭕ | ❌ |
| Intel XPU | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | ✅ | ❌ |
| Ascend NPU | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ |
| Device | FP8 W8A8 | Int8 W8A8 | BitsAndBytes W4 | ModelOpt | MXFP8 W8A8 | MXFP4 W4A4 | AutoRound | msModelSlim |
|--------|----------|-----------|------------------|----------|------------|------------|-----------|-------------|
| NVIDIA Blackwell GPU (SM 100+) | ✅ | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ❌ |
| NVIDIA Ada/Hopper GPU (SM 89+) | ✅ | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ❌ |
| NVIDIA Ampere GPU (SM 80+) | ✅ | ✅ | ✅ | ⭕ | ⭕ | ⭕ | ✅ | ❌ |
| AMD ROCm | ⭕ | ⭕ | ❌ | ⭕ | ⭕ | ✅ | ⭕ | ❌ |
| Intel XPU | ⭕ | ⭕ | ❌ | ⭕ | ⭕ | ⭕ | ✅ | ❌ |
| Ascend NPU | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ |
Legend: `✅` supported, `❌` unsupported, `⭕` not verified in this
guide. FP8 on Ampere may use a weight-only path where available.
@@ -43,10 +43,10 @@ otherwise.
|--------|-------|------|----------------|--------|
| FP8 W8A8 | [FP8](fp8.md) | Online W8A8 or checkpoint FP8 | Qwen-Image; Wan2.2 is not validated | Validated for Qwen-Image family and other DiT models |
| Int8 W8A8 | [Int8](int8.md) | Online or serialized W8A8 | Qwen-Image; Wan2.2 is not validated | Validated for Qwen-Image and Z-Image |
| BitsAndBytes W4 | [BitsAndBytes](bitsandbytes.md) | Online W4 weight-only | Z-Image-Turbo; Qwen-Image/Wan2.2 not validated | Validated for Z-Image-Turbo |
| ModelOpt | [ModelOpt](modelopt.md) | Pre-quantized FP8 checkpoints | Qwen-Image, Z-Image, FLUX.2, HunyuanImage-3.0 | Validated for ModelOpt FP8 diffusion checkpoints |
| MXFP8 W8A8 | [MXFP8](mxfp8.md) | Online W8A8 or offline pre-quantized | Wan2.2-T2V-A14B, I2V-A14B, TI2V-5B | Ascend NPU only; validated for Wan2.2 |
| MXFP4 W4A4 | [MXFP4](mxfp4.md) | `mxfp4`: online single-scale only; `mxfp4_dualscale`: online or offline dual-scale (offline recommended) | Wan2.2-T2V-A14B, I2V-A14B | Ascend NPU only; validated for Wan2.2 A14B cascade models; TI2V-5B not supported; offline `mxfp4_dualscale` uses calibrated `mul_scale` for best accuracy |
| GGUF | [GGUF](gguf.md) | Pre-quantized transformer weights | Qwen-Image | Validated where a model-specific GGUF adapter exists |
| AutoRound | [AutoRound](autoround.md) | Pre-quantized W4A16 checkpoints | FLUX.1-dev; Qwen-Image/Wan2.2 not validated | Checkpoint-driven |
| msModelSlim | [msModelSlim](msmodelslim.md) | Pre-quantized Ascend checkpoints | Wan2.2 recipe; HunyuanImage-3.0 inference target | Ascend/NPU path |
@@ -61,9 +61,9 @@ in BF16 unless the model guide explicitly adds support.
|--------|-------|-------|----------------|--------|
| ModelOpt | [ModelOpt](modelopt.md) | Thinker or language-model checkpoint config | Qwen3-Omni thinker | ModelOpt checkpoint path |
| Int8 | [Int8](int8.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
| BitsAndBytes | [BitsAndBytes](bitsandbytes.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
| MXFP8 | [MXFP8](mxfp8.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
| MXFP4 | [MXFP4](mxfp4.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
| GGUF | [GGUF](gguf.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
| AutoRound | [AutoRound](autoround.md) | Thinker or language-model checkpoint config | Qwen2.5-Omni, Qwen3-Omni | Supported through AutoRound checkpoints |
| msModelSlim | [msModelSlim](msmodelslim.md) | Not currently validated for omni/TTS stages | Qwen3-Omni, Qwen3-TTS | Not validated |
@@ -76,10 +76,10 @@ attached to the intended stage rather than applied globally.
|--------|-------|-------|----------------|--------|
| FP8 | [FP8](fp8.md) | Stage-specific DiT or transformer module | BAGEL, GLM-Image | Requires model-specific validation |
| Int8 | [Int8](int8.md) | Stage-specific DiT or transformer module | BAGEL, GLM-Image | Requires model-specific validation |
| BitsAndBytes | [BitsAndBytes](bitsandbytes.md) | Stage-specific transformer or DiT module | BAGEL, GLM-Image | Not validated |
| ModelOpt | [ModelOpt](modelopt.md) | Checkpoint-defined diffusion stage | BAGEL, GLM-Image | Requires model-specific validation |
| MXFP8 | [MXFP8](mxfp8.md) | Stage-specific DiT or transformer module | BAGEL, GLM-Image | Not validated |
| MXFP4 | [MXFP4](mxfp4.md) | Stage-specific DiT or transformer module | BAGEL, GLM-Image | Not validated |
| GGUF | [GGUF](gguf.md) | Stage-specific transformer weights | BAGEL, GLM-Image | No validated adapter listed |
| AutoRound | [AutoRound](autoround.md) | Checkpoint-defined stage | BAGEL, GLM-Image | No validated checkpoint listed |
| msModelSlim | [msModelSlim](msmodelslim.md) | Ascend-generated stage weights | GLM-Image | Requires model-specific adaptation |
@@ -106,7 +106,7 @@ config = build_quant_config({
| Component | Default quantized? | Notes |
|-----------|--------------------|-------|
| Diffusion transformer | Yes | Primary target for FP8, Int8, ModelOpt, MXFP8, MXFP4, GGUF, AutoRound, and msModelSlim |
| Diffusion transformer | Yes | Primary target for FP8, Int8, BitsAndBytes, ModelOpt, MXFP8, MXFP4, AutoRound, and msModelSlim |
| Text encoder | No | Keep BF16 unless a method-specific guide documents support |
| VAE | No | Keep BF16; storage-only paths are method-specific |
| Scheduler/tokenizer | No | Loaded from the base model repository |
@@ -139,8 +139,8 @@ from vllm_omni.quantization import build_quant_config
build_quant_config("fp8")
build_quant_config({"method": "fp8", "activation_scheme": "static"})
build_quant_config("bitsandbytes")
build_quant_config("auto-round", bits=4, group_size=128)
build_quant_config({"method": "gguf", "gguf_model": "/path/to/model.gguf"})
build_quant_config({"transformer": {"method": "fp8"}, "vae": None})
build_quant_config(None)
```
@@ -60,8 +60,12 @@ from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.utils.param_utils import apply_declared_extra_args
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.lora.request import LoRARequest
from vllm_omni.lora.utils import stable_lora_int_id
from vllm_omni.model_extras import (
build_image_to_video_prompt as build_model_image_to_video_prompt,
)
from vllm_omni.model_extras import (
build_image_to_video_prompt,
get_extra_body_params,
get_model_class_name,
)
@@ -83,6 +87,22 @@ def parse_json_object(value: str, flag_name: str = "argument") -> dict[str, Any]
parse_profiler_config = functools.partial(parse_json_object, flag_name="--profiler-config")
def build_image_to_video_prompt(
prompt: str,
negative_prompt: str | None,
media_inputs: dict[str, Any],
) -> dict[str, Any]:
"""Build the canonical request envelope for the shared I2V example."""
result: dict[str, Any] = {
"prompt": prompt,
"modalities": ["video"],
"multi_modal_data": media_inputs,
}
if negative_prompt is not None:
result["negative_prompt"] = negative_prompt
return result
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
@@ -294,6 +314,30 @@ def parse_args() -> argparse.Namespace:
default=None,
help='JSON profiler config for torch/cuda profiling, e.g. \'{"profiler":"torch","torch_profiler_dir":"./perf"}\'.',
)
parser.add_argument(
"--lora-path",
type=str,
nargs="+",
default=None,
help="Path to a LoRA adapter folder (PEFT format) or concrete LoRA checkpoint files. "
"For Wan2.2 MoE models, pass the high-noise checkpoint first and the low-noise checkpoint second.",
)
parser.add_argument(
"--lora-scale",
type=float,
default=1.0,
help="Scale factor for PEFT LoRA weights (default: 1.0).",
)
parser.add_argument(
"--lora-backend",
type=str,
default="peft",
choices=["peft", "distill"],
help=(
"LoRA backend. 'peft' loads a PEFT-format adapter for request-time activation; "
"'distill' fuses one or more concrete LoRA checkpoint files at initialization."
),
)
parser.add_argument(
"--extra-body",
type=functools.partial(parse_json_object, flag_name="--extra-body"),
@@ -487,6 +531,12 @@ def main():
omni_kwargs["flow_shift"] = flow_shift
if args.quantization is not None:
omni_kwargs["quantization"] = args.quantization
if args.lora_path is not None:
lora_path = args.lora_path
if len(lora_path) == 1:
lora_path = lora_path[0]
omni_kwargs["lora_path"] = lora_path
omni_kwargs["lora_backend"] = args.lora_backend
# Cosmos3 loads its (gated) guardrail models at build time, so the guardrails
# gate is an engine-level config (offline analog of the server's --no-guardrails).
if args.extra_body and "guardrails" in args.extra_body:
@@ -517,19 +567,39 @@ def main():
print(f" Video size: {width}x{height}")
print(f"{'=' * 60}\n")
lora_request = None
if args.lora_path and args.lora_backend == "peft":
if len(args.lora_path) != 1:
raise ValueError("Only one LoRA path is expected for PEFT backend.")
lora_path = args.lora_path[0]
lora_request = LoRARequest(
lora_name=Path(lora_path).stem,
lora_int_id=stable_lora_int_id(lora_path),
lora_path=lora_path,
)
negative_prompt = args.negative_prompt
if negative_prompt is None and not is_ltx2:
# Preserve the historical empty-prompt behavior for non-LTX examples.
negative_prompt = ""
prompt_dict = build_image_to_video_prompt(
model_class_name=model_class_name,
prompt=args.prompt,
negative_prompt=negative_prompt,
media_inputs=media_inputs,
)
prompt_dict = build_model_image_to_video_prompt(
model_class_name=model_class_name,
prompt=prompt_dict,
height=height,
width=width,
num_frames=num_frames,
)
sampling_extra_args = {"sample_solver": args.sample_solver}
if lora_request is not None:
sampling_extra_args["lora_request"] = lora_request
sampling_extra_args["lora_scale"] = args.lora_scale
sampling_params = OmniDiffusionSamplingParams(
height=height,
width=width,
@@ -540,7 +610,7 @@ def main():
num_inference_steps=num_inference_steps,
num_frames=num_frames,
frame_rate=frame_rate,
extra_args={"sample_solver": args.sample_solver},
extra_args=sampling_extra_args,
)
if flow_shift is not None:
sampling_params.extra_args["flow_shift"] = flow_shift
@@ -0,0 +1,319 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import argparse
import json
import re
import subprocess
import tempfile
import wave
from functools import lru_cache
from pathlib import Path
from typing import Any
import imageio
import numpy as np
import torch
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.models.longcat_video.pipeline_longcat_video_avatar import (
prepare_longcat_video_avatar_model_for_omni,
)
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.platforms import current_omni_platform
def parse_args():
parser = argparse.ArgumentParser(description="Generate LongCat-Video-Avatar A2V/AI2V video.")
parser.add_argument("--model", default="meituan-longcat/LongCat-Video-Avatar-1.5")
parser.add_argument("--base-model-dir", default=None, help="Optional local LongCat-Video base model directory.")
parser.add_argument("--stage", choices=["at2v", "ai2v"], default="at2v")
parser.add_argument("--prompt", default="A person speaks calmly while facing the camera.")
parser.add_argument("--negative-prompt", default="low quality, blurry, watermark, text")
parser.add_argument("--audio", default=None, help="Input speech audio path.")
parser.add_argument("--image", default=None, help="Input reference image path for AI2V.")
parser.add_argument("--input-json", default=None, help="Official LongCat Avatar JSON case for AI2V.")
parser.add_argument("--output", default="longcat_avatar_output.mp4")
parser.add_argument("--resolution", choices=["480p", "720p"], default="480p")
parser.add_argument("--num-frames", type=int, default=93)
parser.add_argument("--num-segments", default="1", help="Number of AVC segments, or 'auto' to cover full audio.")
parser.add_argument("--num-cond-frames", type=int, default=13)
parser.add_argument("--ref-img-index", type=int, default=10)
parser.add_argument("--mask-frame-range", type=int, default=3)
parser.add_argument("--fps", type=int, default=25)
parser.add_argument("--num-inference-steps", type=int, default=8)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--use-distill", action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--use-int8", action=argparse.BooleanOptionalAction, default=True)
parser.add_argument(
"--build-components-on-gpu",
action="store_true",
default=False,
help="Build large Avatar components directly on GPU for faster startup. Requires more peak VRAM.",
)
parser.add_argument("--use-kv-cache", action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--offload-kv-cache", action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("--enforce-eager", action="store_true")
return parser.parse_args()
def _extract_frames(output: Any):
if isinstance(output, list):
output = output[0] if output else None
if isinstance(output, OmniRequestOutput):
if not output.images:
raise ValueError("No video frames found in OmniRequestOutput.")
frames = output.images[0] if len(output.images) == 1 else output.images
if isinstance(frames, tuple) and len(frames) == 2:
frames = frames[0]
if isinstance(frames, dict):
frames = frames.get("frames") or frames.get("video")
else:
frames = output
if frames is None:
raise ValueError("No video frames found in output.")
return frames
def _frames_to_uint8(frames):
frames_np = np.asarray([np.asarray(frame) for frame in frames])
if frames_np.ndim == 5:
frames_np = frames_np[0]
if np.issubdtype(frames_np.dtype, np.floating):
frames_np = (np.clip(frames_np, 0.0, 1.0) * 255).round()
return np.clip(frames_np, 0, 255).astype(np.uint8)
@lru_cache(maxsize=1)
def _ffmpeg_exe() -> str:
try:
import imageio_ffmpeg
return imageio_ffmpeg.get_ffmpeg_exe()
except Exception as exc:
raise RuntimeError("LongCat Avatar example requires the Python imageio[ffmpeg] dependency.") from exc
def _wav_duration(path: Path) -> float:
with wave.open(str(path), "rb") as audio:
return audio.getnframes() / float(audio.getframerate())
def _save_video_with_audio(frames, output_path: Path, audio_path: str, fps: int) -> None:
frames_np = _frames_to_uint8(frames)
output_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="longcat_avatar_export_") as tmp:
tmp_dir = Path(tmp)
temp_video = tmp_dir / "video.mp4"
crop_audio = tmp_dir / "audio.wav"
crop_video = tmp_dir / "video_cropped.mp4"
writer = imageio.get_writer(str(temp_video), fps=fps, quality=5)
try:
for frame in frames_np:
writer.append_data(frame)
finally:
writer.close()
duration = len(frames_np) / fps
ffmpeg = _ffmpeg_exe()
subprocess.run([ffmpeg, "-y", "-i", audio_path, "-t", f"{duration}", str(crop_audio)], check=True)
audio_duration = _wav_duration(crop_audio)
subprocess.run(
[
ffmpeg,
"-y",
"-i",
str(temp_video),
"-t",
f"{audio_duration}",
"-c:v",
"copy",
str(crop_video),
],
check=True,
)
subprocess.run(
[
ffmpeg,
"-y",
"-i",
str(crop_video),
"-i",
str(crop_audio),
"-c:v",
"libx264",
"-c:a",
"aac",
"-shortest",
str(output_path),
],
check=True,
)
def _infer_asset_root_from_input_json(input_json: Path) -> Path:
resolved = input_json.resolve()
parts = resolved.parts
if "assets" not in parts:
return resolved.parent
asset_idx = parts.index("assets")
return Path(*parts[:asset_idx])
def _resolve_case_path(path: str, asset_root: Path) -> str:
raw_path = Path(path)
if raw_path.is_absolute():
return str(raw_path)
candidate = asset_root / raw_path
return str(candidate if candidate.exists() else raw_path)
def _speaker_sort_key(name: str) -> tuple[int, str]:
match = re.search(r"(\d+)$", str(name))
return (int(match.group(1)) if match else 10_000, str(name))
def _load_input_case(input_json: str) -> tuple[dict[str, Any], Path]:
input_json_path = Path(input_json)
with input_json_path.open("r", encoding="utf-8") as f:
case = json.load(f)
return case, _infer_asset_root_from_input_json(input_json_path)
def _create_merged_audio_for_case(
audio_paths: dict[str, str],
audio_type: str,
output_path: Path,
) -> str:
sorted_paths = [path for _, path in sorted(audio_paths.items(), key=lambda item: _speaker_sort_key(str(item[0])))]
if len(sorted_paths) == 1:
return sorted_paths[0]
inputs = [arg for path in sorted_paths for arg in ("-i", path)]
audio_type = audio_type.lower()
if audio_type == "para":
filter_complex = f"amix=inputs={len(sorted_paths)}:duration=longest:normalize=0"
elif audio_type == "add":
filter_complex = "".join(f"[{idx}:a]" for idx in range(len(sorted_paths)))
filter_complex += f"concat=n={len(sorted_paths)}:v=0:a=1"
else:
raise NotImplementedError(f"Unsupported LongCat Avatar audio_type {audio_type!r}.")
subprocess.run([_ffmpeg_exe(), "-y", *inputs, "-filter_complex", filter_complex, str(output_path)], check=True)
return str(output_path)
def main():
args = parse_args()
input_case = None
input_asset_root = None
audio_for_mux = args.audio
if args.input_json:
input_case, input_asset_root = _load_input_case(args.input_json)
args.stage = "ai2v"
args.prompt = input_case.get("prompt") or args.prompt
args.image = _resolve_case_path(input_case["cond_image"], input_asset_root)
args.audio = None
if not args.input_json and not args.audio:
raise ValueError("--audio is required unless --input-json is provided.")
if args.stage == "ai2v" and not args.image:
raise ValueError("--image is required when --stage=ai2v.")
model_for_omni = prepare_longcat_video_avatar_model_for_omni(args.model, args.use_int8)
additional_config = {
"model_type": "avatar-v1.5",
"resolution": args.resolution,
"use_distill": args.use_distill,
"use_int8": args.use_int8,
"build_components_on_gpu": args.build_components_on_gpu,
}
if args.base_model_dir:
additional_config["base_model_dir"] = args.base_model_dir
omni = Omni(
model=model_for_omni,
model_class_name="LongCatVideoAvatarPipeline",
dtype="bfloat16",
enforce_eager=args.enforce_eager,
additional_config=additional_config,
parallel_config=DiffusionParallelConfig(
ulysses_degree=1,
ring_degree=1,
cfg_parallel_size=1,
tensor_parallel_size=1,
vae_patch_parallel_size=1,
pipeline_parallel_size=1,
),
)
audio_input: str | dict[str, str] | None = args.audio
additional_information = {"stage": args.stage, "resolution": args.resolution}
if input_case is not None and input_asset_root is not None:
audio_input = {
speaker: _resolve_case_path(path, input_asset_root)
for speaker, path in sorted(
input_case["cond_audio"].items(),
key=lambda item: _speaker_sort_key(str(item[0])),
)
}
additional_information.update(
{
"input_json": args.input_json,
"audio_type": input_case.get("audio_type", "para"),
"bbox": input_case.get("bbox", {}),
}
)
prompt = {
"prompt": args.prompt,
"negative_prompt": args.negative_prompt,
"multi_modal_data": {"audio": audio_input},
"additional_information": additional_information,
}
if args.stage == "ai2v":
prompt["multi_modal_data"]["image"] = args.image
try:
output = omni.generate(
prompt,
OmniDiffusionSamplingParams(
generator=torch.Generator(device=current_omni_platform.device_type).manual_seed(args.seed),
seed=args.seed,
guidance_scale=1.0 if args.use_distill else 4.0,
guidance_scale_2=1.0 if args.use_distill else 4.0,
num_inference_steps=args.num_inference_steps,
num_frames=args.num_frames,
fps=args.fps,
extra_args={
"stage": args.stage,
"resolution": args.resolution,
"save_fps": args.fps,
"use_distill": args.use_distill,
"use_int8": args.use_int8,
"input_json": args.input_json,
"num_segments": args.num_segments,
"num_cond_frames": args.num_cond_frames,
"ref_img_index": args.ref_img_index,
"mask_frame_range": args.mask_frame_range,
"use_kv_cache": args.use_kv_cache,
"offload_kv_cache": args.offload_kv_cache,
},
),
use_tqdm=True,
)
finally:
omni.close()
with tempfile.TemporaryDirectory(prefix="longcat_avatar_audio_mux_") as tmp:
if input_case is not None and isinstance(audio_input, dict):
audio_for_mux = _create_merged_audio_for_case(
audio_input,
input_case.get("audio_type", "para"),
Path(tmp) / "merged_audio.wav",
)
_save_video_with_audio(_extract_frames(output), Path(args.output), audio_for_mux, args.fps)
print(f"Saved video to {args.output}")
if __name__ == "__main__":
main()
@@ -57,8 +57,9 @@ python examples/offline_inference/text_to_image/text_to_image.py \
--output lingbot_t2i.png
```
The LingBot prompt builder selects the `image` output modality and requests one
frame. Passing an input image is not supported in LingBot T2I mode.
The shared T2I entry point selects the `image` output modality. The LingBot
pipeline validates that T2I requests produce one frame and do not include an
input image.
## Quick Start
@@ -109,8 +110,9 @@ python text_to_image.py \
| `--ring-degree` | int | `1` | Ring sequence parallel degree for hybrid Ulysses + Ring inference |
| `--ulysses-mode` | str | `"strict"` | Ulysses SP mode: `"strict"` or `"advanced_uaa"` |
| `--enable-cpu-offload` | flag | off | Enable CPU offloading for diffusion models |
| `--lora-path` | str | — | Path to PEFT LoRA adapter folder |
| `--lora-path` | str | — | Path to PEFT LoRA adapter folder or checkpoint file |
| `--lora-scale` | float | `1.0` | Scale factor for LoRA weights |
| `--lora-backend` | str |`"peft"`| LoRA backend for loading LoRA adapters. Default: peft. Available options: peft, distill |
| `--use-system-prompt` | str | `None` | System prompt preset: `en_unified`, `en_vanilla`, `en_recaption`, `en_think_recaption`, `dynamic`, `None`, or custom text. Recommended: `en_unified`. Only for HunyuanImage-3.0.|
| `--system-prompt` | str | `None` | Custom system prompt text. Only used when `--use-system-prompt` is set to `custom`. Only for HunyuanImage-3.0.|
| `--auxiliary-text-encoder` | str | `None` | Supplementary auxiliary text encoder parameters model name or path (especially for Hidream-l1-full). |
@@ -8,7 +8,9 @@ import time
from pathlib import Path
from typing import Any
import numpy as np
import torch
from diffusers.utils import numpy_to_pil
from vllm_omni.diffusion.data import logger
from vllm_omni.diffusion.utils.image_output import extract_images_from_outputs
@@ -19,7 +21,9 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.lora.request import LoRARequest
from vllm_omni.lora.utils import stable_lora_int_id
from vllm_omni.model_extras import (
build_text_to_image_prompt,
build_text_to_image_prompt as build_model_text_to_image_prompt,
)
from vllm_omni.model_extras import (
get_extra_body_params,
get_model_class_name,
should_init_extra_args_for_non_diffusion_stages,
@@ -54,6 +58,28 @@ def parse_json_object(value: str, flag_name: str = "argument") -> dict[str, Any]
parse_profiler_config = functools.partial(parse_json_object, flag_name="--profiler-config")
def build_text_to_image_prompt(prompt: str, negative_prompt: str | None) -> dict[str, Any]:
"""Build the canonical request envelope for the shared T2I example."""
result: dict[str, Any] = {
"prompt": prompt,
"modalities": ["image"],
}
if negative_prompt is not None:
result["negative_prompt"] = negative_prompt
return result
def _normalize_images_for_save(images: list[Any]) -> list[Any]:
"""Convert NumPy diffusion outputs to PIL images before saving."""
normalized = []
for image in images:
if isinstance(image, np.ndarray):
normalized.extend(numpy_to_pil(image))
else:
normalized.append(image)
return normalized
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate an image with supported diffusion models.")
parser.add_argument(
@@ -247,6 +273,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--lora-path",
type=str,
nargs="+",
default=None,
help="Path to LoRA adapter folder (PEFT format). Loaded at initialization and used for generation.",
)
@@ -256,6 +283,15 @@ def parse_args() -> argparse.Namespace:
default=1.0,
help="Scale factor for LoRA weights (default: 1.0).",
)
parser.add_argument(
"--lora-backend",
type=str,
default="peft",
choices=["peft", "distill"],
help="LoRA backend for loading LoRA adapters. Default: peft"
"'peft' loads a PEFT-format adapter folder, used e.g. for RL"
"'distill' fuses one or more concrete LoRA checkpoint files, used e.g. for distilled few-step LoRAs",
)
parser.add_argument(
"--vae-patch-parallel-size",
type=int,
@@ -391,8 +427,12 @@ def main():
# Prepare LoRA kwargs for Omni initialization
lora_args: dict[str, Any] = {}
if args.lora_path:
lora_args["lora_path"] = args.lora_path
print(f"Using LoRA from: {args.lora_path}")
lora_path = args.lora_path
if len(lora_path) == 1:
lora_path = lora_path[0]
lora_args["lora_path"] = lora_path
lora_args["lora_backend"] = args.lora_backend
print(f"Using LoRA from: {lora_path} with backend: {args.lora_backend}")
# Build quantization kwargs: use quantization_config dict when
# ignored_layers is specified so the list flows through OmniDiffusionConfig
@@ -481,20 +521,27 @@ def main():
# Build LoRA request when --lora-path is set
lora_request = None
if args.lora_path:
lora_request_id = stable_lora_int_id(args.lora_path)
if args.lora_path and args.lora_backend == "peft":
if len(args.lora_path) != 1:
raise ValueError("Only one LoRA path is expected for PEFT backend.")
lora_path = args.lora_path[0]
lora_request_id = stable_lora_int_id(lora_path)
lora_request = LoRARequest(
lora_name=Path(args.lora_path).stem,
lora_name=Path(lora_path).stem,
lora_int_id=lora_request_id,
lora_path=args.lora_path,
lora_path=lora_path,
)
generation_start = time.perf_counter()
prompt_dict = build_text_to_image_prompt(
model_class_name=model_class_name,
prompt=args.prompt,
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,
)
@@ -619,6 +666,7 @@ def main():
if not images:
raise ValueError("No images found in request_output")
images = _normalize_images_for_save(images)
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
@@ -24,6 +24,7 @@ list of supported architectures across all modalities, see
| Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | 2 (talker + code2wav) | ✓ (Base) | ✓ | 3 task variants | 24 kHz |
| VoxCPM2 | `openbmb/VoxCPM2` | single (native AR) | ✓ | ✓ (online) | continuation | 48 kHz |
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | 2 (AR talker + S2Mel DiT + BigVGAN) | ✓ (required) | — | emotion control (`--emo-audio`, `--emo-text`, `--emo-vector`) | 22.05 kHz |
| IndexTTS-2.5 | native `checkpoints/` bundle | 2 (AR talker + EnhancedCodec + S2Mel DiT + BigVGAN) | ✓ (required) | — | multilingual (`--lang`) + emotion control | 22.05 kHz |
| Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | varies | ✓ | ✓ | voice presets | 24 kHz |
## Common Quick Start
@@ -437,11 +438,14 @@ Streaming is exposed through the online OpenAI Speech API (`stream=true`). See [
---
## IndexTTS-2
## IndexTTS-2 and IndexTTS-2.5
2-stage TTS pipeline (GPT AR talker + S2Mel CFM DiT + BigVGAN vocoder) at 22.05 kHz. Every request requires reference audio for zero-shot voice cloning. Supports emotion conditioning via audio, text, or 8-dim vector.
Both versions use a 2-stage TTS pipeline and produce 22.05 kHz mono audio. Stage 0 is a GPT AR talker. IndexTTS-2 Stage 1 uses RepCodec semantic embeddings, GPT latent, S2Mel CFM/DiT, and BigVGAN. IndexTTS-2.5 replaces RepCodec with EnhancedCodec and uses the official code-only path (`use_gpt_latent=false`). Every request requires reference audio for zero-shot voice cloning.
### Quick start
IndexTTS-2:
```bash
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
--model IndexTeam/IndexTTS-2 \
@@ -449,6 +453,24 @@ python examples/offline_inference/text_to_speech/indextts2/end2end.py \
--ref-audio /path/to/reference.wav
```
IndexTTS-2 uses its existing frontend and does not require the IndexTTS-2.5 text dependencies. Before running IndexTTS-2.5, install its optional frontend dependencies:
```bash
pip install 'vllm-omni[indextts2]'
```
IndexTTS-2.5 with a native repository/lab bundle whose model root contains `checkpoints/`:
```bash
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
--model /path/to/indextts-2.5 \
--model-version 2.5 \
--lang ja \
--text "こんにちは、音声合成のテストです。" \
--ref-audio /path/to/reference.wav
```
### Emotion control
```bash
# Emotion from reference audio
@@ -474,10 +496,13 @@ python examples/offline_inference/text_to_speech/indextts2/end2end.py \
```
### Notes
- `--ref-audio` is **required** — IndexTTS-2 does not support text-only synthesis.
- Stage 0 (AR Talker): GPT-2 generates mel codes from text + reference audio.
- Stage 1 (S2Mel + BigVGAN): CFM DiT converts mel codes to waveform at 22.05 kHz.
- Deploy config: `vllm_omni/deploy/indextts2.yaml`. Stage 1 runs with `enforce_eager: true` (DiT has dynamic shapes).
- `--ref-audio` is **required** — neither version provides text-only synthesis.
- IndexTTS-2.5 Stage 0 (AR Talker) generates mel codes from text + reference audio using plain vLLM sampling. It does not reproduce the official default `num_beams=3` beam search. For parity comparisons, run upstream with `num_beams=1`; output quality can differ from the official beam-search result.
- IndexTTS-2.5 accepts language codes such as `zh`, `en`, `zhen` (mixed Chinese/English), `ja`, and `yue`. `Mandarin` is a vLLM-Omni convenience alias for `zh`. Japanese (`ja`) uses `fugashi` tokenization and produces audio, but it does not automatically expand numbers, dates, or percentages; write those inputs as readable Japanese text before calling the model. Use `--no-text-normalization` only when the input is already normalized.
- A seed controls Stage 0 AR sampling and per-request CFM noise. Different concurrent batch compositions do not guarantee a bit-identical waveform.
- Stage 1 (semantic codec + S2Mel + BigVGAN): CFM/DiT converts the complete semantic-code sequence to waveform.
- Deploy configs: `vllm_omni/deploy/indextts2.yaml` and `indextts2_5.yaml`.
- `async_chunk=false` is intentional for IndexTTS-2.5 correctness: S2Mel consumes the completed semantic sequence.
---
@@ -1,13 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Offline inference example for IndexTTS2 via vLLM-Omni.
"""Offline inference example for IndexTTS 2.0 and 2.5 via vLLM-Omni.
Two-stage pipeline: GPT AR (Stage 0) → S2Mel + BigVGAN (Stage 1).
Two-stage pipeline: GPT AR (Stage 0) → semantic codec + S2Mel + BigVGAN
(Stage 1).
Output is 22050 Hz mono WAV.
Usage:
python end2end.py \
--model /path/to/IndexTeam/IndexTTS-2 \
--model /path/to/indextts-2.5 \
--model-version 2.5 \
--lang zh \
--text "你好,这是一个语音合成测试。" \
--ref-audio /path/to/ref.wav
@@ -47,6 +50,10 @@ def build_request(
emo_alpha: float | None = None,
use_emo_text: bool = False,
use_random: bool = False,
model_type: str = "indextts2",
lang: str = "zh",
text_normalization: bool = True,
tokenizer_file: str | None = None,
) -> dict:
additional: dict = {"text": [text]}
if ref_audio_path:
@@ -63,12 +70,44 @@ def build_request(
additional["use_emo_text"] = [True]
if use_random:
additional["use_random"] = [True]
if model_type == "indextts2_5":
additional["lang"] = [lang]
additional["text_normalization"] = [text_normalization]
prompt_kwargs = {
"model_type": model_type,
"lang": lang,
"text_normalization": text_normalization,
}
if tokenizer_file is not None:
prompt_kwargs["tokenizer_file"] = tokenizer_file
return {
"prompt_token_ids": build_indextts2_prefill_prompt_ids(model, text),
"prompt_token_ids": build_indextts2_prefill_prompt_ids(
model,
text,
**prompt_kwargs,
),
"additional_information": additional,
}
def _get_stage0_tokenizer_file(omni: Omni) -> str | None:
"""Read the effective tokenizer override from the resolved Stage 0 config."""
stage_configs = omni.stage_configs
if not stage_configs:
return None
engine_args = getattr(stage_configs[0], "engine_args", None)
if engine_args is None:
return None
hf_overrides = getattr(engine_args, "hf_overrides", None)
if hf_overrides is None and isinstance(engine_args, dict):
hf_overrides = engine_args.get("hf_overrides")
if hf_overrides is None:
return None
if hasattr(hf_overrides, "get"):
return hf_overrides.get("tokenizer_file")
return getattr(hf_overrides, "tokenizer_file", None)
def save_audio(waveform: torch.Tensor, path: str, sample_rate: int = 22050) -> None:
audio_np = waveform.float().numpy()
sf.write(path, audio_np, sample_rate)
@@ -95,9 +134,14 @@ def extract_audio(mm: dict) -> tuple[torch.Tensor | None, int]:
def main(args) -> None:
model_type = "indextts2_5" if args.model_version == "2.5" else "indextts2"
deploy_config = args.deploy_config
if deploy_config is None and args.model_version == "2.5":
repo_root = Path(__file__).resolve().parents[4]
deploy_config = str(repo_root / "vllm_omni" / "deploy" / "indextts2_5.yaml")
omni = Omni(
model=args.model,
deploy_config=args.deploy_config,
deploy_config=deploy_config,
stage_init_timeout=args.stage_init_timeout,
)
@@ -126,7 +170,7 @@ def main(args) -> None:
print(f"Synthesizing: {args.text!r}")
if args.ref_audio:
print(f" ref_audio: {args.ref_audio}")
inputs = build_request(
request_kwargs = dict(
model=args.model,
text=args.text,
ref_audio_path=args.ref_audio,
@@ -136,7 +180,17 @@ def main(args) -> None:
emo_alpha=args.emo_alpha,
use_emo_text=args.use_emo_text,
use_random=args.use_random,
model_type=model_type,
lang=args.lang,
text_normalization=args.text_normalization,
)
if model_type == "indextts2_5":
tokenizer_file = _get_stage0_tokenizer_file(omni)
if tokenizer_file is not None:
request_kwargs["tokenizer_file"] = tokenizer_file
inputs = build_request(**request_kwargs)
if model_type == "indextts2_5":
inputs["additional_information"]["duration_factor"] = [1.0 / getattr(args, "speed", 1.0)]
for i, omni_out in enumerate(omni.generate(inputs, sampling_params_list=sampling_params)):
mm = omni_out.multimodal_output
@@ -154,8 +208,37 @@ def main(args) -> None:
def parse_args():
parser = FlexibleArgumentParser(description="IndexTTS2 offline inference")
parser.add_argument("--model", required=True, help="HF model path for IndexTTS2.")
parser = FlexibleArgumentParser(description="IndexTTS 2.0/2.5 offline inference")
parser.add_argument(
"--model",
required=True,
help="HF model path or native bundle containing checkpoints/.",
)
parser.add_argument(
"--model-version",
choices=("2.0", "2.5"),
default="2.0",
)
parser.add_argument(
"--lang",
default="zh",
help=(
"IndexTTS 2.5 language code, for example zh/en/zhen/ja/yue; "
"zhen is mixed Chinese/English and Mandarin is a vLLM-Omni alias for zh."
),
)
parser.add_argument(
"--no-text-normalization",
action="store_false",
dest="text_normalization",
help="Disable IndexTTS 2.5 text normalization.",
)
parser.add_argument(
"--speed",
type=float,
default=1.0,
help="IndexTTS 2.5 native synthesis speed in [0.5, 2.0]; 2.0 is faster.",
)
parser.add_argument("--text", default="你好,这是IndexTTS2语音合成测试。")
parser.add_argument("--ref-audio", required=True, help="Reference audio for voice cloning.")
parser.add_argument("--emo-audio", default=None, help="Emotion reference audio.")
@@ -170,7 +253,15 @@ def parse_args():
parser.add_argument("--emo-alpha", type=float, default=None, help="Emotion weight in [0, 1].")
parser.add_argument("--use-emo-text", action="store_true", help="Infer emotion vector from emo-text or text.")
parser.add_argument("--use-random", action="store_true", help="Use random emotion prototypes.")
parser.add_argument("--seed", type=int, default=None)
parser.add_argument(
"--seed",
type=int,
default=None,
help=(
"Seed Stage 0 AR sampling and per-request CFM noise; changing the "
"concurrent batch composition may still change the final waveform."
),
)
parser.add_argument(
"--output-dir",
default=os.path.join(
@@ -180,7 +271,10 @@ def parse_args():
)
parser.add_argument("--deploy-config", default=None)
parser.add_argument("--stage-init-timeout", type=int, default=600)
return parser.parse_args()
args = parser.parse_args()
if args.model_version == "2.5" and not 0.5 <= args.speed <= 2.0:
parser.error("IndexTTS 2.5 --speed must be between 0.5 and 2.0")
return args
if __name__ == "__main__":
@@ -218,6 +218,9 @@ python text_to_video.py \
- `--audio-sample-rate`: fallback audio sample rate when the pipeline returns audio.
- `--quantization`: quantization method (such as `fp8` for FP8).
- `--flow-shift`: scheduler flow_shift parameter.
- `--lora-path`: path to PEFT LoRA adapter folder or checkpoint file.
- `--lora-scale`: scale factor for LoRA weights.
- `--lora-backend`: backend for loading LoRA adapters. Default: peft. Available options: peft, distill.
- `--extra-body`: JSON object of model-specific generation params, filtered against the model's declared `extra_body_params` (see [`vllm_omni/model_extras`](../../../vllm_omni/model_extras)). Used by Cosmos3 (see above).
### Wan2.2-specific
@@ -14,6 +14,8 @@ from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.utils.param_utils import apply_declared_extra_args
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.lora.request import LoRARequest
from vllm_omni.lora.utils import stable_lora_int_id
from vllm_omni.model_extras import get_extra_body_params, get_model_class_name
from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.platforms import current_omni_platform
@@ -334,6 +336,29 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Enable expert parallelism for MoE layers.",
)
parser.add_argument(
"--lora-path",
type=str,
nargs="+",
default=None,
help="Path to LoRA adapter folder (PEFT format) or concrete LoRA checkpoint files. Loaded at initialization and used for generation."
"Note: for Wan2.2 MoE models, two checkpoints positionally mapped to high-noise and low-noise modules.",
)
parser.add_argument(
"--lora-scale",
type=float,
default=1.0,
help="Scale factor for LoRA weights (default: 1.0).",
)
parser.add_argument(
"--lora-backend",
type=str,
default="peft",
choices=["peft", "distill"],
help="LoRA backend for loading LoRA adapters. Default: peft"
"'peft' loads a PEFT-format adapter folder, used e.g. for RL"
"'distill' fuses one or more concrete LoRA checkpoint files, used e.g. for distilled few-step LoRAs",
)
parser.add_argument(
"--use-hsdp",
action="store_true",
@@ -440,6 +465,12 @@ def main():
omni_kwargs["cache_backend"] = args.cache_backend
omni_kwargs["cache_config"] = cache_config
omni_kwargs["enable_cache_dit_summary"] = args.enable_cache_dit_summary
if args.lora_path is not None:
lora_path = args.lora_path
if len(lora_path) == 1:
lora_path = lora_path[0]
omni_kwargs["lora_path"] = lora_path
omni_kwargs["lora_backend"] = args.lora_backend
# Cosmos3 loads its (gated) guardrail models at build time, so the guardrails
# gate is an engine-level config (offline analog of the server's --no-guardrails).
@@ -469,6 +500,19 @@ def main():
print(f" Video size: {args.width}x{args.height}")
print(f"{'=' * 60}\n")
lora_request = None
if args.lora_path and args.lora_backend == "peft":
if len(args.lora_path) != 1:
raise ValueError("Only one LoRA path is expected for PEFT backend.")
lora_path = args.lora_path[0]
lora_request_id = stable_lora_int_id(lora_path)
lora_request = LoRARequest(
lora_name=Path(lora_path).stem,
lora_int_id=lora_request_id,
lora_path=lora_path,
)
prompt_dict = {"prompt": args.prompt}
if args.negative_prompt is not None:
prompt_dict["negative_prompt"] = args.negative_prompt
@@ -476,6 +520,11 @@ def main():
# Preserve the historical empty-prompt behavior for non-LTX examples.
prompt_dict["negative_prompt"] = ""
extra_args = {}
if lora_request:
extra_args["lora_request"] = lora_request
extra_args["lora_scale"] = args.lora_scale
sampling_kwargs = dict(
height=args.height,
width=args.width,
@@ -483,6 +532,7 @@ def main():
guidance_scale=args.guidance_scale,
num_inference_steps=args.num_inference_steps,
num_frames=args.num_frames,
extra_args=extra_args,
)
if args.guidance_scale_high is not None:
sampling_kwargs["guidance_scale_2"] = args.guidance_scale_high
@@ -1,6 +1,9 @@
# Image-To-Video
This example demonstrates how to deploy the Wan2.2 image-to-video model for online video generation using vLLM-Omni.
This example demonstrates online image-to-video generation with vLLM-Omni.
The startup script defaults to Wan2.2, but `MODEL` can select any supported
image-to-video model. The existing curl helper remains a concrete Wan2.2
request through the standard Videos API.
## Start Server
@@ -20,9 +23,7 @@ bash run_server.sh
The script allows overriding:
- `MODEL` (default: `Wan-AI/Wan2.2-I2V-A14B-Diffusers`)
- `PORT` (default: `8091`)
- `BOUNDARY_RATIO` (default: `0.875`)
- `FLOW_SHIFT` (default: `12.0`)
- `PORT` (default: `8099`)
- `CACHE_BACKEND` (default: `none`)
- `ENABLE_CACHE_DIT_SUMMARY` (default: `0`)
@@ -1,12 +1,12 @@
#!/bin/bash
# Wan2.2 image-to-video server start script
# Shared image-to-video server start script
MODEL="${MODEL:-Wan-AI/Wan2.2-I2V-A14B-Diffusers}"
PORT="${PORT:-8099}"
CACHE_BACKEND="${CACHE_BACKEND:-none}"
ENABLE_CACHE_DIT_SUMMARY="${ENABLE_CACHE_DIT_SUMMARY:-0}"
echo "Starting Wan2.2 I2V server..."
echo "Starting image-to-video server..."
echo "Model: $MODEL"
echo "Port: $PORT"
echo "Cache backend: $CACHE_BACKEND"
+3 -3
View File
@@ -23,10 +23,10 @@ including `librosa`.
The deploy config auto-loads via `--omni`.
The default `vllm_omni/deploy/minicpmo_4_5.yaml` keeps all three stages on
logical device 0 with memory budgets of 55%, 15%, and 15%, leaving headroom
logical device 0 with memory budgets of 55%, 15%, and 18%, leaving headroom
for runtime kernels such as the HiFi-GAN vocoder's cuDNN workspace. The
profile admits at most four sequences per stage and bounds startup video
profiling to 32 frames per video. For throughput,
profile admits at most four sequences per stage and bounds Talker context
to 4096 tokens. For throughput,
`minicpmo_4_5_2gpu.yaml` gives the Thinker
GPU 0 (90%) and colocates the Talker (55%) and Code2Wav (35%) on GPU 1. That
profile admits at most four concurrent sequences per stage.
@@ -196,6 +196,7 @@ async def run_demo(args: argparse.Namespace) -> dict[str, object]:
args.model,
ref_audio=_ref_audio_data_url(args.ref_audio),
session_id=args.session_id,
temperature=args.temperature,
timeout_s=args.timeout_s,
)
stream_event_cursor = len(client.events.events)
@@ -383,6 +384,12 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--output-dir", default="/tmp/minicpmo_realtime_duplex_demo")
parser.add_argument("--chunk-ms", type=int, default=200)
parser.add_argument("--timeout-s", type=float, default=60.0)
parser.add_argument(
"--temperature",
type=float,
default=None,
help="Stage0 sampling temperature; response-required soft-interrupt uses 0.0.",
)
parser.add_argument("--no-realtime-pacing", action="store_true")
parser.add_argument("--require-audio", action="store_true")
return parser.parse_args()
@@ -1,6 +1,9 @@
# Text-To-Image
This example demonstrates how to deploy Qwen-Image model for online image generation service using vLLM-Omni.
This example demonstrates online text-to-image generation with vLLM-Omni. The
startup script and Python clients default to Qwen-Image, but `MODEL` can select
any supported text-to-image model. The existing curl helper remains a concrete
Qwen-Image request through the standard Images API.
## Start Server
@@ -115,7 +118,23 @@ python gradio_demo.py
## LoRA
This example supports Peft-compatible LoRA (Low-Rank Adaptation) adapters for diffusion models. The LoRA adapter path must be readable on the **server** machine (usually a local path or a mounted directory).
This example supports both startup-time distilled LoRAs and request-time
PEFT-compatible adapters. The LoRA path must be readable on the **server**
machine (usually a local path or a mounted directory).
### Start Server with a Distilled LoRA
```bash
vllm serve Qwen/Qwen-Image-2512 \
--omni \
--port 8091 \
--lora-backend distill \
--lora-path /path/to/Qwen-Image-2512-Lightning-4steps.safetensors
```
The distilled LoRA is fused once during server initialization and applies to
every request. Use the checkpoint's few-step sampling settings, for example
`num_inference_steps=4` and `true_cfg_scale=1.0`.
### Using Python Client with LoRA
@@ -226,8 +245,8 @@ count, use `size` and `n` rather than `height`, `width`, or
| `height` | int | None | Image height in pixels |
| `width` | int | None | Image width in pixels |
| `size` | str | None | Image size (e.g., "1024x1024") |
| `num_inference_steps` | int | 50 | Number of denoising steps |
| `true_cfg_scale` | float | 4.0 | Qwen-Image CFG scale |
| `num_inference_steps` | int | model | Number of denoising steps |
| `true_cfg_scale` | float | model | True CFG scale when supported |
| `seed` | int | None | Random seed (reproducible) |
| `negative_prompt` | str | None | Negative prompt |
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Qwen-Image Gradio Demo for online serving.
Shared text-to-image Gradio demo for online serving.
Usage:
python gradio_demo.py [--server http://localhost:8091] [--port 7860]
@@ -77,9 +77,9 @@ def generate_image(
def create_demo(server_url: str):
"""Create Gradio demo interface."""
with gr.Blocks(title="Qwen-Image Demo") as demo:
gr.Markdown("# Qwen-Image Online Generation")
gr.Markdown("Generate images using Qwen-Image model")
with gr.Blocks(title="Text-to-Image Demo") as demo:
gr.Markdown("# Text-to-Image Online Generation")
gr.Markdown("Generate images using the model configured on the server")
with gr.Row():
with gr.Column(scale=1):
@@ -115,7 +115,6 @@ def create_demo(server_url: str):
label="Inference Steps",
minimum=10,
maximum=100,
# Default steps aligned with run_curl_text_to_image.sh to 100
value=100,
step=5,
)
@@ -173,7 +172,7 @@ def create_demo(server_url: str):
def main():
parser = argparse.ArgumentParser(description="Qwen-Image Gradio Demo")
parser = argparse.ArgumentParser(description="Shared text-to-image Gradio demo")
parser.add_argument("--server", default="http://localhost:8091", help="Server URL")
parser.add_argument("--port", type=int, default=7860, help="Gradio port")
parser.add_argument("--share", action="store_true", help="Create public link")
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Qwen-Image OpenAI-compatible image generation client.
Shared OpenAI-compatible text-to-image generation client.
Usage:
python openai_chat_client.py --prompt "A beautiful landscape" --output output.png
@@ -39,7 +39,7 @@ def generate_image(
height: Image height in pixels
width: Image width in pixels
steps: Number of diffusion steps
true_cfg_scale: Qwen-Image CFG scale
true_cfg_scale: True CFG scale for models that support it
seed: Random seed
negative_prompt: Negative prompt
num_outputs_per_prompt: Number of images to generate
@@ -70,7 +70,7 @@ def generate_image(
payload["num_inference_steps"] = steps
if true_cfg_scale is not None:
payload["true_cfg_scale"] = true_cfg_scale
if negative_prompt:
if negative_prompt is not None:
payload["negative_prompt"] = negative_prompt
if seed is not None:
payload["seed"] = seed
@@ -115,15 +115,15 @@ def generate_image(
def main():
parser = argparse.ArgumentParser(description="Qwen-Image chat client")
parser = argparse.ArgumentParser(description="Shared text-to-image client")
parser.add_argument("--prompt", "-p", default="a cup of coffee on the table", help="Text prompt")
parser.add_argument("--output", "-o", default="qwen_image_output.png", help="Output file")
parser.add_argument("--output", "-o", default="text_to_image_output.png", help="Output file")
parser.add_argument("--server", "-s", default="http://localhost:8091", help="Server URL")
parser.add_argument("--height", type=int, default=1024, help="Image height")
parser.add_argument("--width", type=int, default=1024, help="Image width")
parser.add_argument("--steps", type=int, default=50, help="Inference steps")
parser.add_argument("--cfg-scale", type=float, default=4.0, help="True CFG scale")
parser.add_argument("--seed", type=int, default=0, help="Random seed")
parser.add_argument("--height", type=int, help="Image height; defaults to the model setting")
parser.add_argument("--width", type=int, help="Image width; defaults to the model setting")
parser.add_argument("--steps", type=int, help="Inference steps; defaults to the model setting")
parser.add_argument("--cfg-scale", type=float, help="True CFG scale when supported")
parser.add_argument("--seed", type=int, help="Random seed")
parser.add_argument("--negative", help="Negative prompt")
parser.add_argument("--lora-path", default=None, help="Server-local LoRA adapter folder (PEFT format)")
@@ -1,10 +1,10 @@
#!/bin/bash
# Qwen-Image online serving startup script
# Shared text-to-image online serving startup script
MODEL="${MODEL:-Qwen/Qwen-Image}"
PORT="${PORT:-8091}"
echo "Starting Qwen-Image server..."
echo "Starting text-to-image server..."
echo "Model: $MODEL"
echo "Port: $PORT"
@@ -16,7 +16,8 @@ For the full list of supported architectures across all modalities, see
|---|---|---|---|---|---|
| Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ |
| GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ |
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✓ (`ref_audio` or uploaded `voice`) | compat only, non-chunk | uploaded audio voice only; no presets | — |
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✓ (`ref_audio` or uploaded `voice`) | `stream=true` response, non-chunk | uploaded audio voice only; no presets | — |
| IndexTTS-2.5 | native `checkpoints/` bundle | ✓ (`ref_audio` or uploaded `voice`) | `stream=true` response, non-chunk | uploaded audio voice only; no presets | — |
| Ming-omni-tts | `inclusionAI/Ming-omni-tts-0.5B` | ✓ (`ref_audio` / `speaker_embedding`) | ✓ (PCM stream) | IP labels + structured `instructions` | — |
| Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | — (caption-controlled) | — | caption fields (`instructions`) | — |
| MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | ✓ (`ref_audio` required) | ✓ (PCM stream) | — | ✓ |
@@ -138,15 +139,29 @@ bash examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh
---
## IndexTTS-2
## IndexTTS-2 and IndexTTS-2.5
2-stage TTS (GPT AR + S2Mel CFM DiT + BigVGAN) at 22.05 kHz. Requests use `ref_audio` for voice cloning, or an uploaded audio `voice` from `/v1/audio/voices`. Supports emotion conditioning via `emo_audio`, `emo_text`, or `emo_vector` passed in `extra_params`.
2-stage TTS at 22.05 kHz. Requests use `ref_audio` for voice cloning, or an uploaded audio `voice` from `/v1/audio/voices`. IndexTTS-2.5 uses a multilingual tokenizer, CAMPPlus speaker projection, EnhancedCodec, and code-only Stage 0→1 transfer by default. Both versions support emotion conditioning via `emo_audio`, `emo_text`, or `emo_vector` passed in `extra_params`.
### Prerequisites
IndexTTS-2 uses its existing frontend and does not require the IndexTTS-2.5 text dependencies. Before launching IndexTTS-2.5, install its optional frontend dependencies:
```bash
pip install 'vllm-omni[indextts2]'
```
### Launch
```bash
vllm serve IndexTeam/IndexTTS-2 --omni --trust-remote-code --port 8092
# or, to pass the bundled deploy config explicitly:
bash examples/online_serving/text_to_speech/indextts2/run_server.sh
# IndexTTS-2.5 default: use_gpt_latent=false
MODEL_VERSION=2.5 \
MODEL=/path/to/indextts-2.5 \
bash examples/online_serving/text_to_speech/indextts2/run_server.sh
```
### Sending requests
@@ -161,14 +176,35 @@ python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--text "今天心情很好!" \
--ref-audio /path/to/ref.wav \
--emo-audio /path/to/happy.wav
# IndexTTS-2.5 Japanese request
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--model-version 2.5 \
--model /path/to/indextts-2.5 \
--lang ja \
--text "こんにちは、音声合成のテストです。" \
--ref-audio /path/to/ref.wav
# IndexTTS-2.5 native speed control
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--model-version 2.5 \
--model /path/to/indextts-2.5 \
--speed 2.0 \
--text "这是两倍速度的语音合成测试。" \
--ref-audio /path/to/ref.wav
```
### Notes
- Output: 22.05 kHz mono WAV.
- Provide `ref_audio` on the documented raw request path, or pass `voice` only when it names an uploaded audio voice; IndexTTS-2 does not provide a built-in text-only preset voice.
- Provide `ref_audio` on the documented raw request path, or pass `voice` only when it names an uploaded audio voice; neither version provides a built-in text-only preset voice.
- IndexTTS-2.5 request controls `lang` and `text_normalization` are carried in `extra_params`; the client does this for `--model-version 2.5`.
- IndexTTS-2.5 accepts the public `speed` request field in `[0.5, 2.0]`: `2.0` generates shorter, faster speech and `0.5` generates longer, slower speech. This is native Stage 1 duration control, so serving does not apply the generic playback-speed adjustment a second time. IndexTTS-2 does not use this control.
- IndexTTS-2.5 accepts language codes such as `zh`, `en`, `zhen` (mixed Chinese/English), `ja`, and `yue`. `Mandarin` is a vLLM-Omni convenience alias for `zh`. Japanese (`ja`) uses `fugashi` tokenization and produces audio, but it does not automatically expand numbers, dates, or percentages; callers should first write those inputs as readable Japanese text.
- A request `seed` controls Stage 0 AR sampling and per-request CFM noise. Different concurrent batch compositions do not guarantee a bit-identical waveform.
- IndexTTS-2.5 Stage 0 uses plain vLLM sampling and does not reproduce the official default `num_beams=3` beam search. For parity comparisons, run upstream with `num_beams=1`; output quality can differ from the official beam-search result.
- Emotion params (`emo_audio`, `emo_text`, `emo_vector`, `emo_alpha`, `use_emo_text`, `use_random`) are passed via the `extra_params` field. Official precedence is `use_emo_text` > `emo_vector` > `emo_audio` > same emotion as the speaker reference.
- `stream=true` is accepted as an OpenAI-compatible response path, but IndexTTS-2 is not async-chunk streaming; audio is produced after S2Mel receives the full mel-code sequence.
- Deploy config: `vllm_omni/deploy/indextts2.yaml` (auto-loaded).
- IndexTTS-2.5 uses `vllm_omni/deploy/indextts2_5.yaml` with the official code-only Stage 0 to Stage 1 contract.
- For IndexTTS-2.5, set `MODEL` to the local native bundle; asset discovery accepts its nested `checkpoints/` layout.
---
@@ -1,20 +1,37 @@
#!/bin/bash
# Launch vLLM-Omni server for IndexTTS2
# Launch vLLM-Omni server for IndexTTS 2.0 or 2.5.
#
# Usage from repository root:
# examples/online_serving/text_to_speech/indextts2/run_server.sh
# CUDA_VISIBLE_DEVICES=0 PORT=8092 MODEL=/path/to/IndexTeam/IndexTTS-2 examples/online_serving/text_to_speech/indextts2/run_server.sh
# MODEL_VERSION=2.5 MODEL=/path/to/native/bundle \
# examples/online_serving/text_to_speech/indextts2/run_server.sh
set -e
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../../../.." && pwd)"
MODEL="${MODEL:-IndexTeam/IndexTTS-2}"
MODEL_VERSION="${MODEL_VERSION:-2.0}"
PORT="${PORT:-8092}"
DEPLOY_CONFIG="${DEPLOY_CONFIG:-$ROOT_DIR/vllm_omni/deploy/indextts2.yaml}"
echo "Starting IndexTTS2 server with model: $MODEL"
if [[ "$MODEL_VERSION" == "2.5" ]]; then
if [[ -z "${MODEL:-}" ]]; then
echo "Usage: MODEL_VERSION=2.5 MODEL=/path/to/native/bundle bash $0" >&2
exit 2
fi
DEFAULT_DEPLOY_CONFIG="$ROOT_DIR/vllm_omni/deploy/indextts2_5.yaml"
elif [[ "$MODEL_VERSION" == "2.0" ]]; then
MODEL="${MODEL:-IndexTeam/IndexTTS-2}"
DEFAULT_DEPLOY_CONFIG="$ROOT_DIR/vllm_omni/deploy/indextts2.yaml"
else
echo "MODEL_VERSION must be 2.0 or 2.5" >&2
exit 2
fi
DEPLOY_CONFIG="${DEPLOY_CONFIG:-$DEFAULT_DEPLOY_CONFIG}"
echo "Starting IndexTTS $MODEL_VERSION server with model: $MODEL"
echo "Deploy config: $DEPLOY_CONFIG"
FLASHINFER_DISABLE_VERSION_CHECK=1 \
vllm serve "$MODEL" \
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""OpenAI-compatible client for IndexTTS2 TTS via /v1/audio/speech endpoint.
"""OpenAI-compatible client for IndexTTS 2.0/2.5 via /v1/audio/speech.
Examples:
# With reference audio for voice cloning
@@ -54,13 +54,44 @@ def main() -> None:
parser.add_argument("--emo-alpha", type=float, default=None, help="Emotion weight in [0, 1]")
parser.add_argument("--use-emo-text", action="store_true", help="Infer emotion vector from emo-text or text")
parser.add_argument("--use-random", action="store_true", help="Use random emotion prototypes")
parser.add_argument("--model", type=str, default="IndexTeam/IndexTTS-2")
parser.add_argument(
"--model-version",
choices=("2.0", "2.5"),
default="2.0",
)
parser.add_argument("--model", type=str, default=None)
parser.add_argument(
"--lang",
default="zh",
help=(
"IndexTTS 2.5 language code, for example zh/en/zhen/ja/yue; "
"zhen is mixed Chinese/English and Mandarin is a vLLM-Omni alias for zh"
),
)
parser.add_argument(
"--no-text-normalization",
action="store_false",
dest="text_normalization",
help="Disable IndexTTS 2.5 text normalization",
)
parser.add_argument(
"--speed",
type=float,
default=1.0,
help="IndexTTS 2.5 native synthesis speed in [0.5, 2.0]; 2.0 is faster",
)
parser.add_argument("--voice", type=str, default=None, help="Uploaded voice name to use instead of --ref-audio")
parser.add_argument("--output", type=str, default="output.wav")
parser.add_argument("--api-base", type=str, default=DEFAULT_API_BASE)
parser.add_argument("--api-key", type=str, default=DEFAULT_API_KEY)
parser.add_argument("--response-format", type=str, default="wav")
args = parser.parse_args()
if args.model is None:
if args.model_version == "2.5":
parser.error("--model is required for IndexTTS 2.5")
args.model = "IndexTeam/IndexTTS-2"
if args.model_version == "2.5" and not 0.5 <= args.speed <= 2.0:
parser.error("IndexTTS 2.5 --speed must be between 0.5 and 2.0")
if not args.ref_audio and not args.voice:
parser.error("IndexTTS2 requires --ref-audio or --voice for voice cloning")
@@ -97,6 +128,10 @@ def main() -> None:
extra_params["use_emo_text"] = True
if args.use_random:
extra_params["use_random"] = True
if args.model_version == "2.5":
payload["speed"] = args.speed
extra_params["lang"] = args.lang
extra_params["text_normalization"] = args.text_normalization
if extra_params:
payload["extra_params"] = extra_params
+19
View File
@@ -84,11 +84,26 @@ forced-aligner = [
"qwen-asr>=0.0.6", # faithful Japanese/Korean word segmentation for the TTS forced aligner
]
indextts2 = [
"fugashi>=1.2.0",
"unidic-lite>=1.0.0",
"wetext>=0.0.9; sys_platform != 'linux'",
"WeTextProcessing; sys_platform == 'linux'",
]
# Local-test-only extras
local = [
"FlagEmbedding",
]
# LongCat-Video-Avatar A2V/AI2V runtime dependencies for vocal
# separation and loudness normalization.
longcat-video-avatar = [
"audio-separator==0.44.2",
"pyloudnorm==0.1.1",
"soundfile>=0.13.1",
]
# Optional Blackwell-only fused-bias FP8 GEMM; package is `quack-kernels` (imported
# as `quack`), not the unrelated `quack`. See docs/user_guide/quantization/fp8.md.
quack = [
@@ -319,3 +334,7 @@ byt5 = "byt5"
# "seeked" is the standard HTML media element DOM event name (fires when a
# seek completes), used by the word-timestamps Gradio demo. Not a typo.
seeked = "seeked"
# IndexTTS 2.5 language codes and speech-emotion-recognition event token.
fo = "fo"
ba = "ba"
SER = "SER"
+308
View File
@@ -0,0 +1,308 @@
# IndexTTS-2.5 for multilingual voice-cloned TTS on 1x GPU
## Summary
- Vendor: IndexTeam
- Model: `IndexTeam/IndexTTS-2.5`
- Task: Multilingual text-to-speech with voice cloning, emotion control, and
native speed control
- Mode: Online serving with the OpenAI-compatible `/v1/audio/speech` API, or
offline inference through `Omni`
- Maintainer: Community
## When to use this recipe
Use this recipe to serve IndexTTS-2.5 as a two-stage TTS system. Stage 0 is an
autoregressive talker; Stage 1 uses EnhancedCodec, S2Mel CFM/DiT, and BigVGAN
to produce 22.05 kHz mono speech. Each request supplies synthesis text and
reference audio, or an uploaded audio voice, for zero-shot voice cloning.
IndexTTS-2.5 adds multilingual text processing and model-native speed control
to the IndexTTS-2 serving contract.
## References
- Upstream model:
[IndexTeam/IndexTTS-2.5 on Hugging Face](https://huggingface.co/IndexTeam/IndexTTS-2.5)
- Online serving example:
[`examples/online_serving/text_to_speech/README.md#indextts-2-and-indextts-25`](../../examples/online_serving/text_to_speech/README.md#indextts-2-and-indextts-25)
- Offline inference example:
[`examples/offline_inference/text_to_speech/README.md#indextts-2-and-indextts-25`](../../examples/offline_inference/text_to_speech/README.md#indextts-2-and-indextts-25)
- OpenAI-compatible client:
[`examples/online_serving/text_to_speech/indextts2/speech_client.py`](../../examples/online_serving/text_to_speech/indextts2/speech_client.py)
- Standard deploy config:
[`vllm_omni/deploy/indextts2_5.yaml`](../../vllm_omni/deploy/indextts2_5.yaml)
## Hardware Support
### GPU
### 1x NVIDIA H20 96GB
#### Environment
- OS: Linux
- Python: 3.10+
- Driver / runtime: NVIDIA CUDA environment
- vLLM version: Match the repository requirements for your checkout
- vLLM-Omni version or commit: Use the commit you are deploying from
Install vLLM-Omni with the IndexTTS text-processing dependencies:
```bash
pip install 'vllm-omni[indextts2]'
```
Obtain the native IndexTTS-2.5 bundle and point `MODEL` at its root. The model
loader accepts the upstream nested `checkpoints/` layout.
#### Command
Start the standard code-only server from the repository root:
```bash
MODEL_VERSION=2.5 \
MODEL=/path/to/indextts-2.5 \
bash examples/online_serving/text_to_speech/indextts2/run_server.sh
```
This selects `vllm_omni/deploy/indextts2_5.yaml`, which uses
`use_gpt_latent=false`. To launch directly:
```bash
vllm serve /path/to/indextts-2.5 \
--omni \
--trust-remote-code \
--port 8092 \
--deploy-config vllm_omni/deploy/indextts2_5.yaml
```
#### Optional: NVIDIA MPS for higher single-GPU throughput
Stage 0 and Stage 1 run in separate processes on the same GPU. Enabling
NVIDIA Multi-Process Service (MPS) can improve steady-state throughput by
allowing work from both stages to overlap more effectively. MPS is an opt-in
deployment optimization: this recipe does not start it automatically, and the
gain depends on the request mix and concurrency, so benchmark it with the
intended workload before enabling it in production.
Pay attention to CUDA device renumbering. For example, if the MPS control
daemon is started with physical `CUDA_VISIBLE_DEVICES=1`, that GPU is exposed
to MPS clients as logical device `0`; launch the server with
`CUDA_VISIBLE_DEVICES=0`, not `1`. Use a dedicated MPS pipe/log directory when
the host is shared with other services, and stop only the MPS daemon owned by
this deployment.
#### Verification
Send a multilingual voice-cloning request with the bundled client. Local audio
paths are converted to base64 data URLs before transmission:
```bash
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--api-base http://localhost:8092 \
--model-version 2.5 \
--model /path/to/indextts-2.5 \
--lang zh \
--text "你好,这是 IndexTTS-2.5 语音合成测试。" \
--ref-audio /path/to/reference.wav \
--output indextts2_5.wav
```
##### Native speed control
The public `speed` field is handled natively. Its accepted range is
`[0.5, 2.0]`; values above `1.0` produce shorter, faster speech. The serving
adapter converts the API convention to the model-native control with
`duration_factor = 1.0 / speed`, so `speed=1.25` uses
`duration_factor=0.8`:
```bash
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--api-base http://localhost:8092 \
--model-version 2.5 \
--model /path/to/indextts-2.5 \
--speed 1.25 \
--text "这是使用模型原生语速控制的测试。" \
--ref-audio /path/to/reference.wav \
--output indextts2_5_speed.wav
```
##### Upload and reuse a named voice
Upload a reference recording once when callers should reuse the same voice
without sending `ref_audio` on every request. `consent` is required by the
voice-storage API:
```bash
curl -X POST http://localhost:8092/v1/audio/voices \
-F "audio_sample=@/path/to/reference.wav" \
-F "consent=user-consent-id" \
-F "name=indextts_demo_voice" \
-F "speaker_description=IndexTTS-2.5 demonstration voice"
```
Confirm that the voice was stored, then synthesize with its name instead of a
reference-audio payload:
```bash
curl http://localhost:8092/v1/audio/voices
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--api-base http://localhost:8092 \
--model-version 2.5 \
--model /path/to/indextts-2.5 \
--voice indextts_demo_voice \
--lang zh \
--text "你好,这是复用已上传音色的语音合成测试。" \
--output indextts2_5_named_voice.wav
```
There are no built-in text-only preset voices. Each named IndexTTS voice must
first be created from an uploaded reference recording.
##### Multilingual synthesis and text normalization
The client accepts `zh` (Mandarin), `en` (English), `zhen` (mixed Chinese and
English), `ja` (Japanese), and `yue` (Cantonese). The following commands use
the same reference voice across all supported language modes:
```bash
CLIENT=examples/online_serving/text_to_speech/indextts2/speech_client.py
MODEL=/path/to/indextts-2.5
REF_AUDIO=/path/to/reference.wav
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
--ref-audio "$REF_AUDIO" --lang zh \
--text "你好,这是中文语音合成测试。" --output indextts2_5_zh.wav
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
--ref-audio "$REF_AUDIO" --lang en \
--text "Hello, this is an English speech synthesis test." \
--output indextts2_5_en.wav
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
--ref-audio "$REF_AUDIO" --lang zhen \
--text "Hello,欢迎使用 IndexTTS 二点五。" --output indextts2_5_zhen.wav
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
--ref-audio "$REF_AUDIO" --lang ja \
--text "こんにちは、これは日本語の音声合成テストです。" \
--output indextts2_5_ja.wav
python "$CLIENT" --model-version 2.5 --model "$MODEL" \
--ref-audio "$REF_AUDIO" --lang yue \
--text "你好,呢段係粵語語音合成測試。" --output indextts2_5_yue.wav
```
Text normalization is enabled by default. Disable it only when the input is
already written exactly as it should be spoken:
```bash
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--model-version 2.5 --model /path/to/indextts-2.5 \
--ref-audio /path/to/reference.wav --lang en --no-text-normalization \
--text "The temperature is twenty five degrees." \
--output indextts2_5_pre_normalized.wav
```
For raw HTTP requests, put the same language and text-normalization controls
under `extra_params`:
```bash
curl http://localhost:8092/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"model": "/path/to/indextts-2.5",
"input": "This is an IndexTTS-2.5 test.",
"response_format": "wav",
"speed": 1.0,
"ref_audio": "data:audio/wav;base64,<BASE64_ENCODED_AUDIO>",
"extra_params": {
"lang": "en",
"text_normalization": true
}
}' \
--output indextts2_5_en.wav
```
##### Emotion control
IndexTTS-2.5 supports an explicit eight-value emotion vector, an emotion text
description, or a separate emotion-reference recording. Use one source at a
time. Vector values are ordered as `happy`, `angry`, `sad`, `afraid`,
`disgusted`, `melancholic`, `surprised`, and `calm`; each value must be in
`[0, 1.2]`. `emo_alpha` controls the strength in `[0, 1]`.
Use an explicit emotion vector for deterministic control:
```bash
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--model-version 2.5 --model /path/to/indextts-2.5 \
--ref-audio /path/to/reference.wav --lang zh \
--text "今天真是令人开心的一天!" \
--emo-vector 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \
--emo-alpha 0.8 --output indextts2_5_happy_vector.wav
```
Infer the emotion from a natural-language description with `use_emo_text`:
```bash
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--model-version 2.5 --model /path/to/indextts-2.5 \
--ref-audio /path/to/reference.wav --lang zh \
--text "我们终于完成了这项工作!" \
--use-emo-text --emo-text "开心、兴奋而且充满活力" \
--emo-alpha 0.8 --output indextts2_5_happy_text.wav
```
Transfer emotion from a recording while retaining the speaker identity from
`ref_audio`:
```bash
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
--model-version 2.5 --model /path/to/indextts-2.5 \
--ref-audio /path/to/reference.wav --lang zh \
--text "请用情绪参考音频中的表达方式朗读这句话。" \
--emo-audio /path/to/emotion_reference.wav \
--emo-alpha 0.8 --output indextts2_5_emotion_audio.wav
```
If multiple emotion sources are supplied, the official precedence is
`use_emo_text` > `emo_vector` > `emo_audio` > the emotion in the speaker
reference. `--use-random` is also available for exploratory random emotion
prototypes, but should not be used when reproducible conditioning is required.
For offline inference:
```bash
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
--model /path/to/indextts-2.5 \
--model-version 2.5 \
--lang zh \
--speed 1.0 \
--text "你好,这是离线语音合成测试。" \
--ref-audio /path/to/reference.wav
```
#### Notes
- Hardware scope: the standard recipe has been exercised on one NVIDIA H200.
It does not claim an unverified throughput or quality result.
- Audio output: 22.05 kHz mono WAV.
- Voice cloning: reference audio is required on the documented raw request
path. Alternatively, `voice` may name an uploaded audio voice; there is no
built-in text-only preset voice.
- Native speed: serving maps `speed` to the model's duration factor and skips
generic waveform speed adjustment, so speed is applied exactly once.
- Languages: common codes include `zh`, `en`, `zhen` (mixed Chinese/English),
`ja`, and `yue`; `Mandarin` is accepted as an alias for `zh`.
- Japanese normalization: `ja` tokenization does not automatically expand
numbers, dates, or percentages. Write these inputs as readable Japanese text
before inference.
- Emotion controls: `use_emo_text`, `emo_vector`, and `emo_audio` are
alternative conditioning modes. Their precedence is `use_emo_text` >
`emo_vector` > `emo_audio` > the speaker-reference emotion.
- Sampling difference: Stage 0 uses plain vLLM sampling, not the upstream
default `num_beams=3` beam search. Use upstream `num_beams=1` for parity
comparisons; output may differ from the official beam-search result.
+169
View File
@@ -0,0 +1,169 @@
# MiniMax-H3 on RTX 4090
This recipe uses BF16 weights, tiled VAE decode, tensor parallelism, optional
Ulysses sequence parallelism, and distributed layerwise offload (DLO). It is the
24 GiB sibling of [MiniMax-H3-5090.md](MiniMax-H3-5090.md): the two-GPU topology
matches that recipe, but the resident DiT-block count is lower so the per-rank
peak fits in 24 GiB. Lower resident counts reduce HBM use and increase
CPU-to-GPU transfer time.
## Capacity requirements
| Resource | Two RTX 4090s | Four RTX 4090s |
| --- | ---: | ---: |
| GPU HBM | 24 GiB per GPU | 24 GiB per GPU |
| Checkpoint storage | 135 GiB per partition | 135 GiB per partition |
| Available system RAM | 200 GiB minimum | 200 GiB minimum |
| Recommended system RAM | 384 GiB | 384 GiB |
`FL2VA` and `Ref2VA` are separate 135 GiB checkpoint partitions. Start one
server at a time. DLO keeps rank-local weights in pinned host memory; increasing
`--dlo-resident-layers` improves latency but does **not** reduce host RAM in the
current implementation because resident layers retain pinned CPU master copies.
A single RTX 4090 is not covered here. The one-GPU DLO profile in the RTX 5090
recipe peaked at 26.50 GiB with 12 resident layers, which exceeds 24 GiB. On one
4090, use the model-level CPU offload command in
[MiniMax-H3.md](MiniMax-H3.md#single-gpu-accuracy-and-memory-first) instead, or
lower the resident count and re-measure before trusting it.
> **Modular H3:** after #5720 lands, preserve this recipe's one-partition
> behavior with `--task-type fl2va` or `--task-type ref2va`.
## Two RTX 4090s: 1024x576, 5 seconds
Use TP2 and 12 resident DiT layers. This is the `rtx4090` profile from the main
recipe. On two RTX 4090s it peaked at 15,620 MiB per rank for T2VA and
14,918 MiB per rank for Ref2VA, leaving roughly 9 GiB of headroom per card. See
[Target-hardware validation](#target-hardware-validation) for the full numbers.
```bash
export VLLM_WORKER_MULTIPROC_METHOD=spawn
export VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400
CUDA_VISIBLE_DEVICES=0,1 vllm serve /path/to/MiniMax-H3/FL2VA \
--omni --trust-remote-code --host 0.0.0.0 --port 8000 \
--num-gpus 2 --tensor-parallel-size 2 --text-encoder-tp-size 2 \
--usp 1 --ring 1 --vae-patch-parallel-size 2 \
--vae-parallel-mode tile --vae-use-tiling \
--enable-distributed-layerwise-offload --dlo-no-use-allgather \
--dlo-resident-layers 12 --enforce-eager \
--diffusion-attention-backend CUDNN_ATTN
```
For Ref2VA, stop the FL2VA server and restart the same command with
`/path/to/MiniMax-H3/Ref2VA`. Ref2VA reference image count and prompt length can
increase activation memory; begin with one request at a time.
`VLLM_OMNI_VIDEO_SYNC_TIMEOUT` matters more here than on larger cards. DLO
streams non-resident DiT blocks over PCIe on every denoising step, so a 60-step
request takes several minutes and would otherwise hit the 600-second default.
## Four RTX 4090s: 1024x576, 5 seconds
Keep TP2 and raise Ulysses sequence parallel to 2 so the world size is
`TP × USP = 4`. Text-encoder TP and VAE patch parallel follow the GPU count.
Resident layers stay at 12: adding USP does not shard DiT weights further, so
per-GPU peak HBM stays close to the two-GPU profile while wall-clock improves.
```bash
export VLLM_WORKER_MULTIPROC_METHOD=spawn
export VLLM_OMNI_VIDEO_SYNC_TIMEOUT=14400
CUDA_VISIBLE_DEVICES=0,1,2,3 vllm serve /path/to/MiniMax-H3/FL2VA \
--omni --trust-remote-code --host 0.0.0.0 --port 8000 \
--num-gpus 4 --tensor-parallel-size 2 --text-encoder-tp-size 4 \
--usp 2 --ring 1 --vae-patch-parallel-size 4 \
--vae-parallel-mode tile --vae-use-tiling \
--enable-distributed-layerwise-offload --dlo-no-use-allgather \
--dlo-resident-layers 12 --enforce-eager \
--diffusion-attention-backend CUDNN_ATTN
```
For Ref2VA, stop the FL2VA server and restart the same command with
`/path/to/MiniMax-H3/Ref2VA`. Pin four dedicated GPUs; if the launcher exposes
fewer than four devices, startup fails with
`Stage 0 requires 4 device(s) based on parallel_config`.
## Target-hardware validation
Measured on RTX 4090 (24,564 MiB each, driver 580.126.09) with the serve
commands above, at vLLM-Omni `0.26.1.dev55+g81b48e83e`, vLLM `0.26.0`, and
PyTorch `2.11.0+cu130`. GPUs used by each server were dedicated. `Client E2E`
and `Peak per GPU` come from the `/v1/videos/sync` response headers
`x-inference-time-s` and `x-peak-memory-mb`; the latter is the rank-0 CUDA
reserved high-water mark, so it is a per-GPU figure.
| GPUs | Topology | Task | Shape | Frames | Steps | Client E2E | Peak per GPU | Output validation |
| ---: | --- | --- | ---: | ---: | ---: | ---: | ---: | --- |
| 2 | TP2 × USP1 | T2VA (`FL2VA`) | 1024x576 | 124 at 24 FPS | 60 | 7 min 9 s | 15.3 GiB | H.264 video + 32 kHz stereo AAC; full `ffmpeg` decode passed |
| 2 | TP2 × USP1 | Ref2VA | 1024x576 | 124 at 24 FPS | 60 | 14 min 52 s | 14.6 GiB | H.264 video + 32 kHz stereo AAC; full `ffmpeg` decode passed |
| 4 | TP2 × USP2 | T2VA (`FL2VA`) | 1024x576 | 124 at 24 FPS | 60 | 4 min 29 s | 15.2 GiB | H.264 video + 32 kHz stereo AAC; full `ffmpeg` decode passed |
| 4 | TP2 × USP2 | Ref2VA | 1024x576 | 124 at 24 FPS | 60 | 9 min 5 s | 16.1 GiB | H.264 video + 32 kHz stereo AAC; successful `/v1/videos/sync` 200 |
All rows use `seed=1101`, `flow_shift=12`, and `audio_flow_shift=3.0`.
Two-GPU repeats: T2VA measured 434.9 s and 429.3 s; Ref2VA measured 891.9 s and
892.1 s. Peak memory was identical across repeats for both tasks, and the two
outputs of each pair agreed to within 115 bytes.
Four-GPU T2VA repeats: 273.8 s and 269.5 s, peaking at 15,560 MiB and
15,612 MiB. Four-GPU Ref2VA is a single successful end-to-end run (545.2 s,
16,448 MiB). Follow-up Ref2VA requests on this build completed diffusion but
then hit the engine's hardcoded 30 s async output wait
(`_ASYNC_OUTPUT_TIMEOUT` in `diffusion_engine.py`) during post-compute D2H,
so they are not reported as repeats.
Relative to the two-GPU baseline on the same shape, four-GPU TP2×USP2 was about
1.6× faster for both tasks, while per-GPU peak HBM stayed in the mid-15 GiB
range because USP does not further shard resident DiT weights. These are
single-request validation runs, not concurrent throughput benchmarks.
## Request examples
`t2va` requires an explicit `aspect_ratio`. Without it the request fails with
`t2va requires an explicit aspect_ratio`, even when `width` and `height` are set.
Text to video and audio, against the `FL2VA` server:
```bash
curl -sS -D headers_t2va.txt -o out_t2va.mp4 --max-time 14400 \
-X POST "http://127.0.0.1:8000/v1/videos/sync" \
-F 'prompt=傍晚的小厨房,夕阳余晖从窗边洒进来,旧木桌上放着洗到一半的马克杯和起雾的玻璃瓶,悬挂的抹布轻轻晃动。画面带有手持拍摄的轻微晃动和逆光曝光波动,生活感十足。环境音是安静的厨房底噪。' \
-F 'aspect_ratio=16:9' \
-F 'width=1024' -F 'height=576' -F 'fps=24' \
-F 'num_inference_steps=60' -F 'flow_shift=12' -F 'seed=1101' \
-F 'extra_params={"task":"t2va","duration":5,"audio_flow_shift":3.0}'
```
Reference to video and audio, against the `Ref2VA` server:
```bash
curl -sS -D headers_ref2va.txt -o out_ref2va.mp4 --max-time 14400 \
-X POST "http://127.0.0.1:8000/v1/videos/sync" \
-F 'prompt=傍晚的小厨房,夕阳余晖从窗边洒进来,旧木桌上放着洗到一半的马克杯和起雾的玻璃瓶,悬挂的抹布轻轻晃动。画面带有手持拍摄的轻微晃动和逆光曝光波动,生活感十足。环境音是安静的厨房底噪。' \
-F 'input_reference=@/path/to/reference.jpg;type=image/jpeg' \
-F 'audio_reference=</path/to/audio_reference.json' \
-F 'width=1024' -F 'height=576' -F 'fps=24' \
-F 'num_inference_steps=60' -F 'flow_shift=12' -F 'seed=1101' \
-F 'extra_params={"task":"ref2va","duration":5,"audio_flow_shift":3.0}'
```
`audio_reference` is a JSON object, which is why the example loads it from a file
with curl's `<` syntax. Its `audio_url` must be an `http(s)` URL or a data URL;
a bare filesystem path is rejected with `Invalid audio_reference.audio_url`.
```json
{"audio_url": "https://example.com/reference.wav"}
```
Reference images are validated before inference: the short edge must be at least
256 pixels, the long edge at most 5760, and the aspect ratio must fall between
0.4 and 2.5.
The response headers carry the measurements used in the table above:
```bash
cat headers_t2va.txt
ffmpeg -v error -i out_t2va.mp4 -f null - && echo DECODE_OK
```
+67 -4
View File
@@ -79,7 +79,8 @@ sharing the tokenizer, processor, text encoder, and VAEs from `FL2VA`.
### Multi-NPU: 768P combined-service configuration
Use Ulysses sequence parallelism degree 8, text-encoder tensor parallelism
degree 8, native tiled VAE patch parallelism degree 8, and layerwise offload:
degree 8, native tiled VAE patch parallelism degree 8, and distributed
layerwise offload:
```bash
export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
@@ -98,7 +99,7 @@ vllm serve "${MODEL}" \
--usp 8 \
--ring 1 \
--text-encoder-tp-size 8 \
--enable-layerwise-offload \
--enable-distributed-layerwise-offload \
--vae-parallel-mode tile \
--vae-use-tiling \
--vae-patch-parallel-size 8 \
@@ -112,6 +113,11 @@ H3 is CFG-distilled, so `--cfg-parallel-size` must remain 1.
The same endpoint accepts `task=t2va`, `task=fl2va`, and `task=ref2va`; no
partition restart is required. Layerwise offload applies to both DiTs.
On Atlas 800I A3 (64 GB HBM per device) the combined service does not fit at
768P without offloading or sharding: use distributed layerwise offload (as
above) or HSDP — see
[§ Memory and attention optimizations](#memory-and-attention-optimizations-a3).
### Optional optimizations
Two independent optimizations may be enabled on top of the configuration
@@ -134,6 +140,63 @@ Keep `--ring 1` when using RainFusion: the `rf_v2` kernel ranks key blocks
over the whole sequence, so ring parallelism would split away the keys it
needs. Scale with `--usp` instead.
## Memory and attention optimizations (A3)
### Fitting 768P into 64 GB HBM: distributed layerwise offload or HSDP
Each Atlas 800I A3 NPU has 64 GB of HBM, which is not enough for the
combined MiniMax-H3 service at 768P. Enable **one** of the following at
server startup:
**Distributed layerwise offload** — DiT layers are offloaded with parameters
gathered across the parallel group instead of replicated per rank:
```bash
--enable-distributed-layerwise-offload
```
Measured peak NPU memory is about 45 GB per device for Ref2VA with a 13.88 s
input video generating a 15 s 768P (1344x768) output video. Host (CPU) memory
usage is high with this option.
**HSDP** — hybrid sharded data parallelism for the DiT parameters. The
multi-stream memory-reuse knob is mandatory with this flag:
```bash
export MULTI_STREAM_MEMORY_REUSE=2
```
```bash
--use-hsdp
```
Host memory usage is small, but large shapes may still OOM; HBM usage
optimizations for this path are ongoing.
### FLASH_ATTN backend with MindIE-SD
Keep `--diffusion-attention-backend FLASH_ATTN` and install MindIE-SD (see
[§ Environment](#environment)). On NPU this backend carries most of the
memory-reduction and performance work: a mask-free packed varlen path that
never materializes the quadratic `full_qk` padding mask, and K/V prefix
slicing, both driven by the packed `cu_seqlens` metadata emitted by the H3
transformer.
### Optional: LaserAttention fused kernel
For an additional attention speedup, select the Ascend LaserAttention fused
kernel before starting the server:
```bash
export MINDIE_SD_FA_TYPE="ascend_laser_attention"
```
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
speedup numbers will be added here.
## HTTP API examples
The request format is identical to the GPU recipe; see
@@ -154,8 +217,8 @@ configuration above:
| Workload | Configuration |
|----------|---------------|
| T2VA, 209 frames, 1344x768 | TE TP8, layerwise offload, Ulysses 8, VPP8 tile, regional compile |
| Ref2VA (prompt + video), 124 frames, 1344x768 | TE TP8, layerwise offload, Ulysses 8, VPP8 tile, regional compile |
| T2VA, 209 frames, 1344x768 | TE TP8, distributed layerwise offload, Ulysses 8, VPP8 tile, regional compile |
| Ref2VA (prompt + video), 124 frames, 1344x768 | TE TP8, distributed layerwise offload, Ulysses 8, VPP8 tile, regional compile |
These measurements describe the validated shapes rather than a general
throughput guarantee.
@@ -0,0 +1,198 @@
# MiniMax-H3 on RTX PRO 5000 Blackwell GPUs
This recipe runs MiniMax-H3 in BF16 on 72 GiB RTX PRO 5000 Blackwell GPUs. It
contains the validated two-GPU DLO configuration and the recommended resident
configurations: TP1 x Ulysses2 with 20 resident layers on two GPUs, TP2 x
Ulysses2 on four GPUs, and TP4 x Ulysses2 on eight GPUs. The four- and
eight-GPU routes do not require offload.
## Capacity requirements
| Resource | Requirement |
| --- | ---: |
| GPUs | 2, 4, or 8 x RTX PRO 5000 Blackwell |
| GPU HBM | 72 GiB per GPU |
| Checkpoint storage | 135 GiB per partition |
| Available system RAM | 200 GiB minimum |
| Recommended system RAM | 384 GiB |
`FL2VA` and `Ref2VA` are separate checkpoint partitions. Start one server at a
time on a host sized for the minimum system-memory requirement.
## PCIe topology and GPU order
RTX PRO 5000 does not provide NVLink. Before starting the server, identify the
closest PCIe pairs and their NUMA affinity:
```bash
nvidia-smi topo -m
nvidia-smi nvlink -s
```
For two GPUs, select the closest PCIe pair on one NUMA node. For four GPUs,
select all cards from one NUMA node. On the validated host,
physical GPUs `(0,1)` and `(2,3)` are the two `PXB` pairs on NUMA node 0. The
order `CUDA_VISIBLE_DEVICES=0,2,1,3` maps the Ulysses groups to those local
pairs. For eight GPUs, the validated order is `0,4,1,5,2,6,3,7`, with host
memory interleaved across NUMA nodes 0 and 1. Do not copy these IDs blindly:
reproduce the same PCIe and NUMA relationships on the target host.
## Recommended serving configurations
### Two GPUs
Two 72 GiB cards require distributed layerwise offload. The validated route
uses TP1 x Ulysses2, keeps 20 leading DiT layers resident, and streams
rank-local weights without AllGather. Eager execution avoids regional-compile
instability on this offload path.
```bash
export MODEL_ROOT=/path/to/MiniMax-H3
export MODEL="${MODEL_ROOT}/FL2VA"
export PORT=8091
CUDA_VISIBLE_DEVICES=0,1 \
VLLM_WORKER_MULTIPROC_METHOD=spawn \
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800 \
numactl --cpunodebind=0 --membind=0 \
vllm serve "${MODEL}" \
--omni \
--host 0.0.0.0 \
--port "${PORT}" \
--trust-remote-code \
--num-gpus 2 \
--tensor-parallel-size 1 \
--usp 2 \
--ring 1 \
--text-encoder-tp-size 2 \
--vae-patch-parallel-size 2 \
--vae-parallel-mode tile \
--vae-use-tiling \
--diffusion-attention-backend CUDNN_ATTN \
--enable-distributed-layerwise-offload \
--dlo-no-use-allgather \
--dlo-resident-layers 20 \
--enforce-eager
```
### Four GPUs
The validated baseline uses TP2 x Ulysses2, text-encoder TP4, VAE patch
parallelism 4, and explicit cuDNN BF16 attention. Selecting the backend
explicitly keeps the recipe independent of platform-default backend changes.
```bash
export MODEL_ROOT=/path/to/MiniMax-H3
export MODEL="${MODEL_ROOT}/FL2VA"
export PORT=8091
CUDA_VISIBLE_DEVICES=0,2,1,3 \
VLLM_WORKER_MULTIPROC_METHOD=spawn \
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800 \
numactl --cpunodebind=0 --membind=0 \
vllm serve "${MODEL}" \
--omni \
--host 0.0.0.0 \
--port "${PORT}" \
--trust-remote-code \
--num-gpus 4 \
--tensor-parallel-size 2 \
--usp 2 \
--ring 1 \
--text-encoder-tp-size 4 \
--vae-patch-parallel-size 4 \
--vae-parallel-mode tile \
--vae-use-tiling \
--diffusion-attention-backend CUDNN_ATTN
```
### Eight GPUs
The recommended eight-GPU route uses TP4 x Ulysses2, text-encoder TP8, VAE
patch parallelism 8, and host-memory interleaving across both NUMA nodes.
```bash
export MODEL_ROOT=/path/to/MiniMax-H3
export MODEL="${MODEL_ROOT}/FL2VA"
export PORT=8091
CUDA_VISIBLE_DEVICES=0,4,1,5,2,6,3,7 \
VLLM_WORKER_MULTIPROC_METHOD=spawn \
VLLM_OMNI_VIDEO_SYNC_TIMEOUT=1800 \
numactl --interleave=0,1 \
vllm serve "${MODEL}" \
--omni \
--host 0.0.0.0 \
--port "${PORT}" \
--trust-remote-code \
--num-gpus 8 \
--tensor-parallel-size 4 \
--usp 2 \
--ring 1 \
--text-encoder-tp-size 8 \
--vae-patch-parallel-size 8 \
--vae-parallel-mode tile \
--vae-use-tiling \
--diffusion-attention-backend CUDNN_ATTN
```
For the four- and eight-GPU resident routes, do not add `--enforce-eager`.
Warm the server once before measuring so regional compilation is outside the
measured request. The two-GPU DLO route intentionally remains eager.
For Ref2VA, stop the FL2VA server and restart the same command with
`MODEL="${MODEL_ROOT}/Ref2VA"`.
## Target-hardware validation
All three configurations were exercised on a PCIe-only, dual-socket host with
eight RTX PRO 5000 GPUs. The run used PyTorch 2.11.0+cu130, CUDA 13.0, driver
580.95.05, 1344x768 output, 124 frames, and two warmups.
The four-GPU route also received a five-step profiling validation for GPU
balance:
| Measurement | Result |
| --- | ---: |
| Maximum externally sampled peak | 69,219 MiB (67.60 GiB) per GPU |
| T2VA worker-reported peak | 65,314 MiB |
| First-frame FL2VA worker-reported peak | 66,780 MiB |
| T2VA maximum GPU kernel-time deviation | 0.56% |
| First-frame FL2VA maximum GPU kernel-time deviation | 0.09% |
The recommended routes produced the following 50-step results. MiniMax-H3
requested 50 denoise steps and executed 49 denoise updates, so per-step latency
is `denoise / 49`.
| GPUs | Workload | Parallelism | E2E (s) | Text encode (s) | Denoise (s) | VAE decode (s) | Per step (ms) | Peak memory (GiB) |
| ---: | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| 2 | T2VA | TP1 x Ulysses2, DLO resident 20 | 515.57 | 1.19 | 504.69 | 8.79 | 10,300 | 36.38 |
| 2 | FL2VA first-frame I2VA | TP1 x Ulysses2, DLO resident 20 | 553.45 | 1.27 | 541.90 | 8.76 | 11,059 | 36.38 |
| 4 | T2VA | TP2 x Ulysses2 | 284.01 | 0.03 | 278.73 | 4.36 | 5,688 | 67.58 |
| 4 | FL2VA first-frame I2VA | TP2 x Ulysses2 | 305.72 | 0.28 | 299.85 | 4.36 | 6,119 | 67.58 |
| 8 | T2VA | TP4 x Ulysses2 | 163.20 | 0.04 | 159.40 | 2.58 | 3,253 | 45.83 |
| 8 | FL2VA first-frame I2VA | TP4 x Ulysses2 | 171.62 | 0.27 | 167.25 | 2.57 | 3,413 | 45.83 |
Peak memory is the maximum per-GPU value sampled externally with
`nvidia-smi`. The two-GPU route leaves about 34.7 GiB below the reported
73,415 MiB device capacity, the four-GPU route leaves about 4.1 GiB, and the
eight-GPU route leaves about 25.9 GiB. Re-measure memory for longer reference
inputs, concurrency greater than one, or a different output shape.
## T2VA request example
```bash
export API_URL="http://127.0.0.1:${PORT}/v1/videos/sync"
curl -sS --max-time 1800 -X POST "${API_URL}" \
-F 'prompt=At night, three cats march into a bedroom playing tiny brass instruments, then abruptly file out, with synchronized room ambience.' \
-F 'width=1344' \
-F 'height=768' \
-F 'aspect_ratio=16:9' \
-F 'fps=24' \
-F 'num_inference_steps=50' \
-F 'flow_shift=12' \
-F 'seed=1101' \
-F 'extra_params={"task":"t2va","duration":5.0,"audio_flow_shift":3.0}' \
-o t2va.mp4
```
+2 -1
View File
@@ -30,8 +30,9 @@ also provided.
`hf_config.version="4.5"`):
- Default single-GPU compatibility layout (auto-loaded):
[`vllm_omni/deploy/minicpmo_4_5.yaml`](../../vllm_omni/deploy/minicpmo_4_5.yaml)
- 2-GPU and 3-GPU layouts:
- Recommended 2-GPU continuous-batching layout:
[`vllm_omni/deploy/minicpmo_4_5_2gpu.yaml`](../../vllm_omni/deploy/minicpmo_4_5_2gpu.yaml),
- 3-GPU layout:
[`vllm_omni/deploy/minicpmo_4_5_3gpu.yaml`](../../vllm_omni/deploy/minicpmo_4_5_3gpu.yaml)
- 8x RTX 4090 layout:
[`vllm_omni/deploy/minicpmo_4_5_8x4090.yaml`](../../vllm_omni/deploy/minicpmo_4_5_8x4090.yaml)
+2
View File
@@ -50,11 +50,13 @@ recipes/
| [`inclusionAI/Ming-flash-omni-2.0.md`](./inclusionAI/Ming-flash-omni-2.0.md) | Online serving for multimodal chat + standalone TTS | 4x H100 / 1x H100 80GB |
| [`inclusionAI/Ming-omni-tts.md`](./inclusionAI/Ming-omni-tts.md) | Offline + online dense Ming TTS/audio generation | 1x H100 80GB / 1x AMD MI300X (ROCm 7.2) |
| [`IndexTeam/IndexTTS-2.md`](./IndexTeam/IndexTTS-2.md) | Online serving for voice-cloned TTS with optional emotion control | 1x L4 24GB or larger CUDA GPU |
| [`IndexTeam/IndexTTS-2_5.md`](./IndexTeam/IndexTTS-2_5.md) | Online + offline multilingual voice-cloned TTS with native speed and emotion control | 1x NVIDIA H20 96GB |
| [`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-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 |
| [`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 |
| [`meituan-longcat/LongCat-Video-Avatar-1.5.md`](./meituan-longcat/LongCat-Video-Avatar-1.5.md) | Audio-driven avatar video generation (AT2V / AI2V, single- and multi-speaker, AVC continuation) | 1x H100 80GB |
| [`mistralai/Voxtral-TTS.md`](./mistralai/Voxtral-TTS.md) | Online serving for TTS | 1x RTX 4090 24GB |
| [`Robbyant/LingBot-Video.md`](./Robbyant/LingBot-Video.md) | Native dense and MoE text-to-video serving | Dense: 1x L20X; MoE: 1x L20X (~67.7 GiB peak) |
| [`cosmos3/Cosmos3-Nano.md`](./cosmos3/Cosmos3-Nano.md) | Text-to-image, text-to-video, image-to-video, video-to-video generation, text to video with sound, action policy | 1x H200 141GB / B300 |
@@ -0,0 +1,282 @@
# LongCat-Video-Avatar-1.5
> Audio-driven avatar video generation (AT2V / AI2V)
## Summary
- Vendor: meituan-longcat
- Model: `meituan-longcat/LongCat-Video-Avatar-1.5`
- Task: Audio-to-video (AT2V) and audio-and-image-to-video (AI2V) avatar generation
- Mode: Offline inference
- Maintainer: Community
## When to use this recipe
Use this recipe to drive a talking or singing avatar from a speech clip, either
from text alone (AT2V) or from a reference image (AI2V), with the native
LongCat-Video-Avatar-1.5 pipeline in vLLM-Omni. It covers:
1. **Single-speaker AT2V / AI2V** — one 93-frame segment from one audio track.
2. **AVC continuation** — chained segments that cover a longer audio track.
3. **Multi-speaker AI2V** — several audio tracks placed on a reference image
through speaker bounding boxes.
The example saves an mp4 with the generated frames muxed with the input audio.
## References
- Upstream model card: <https://huggingface.co/meituan-longcat/LongCat-Video-Avatar-1.5>
- Upstream repository and example assets: <https://github.com/meituan-longcat/LongCat-Video>
- Related example under `examples/`: [`longcat_video/end2end.py`](../../examples/offline_inference/longcat_video/end2end.py)
## Hardware Support
## CUDA
### 1× NVIDIA H100 (80 GB)
#### Environment
- OS: Linux
- Python: 3.10+
- Driver: NVIDIA driver with CUDA 12.x
- vLLM version: Match the repository requirements for your checkout
- vLLM-Omni version or commit: Use the commit you are deploying from
#### Prerequisites
Install the LongCat Avatar optional runtime dependencies:
```bash
pip install "vllm-omni[longcat-video-avatar]"
```
The example uses the `imageio_ffmpeg` executable installed by vLLM-Omni's
`imageio[ffmpeg]` dependency.
#### Command
Audio-to-video (AT2V):
```bash
python examples/offline_inference/longcat_video/end2end.py \
--model meituan-longcat/LongCat-Video-Avatar-1.5 \
--stage at2v \
--audio /path/to/speech.wav \
--prompt "A person speaks calmly while facing the camera." \
--num-frames 93 \
--num-inference-steps 8 \
--output longcat_avatar_at2v.mp4
```
Audio-and-image-to-video (AI2V):
```bash
python examples/offline_inference/longcat_video/end2end.py \
--model meituan-longcat/LongCat-Video-Avatar-1.5 \
--stage ai2v \
--audio /path/to/speech.wav \
--image /path/to/reference.jpg \
--prompt "A person speaks calmly while facing the camera." \
--num-frames 93 \
--num-inference-steps 8 \
--output longcat_avatar_ai2v.mp4
```
#### Official Example Assets
Download the official Avatar example assets:
```bash
mkdir -p longcat_avatar_assets/assets/avatar/single longcat_avatar_assets/assets/avatar/multi
curl -L -o longcat_avatar_assets/assets/avatar/single/man.mp3 \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/single/man.mp3
curl -L -o longcat_avatar_assets/assets/avatar/single/man.png \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/single/man.png
curl -L -o longcat_avatar_assets/assets/avatar/single_example_1.json \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/single_example_1.json
curl -L -o longcat_avatar_assets/assets/avatar/multi/sing.png \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/multi/sing.png
curl -L -o longcat_avatar_assets/assets/avatar/multi/sing_man.WAV \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/multi/sing_man.WAV
curl -L -o longcat_avatar_assets/assets/avatar/multi/sing_woman.WAV \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/multi/sing_woman.WAV
curl -L -o longcat_avatar_assets/assets/avatar/multi/introduce.png \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/multi/introduce.png
curl -L -o longcat_avatar_assets/assets/avatar/multi/introduce_man.mp3 \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/multi/introduce_man.mp3
curl -L -o longcat_avatar_assets/assets/avatar/multi/introduce_woman.mp3 \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/multi/introduce_woman.mp3
curl -L -o longcat_avatar_assets/assets/avatar/multi_example_1.json \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/multi_example_1.json
curl -L -o longcat_avatar_assets/assets/avatar/multi_example_2.json \
https://raw.githubusercontent.com/meituan-longcat/LongCat-Video/main/assets/avatar/multi_example_2.json
export LONGCAT_VIDEO_ASSET_DIR="$PWD/longcat_avatar_assets/assets/avatar"
export AVATAR_MODEL=meituan-longcat/LongCat-Video-Avatar-1.5
export PROMPT="$(python -c 'import json, os; print(json.load(open(os.environ["LONGCAT_VIDEO_ASSET_DIR"] + "/single_example_1.json"))["prompt"])')"
```
`--base-model-dir` points at a local LongCat-Video base checkpoint for the
tokenizer, text encoder and VAE; omit it to download those components from
Hugging Face.
```bash
export BASE_MODEL_DIR=/cache/longcat_models/LongCat-Video
```
#### Official Asset Smoke Test
Audio-to-video with the official audio:
```bash
python examples/offline_inference/longcat_video/end2end.py \
--model "$AVATAR_MODEL" \
--base-model-dir "$BASE_MODEL_DIR" \
--stage at2v \
--audio "$LONGCAT_VIDEO_ASSET_DIR/single/man.mp3" \
--prompt "$PROMPT" \
--num-frames 93 \
--num-inference-steps 8 \
--output official_asset_longcat_avatar_at2v.mp4
```
Audio-and-image-to-video with the official audio and image:
```bash
python examples/offline_inference/longcat_video/end2end.py \
--model "$AVATAR_MODEL" \
--base-model-dir "$BASE_MODEL_DIR" \
--stage ai2v \
--audio "$LONGCAT_VIDEO_ASSET_DIR/single/man.mp3" \
--image "$LONGCAT_VIDEO_ASSET_DIR/single/man.png" \
--prompt "$PROMPT" \
--num-frames 93 \
--num-inference-steps 8 \
--output official_asset_longcat_avatar_ai2v.mp4
```
#### Official Single-Speaker AVC Continuation
`--num-segments` chains AVC segments; `auto` covers the full input audio.
```bash
python examples/offline_inference/longcat_video/end2end.py \
--model "$AVATAR_MODEL" \
--base-model-dir "$BASE_MODEL_DIR" \
--input-json "$LONGCAT_VIDEO_ASSET_DIR/single_example_1.json" \
--num-frames 93 \
--num-cond-frames 13 \
--num-segments 5 \
--num-inference-steps 8 \
--output official_asset_longcat_avatar_single_example_1_ai2v_5seg.mp4
```
Replace `--num-segments 5` with `--num-segments auto` to run until the full
official audio is covered. That is a slow archival validation path rather than a
regular smoke test: about 24m28s on a Modal H100 for the full
`single_example_1.json` audio with 8 inference steps.
#### Official Multi-Speaker AI2V
The multi-speaker cases use several audio tracks with speaker masks and optional
bounding boxes. The command below generates the first 93-frame segment, matching
the first call to the official `generate_ai2v()`.
```bash
python examples/offline_inference/longcat_video/end2end.py \
--model "$AVATAR_MODEL" \
--base-model-dir "$BASE_MODEL_DIR" \
--input-json "$LONGCAT_VIDEO_ASSET_DIR/multi_example_1.json" \
--num-frames 93 \
--num-inference-steps 8 \
--output official_asset_longcat_avatar_multi_example_1_ai2v.mp4
```
Use `multi_example_2.json` for the sequential-speaking case, and add
`--num-cond-frames 13 --num-segments auto` to cover the full audio with AVC
continuation.
#### Verification
```bash
ffprobe -hide_banner longcat_avatar_ai2v.mp4
```
The output must report one video stream and one audio stream: the example muxes
the generated frames with the input audio.
#### Notes
**Support matrix**
| Mode | Speakers | Image required | AVC continuation |
| --- | --- | --- | --- |
| AT2V | Single | No | Yes |
| AI2V | Single | Yes | Yes |
| AT2V | Multi | Not supported | Not supported |
| AI2V | Multi | Yes | Yes |
Multi-speaker Avatar requires AI2V because speaker bounding boxes and masks are
defined on a reference image; AT2V with several tracks fails with an explicit
error. AVC continuation uses full-audio embeddings only when `--num-segments` is
greater than `1` or set to `auto`; single-segment AT2V/AI2V keeps the shorter
first-segment audio path.
**Sampling parameters**
| Parameter | LongCat pipeline value | Description |
| --- | --- | --- |
| `--num-frames` | `num_frames` | Frames generated per segment. The default is 93 frames, or about 3.72 seconds at 25 FPS. LongCat normalizes other values to the temporal-VAE-compatible `4k+1` form. |
| `--num-inference-steps` | `steps` | Denoising steps. The distilled Avatar path uses 8 steps. |
| `--fps` | `save_fps` | Output frame rate, and the rate used to align audio embeddings with video frames. The default is 25 FPS. |
| `sampling_params.guidance_scale` | `text_guidance_scale` | Classifier-free guidance applied to the text prompt. |
| `sampling_params.guidance_scale_2` | `audio_guidance_scale` | Classifier-free guidance applied to the speech audio. |
With the distilled LoRA enabled (the default), the pipeline uses 8 inference
steps and sets both guidance scales to `1.0`. With `--no-use-distill`, the
example sets both to `4.0`. For AVC continuation every segment holds
`num_frames` frames, but each segment after the first reuses `num_cond_frames`
conditioning frames, so the unique frame count is
`num_frames + (num_segments - 1) * (num_frames - num_cond_frames)`.
**Key flags**
- `--stage`: `at2v` for audio-to-video, `ai2v` for audio-and-image-to-video.
- `--resolution`: `480p` or `720p`.
- `--input-json`: official LongCat Avatar JSON case, used for the single-speaker
and multi-speaker AI2V examples.
- `--num-segments`: number of AVC segments, or `auto` to cover the full audio.
- `--num-cond-frames`: previous-frame conditioning window for AVC continuation.
- `--use-int8` / `--no-use-int8`: load the official INT8 Avatar DiT weights by
default, or full precision weights.
- `--use-distill` / `--no-use-distill`: enable the official distilled LoRA path
by default.
- `--build-components-on-gpu`: build large Avatar components directly on GPU for
faster startup, at a higher peak VRAM cost. Disabled by default.
- `--base-model-dir`: local LongCat-Video base model directory for the
tokenizer, text encoder and VAE components.
**Memory usage**
Measured on a Modal H100 running the official 93-frame AI2V example with the
INT8 DiT, distilled LoRA and 8 inference steps:
| Component build | CLI option | Peak GPU memory | Model loading time |
| --- | --- | ---: | ---: |
| CPU (default) | None | About 41.0 GiB | About 130s |
| GPU | `--build-components-on-gpu` | About 56.8 GiB | About 33s |
The reported peak covers the end-to-end run. Building components on CPU only
reduces initialization memory; denoising still runs on GPU. These numbers are
memory guidance rather than a performance benchmark.
**Known limitations**
- Single GPU only. Cache acceleration, sequence/CFG/tensor parallelism, HSDP,
CPU offload, VAE patch parallelism and quantization are not wired up for this
pipeline yet; see the diffusion feature matrix in
[`docs/user_guide/diffusion_features.md`](../../docs/user_guide/diffusion_features.md).
- Offline inference only. Online serving is not supported yet.
+159 -1
View File
@@ -6,13 +6,21 @@ Unit tests for patch.py
"""
import asyncio
import base64
import json
import time
from types import SimpleNamespace
import pytest
from pytest_mock import MockerFixture
from vllm.benchmarks.lib.endpoint_request_func import RequestFuncInput
from vllm_omni.benchmarks.patch.patch import MixRequestFuncOutput, async_request_openai_chat_omni_completions
from vllm_omni.benchmarks.patch.patch import (
MixRequestFuncOutput,
async_request_openai_chat_omni_completions,
async_request_openai_realtime_duplex,
)
from vllm_omni.experimental.fullduplex.client import RealtimeEventCollector
pytestmark = [pytest.mark.core_model, pytest.mark.benchmark, pytest.mark.cpu]
@@ -40,6 +48,156 @@ class MockResponse:
pass
@pytest.mark.asyncio
async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch):
class FakeRealtimeClient:
last_instance = None
def __init__(self, url):
assert url == "ws://localhost:8000/v1/realtime?duplex=1"
self.events = RealtimeEventCollector()
self.configure_kwargs = None
self.sent = []
self.response_count = 0
self.ack_count = 0
type(self).last_instance = self
async def __aenter__(self):
return self
async def __aexit__(self, *_args):
return None
async def configure(self, model, **kwargs):
assert model == "openbmb/MiniCPM-o-4_5"
self.configure_kwargs = kwargs
async def send(self, event):
self.sent.append(event)
if event["type"] != "response.create":
return
self.response_count += 1
response_id = f"resp-{self.response_count}"
now = time.monotonic()
self.events.add(
{"type": "response.created", "response": {"id": response_id}},
received_at_s=now + 0.01,
)
self.events.add(
{
"type": "response.audio_transcript.delta",
"response_id": response_id,
"delta": f"turn {self.response_count}",
},
received_at_s=now + 0.02,
)
self.events.add(
{
"type": "response.audio.delta",
"response_id": response_id,
"delta": base64.b64encode(b"\x00\x00" * 2400).decode(),
"sample_rate_hz": 24_000,
"metadata": {"audio_duration_ms": 100},
},
received_at_s=now + 0.03,
)
self.events.add(
{"type": "response.done", "response": {"id": response_id}},
received_at_s=now + 0.04,
)
async def acknowledge_playback(self):
self.ack_count += 1
async def close_session(self, **_kwargs):
return None
monkeypatch.setattr(
"vllm_omni.benchmarks.patch.patch.RealtimeDuplexClient",
FakeRealtimeClient,
)
request_input = RequestFuncInput(
model="openbmb/MiniCPM-o-4_5",
model_name="openbmb/MiniCPM-o-4_5",
prompt="hello",
api_url="http://localhost:8000/v1/realtime",
prompt_len=1,
output_len=20,
logprobs=None,
multi_modal_content=None,
ignore_eos=False,
extra_body={"save_duplex_request_metrics": True},
)
request_input.seed_tts_speech_extra = {"ref_audio": "data:audio/wav;base64,AAAA"}
request_input.seed_tts_system_prompt = "Speak exactly."
request_input.seed_tts_turns = tuple(
SimpleNamespace(utterance_id=f"utt-{index}", target_text=f"text {index}") for index in range(4)
)
output = await async_request_openai_realtime_duplex(
request_input,
session=None,
)
client = FakeRealtimeClient.last_instance
assert client.configure_kwargs["native_duplex"] is False
assert client.configure_kwargs["extra_body"] == {
"ref_audio": "data:audio/wav;base64,AAAA",
}
assert client.sent == [
event
for index in range(4)
for event in (
{
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": f"text {index}"}],
},
},
{"type": "response.create"},
)
]
assert client.ack_count == 4
assert output.success is True
assert output.generated_text == "turn 1 turn 2 turn 3 turn 4"
assert output.audio_duration == pytest.approx(0.4)
assert output.ttft > 0
assert output.audio_ttfp > output.ttft
assert output.audio_rtf > 0
assert output.latency > 0
assert output.tts_turn_pcm_bytes == [b"\x00\x00" * 2400] * 4
assert output.tts_output_pcm_bytes == b"\x00\x00" * 9600
session_id = output.duplex_request_metrics[0]["session_id"]
assert len(output.duplex_request_metrics) == 4
for index, metrics in enumerate(output.duplex_request_metrics):
assert metrics == {
"session_id": session_id,
"request_index": index,
"utterance_id": f"utt-{index}",
"response_id": f"resp-{index + 1}",
"source": "client_monotonic_receive",
"measurement_origin": {
"ttft": "conversation.item.create client send to first non-empty text delta",
"ttfp": "conversation.item.create client send to first audio packet",
"rtf": "request-start-to-last-audio receive time divided by emitted audio duration",
},
"ttft_ms": pytest.approx(20.0, abs=2.0),
"ttfp_ms": pytest.approx(30.0, abs=2.0),
"rtf": pytest.approx(0.3, abs=0.03),
"audio_generation_ms": pytest.approx(30.0, abs=2.0),
"audio_duration_ms": 100.0,
}
assert output.duplex_session_metrics == {
"session_id": session_id,
"audio_turn_count": 4,
"mean_ttft_ms": pytest.approx(20.0, abs=2.0),
"mean_ttfp_ms": pytest.approx(30.0, abs=2.0),
"mean_rtf": pytest.approx(0.3, abs=0.03),
}
def create_sse_chunk(data_dict):
"""Helper to create SSE formatted chunk"""
return f"data: {json.dumps(data_dict)}\n\n".encode()
+23 -1
View File
@@ -42,7 +42,10 @@ from benchmarks.accuracy.text_to_image.gbench import (
summarize_generated_records as summarize_gebench_generated_records,
summarize_gebench_results,
)
from tests.e2e.accuracy.qwen3_omni.qwen3_omni_acc_bench_core import seed_tts_bench_argv
from tests.e2e.accuracy.qwen3_omni.qwen3_omni_acc_bench_core import (
build_serve_common_argv,
seed_tts_bench_argv,
)
from tests.e2e.accuracy.qwen3_omni.run_qwen_omni_acc_benchmark import sync_dataset_env_from_ns
from vllm_omni.benchmarks.data_modules.seed_tts_dataset import resolve_seed_tts_root
@@ -57,6 +60,25 @@ def test_seed_tts_bench_argv_preserves_hf_repo_id_from_env(monkeypatch):
assert argv[dataset_idx + 1] == "zhaochenyang20/seed-tts-eval"
def test_build_serve_common_argv_accepts_realtime_seed_tts_route(tmp_path):
argv = build_serve_common_argv(
host="127.0.0.1",
port=8000,
model="openbmb/MiniCPM-o-4_5",
num_prompts=4,
max_concurrency=1,
num_warmups=0,
percentile_metrics="ttft,audio_ttfp,audio_rtf",
result_dir=tmp_path,
result_filename="result.json",
backend="openai-realtime-tts",
endpoint="/v1/realtime",
)
assert argv[argv.index("--backend") + 1] == "openai-realtime-tts"
assert argv[argv.index("--endpoint") + 1] == "/v1/realtime"
def test_sync_dataset_env_preserves_seed_tts_hf_repo_id(monkeypatch):
ns = argparse.Namespace(
daily_omni_repo=None,
+36
View File
@@ -51,6 +51,14 @@ def test_load_model_configs(model_configs_path: Path) -> None:
assert configs["test/ModelA"]["supported_tasks"] == ["voice_clone", "default_voice"]
def test_indextts25_is_registered_in_shared_model_configs() -> None:
config_path = Path(bench_tts.__file__).with_name("model_configs.yaml")
config = bench_tts.load_model_configs(config_path)["IndexTeam/IndexTTS-2.5"]
assert config["supported_tasks"] == ["voice_clone"]
assert config["task_extra_body"]["voice_clone"]["extra_params"]["lang"] == "en"
def test_build_bench_args_voice_clone(model_configs_path: Path) -> None:
configs = bench_tts.load_model_configs(model_configs_path)
cmd = bench_tts.build_bench_args(
@@ -120,6 +128,34 @@ def test_build_bench_args_wer_eval_adds_flag(model_configs_path: Path) -> None:
assert "--seed-tts-wer-eval" in cmd
def test_build_bench_args_supports_local_model_and_shared_sweep_options(model_configs_path: Path) -> None:
configs = bench_tts.load_model_configs(model_configs_path)
cmd = bench_tts.build_bench_args(
host="localhost",
port=8092,
model="test/ModelA",
served_model_name="/models/indextts25",
task="voice_clone",
model_cfg=configs["test/ModelA"],
locale="en",
num_prompts=500,
num_warmups=5,
request_seed=42,
concurrency=8,
dataset_path="/data/seed-tts",
wer_eval=False,
output_dir=None,
result_filename=None,
extra_cli_args=["--", "--tokenizer", "/models/indextts25/qwen0.6bemo4-merge"],
)
assert cmd[cmd.index("--model") + 1] == "/models/indextts25"
assert cmd[cmd.index("--num-warmups") + 1] == "5"
assert cmd[-2:] == ["--tokenizer", "/models/indextts25/qwen0.6bemo4-merge"]
extra_body = json.loads(cmd[cmd.index("--extra-body") + 1])
assert extra_body == {"task_type": "Base", "seed": 42}
def test_unsupported_task_exits(model_configs_path: Path, capsys: pytest.CaptureFixture, mocker) -> None:
# ModelB does not support voice_design
mocker.patch.object(
@@ -28,10 +28,13 @@ if _MODULE_NAME not in sys.modules:
_spec.loader.exec_module(_mod)
from vllm_omni.benchmarks.data_modules.seed_tts_dataset import ( # noqa: E402
SeedTTSDataset,
SeedTTSDesignDataset,
SeedTTSDesignSampleRequest,
SeedTTSSampleRequest,
SeedTTSTextDataset,
SeedTTSTextSampleRequest,
SeedTTSTurn,
)
# ---------------------------------------------------------------------------
@@ -70,6 +73,94 @@ def mock_tokenizer(mocker):
# ---------------------------------------------------------------------------
def test_seed_tts_dataset_groups_four_turns_with_one_reference(seed_tts_root, mock_tokenizer):
ds = SeedTTSDataset(
dataset_path=str(seed_tts_root),
random_seed=0,
locale="en",
disable_shuffle=True,
)
requests = ds.sample(
mock_tokenizer,
num_requests=1,
turns_per_session=4,
no_oversample=True,
)
assert len(requests) == 1
request = requests[0]
assert [turn.utterance_id for turn in request.seed_tts_turns] == [
"utt000",
"utt001",
"utt002",
"utt003",
]
assert [turn.target_text for turn in request.seed_tts_turns] == [
"target text 0",
"target text 1",
"target text 2",
"target text 3",
]
assert request.seed_tts_speech_extra["ref_text"] == "ref text 0"
assert request.seed_tts_ref_wav_path.endswith("utt000.wav")
def test_seed_tts_eval_expands_grouped_pcm_by_turn():
from vllm_omni.benchmarks.data_modules.seed_tts_eval import (
_expand_seed_tts_turn_outputs,
)
request = SeedTTSSampleRequest(
prompt="target 0",
prompt_len=4,
expected_output_len=100,
multi_modal_data=None,
request_id="session-0",
seed_tts_turns=tuple(
SeedTTSTurn(utterance_id=f"utt-{index}", target_text=f"target {index}") for index in range(4)
),
)
output = types.SimpleNamespace(
success=True,
tts_turn_pcm_bytes=[bytes([index]) for index in range(4)],
)
requests, outputs = _expand_seed_tts_turn_outputs([request], [output])
assert [item.prompt for item in requests] == [f"target {index}" for index in range(4)]
assert [item.seed_tts_utterance_id for item in requests] == [f"utt-{index}" for index in range(4)]
assert [item.tts_output_pcm_bytes for item in outputs] == [bytes([index]) for index in range(4)]
def test_seed_tts_eval_keeps_session_pcm_for_single_turn_chat():
"""Chat-omni Seed-TTS sets session PCM only; expand must not wipe it."""
from vllm_omni.benchmarks.data_modules.seed_tts_eval import (
_expand_seed_tts_turn_outputs,
)
request = SeedTTSSampleRequest(
prompt="target 0",
prompt_len=4,
expected_output_len=100,
multi_modal_data=None,
request_id="session-0",
seed_tts_turns=(SeedTTSTurn(utterance_id="utt-0", target_text="target 0"),),
)
pcm = b"\x01\x02\x03\x04"
output = types.SimpleNamespace(
success=True,
tts_output_pcm_bytes=pcm,
tts_turn_pcm_bytes=None,
)
requests, outputs = _expand_seed_tts_turn_outputs([request], [output])
assert len(requests) == 1
assert requests[0].prompt == "target 0"
assert outputs[0].tts_output_pcm_bytes == pcm
def test_seed_tts_text_dataset_omits_ref_audio(seed_tts_root, mock_tokenizer):
ds = SeedTTSTextDataset(
dataset_path=str(seed_tts_root),
+30 -1
View File
@@ -2206,6 +2206,24 @@ class TestBaseConfigInheritance:
class TestPlatformOverrides:
"""Test platform-specific deploy config overrides."""
def test_qwen3_omni_musa_clears_parent_quantization_for_audio_stages(self):
pipeline = resolve_pipeline_config(
"qwen3_omni_moe",
Q3_OMNI_ALL_STAGES_HF_CONFIG,
)
assert isinstance(pipeline, PipelineConfig)
deploy_path = Path(get_deploy_config_path("qwen3_omni_moe.yaml"))
base_stages = merge_pipeline_deploy(pipeline, load_deploy_config(deploy_path))
assert "hf_overrides" not in base_stages[1].yaml_engine_args
assert "hf_overrides" not in base_stages[2].yaml_engine_args
musa = _apply_platform_overrides(load_deploy_config(deploy_path), platform="musa")
musa_stages = merge_pipeline_deploy(pipeline, musa)
expected = {"quantization_config": None}
assert musa_stages[1].yaml_engine_args["hf_overrides"] == expected
assert musa_stages[2].yaml_engine_args["hf_overrides"] == expected
def test_qwen3_tts_rocm_disables_code2wav_outer_cudagraph(self):
deploy_path = Path(get_deploy_config_path("qwen3_tts.yaml"))
@@ -2241,7 +2259,8 @@ class TestPlatformOverrides:
load_deploy_config(Path(get_deploy_config_path(filename))), platform="cuda"
)
replica_stages = merge_pipeline_deploy(pipeline, replica)
assert replica_stages[1].yaml_engine_args["kv_cache_memory_bytes"] is None
# Explicit null clears the inherited single-GPU 2 GiB CUDA cap.
assert replica_stages[1].yaml_engine_args.get("kv_cache_memory_bytes") is None
def test_npu_overrides(self):
deploy_path = Path(get_deploy_config_path("qwen3_omni_moe.yaml"))
@@ -2257,6 +2276,16 @@ class TestPlatformOverrides:
# Stage 2 unaffected fields stay at base
assert deploy.stages[2].enforce_eager is False
def test_qwen2_5_omni_xpu_uses_eager_ar_stages(self):
deploy_path = Path(get_deploy_config_path("qwen2_5_omni.yaml"))
deploy = load_deploy_config(deploy_path)
deploy = _apply_platform_overrides(deploy, platform="xpu")
assert deploy.stages[0].enforce_eager is True
assert deploy.stages[1].enforce_eager is True
assert deploy.stages[2].enforce_eager is True
def test_xpu_overrides(self):
deploy_path = Path(get_deploy_config_path("qwen3_omni_moe.yaml"))
if not deploy_path.exists():
+2
View File
@@ -474,6 +474,8 @@ def test_sub_config_fields_match_structured_scopes():
"moe_backend",
"hf_overrides",
"limit_mm_per_prompt",
"interleave_mm_strings",
"media_io_kwargs",
"active_stream_window",
"duplex_max_sessions",
"enable_sleep_mode",
@@ -92,6 +92,9 @@ class _Request:
self.num_computed_tokens = 0
self.num_in_flight_tokens = 0
self.num_output_placeholders = 0
# vLLM 0.27 (a0c092ee72): Request gained num_stale_output_tokens to
# track in-flight outputs discarded at preemption/streaming-stop.
self.num_stale_output_tokens = 0
self.has_encoder_inputs = False
self.pooling_params = None
self.resumable = True
@@ -0,0 +1,223 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Investigation tests for the num_stale_output_tokens drain (59b9e719).
The seed-TTS WER regression appeared with the vLLM 0.27 rebase (nightly
2946 -> 2949: mean WER 0.2695 -> 0.3936, median 0.0 in both), whose prime
suspect is the reimplemented async-token discard: vLLM 0.26 drained
``async_tokens_to_discard`` one arriving frame at a time and suppressed
only the token append; the 0.27 adaptation seeds
``num_stale_output_tokens`` with the discarded *placeholder count* and
drains it by each arriving frame's ``num_tokens_scheduled``, dropping the
whole per-request update. Whenever seed and arrivals disagree, the counter
outlives the old segment and the drain consumes *valid* frames of the new
segment — a hole in the talker's codec stream (garbled audio, text/audio
mismatch) — or underflows its own assert on the new segment's prefill
frame (engine-core death mid-step).
The fix seeds the counter from ``num_in_flight_tokens`` — the same
scheduled-token units the drain subtracts — so every pre-discard frame
drains exactly its own contribution and new-segment frames can never be
swallowed. These tests pin that contract one frame at a time; the
delivered / swallowed observable is whether ``_update_request_with_output``
runs for the frame.
"""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
# Imports must run in this order: vllm_omni applies patches to vllm.v1.request
# before Request / StreamingUpdate are bound in this module.
# isort: off
import vllm_omni # noqa: F401 - import for side effects (patch vLLM)
from vllm.sampling_params import SamplingParams
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.outputs import ModelRunnerOutput
from vllm.v1.request import Request, RequestStatus, StreamingUpdate
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
# isort: on
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_session() -> Request:
session = Request(
request_id="req-stale-drain-test",
prompt_token_ids=[1, 2, 3],
sampling_params=SamplingParams(max_tokens=32),
pooling_params=None,
arrival_time=100.0,
block_hasher=None,
)
session.status = RequestStatus.RUNNING
if not hasattr(session, "num_stale_output_tokens"):
# vLLM 0.27 defines this Request field; 0.26 (local dev) does not.
session.num_stale_output_tokens = 0
if not hasattr(session, "num_in_flight_tokens"):
session.num_in_flight_tokens = 0
return session
def _replace_streaming_session(session: Request) -> None:
"""Run the real stage-0 streaming replacement (the discard/seed site)."""
sched = OmniARScheduler.__new__(OmniARScheduler)
sched._new_prompt_len_snapshot = {}
sched.vllm_config = SimpleNamespace(model_config=SimpleNamespace(stage_id=0))
sched.num_waiting_for_streaming_input = 0
sched.log_stats = False
sched.chunk_transfer_adapter = None
sched.skipped_waiting = set()
session.status = RequestStatus.WAITING_FOR_STREAMING_REQ
update = StreamingUpdate(
mm_features=None,
prompt_token_ids=[10, 20],
max_tokens=32,
arrival_time=200.0,
sampling_params=SamplingParams(max_tokens=16),
)
sched._update_request_as_session(session, update)
session.status = RequestStatus.RUNNING
def _make_drain_sched(session: Request) -> MagicMock:
sched = MagicMock()
sched.requests = {session.request_id: session}
sched.perf_metrics = None
sched.structured_output_manager.should_advance.return_value = False
sched._update_request_with_output.return_value = ([42], False)
sched._process_kv_transfer_trigger.return_value = False
sched.chunk_transfer_adapter = MagicMock()
sched.running = [session]
sched.waiting_for_transfer_free = set()
sched.transfer_triggered_requests = set()
sched.active_kv_transfers = set()
sched.pending_stop_after_extraction = set()
sched.connector = None
sched.kv_cache_manager.take_events.return_value = None
sched.finished_req_ids_dict = {}
sched.make_stats.return_value = None
return sched
def _run_step(sched: MagicMock, session: Request, *, num_scheduled: int, token: int) -> bool:
"""Feed one model-runner frame through update_from_output.
Returns True when the frame's tokens were delivered (the append ran),
False when the frame was swallowed by the stale drain.
"""
scheduler_output = MagicMock(spec=SchedulerOutput)
scheduler_output.num_scheduled_tokens = {session.request_id: num_scheduled}
scheduler_output.scheduled_spec_decode_tokens = {}
scheduler_output.num_invalid_spec_tokens = 0
model_runner_output = MagicMock(spec=ModelRunnerOutput)
model_runner_output.sampled_token_ids = [[token]]
model_runner_output.logprobs = None
model_runner_output.prompt_logprobs_dict = {}
model_runner_output.pooler_output = None
model_runner_output.num_nans_in_logits = None
model_runner_output.kv_connector_output = None
model_runner_output.cudagraph_stats = None
model_runner_output.req_id_to_index = {session.request_id: 0}
model_runner_output.routed_experts = None
model_runner_output.inter_stage_outputs = None
sched._update_request_with_output.reset_mock()
OmniARScheduler.update_from_output(sched, scheduler_output, model_runner_output)
return sched._update_request_with_output.called
def test_exact_drain_delivers_new_segment_frame() -> None:
"""One in-flight decode frame at replacement: its late output drains the
counter exactly and the new segment's first frame is delivered."""
session = _make_session()
session.append_output_token_ids([7, 8, 9])
session.num_computed_tokens = 6
session.num_output_placeholders = 1
session.num_in_flight_tokens = 1
_replace_streaming_session(session)
assert session.num_stale_output_tokens == 1
sched = _make_drain_sched(session)
assert _run_step(sched, session, num_scheduled=1, token=42) is False # late frame dropped
assert session.num_stale_output_tokens == 0
assert _run_step(sched, session, num_scheduled=1, token=43) is True # new segment survives
def test_two_in_flight_frames_drain_without_swallowing() -> None:
"""Two in-flight decode frames: both late frames drop, the new segment's
first valid frame is delivered. A placeholder-based seed swallowed it
whenever fewer frames arrived than placeholders counted (the driver of
the seed-TTS WER regression, nightly 2946 -> 2949: 0.2695 -> 0.3936)."""
session = _make_session()
session.append_output_token_ids([7, 8, 9])
session.num_computed_tokens = 7
session.num_output_placeholders = 2
session.num_in_flight_tokens = 2
_replace_streaming_session(session)
assert session.num_stale_output_tokens == 2
sched = _make_drain_sched(session)
assert _run_step(sched, session, num_scheduled=1, token=42) is False # late frame 1
assert _run_step(sched, session, num_scheduled=1, token=44) is False # late frame 2
assert session.num_stale_output_tokens == 0
delivered = _run_step(sched, session, num_scheduled=1, token=43) # new segment, frame 1
assert delivered is True, (
"valid new-segment frame was swallowed by leftover stale count "
f"(num_stale_output_tokens left at {session.num_stale_output_tokens})"
)
def test_spec_placeholder_asymmetry_does_not_swallow_valid_frame() -> None:
"""Placeholder counts can exceed scheduled counts (e.g. a draft
invalidated before execution): 1 in-flight MTP frame scheduled 2 tokens
but contributed 3 placeholders. Seeding from num_in_flight_tokens keeps
seed == drain, so the new segment's valid frame is delivered."""
session = _make_session()
session.append_output_token_ids([7])
session.num_computed_tokens = 7
session.num_output_placeholders = 3 # 1 sampled + 2 spec placeholders
session.num_in_flight_tokens = 2 # the frame actually scheduled 1 + 1 tokens
_replace_streaming_session(session)
assert session.num_stale_output_tokens == 2
sched = _make_drain_sched(session)
assert _run_step(sched, session, num_scheduled=2, token=42) is False # late MTP frame
assert session.num_stale_output_tokens == 0
delivered = _run_step(sched, session, num_scheduled=1, token=43) # new segment, frame 1
assert delivered is True, (
"valid new-segment frame swallowed after spec placeholder asymmetry "
f"(num_stale_output_tokens left at {session.num_stale_output_tokens})"
)
def test_in_flight_prefill_chunk_drains_exactly_without_underflow() -> None:
"""An in-flight prefill chunk carries no placeholders but 5 scheduled
tokens. The placeholder-based seed missed it entirely (its late output
was appended to the rolled-back session) and any leftover counter
underflowed 'assert num_stale_output_tokens >= 0' on the chunk's arrival,
killing the engine-core step. Seeding from num_in_flight_tokens drops the
late chunk and drains to exactly zero."""
session = _make_session()
session.append_output_token_ids([7, 8, 9])
session.num_computed_tokens = 7
session.num_output_placeholders = 0 # prefill chunk: no placeholders
session.num_in_flight_tokens = 5 # the chunk's scheduled tokens
_replace_streaming_session(session)
assert session.num_stale_output_tokens == 5
sched = _make_drain_sched(session)
assert _run_step(sched, session, num_scheduled=5, token=42) is False # late prefill chunk dropped
assert session.num_stale_output_tokens == 0
assert _run_step(sched, session, num_scheduled=1, token=43) is True # new segment survives
@@ -126,6 +126,7 @@ class TestFullPayloadCoordinatorSelection(unittest.TestCase):
("AudexCode2Wav", "audex_code2wav"),
("AudexXCodec1", "audex_xcodec"),
("IndexTTS2S2MelDecoder", "indextts2_s2mel_decoder"),
("IndexTTS25S2MelDecoder", "indextts2_5_s2mel_decoder"),
("DyninOmniForConditionalGeneration", "token2image"),
("DyninOmniForConditionalGeneration", "token2audio"),
}
+9
View File
@@ -752,3 +752,12 @@ def pytest_addoption(parser: pytest.Parser) -> None:
default=None,
help=("Path to benchmark config JSON. Example: --test-config-file tests/dfx/perf/tests/test_tts.json"),
)
parser.addoption(
"--assert-baseline",
action="store_true",
default=False,
help=(
"When set, omni/diffusion perf runners compare metrics against the baseline block in the JSON config "
"(default: off)."
),
)
+15 -3
View File
@@ -157,8 +157,19 @@ def benchmark_params(request):
}
def assert_result(result, num_prompt) -> None:
def assert_result(result, params, num_prompt) -> None:
assert result["completed"] == num_prompt, "Request failures exist"
expected_audio_turns = params.get("expected_duplex_audio_turns_per_session")
if expected_audio_turns is not None:
session_metrics = result.get("duplex_session_metrics")
assert isinstance(session_metrics, list), "Duplex session metrics are missing"
assert len(session_metrics) == num_prompt, (
f"Expected {num_prompt} duplex session metric rows, got {len(session_metrics)}"
)
assert all(
isinstance(metric, dict) and metric.get("audio_turn_count") == expected_audio_turns
for metric in session_metrics
), f"Not every duplex session emitted {expected_audio_turns} audio turns"
@pytest.mark.benchmark
@@ -212,6 +223,7 @@ def test_performance_benchmark(omni_server, benchmark_params):
"enabled",
"eval_phase",
"trust_remote_code",
"expected_duplex_audio_turns_per_session",
}
for key, value in params.items():
@@ -251,7 +263,7 @@ def test_performance_benchmark(omni_server, benchmark_params):
random_output_len=params.get("random_output_len"),
resource_label=resource_label,
)
assert_result(result, num_prompt)
assert_result(result, params, num_prompt)
# concurrency test
for sweep_index, (concurrency, num_prompt) in enumerate(zip(max_concurrency_list, num_prompt_list)):
@@ -268,4 +280,4 @@ def test_performance_benchmark(omni_server, benchmark_params):
random_output_len=params.get("random_output_len"),
resource_label=resource_label,
)
assert_result(result, num_prompt)
assert_result(result, params, num_prompt)
@@ -0,0 +1,87 @@
[
{
"test_name": "test_minicpmo_4_5",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100",
"npu": "A3"
},
"num_cards": 1
}
},
"full_model",
"omni"
],
"server_params": {
"model": "openbmb/MiniCPM-o-4_5",
"extra_cli_args": [
"--deploy-config",
"vllm_omni/deploy/minicpmo_4_5.yaml",
"--trust-remote-code"
]
},
"benchmark_params": [
{
"dataset_name": "seed-tts",
"dataset_path": "zhaochenyang20/seed-tts-eval",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
32,
64,
128
],
"max_concurrency": [
1,
4,
8
],
"no_oversample": true,
"trust_remote_code": true,
"seed_tts_locale": "en",
"extra_body": {
"modalities": [
"text",
"audio"
],
"chat_template_kwargs": {
"enable_thinking": false,
"use_tts_template": true
}
},
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration",
"baseline": {
"H100": {
"request_throughput": [
0.7803,
1.1008,
1.2027
],
"mean_ttft_ms": [
166.9764,
307.7133,
323.584
],
"mean_e2el_ms": [
1361.1682,
3800.6332,
6961.8636
],
"mean_audio_ttfp_ms": [
635.8099,
1615.6591,
2888.9831
],
"mean_audio_rtf": [
0.3151,
0.8568,
1.5425
]
}
}
}
]
}
]
@@ -0,0 +1,68 @@
[
{
"test_name": "test_minicpmo_4_5_duplex_seed_tts",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100",
"npu": "A3"
},
"num_cards": 1
}
},
"full_model",
"omni"
],
"server_params": {
"model": "openbmb/MiniCPM-o-4_5",
"extra_cli_args": [
"--deploy-config",
"vllm_omni/deploy/minicpmo_4_5_duplex.yaml",
"--trust-remote-code"
]
},
"benchmark_params": [
{
"dataset_name": "seed-tts",
"dataset_path": "zhaochenyang20/seed-tts-eval",
"backend": "openai-realtime-tts",
"endpoint": "/v1/realtime",
"num_prompts": [
1
],
"max_concurrency": [
1
],
"no_oversample": true,
"trust_remote_code": true,
"seed_tts_locale": "en",
"seed_tts_turns_per_session": 4,
"expected_duplex_audio_turns_per_session": 4,
"extra_body": {
"save_duplex_request_metrics": true
},
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration",
"baseline": {
"H100": {
"request_throughput": [
0.1594
],
"mean_ttft_ms": [
274.164
],
"mean_e2el_ms": [
6250.7696
],
"mean_audio_ttfp_ms": [
994.422
],
"mean_audio_rtf": [
0.46163
]
}
}
}
]
}
]
@@ -485,3 +485,30 @@ def test_diffusion_build_run_params_resolves_baseline_per_sweep(tmp_path, monkey
assert sweeps[2]["params"]["baseline"]["H100"]["throughput_qps"] == 0.3
assert "A3" in sweeps[2]["params"]["baseline"]
assert not isinstance(sweeps[2]["params"]["baseline"]["H100"]["throughput_qps"], list)
def test_omni_duplex_expected_audio_turns_accepts_complete_session():
from tests.dfx.perf.scripts.run_benchmark import assert_result
assert_result(
{
"completed": 1,
"duplex_session_metrics": [{"audio_turn_count": 4}],
},
{"expected_duplex_audio_turns_per_session": 4},
1,
)
def test_omni_duplex_expected_audio_turns_rejects_incomplete_session():
from tests.dfx.perf.scripts.run_benchmark import assert_result
with pytest.raises(AssertionError, match="emitted 4 audio turns"):
assert_result(
{
"completed": 1,
"duplex_session_metrics": [{"audio_turn_count": 3}],
},
{"expected_duplex_audio_turns_per_session": 4},
1,
)
@@ -50,12 +50,12 @@ def _prompt() -> str:
return "What is the capital of China? Answer in 20 words."
@hardware_test(res={"cuda": "H100", "npu": "A2"}, num_cards=1)
@hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1)
@pytest.mark.parametrize("omni_server", _MINICPMO_SERVER, indirect=True)
def test_invalid_audio_format_rejected(omni_server: OmniServerParams, openai_client: OpenAIClientHandler) -> None:
"""
Test that invalid audio format is properly rejected.
Deploy Setting: default 2GPU
Deploy Setting: default single GPU
Input Modal: text + invalid audio format
Output Modal: error
"""
@@ -315,11 +315,32 @@ def _assert_post_fault_health_terminal(host: str, port: int, *, scenario: str) -
pytest.fail(f"[{scenario} health] no terminal post-fault health observed: {last_observation}")
def _assert_server_alive_after_fault(host: str, port: int, *, scenario: str) -> None:
"""#4285: a single-stage engine death must NOT take down the API server.
The process must stay up and keep answering /health (503 is acceptable —
errored but listening). A connection-refused means the server process
exited, which is the bug this fix closes.
"""
deadline = time.monotonic() + 15.0
responded = False
while time.monotonic() < deadline:
try:
status, _ = get_health_raw(host, port, timeout_sec=5)
responded = True
assert status in (200, 503), f"[{scenario} alive] unexpected /health status={status}"
except Exception as exc: # noqa: BLE001
if _looks_like_server_unreachable(exc):
pytest.fail(f"[{scenario} alive] server exited after single-stage OOM (#4285): {exc!r}")
raise
time.sleep(0.5)
assert responded, f"[{scenario} alive] /health never responded after fault"
QWEN_PARAMS = create_reliability_omni_server_params(RELIABILITY_SCENARIOS, DEPLOY_CONFIGS_DIR)
@pytest.mark.slow
@pytest.mark.skip(reason="issue#4285")
@hardware_test(res={"cuda": "H100"}, num_cards=2)
@pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True)
def test_reliability_fault_gpu_oom_error_contract_consistent_chat_speech(
@@ -386,9 +407,11 @@ def test_reliability_fault_gpu_oom_error_contract_consistent_chat_speech(
assert "code" in chat_error, f"chat error lacks code field: {chat_error!r}"
assert "code" in speech_error, f"speech error lacks code field: {speech_error!r}"
# #4285: a single dead stage must not take the whole API server down.
_assert_server_alive_after_fault(host, port, scenario="oom_contract")
@pytest.mark.slow
@pytest.mark.skip(reason="issue#4285")
@hardware_test(res={"cuda": "H100"}, num_cards=2)
@pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True)
def test_reliability_fault_gpu_oom_chat_large_payload_failure(omni_server_function, openai_client_function) -> None:
@@ -431,7 +454,6 @@ def test_reliability_fault_gpu_oom_chat_large_payload_failure(omni_server_functi
@pytest.mark.slow
@pytest.mark.skip(reason="issue#4285")
@hardware_test(res={"cuda": "H100"}, num_cards=2)
@pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True)
def test_reliability_fault_gpu_oom_concurrent_pressure_failure(omni_server_function, openai_client_function) -> None:
@@ -774,7 +796,11 @@ def test_reliability_fault_process_kill_tree_with_load_fast_fail_and_cleanup(
@pytest.mark.slow
@pytest.mark.skip(reason="issue#4285")
@pytest.mark.skip(
reason="needs request-scoped OOM recovery: under transient pressure the stage "
"EngineCore still dies and is evicted (not respawned), so the engine does not "
"recover after the hog stops. Out of scope for the #4285 fault-isolation fix."
)
@hardware_test(res={"cuda": "H100"}, num_cards=2)
@pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True)
def test_reliability_fault_gpu_oom_state_converges_after_fault_removed(
@@ -9,6 +9,10 @@ This script tests two main scenarios:
2. Case 2: Comparing FlashAttention and SDPA backends for batch_size=2 with padding
"""
import sys
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import torch
@@ -371,6 +375,264 @@ def test_packed_varlen_metadata_must_be_complete(monkeypatch):
impl.forward_cuda(query, query, query, metadata)
# ---------------------------------------------------------------------------
# NPU mask-free packed attention (PR #5891).
#
# These tests cover the backend-side NPU branches that ``forward_fa_npu`` adds:
# - boundary resolution in ``_resolve_packed_seq_npu`` (pure Python, no device
# sync, no MindIE-SD import);
# - env dispatch in ``forward_fa_npu`` (which op a packed forward takes);
# - the laser input pre-scaling math in ``_forward_prefix_kv_slice_npu``.
#
# They run on CPU without a real NPU by injecting a fake ``mindiesd`` module
# into ``sys.modules`` (same pattern as ``test_paged_attention`` /
# ``benchmarks/conftest``). No production code is exercised for real kernels.
def _npu_impl(causal: bool = False) -> FlashAttentionImpl:
return FlashAttentionImpl(num_heads=2, head_size=4, softmax_scale=0.5, causal=causal)
def _fake_mindiesd(monkeypatch, *, attention_forward=None, attention_forward_varlen=None):
"""Install a stub ``mindiesd`` so local ``from mindiesd import ...`` works.
Restored automatically by monkeypatch. Defaults are no-op callables so the
top-level import in ``forward_fa_npu`` succeeds even when the test mocks the
instance methods that would otherwise call them.
"""
fake = SimpleNamespace(
attention_forward=attention_forward or Mock(return_value=torch.zeros(1)),
attention_forward_varlen=attention_forward_varlen or Mock(return_value=torch.zeros(1)),
)
monkeypatch.setitem(sys.modules, "mindiesd", fake)
return fake
# --- Test group A: boundary resolution (_resolve_packed_seq_npu) -------------
def test_resolve_packed_seq_accepts_real_plus_pad_contract():
impl = _npu_impl()
q = torch.randn(1, 8, 2, 4)
cu = torch.tensor([0, 5, 8], dtype=torch.int32)
extra = {"cu_seqlens_q": cu, "cu_seqlens_k": cu, "max_seqlen_q": 5, "max_seqlen_k": 5}
assert impl._resolve_packed_seq_npu(q, q, extra) == ([5, 8], [5, 8])
def test_resolve_packed_seq_single_document_no_padding():
impl = _npu_impl()
q = torch.randn(1, 8, 2, 4)
cu = torch.tensor([0, 8], dtype=torch.int32)
extra = {"cu_seqlens_q": cu, "cu_seqlens_k": cu, "max_seqlen_q": 8, "max_seqlen_k": 8}
assert impl._resolve_packed_seq_npu(q, q, extra) == ([8], [8])
@pytest.mark.parametrize(
"case",
[
"causal",
"missing_key",
"batch_ne_1",
"cu_too_long",
"cu_qk_mismatch",
"max_seqlen_not_int",
"used_gt_total",
"used_zero",
"pad_longer_than_real",
],
)
def test_resolve_packed_seq_rejects_malformed_metadata(case):
"""Any metadata outside the exact [real, pad] contract returns None so the
caller falls back to the masked path."""
cu3 = torch.tensor([0, 5, 8], dtype=torch.int32)
cu2 = torch.tensor([0, 8], dtype=torch.int32)
cu4 = torch.tensor([0, 3, 5, 8], dtype=torch.int32)
cu_short = torch.tensor([0, 3, 8], dtype=torch.int32)
base = {"cu_seqlens_q": cu3, "cu_seqlens_k": cu3, "max_seqlen_q": 5, "max_seqlen_k": 5}
q = torch.randn(1, 8, 2, 4)
impl = _npu_impl(causal=(case == "causal"))
if case == "missing_key":
extra = {"cu_seqlens_q": cu3, "max_seqlen_q": 5} # 3 of 4 keys absent
elif case == "batch_ne_1":
q = torch.randn(2, 8, 2, 4)
extra = base
elif case == "cu_too_long":
extra = {**base, "cu_seqlens_q": cu4, "cu_seqlens_k": cu4}
elif case == "cu_qk_mismatch":
extra = {**base, "cu_seqlens_q": cu3, "cu_seqlens_k": cu2}
elif case == "max_seqlen_not_int":
extra = {**base, "max_seqlen_q": 5.0}
elif case == "used_gt_total":
extra = {**base, "max_seqlen_q": 9} # 9 > total length 8
elif case == "used_zero":
extra = {**base, "max_seqlen_q": 0, "max_seqlen_k": 0}
elif case == "pad_longer_than_real":
# real=3, pad=5: the real document must be the longer one.
extra = {
"cu_seqlens_q": cu_short,
"cu_seqlens_k": cu_short,
"max_seqlen_q": 3,
"max_seqlen_k": 3,
}
else:
extra = base
assert impl._resolve_packed_seq_npu(q, q, extra) is None
# --- Test group B: env dispatch in forward_fa_npu (current behavior) --------
def _npu_impl_with_mocked_paths() -> tuple[FlashAttentionImpl, torch.Tensor]:
"""FlashAttentionImpl whose two packed paths are Mocks returning a sentinel.
Lets a test assert which branch ``forward_fa_npu`` dispatched to without
running any real kernel.
"""
impl = _npu_impl()
sentinel = torch.tensor([1.0])
impl._forward_varlen_packed_npu = Mock(return_value=sentinel)
impl._forward_prefix_kv_slice_npu = Mock(return_value=sentinel)
return impl, sentinel
def test_npu_env_unset_uses_varlen(monkeypatch):
monkeypatch.delenv("MINDIE_SD_FA_TYPE", raising=False)
_fake_mindiesd(monkeypatch)
impl, _ = _npu_impl_with_mocked_paths()
metadata = AttentionMetadata(extra={"npu_attn_varlen": True})
impl.forward_fa_npu(torch.randn(1, 8, 2, 4), *[torch.randn(1, 8, 2, 4)] * 2, metadata)
impl._forward_varlen_packed_npu.assert_called_once()
impl._forward_prefix_kv_slice_npu.assert_not_called()
def test_npu_env_laser_uses_slice(monkeypatch):
monkeypatch.setenv("MINDIE_SD_FA_TYPE", "ascend_laser_attention")
_fake_mindiesd(monkeypatch)
impl, _ = _npu_impl_with_mocked_paths()
metadata = AttentionMetadata(extra={"npu_attn_varlen": True})
impl.forward_fa_npu(torch.randn(1, 8, 2, 4), *[torch.randn(1, 8, 2, 4)] * 2, metadata)
impl._forward_prefix_kv_slice_npu.assert_called_once()
impl._forward_varlen_packed_npu.assert_not_called()
@pytest.mark.parametrize("fa_type", ["prompt_flash_attn", "fused_attn_score", "ascend_laser_attenton", "garbage"])
def test_npu_env_non_laser_value_falls_back_to_varlen(monkeypatch, fa_type):
"""Locks in the CURRENT dispatch behavior (PR #5891).
Any value other than ``ascend_laser_attention`` — including the valid
``prompt_flash_attn``/``fused_attn_score`` and typos — is silently routed to
the TND varlen op, which cannot honor ``MINDIE_SD_FA_TYPE``. This is review
comment #1 on the PR and is intentionally captured here as a regression
net: update this test when the dispatch starts honoring/rejecting these.
"""
monkeypatch.setenv("MINDIE_SD_FA_TYPE", fa_type)
_fake_mindiesd(monkeypatch)
impl, _ = _npu_impl_with_mocked_paths()
metadata = AttentionMetadata(extra={"npu_attn_varlen": True})
impl.forward_fa_npu(torch.randn(1, 8, 2, 4), *[torch.randn(1, 8, 2, 4)] * 2, metadata)
impl._forward_varlen_packed_npu.assert_called_once()
impl._forward_prefix_kv_slice_npu.assert_not_called()
def test_npu_varlen_opt_in_unset_takes_mask_path(monkeypatch):
"""Models that do not set ``npu_attn_varlen`` (e.g. Wan/Cosmos) keep using
the existing masked attention_forward path; the new branches are skipped."""
fake_forward = Mock(return_value=torch.zeros(1, 8, 2, 4))
_fake_mindiesd(monkeypatch, attention_forward=fake_forward)
impl, _ = _npu_impl_with_mocked_paths()
metadata = AttentionMetadata(
attn_mask=torch.tensor([[True, True, True, True, True, False, False, False]]),
extra={}, # no npu_attn_varlen opt-in
)
q = torch.randn(1, 8, 2, 4)
out = impl.forward_fa_npu(q, q, q, metadata)
impl._forward_varlen_packed_npu.assert_not_called()
impl._forward_prefix_kv_slice_npu.assert_not_called()
fake_forward.assert_called_once()
assert out is fake_forward.return_value
# --- Test group C: laser input pre-scaling in _forward_prefix_kv_slice_npu --
def test_prefix_kv_slice_applies_laser_input_scaling(monkeypatch):
monkeypatch.setenv("MINDIE_SD_FA_TYPE", "ascend_laser_attention")
captured: dict = {}
def fake_attention_forward(query, key, value, **kwargs):
captured["query"] = query
captured["key"] = key
captured["value"] = value
captured.update(kwargs)
return torch.ones_like(query) # deterministic stand-in kernel output
_fake_mindiesd(monkeypatch, attention_forward=fake_attention_forward)
impl = _npu_impl() # softmax_scale == 0.5
q = torch.randn(1, 8, 2, 4)
cu = torch.tensor([0, 5, 8], dtype=torch.int32)
extra = {
"cu_seqlens_q": cu,
"cu_seqlens_k": cu,
"max_seqlen_q": 5,
"max_seqlen_k": 5,
"laser_input_scale": 256.0,
}
out = impl._forward_prefix_kv_slice_npu(q, q, q, extra)
# K/V sliced to the valid prefix (used_k == 5); q keeps full length.
assert captured["key"].shape == (1, 5, 2, 4)
assert captured["value"].shape == (1, 5, 2, 4)
assert captured["query"].shape == (1, 8, 2, 4)
assert captured["attn_mask"] is None
# Pre-divide q/k/v by the factor and compensate the kernel scale by its
# square (exact for power-of-two); output scaled back by the factor.
torch.testing.assert_close(captured["query"], q / 256.0)
torch.testing.assert_close(captured["key"], q[:, :5] / 256.0)
assert captured["scale"] == pytest.approx(0.5 * 256.0**2)
torch.testing.assert_close(out, torch.ones(1, 8, 2, 4) * 256.0)
def test_prefix_kv_slice_no_scaling_without_factor(monkeypatch):
monkeypatch.setenv("MINDIE_SD_FA_TYPE", "ascend_laser_attention")
captured: dict = {}
def fake_attention_forward(query, key, value, **kwargs):
captured["query"] = query
captured.update(kwargs)
return torch.ones_like(query)
_fake_mindiesd(monkeypatch, attention_forward=fake_attention_forward)
impl = _npu_impl()
q = torch.randn(1, 8, 2, 4)
cu = torch.tensor([0, 5, 8], dtype=torch.int32)
extra = {
"cu_seqlens_q": cu,
"cu_seqlens_k": cu,
"max_seqlen_q": 5,
"max_seqlen_k": 5,
# no laser_input_scale → no pre-scaling
}
out = impl._forward_prefix_kv_slice_npu(q, q, q, extra)
torch.testing.assert_close(captured["query"], q)
assert captured["scale"] == pytest.approx(0.5)
torch.testing.assert_close(out, torch.ones(1, 8, 2, 4))
if __name__ == "__main__":
print("Running FlashAttention Padding Tests...")
print("=" * 60)
@@ -5,10 +5,9 @@
The grids here are the ones MiniMax-H3 FL2VA actually produces for an 8.7s clip:
1280x768 gives a 62x24x40 latent grid whose 59520 video rows land on the 128-row
kernel block, and 1344x768 gives 62x24x42 whose 62496 rows straddle it. Only the
first can run sparse: rf_v2 tiles the sequence as a whole but pools its block
mask from the video and prefix segments separately, so the two only agree on the
block count when the video segment ends on a boundary.
kernel block, and 1344x768 gives 62x24x42 whose 62496 rows do not. Both are
handed to MindIE-SD: its rf_v2 preprocessing promotes an irregular video tail
to the always-kept prefix segment before generating the block mask.
"""
import dataclasses
@@ -57,10 +56,13 @@ def test_block_aligned_video_segment_runs_sparse():
assert plan.latent_shape == list(ALIGNED_GRID)
def test_misaligned_video_segment_stays_dense():
# Realigning the sequence would mean padding it, and rf_v2 ignores attn_mask:
# pad keys would take a share of every real query's softmax denominator.
assert make_impl()._resolve_plan(make_metadata(MISALIGNED_GRID)) is None
def test_misaligned_video_segment_runs_sparse_with_updated_mindiesd():
plan = make_impl()._resolve_plan(make_metadata(MISALIGNED_GRID))
assert plan is not None
assert plan.prefix_len == PREFIX_ROWS
assert plan.used_len == PREFIX_ROWS + 62496
assert plan.latent_shape == list(MISALIGNED_GRID)
@pytest.mark.parametrize("prefix_len", [1, 127, _BLOCK_SIZE, 710, 900])
@@ -155,3 +157,28 @@ def test_fully_populated_mask_reproduces_dense_attention():
).transpose(1, 2)
error = (out.float() - reference.float()).abs().mean() / reference.float().abs().mean()
assert error < 2e-3, f"mean relative error {error:.4%} against dense attention"
@pytest.mark.skipif(not current_omni_platform.is_npu(), reason="rf_v2 runs on Ascend NPU only.")
def test_irregular_video_tail_reproduces_dense_attention_with_updated_mindiesd():
"""vLLM forwards the full irregular grid; MindIE-SD owns its tail handling."""
torch.manual_seed(1)
prefix_len, grid = 384, (4, 16, 10)
video_len = grid[0] * grid[1] * grid[2]
used = prefix_len + video_len
heads, head_dim = 4, 128
q, k, v = (torch.randn(1, used, heads, head_dim, dtype=torch.bfloat16, device="npu") for _ in range(3))
impl = make_impl()
impl.rainfusion = dataclasses.replace(impl.rainfusion, sparsity=0.0)
plan = RainFusionPlan(prefix_len=prefix_len, used_len=used, latent_shape=list(grid))
out = impl._forward_sparse_npu(q, k, v, plan)
reference = torch.nn.functional.scaled_dot_product_attention(
q.transpose(1, 2),
k.transpose(1, 2),
v.transpose(1, 2),
scale=impl.softmax_scale,
).transpose(1, 2)
error = (out.float() - reference.float()).abs().mean() / reference.float().abs().mean()
assert error < 2e-3, f"mean relative error {error:.4%} against dense attention"
+8
View File
@@ -488,6 +488,14 @@ class _MiniMaxH3FakeAttention(nn.Module):
class FakeBackend:
supports_prefix_kv_slicing = False
# Mirror AttentionBackend.supports_packed_mask_free: H3's packed
# attention path calls attn_backend.supports_packed_mask_free() to decide
# whether to build the padding mask. Return False so the fake exercises
# the masked path (the abstract backend default).
@classmethod
def supports_packed_mask_free(cls) -> bool:
return False
def __init__(self, **kwargs):
del kwargs
super().__init__()
@@ -0,0 +1,110 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest
from vllm_omni.diffusion.diffusion_kv.metadata import (
DiffusionKVContextMetadata,
DiffusionKVMetadata,
DiffusionKVSequenceMetadata,
)
pytestmark = [pytest.mark.diffusion, pytest.mark.core_model, pytest.mark.cpu]
def test_diffusion_kv_metadata_uses_native_cache_group_block_ids() -> None:
context = DiffusionKVContextMetadata(
context_id="text",
cache_role="cross_attention",
num_tokens=3,
block_ids=([7], [11]),
)
sequence = DiffusionKVSequenceMetadata(
sequence_id=1,
prefix_len=4,
target_len=2,
seq_len=8,
block_ids=([1, 2], [5, 6]),
context_ids=(context.context_id,),
)
metadata = DiffusionKVMetadata(
request_id="req-0",
allocation_generation=3,
sequences=(sequence,),
contexts=(context,),
)
assert metadata.sequences[0].block_ids == ([1, 2], [5, 6])
assert metadata.contexts[0].block_ids == ([7], [11])
def test_cfg_sequences_can_share_one_request_context() -> None:
shared_context = DiffusionKVContextMetadata(
context_id="shared-text",
cache_role="cross_attention",
num_tokens=3,
block_ids=([7], [11]),
)
sequences = tuple(
DiffusionKVSequenceMetadata(
sequence_id=sequence_id,
prefix_len=4,
target_len=2,
seq_len=8,
block_ids=([sequence_id + 1],),
context_ids=(shared_context.context_id,),
)
for sequence_id in range(2)
)
metadata = DiffusionKVMetadata(
request_id="req-shared-context",
allocation_generation=1,
sequences=sequences,
contexts=(shared_context,),
)
assert metadata.contexts == (shared_context,)
assert metadata.sequences[0].context_ids == (shared_context.context_id,)
assert metadata.sequences[1].context_ids == (shared_context.context_id,)
def test_cfg_sequences_can_reference_branch_specific_contexts() -> None:
conditional_context = DiffusionKVContextMetadata(
context_id="conditional-text",
cache_role="cross_attention",
num_tokens=3,
block_ids=([7], [11]),
)
unconditional_context = DiffusionKVContextMetadata(
context_id="unconditional-text",
cache_role="cross_attention",
num_tokens=3,
block_ids=([8], [12]),
)
metadata = DiffusionKVMetadata(
request_id="req-branch-contexts",
allocation_generation=1,
sequences=(
DiffusionKVSequenceMetadata(
sequence_id=0,
prefix_len=4,
target_len=2,
seq_len=8,
block_ids=([1],),
context_ids=(conditional_context.context_id,),
),
DiffusionKVSequenceMetadata(
sequence_id=1,
prefix_len=4,
target_len=2,
seq_len=8,
block_ids=([2],),
context_ids=(unconditional_context.context_id,),
),
),
contexts=(conditional_context, unconditional_context),
)
assert metadata.sequences[0].context_ids == (conditional_context.context_id,)
assert metadata.sequences[1].context_ids == (unconditional_context.context_id,)
@@ -0,0 +1,290 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from vllm_omni.diffusion.data import DiffusionOutput
from vllm_omni.diffusion.diffusion_kv.config import DiffusionKVCacheMode
from vllm_omni.diffusion.diffusion_kv.metadata import (
DiffusionKVMetadata,
DiffusionKVSequenceMetadata,
)
from vllm_omni.diffusion.executor.multiproc_executor import MultiprocDiffusionExecutor
from vllm_omni.diffusion.sched.interface import (
CachedRequestData,
DiffusionSchedulerOutput,
NewRequestData,
)
from vllm_omni.diffusion.worker.diffusion_model_runner import DiffusionModelRunner
from vllm_omni.diffusion.worker.diffusion_worker import DiffusionWorker, WorkerWrapperBase
pytestmark = [pytest.mark.diffusion, pytest.mark.core_model, pytest.mark.cpu]
def make_metadata(request_id: str = "req-0") -> DiffusionKVMetadata:
return DiffusionKVMetadata(
request_id=request_id,
allocation_generation=1,
sequences=(
DiffusionKVSequenceMetadata(
sequence_id=0,
prefix_len=4,
target_len=2,
seq_len=8,
block_ids=([1, 2],),
),
),
)
def make_runner(mode: DiffusionKVCacheMode) -> DiffusionModelRunner:
runner = object.__new__(DiffusionModelRunner)
runner.od_config = SimpleNamespace(diffusion_kv_mode=mode)
return runner
def make_scheduler_output(*new_reqs: NewRequestData) -> DiffusionSchedulerOutput:
return DiffusionSchedulerOutput(
step_id=0,
scheduled_new_reqs=list(new_reqs),
scheduled_cached_reqs=CachedRequestData.make_empty(),
finished_req_ids=set(),
num_running_reqs=len(new_reqs),
num_waiting_reqs=0,
)
def make_executor() -> tuple[MultiprocDiffusionExecutor, list[tuple]]:
executor = object.__new__(MultiprocDiffusionExecutor)
executor.od_config = SimpleNamespace(
enable_distributed_layerwise_offload=False,
dlo_use_allgather=True,
)
executor._ensure_open = lambda: None
calls: list[tuple] = []
def collective_rpc(method, *, args, unique_reply_rank, exec_all_ranks):
calls.append((method, args, unique_reply_rank, exec_all_ranks))
return DiffusionOutput(output=None)
executor.collective_rpc = collective_rpc
return executor, calls
def test_new_request_data_carries_scheduler_allocation_atomically() -> None:
req = SimpleNamespace(request_id="req-0")
state = SimpleNamespace(request_id="req-0", req=req)
metadata = make_metadata()
new_req = NewRequestData.from_state(state, diffusion_kv_metadata=metadata)
assert new_req.req is req
assert new_req.diffusion_kv_metadata is metadata
def test_paged_request_without_metadata_reaches_request_forward() -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
expected_output = DiffusionOutput(output=None)
runner._execute_request_list = Mock(
return_value=SimpleNamespace(
runner_outputs=[SimpleNamespace(result=expected_output)],
)
)
req = SimpleNamespace(request_id="req-0")
assert runner.execute_model(req) is expected_output
runner._execute_request_list.assert_called_once()
def test_dense_request_rejects_metadata_before_request_forward() -> None:
runner = make_runner(DiffusionKVCacheMode.DENSE_LEGACY)
runner._execute_request_list = Mock()
with pytest.raises(ValueError, match="dense_legacy.*must not carry"):
runner.execute_model(
SimpleNamespace(request_id="req-0"),
diffusion_kv_metadata=make_metadata(),
)
runner._execute_request_list.assert_not_called()
def test_paged_request_rejects_mismatched_metadata_identity() -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
runner._execute_request_list = Mock()
with pytest.raises(ValueError, match="request mismatch"):
runner.execute_model(
SimpleNamespace(request_id="req-0"),
diffusion_kv_metadata=make_metadata("stale"),
)
runner._execute_request_list.assert_not_called()
def test_request_rpc_rejects_envelope_request_identity_mismatch() -> None:
executor, calls = make_executor()
req = SimpleNamespace(request_id="actual-request-id")
new_req = NewRequestData(
request_id="envelope-id",
req=req,
diffusion_kv_metadata=make_metadata("envelope-id"),
)
with pytest.raises(ValueError, match="request identity mismatch"):
executor.execute_request(make_scheduler_output(new_req))
assert calls == []
def test_batch_path_allows_missing_metadata_before_forward() -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
expected_output = object()
runner._execute_request_list = Mock(return_value=expected_output)
new_req = NewRequestData(
request_id="req-0",
req=SimpleNamespace(request_id="req-0"),
)
assert runner.execute_model_batch(make_scheduler_output(new_req), runner.od_config) is expected_output
runner._execute_request_list.assert_called_once()
def test_batch_path_rejects_envelope_request_identity_mismatch() -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
runner._execute_request_list = Mock()
new_req = NewRequestData(
request_id="envelope-id",
req=SimpleNamespace(request_id="actual-request-id"),
diffusion_kv_metadata=make_metadata("envelope-id"),
)
with pytest.raises(ValueError, match="request identity mismatch"):
runner.execute_model_batch(make_scheduler_output(new_req), runner.od_config)
runner._execute_request_list.assert_not_called()
def test_step_path_allows_missing_metadata_before_step_support_check() -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
runner.pipeline = object()
runner._supports_step_mode = Mock(return_value=False)
new_req = NewRequestData(
request_id="req-0",
req=SimpleNamespace(request_id="req-0"),
)
with pytest.raises(ValueError, match="does not support step execution"):
runner.execute_stepwise(make_scheduler_output(new_req))
runner._supports_step_mode.assert_called_once()
def test_step_path_rejects_envelope_request_identity_mismatch() -> None:
runner = make_runner(DiffusionKVCacheMode.PAGED_SCHEDULER)
runner.pipeline = object()
runner._supports_step_mode = Mock()
new_req = NewRequestData(
request_id="envelope-id",
req=SimpleNamespace(request_id="actual-request-id"),
diffusion_kv_metadata=make_metadata("envelope-id"),
)
with pytest.raises(ValueError, match="request identity mismatch"):
runner.execute_stepwise(make_scheduler_output(new_req))
runner._supports_step_mode.assert_not_called()
def test_dense_request_rpc_keeps_legacy_positional_shape() -> None:
executor, calls = make_executor()
prepared_layout = object()
req = SimpleNamespace(request_id="req-0", prepared_layout=prepared_layout)
new_req = NewRequestData(request_id="req-0", req=req)
result = executor.execute_request(make_scheduler_output(new_req))
assert result.request_ids == ["req-0"]
assert calls == [
(
"execute_model",
(req, executor.od_config, None),
0,
True,
)
]
assert calls[0][1][0].prepared_layout is prepared_layout
def test_request_rpc_sends_attached_metadata_for_only_that_request() -> None:
executor, calls = make_executor()
req_0 = SimpleNamespace(request_id="req-0")
req_1 = SimpleNamespace(request_id="req-1")
metadata_0 = make_metadata("req-0")
metadata_1 = make_metadata("req-1")
new_reqs = (
NewRequestData(request_id="req-0", req=req_0, diffusion_kv_metadata=metadata_0),
NewRequestData(request_id="req-1", req=req_1, diffusion_kv_metadata=metadata_1),
)
result = executor.execute_request(make_scheduler_output(*new_reqs))
assert result.request_ids == ["req-0", "req-1"]
assert [call[1] for call in calls] == [
(req_0, executor.od_config, None, metadata_0),
(req_1, executor.od_config, None, metadata_1),
]
def test_worker_wrapper_only_extends_paged_request_rpc() -> None:
calls: list[tuple] = []
class Worker:
def execute_model(self, req, od_config, **kwargs):
calls.append((req, od_config, kwargs))
return DiffusionOutput(output=None)
wrapper = object.__new__(WorkerWrapperBase)
wrapper.worker = Worker()
req = SimpleNamespace(request_id="req-0")
od_config = SimpleNamespace()
metadata = make_metadata()
wrapper.execute_model(req, od_config)
wrapper.execute_model(req, od_config, diffusion_kv_metadata=metadata)
assert calls == [
(req, od_config, {"kv_prefetch_job": None}),
(
req,
od_config,
{
"kv_prefetch_job": None,
"diffusion_kv_metadata": metadata,
},
),
]
def test_dense_worker_call_does_not_extend_model_runner_signature() -> None:
calls: list[tuple] = []
class DenseModelRunner:
def execute_model(self, req, kv_prefetch_job=None):
calls.append((req, kv_prefetch_job))
return DiffusionOutput(output=None)
worker = object.__new__(DiffusionWorker)
worker.model_runner = DenseModelRunner()
worker.lora_manager = None
worker._get_profiler = lambda: None
req = SimpleNamespace(request_id="req-0")
worker.execute_model(req, SimpleNamespace())
assert calls == [(req, None)]
@@ -64,6 +64,7 @@ def test_moe_ep_maps_diffusion_sp_cfg_dp_to_vllm_groups(monkeypatch):
local_rank,
backend,
group_name,
use_all2all=False,
):
del backend
group = _FakeGroup(
@@ -73,6 +74,7 @@ def test_moe_ep_maps_diffusion_sp_cfg_dp_to_vllm_groups(monkeypatch):
device_communicator=object(),
reduce_scatter=lambda tensor, **kwargs: tensor,
)
group.use_all2all = use_all2all
created_groups.append(group)
return group
@@ -121,6 +123,9 @@ def test_moe_ep_maps_diffusion_sp_cfg_dp_to_vllm_groups(monkeypatch):
assert parallel_state.vllm_parallel_state._PCP.device_communicator is not None
assert parallel_state.vllm_parallel_state._DP.device_communicator is not None
assert parallel_state.vllm_parallel_state._EP.device_communicator is not None
# vLLM 0.27's MoE oracle asserts the EP communicator has an all2all
# manager, which vLLM only builds for groups created with use_all2all.
assert parallel_state.vllm_parallel_state._EP.use_all2all is True
assert hasattr(parallel_state.vllm_parallel_state._PCP, "reduce_scatter")
assert hasattr(parallel_state.vllm_parallel_state._DP, "reduce_scatter")
assert hasattr(parallel_state.vllm_parallel_state._EP, "reduce_scatter")
@@ -313,3 +318,35 @@ def test_non_moe_ep_fails_before_vllm_ep_remap(monkeypatch):
assert parallel_state.vllm_parallel_state._PCP is None
assert parallel_state.vllm_parallel_state._EP is None
assert parallel_state._EXPERT_PARALLEL_GROUP_RANKS is None
@pytest.mark.cpu
@pytest.mark.core_model
def test_init_vllm_group_forwards_use_all2all_only_when_supported(monkeypatch):
"""use_all2all reaches vLLM only on versions whose API accepts it.
vLLM 0.27 builds the EP all2all manager only for groups created with
use_all2all=True (its MoE oracle then asserts the manager exists —
build 2953, HunyuanImage3-DIT accuracy). Older vLLM has no such kwarg,
so forwarding it unconditionally would TypeError at init.
"""
received = {}
def modern(group_ranks, local_rank, backend, group_name=None, use_device_communicator=True, use_all2all=False):
received["modern"] = use_all2all
return object()
monkeypatch.setattr(parallel_state.vllm_parallel_state, "init_model_parallel_group", modern)
parallel_state.init_vllm_model_parallel_group([[0]], 0, "gloo", "ep", use_all2all=True)
assert received["modern"] is True
parallel_state.init_vllm_model_parallel_group([[0]], 0, "gloo", "tp")
assert received["modern"] is False
def legacy(group_ranks, local_rank, backend, group_name=None, use_device_communicator=True):
received["legacy"] = True
return object()
monkeypatch.setattr(parallel_state.vllm_parallel_state, "init_model_parallel_group", legacy)
parallel_state.init_vllm_model_parallel_group([[0]], 0, "gloo", "ep", use_all2all=True)
assert received["legacy"] is True
+3 -3
View File
@@ -110,7 +110,7 @@ class TestFusedMoEFactory:
monkeypatch.setitem(
sys.modules,
"vllm.model_executor.layers.fused_moe",
SimpleNamespace(FusedMoE=mock_vllm_fused_moe),
SimpleNamespace(FusedMoEFactory=mock_vllm_fused_moe),
)
result = fused_moe.FusedMoE(prefix="test", a=1, reduce_results=True)
@@ -139,7 +139,7 @@ class TestFusedMoEFactory:
monkeypatch.setitem(
sys.modules,
"vllm.model_executor.layers.fused_moe",
SimpleNamespace(FusedMoE=mocker.MagicMock(return_value=mock_runner)),
SimpleNamespace(FusedMoEFactory=mocker.MagicMock(return_value=mock_runner)),
)
mocker.patch.object(fused_moe, "current_omni_platform", mocker.MagicMock())
mock_set_num_tokens = mocker.patch.object(fused_moe, "_set_forward_context_num_tokens")
@@ -169,7 +169,7 @@ class TestFusedMoEFactory:
monkeypatch.setitem(
sys.modules,
"vllm.model_executor.layers.fused_moe",
SimpleNamespace(FusedMoE=mocker.MagicMock(return_value=mock_runner)),
SimpleNamespace(FusedMoEFactory=mocker.MagicMock(return_value=mock_runner)),
)
mocker.patch.object(fused_moe, "current_omni_platform", mocker.MagicMock())
mock_set_num_tokens = mocker.patch.object(fused_moe, "_set_forward_context_num_tokens")
+438
View File
@@ -0,0 +1,438 @@
from collections import defaultdict
import pytest
import torch
import torch.nn as nn
from diffusers.loaders.lora_conversion_utils import (
_convert_non_diffusers_qwen_lora_to_diffusers,
)
from pytest_mock import MockerFixture
from torch.testing import assert_close
from vllm_omni.diffusion.lora.loader import (
LoraLoaderMixin,
QwenImageLoraLoaderMixin,
WanLoraLoaderMixin,
_prepare_lora_delta,
_remap_state_dict_keys,
)
HEAD_DIM = 24
RANK = 2
NUM_LAYERS = 10
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class DummyTransformerBlock(nn.Module):
def __init__(
self,
d_model: int,
bias: bool = False,
stacked_params: bool = False,
):
super().__init__()
self.attn = nn.Module()
if stacked_params:
self.attn.to_qkv = nn.Linear(d_model * 3, d_model, bias=bias)
else:
self.attn.to_q = nn.Linear(d_model, d_model, bias=bias)
self.attn.to_k = nn.Linear(d_model, d_model, bias=bias)
self.attn.to_v = nn.Linear(d_model, d_model, bias=bias)
self.attn.to_out = nn.Linear(d_model, d_model, bias=bias)
class DummyTransformer(nn.Module):
def __init__(self, num_layers: int, d_model: int, bias: bool = False, stacked_params: bool = False):
super().__init__()
self.blocks = nn.ModuleList(
[DummyTransformerBlock(d_model, bias=bias, stacked_params=stacked_params) for _ in range(num_layers)]
)
if stacked_params:
self.stacked_params_mapping = [
(".attn.to_qkv", ".attn.to_q", "q"),
(".attn.to_qkv", ".attn.to_k", "k"),
(".attn.to_qkv", ".attn.to_v", "v"),
]
class DummyPipeline(nn.Module, LoraLoaderMixin):
def __init__(self, num_layers: int, d_model: int, bias: bool = False, stacked_params: bool = False):
super().__init__()
self.transformer = DummyTransformer(num_layers, d_model, bias=bias, stacked_params=stacked_params)
# ======================================================
# helper functions for test
# ======================================================
def _filter_parameters(named_params, mode: str = "all"):
if mode == "all":
return named_params
filtered = []
for name, param in named_params:
if name.endswith(mode):
filtered.append((name, param))
return filtered
def assert_params_equal(module, original_params, mode: str = "all"):
for name, param in _filter_parameters(module.named_parameters(), mode):
assert_close(param, original_params[name])
def assert_params_not_equal(module, original_params, mode: str = "all"):
for name, param in _filter_parameters(module.named_parameters(), mode):
assert not torch.allclose(param, original_params[name])
def make_dummy_lora_state_dict(
base_key,
weight_a: torch.Tensor | None = None,
weight_b: torch.Tensor | None = None,
weight_bias: torch.Tensor | None = None,
bias: bool = False,
lora_a_suffix: str = "lora_A.weight",
lora_b_suffix: str = "lora_B.weight",
lora_bias_suffix: str = "bias",
):
"""
Create a dummy LoRA state dict.
Returns:
tuple: (state_dict, expected_delta)
"""
key_a = f"{base_key}.{lora_a_suffix}"
key_b = f"{base_key}.{lora_b_suffix}"
key_bias = f"{base_key}.{lora_bias_suffix}" if bias else None
if weight_a is None:
weight_a = torch.randn(RANK, HEAD_DIM)
if weight_b is None:
weight_b = torch.randn(HEAD_DIM, RANK)
state_dict = {}
expected_delta = None
if bias:
state_dict[key_a] = weight_a
state_dict[key_b] = weight_b
weight_bias = torch.randn(HEAD_DIM)
state_dict[key_bias] = weight_bias
expected_delta = weight_bias
else:
state_dict[key_a] = weight_a
state_dict[key_b] = weight_b
expected_delta = weight_b @ weight_a
return state_dict, expected_delta
def make_lora_state_dict_for_module(
module,
lora_rank=RANK,
bias=False,
lora_a_suffix: str = "lora_A.weight",
lora_b_suffix: str = "lora_B.weight",
lora_bias_suffix: str = "bias",
prefix: str = "transformer",
remap_proj_out: bool = True,
):
param_to_weight_names = defaultdict(list)
if hasattr(module, "stacked_params_mapping"):
for param_name, weight_name, _ in module.stacked_params_mapping:
param_to_weight_names[param_name].append(weight_name)
state_dict = {}
for name, param in module.named_parameters(prefix=prefix):
if name.endswith(".weight"):
base_key = name[: -len(".weight")]
if remap_proj_out and base_key.endswith(".to_out"):
base_key = base_key.replace(".to_out", ".to_out.0")
d_in, d_out = param.shape
is_stacked_param = False
for param_name, weight_names in param_to_weight_names.items():
if param_name not in name:
continue
is_stacked_param = True
target_in = d_in // len(weight_names)
for weight_name in weight_names:
lora_key = base_key.replace(param_name, weight_name)
weight_a = torch.randn(lora_rank, d_out)
weight_b = torch.randn(target_in, lora_rank)
state_dict[f"{lora_key}.{lora_a_suffix}"] = weight_a
state_dict[f"{lora_key}.{lora_b_suffix}"] = weight_b
if is_stacked_param:
continue
weight_a = torch.randn(lora_rank, d_out)
weight_b = torch.randn(d_in, lora_rank)
key_a = f"{base_key}.{lora_a_suffix}"
key_b = f"{base_key}.{lora_b_suffix}"
state_dict[key_a] = weight_a
state_dict[key_b] = weight_b
elif name.endswith(".bias") and bias:
base_key = name[: -len(".bias")]
if remap_proj_out and base_key.endswith(".to_out"):
base_key = base_key.replace(".to_out", ".to_out.0")
d_bias = param.shape[0]
is_stacked_param = False
for param_name, weight_names in param_to_weight_names.items():
if param_name not in name:
continue
is_stacked_param = True
target_bias = d_bias // len(weight_names)
for weight_name in weight_names:
lora_key = base_key.replace(param_name, weight_name)
weight_bias = torch.randn(target_bias)
state_dict[f"{lora_key}.{lora_bias_suffix}"] = weight_bias
if is_stacked_param:
continue
key_bias = f"{base_key}.{lora_bias_suffix}"
state_dict[key_bias] = torch.randn_like(param)
return state_dict
# ======================================================
# Test loader helper functions
# ======================================================
def test_prepare_lora_delta_with_stacked_params():
base_key = "transformer.attn.to_qkv"
sd0, delta0 = make_dummy_lora_state_dict("transformer.attn.to_q")
sd1, delta1 = make_dummy_lora_state_dict("transformer.attn.to_k")
sd2, delta2 = make_dummy_lora_state_dict("transformer.attn.to_v")
lora_state_dict = {}
lora_state_dict.update(sd0)
lora_state_dict.update(sd1)
lora_state_dict.update(sd2)
expected_delta = torch.concat([delta0, delta1, delta2])
stacked_mapping = {".attn.to_qkv": [".attn.to_q", ".attn.to_k", ".attn.to_v"]}
actual_delta, used_keys = _prepare_lora_delta(
lora_state_dict,
base_key,
stacked_mapping,
)
# validate the delta
assert actual_delta is not None
assert_close(actual_delta, expected_delta)
# validate the used keys
key_q_a = "transformer.attn.to_q.lora_A.weight"
key_q_b = "transformer.attn.to_q.lora_B.weight"
key_k_a = "transformer.attn.to_k.lora_A.weight"
key_k_b = "transformer.attn.to_k.lora_B.weight"
key_v_a = "transformer.attn.to_v.lora_A.weight"
key_v_b = "transformer.attn.to_v.lora_B.weight"
for key in [key_q_a, key_q_b, key_k_a, key_k_b, key_v_a, key_v_b]:
assert key in used_keys
def test_remap_state_dict_keys_matched_two():
lora_state_dict, _ = make_dummy_lora_state_dict(
"transformer.attn.to_out.0", bias=True, lora_bias_suffix="lora_B.bias"
)
lora_state_dict = _remap_state_dict_keys(
lora_state_dict,
[(".to_out.0.", ".to_out."), (".lora_B.bias", ".bias")],
)
assert len(lora_state_dict) == 3
assert "transformer.attn.to_out.bias" in lora_state_dict
# ======================================================
# Test LoraLoaderMixin
# ======================================================
class TestLoraLoaderMixin:
@pytest.mark.parametrize(
"bias, stacked_params",
[
(False, False),
(True, False),
(False, True),
(True, True),
],
ids=[
"basic",
"bias",
"stacked_params",
"bias_stacked_params",
],
)
def test_load_lora_into_module(self, bias: bool, stacked_params: bool):
pipeline = DummyPipeline(NUM_LAYERS, HEAD_DIM, bias=bias, stacked_params=stacked_params)
original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()}
lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer, bias=bias)
assert len(lora_state_dict) > 0
# remap the keys to vllm-omni format
lora_state_dict = _remap_state_dict_keys(lora_state_dict, [(".to_out.0.", ".to_out.")])
used_keys = pipeline.load_lora_into_module(lora_state_dict, pipeline.transformer)
# validate the parameters are updated
assert_params_not_equal(pipeline.transformer, original_params)
# validate all keys are used
missing_keys = set(lora_state_dict.keys()) - used_keys
assert len(missing_keys) == 0
def test_unload_module_lora(self):
pipeline = DummyPipeline(NUM_LAYERS, HEAD_DIM)
original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()}
lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer)
# validate the state dict is valid
assert len(lora_state_dict) > 0
# remap the keys to vllm-omni format
lora_state_dict = _remap_state_dict_keys(lora_state_dict, [(".to_out.0.", ".to_out.")])
used_keys = pipeline.load_lora_into_module(lora_state_dict, pipeline.transformer)
# validate the weights are updated
assert_params_not_equal(pipeline.transformer, original_params)
# validate all keys are used
missing_keys = set(lora_state_dict.keys()) - used_keys
assert len(missing_keys) == 0
unload_used_keys = pipeline.unload_module_lora(lora_state_dict, pipeline.transformer)
missing_keys = set(lora_state_dict.keys()) - unload_used_keys
assert len(missing_keys) == 0
assert len(unload_used_keys - used_keys) == 0
# validate the weights are restored
assert_params_equal(pipeline.transformer, original_params)
# ======================================================
# Test QwenImageLoraLoaderMixin
# ======================================================
class DummyQwenImagePipeline(nn.Module, QwenImageLoraLoaderMixin):
def __init__(self, num_layers: int, d_model: int, bias: bool = False):
super().__init__()
self.transformer = DummyTransformer(num_layers, d_model, bias=bias, stacked_params=True)
class TestQwenImageLoraLoaderMixin:
def test_load_lora_weights(self, mocker: MockerFixture):
pipeline = DummyQwenImagePipeline(NUM_LAYERS, HEAD_DIM)
original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()}
lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer)
assert len(lora_state_dict) > 0
mocker.patch(
"vllm_omni.diffusion.lora.loader.get_converter_by_pipeline",
return_value=_convert_non_diffusers_qwen_lora_to_diffusers,
)
pipeline.load_lora_weights(lora_state_dict, "adapter0")
# validate the weights are updated after lora loaded
assert_params_not_equal(pipeline.transformer, original_params)
# validate lora_loaded map is updated after lora loaded
assert "adapter0" in pipeline.lora_loaded
def test_unload_lora_weights(self, mocker: MockerFixture):
pipeline = DummyQwenImagePipeline(NUM_LAYERS, HEAD_DIM)
original_params = {name: param.clone() for name, param in pipeline.transformer.named_parameters()}
lora_state_dict = make_lora_state_dict_for_module(pipeline.transformer)
assert len(lora_state_dict) > 0
mocker.patch(
"vllm_omni.diffusion.lora.loader.get_converter_by_pipeline",
return_value=_convert_non_diffusers_qwen_lora_to_diffusers,
)
pipeline.load_lora_weights(lora_state_dict, "adapter0")
# validate the weights are updated after lora loaded
assert_params_not_equal(pipeline.transformer, original_params)
# validate lora_loaded map is updated after lora loaded
assert "adapter0" in pipeline.lora_loaded
pipeline.unload_lora_weights("adapter0")
# validate the weights are restored after lora unloaded
assert_params_equal(pipeline.transformer, original_params)
# validate lora_loaded map is updated
assert "adapter0" not in pipeline.lora_loaded
def test_load_diffusers_lora_applies_non_unit_alpha(self, mocker: MockerFixture):
pipeline = DummyQwenImagePipeline(1, HEAD_DIM)
to_out = pipeline.transformer.blocks[0].attn.to_out
original_weight = to_out.weight.detach().clone()
base_key = "blocks.0.attn.to_out.0"
lora_a = torch.randn(RANK, HEAD_DIM)
lora_b = torch.randn(HEAD_DIM, RANK)
alpha = torch.tensor(6.0)
state_dict = {
f"{base_key}.lora_A.weight": lora_a,
f"{base_key}.lora_B.weight": lora_b,
f"{base_key}.alpha": alpha,
}
mocker.patch(
"vllm_omni.diffusion.lora.loader.get_converter_by_pipeline",
return_value=_convert_non_diffusers_qwen_lora_to_diffusers,
)
pipeline.load_lora_weights(state_dict, "adapter0")
expected_weight = original_weight + (lora_b @ lora_a) * (alpha.item() / RANK)
assert_close(to_out.weight, expected_weight)
pipeline.unload_lora_weights("adapter0")
assert_close(to_out.weight, original_weight)
# ======================================================
# Test WanLoraLoaderMixin
# ======================================================
class DummyWanPipeline(nn.Module, WanLoraLoaderMixin):
def __init__(self, num_layers: int, d_model: int):
super().__init__()
self.has_transformer_2 = True
self.transformer = DummyTransformer(num_layers, d_model, stacked_params=True)
self.transformer_2 = DummyTransformer(num_layers, d_model, stacked_params=True)
def test_wan_load_lora_weights_targets_both_transformers(mocker: MockerFixture):
pipeline = DummyWanPipeline(NUM_LAYERS, HEAD_DIM)
original_high = {name: param.clone() for name, param in pipeline.transformer.named_parameters()}
original_low = {name: param.clone() for name, param in pipeline.transformer_2.named_parameters()}
high_state_dict = make_lora_state_dict_for_module(pipeline.transformer)
low_state_dict = make_lora_state_dict_for_module(pipeline.transformer_2)
mocker.patch(
"vllm_omni.diffusion.lora.loader._load_lora_state_dict",
side_effect=[high_state_dict, low_state_dict],
)
pipeline.load_lora_weights(["high.safetensors", "low.safetensors"], "adapter0")
assert_params_not_equal(pipeline.transformer, original_high)
assert_params_not_equal(pipeline.transformer_2, original_low)
assert set(pipeline.lora_loaded["adapter0"]) == {"transformer", "transformer_2"}
pipeline.unload_lora_weights("adapter0")
assert_params_equal(pipeline.transformer, original_high)
assert_params_equal(pipeline.transformer_2, original_low)
assert "adapter0" not in pipeline.lora_loaded
@@ -348,3 +348,53 @@ def test_normalize_lingbot_request_model_size_overrides_sampling_dimensions():
num_frames=81,
)
assert (config.height, config.width) == (480, 480)
def test_normalize_lingbot_request_reads_size_from_extra_args():
"""Regression: /v1/videos delivers `extra_params` via sampling.extra_args.
The nightly TI2V case sends `extra_params={"size": "320x192"}` with no
height/width form fields; the size used to be dropped and the video came
out at the 480x480 default (build 2952, LingBot Video MoE Function Test).
"""
config = _normalize(
{
"prompt": "motion",
"modalities": ["video"],
"multi_modal_data": {"image": Image.new("RGB", (320, 192))},
},
num_frames=9,
extra_args={"size": "320x192"},
)
assert config.mode is LingBotGenerationMode.TI2V
assert (config.height, config.width) == (192, 320)
def test_normalize_lingbot_request_extra_args_size_overrides_sampling_dimensions():
config = _normalize(
{"prompt": "motion", "modalities": ["video"]},
width=480,
height=480,
num_frames=81,
extra_args={"size": "320x192"},
)
assert (config.height, config.width) == (192, 320)
def test_normalize_lingbot_request_reads_width_height_from_extra_args():
# Parity with the pre-#5311 pipeline, which honored extra_args height/width.
config = _normalize(
{"prompt": "motion", "modalities": ["video"]},
num_frames=81,
extra_args={"width": 320, "height": 192},
)
assert (config.height, config.width) == (192, 320)
def test_resolve_lingbot_output_dimensions_reads_extra_fields():
assert resolve_lingbot_output_dimensions(extra_fields={"size": "320x192"}) == (320, 192)
assert resolve_lingbot_output_dimensions(
sampling_width=480,
sampling_height=480,
extra_fields={"resolution": "720p", "ratio": "16:9"},
) == (736, 1280)
@@ -0,0 +1,266 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import json
import numpy as np
import pytest
import torch
from PIL import Image
from vllm_omni.diffusion.models.longcat_video.longcat_video_avatar_transformer import (
LongCatVideoAvatarTransformer3DModel,
_read_config,
replace_linear_with_quantized,
)
from vllm_omni.diffusion.models.longcat_video.pipeline_longcat_video_avatar import (
_avatar_model_allow_patterns,
_build_multi_speaker_ref_target_masks,
_default_at2v_shape,
_infer_asset_root_from_path,
_prepare_multi_speaker_audio_arrays,
_resolve_num_segments,
prepare_longcat_video_avatar_model_for_omni,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
@pytest.mark.parametrize(
("resolution", "expected_shape"),
[
("480p", (480, 832)),
("720p", (736, 1248)),
],
)
def test_longcat_video_avatar_at2v_default_shape_uses_resolution_bucket(
resolution: str,
expected_shape: tuple[int, int],
):
assert _default_at2v_shape(resolution) == expected_shape
@pytest.mark.parametrize(
("use_int8", "expected_weight_dir", "unexpected_weight_dir"),
[
(True, "base_model_int8/*", "base_model/*"),
(False, "base_model/*", "base_model_int8/*"),
],
)
def test_longcat_video_avatar_allow_patterns_download_one_weight_set(
use_int8: bool,
expected_weight_dir: str,
unexpected_weight_dir: str,
):
allow_patterns = _avatar_model_allow_patterns(use_int8)
assert expected_weight_dir in allow_patterns
assert unexpected_weight_dir not in allow_patterns
assert "whisper-large-v3/*" not in allow_patterns
assert "whisper-large-v3/model.safetensors" in allow_patterns
assert "whisper-large-v3/pytorch_model.bin" not in allow_patterns
assert "whisper-large-v3/flax_model.msgpack" not in allow_patterns
assert "vocal_separator/*" in allow_patterns
def test_longcat_video_avatar_prepare_model_adds_omni_metadata(tmp_path):
model_dir = tmp_path / "LongCat-Video-Avatar-1.5"
base_model_dir = model_dir / "base_model_int8"
base_model_dir.mkdir(parents=True)
(base_model_dir / "config.json").write_text('{"model_type": "longcat_avatar"}', encoding="utf-8")
(model_dir / "model_index.json").write_text("{}", encoding="utf-8")
prepared = prepare_longcat_video_avatar_model_for_omni(str(model_dir), use_int8=True)
assert prepared == str(model_dir)
model_index = json.loads((model_dir / "model_index.json").read_text(encoding="utf-8"))
assert model_index["_class_name"] == "LongCatVideoAvatarPipeline"
assert model_index["_diffusers_version"] == "0.38.0"
transformer_config = json.loads((model_dir / "transformer" / "config.json").read_text(encoding="utf-8"))
assert transformer_config == {"model_type": "longcat_avatar"}
def test_longcat_video_avatar_infers_repo_root_from_official_asset_path(tmp_path):
input_json = tmp_path / "assets" / "avatar" / "single_example_1.json"
input_json.parent.mkdir(parents=True)
input_json.write_text("{}", encoding="utf-8")
assert _infer_asset_root_from_path(input_json) == tmp_path
def test_longcat_video_avatar_falls_back_to_json_parent_without_official_layout(tmp_path):
input_json = tmp_path / "single_example_1.json"
input_json.write_text("{}", encoding="utf-8")
assert _infer_asset_root_from_path(input_json) == tmp_path
@pytest.mark.parametrize(
("value", "audio_duration", "expected"),
[
(None, 26.6, 1),
("1", 26.6, 1),
(3, 26.6, 3),
("auto", 3.0, 1),
("auto", 23.8, 8),
("auto", 26.6, 9),
],
)
def test_longcat_video_avatar_resolve_num_segments(
value,
audio_duration: float,
expected: int,
):
assert (
_resolve_num_segments(
value,
audio_duration=audio_duration,
num_frames=93,
num_cond_frames=13,
save_fps=25,
)
== expected
)
def test_longcat_video_avatar_read_config_filters_non_constructor_metadata(tmp_path):
config_path = tmp_path / "config.json"
config_path.write_text(
json.dumps(
{
"_class_name": "LongCatVideoAvatarTransformer3DModel",
"architectures": ["LongCatVideoAvatarTransformer3DModel"],
"_diffusers_version": "0.35.1",
"model_max_length": 512,
"hidden_size": 4096,
"depth": 48,
"num_heads": 32,
}
),
encoding="utf-8",
)
config = _read_config(config_path)
assert config == {
"hidden_size": 4096,
"depth": 48,
"num_heads": 32,
}
def test_longcat_video_avatar_load_weights_supports_int8_buffers():
model = LongCatVideoAvatarTransformer3DModel(
hidden_size=4,
depth=0,
num_heads=1,
caption_channels=4,
intermediate_dim=4,
output_dim=4,
audio_channel=4,
context_tokens=1,
)
replace_linear_with_quantized(model)
buffer_name = "t_embedder.mlp.0.weight_int8"
buffers = dict(model.named_buffers())
loaded_weight = torch.ones_like(buffers[buffer_name])
loaded_params = model.load_weights([(buffer_name, loaded_weight)])
assert buffer_name in loaded_params
assert torch.equal(dict(model.named_buffers())[buffer_name], loaded_weight)
def test_longcat_video_avatar_multi_speaker_para_audio_arrays_are_aligned():
left = np.ones(3, dtype=np.float32)
right = np.ones(5, dtype=np.float32) * 2
left_out, right_out = _prepare_multi_speaker_audio_arrays(
[left, right],
audio_type="para",
generate_duration=0.5,
sample_rate=10,
)
assert left_out.tolist() == [1, 1, 1, 0, 0]
assert right_out.tolist() == [2, 2, 2, 2, 2]
def test_longcat_video_avatar_multi_speaker_add_audio_arrays_are_sequential():
left = np.ones(3, dtype=np.float32)
right = np.ones(2, dtype=np.float32) * 2
left_out, right_out = _prepare_multi_speaker_audio_arrays(
[left, right],
audio_type="add",
generate_duration=0.1,
sample_rate=10,
)
assert left_out.tolist() == [1, 1, 1, 0, 0]
assert right_out.tolist() == [0, 0, 0, 2, 2]
def test_longcat_video_avatar_multi_speaker_masks_use_explicit_bboxes():
image = Image.new("RGB", (8, 6))
masks, use_background = _build_multi_speaker_ref_target_masks(
image,
{
"person1": [1, 1, 4, 3],
"person2": [2, 4, 5, 7],
},
)
assert not use_background
assert masks.shape == (3, 6, 8)
assert masks[0, 1:4, 1:3].sum() == 6
assert masks[1, 2:5, 4:7].sum() == 9
assert masks[2, 0, 0] == 1
assert masks[2, 2, 2] == 0
def test_longcat_video_avatar_multi_speaker_masks_default_to_left_right_split():
image = Image.new("RGB", (10, 8))
masks, use_background = _build_multi_speaker_ref_target_masks(image, None)
assert not use_background
assert masks.shape == (3, 8, 10)
assert masks[0, :, :5].sum() > 0
assert masks[0, :, 5:].sum() == 0
assert masks[1, :, :5].sum() == 0
assert masks[1, :, 5:].sum() > 0
def test_longcat_video_avatar_transformer_accepts_multi_speaker_masks():
model = LongCatVideoAvatarTransformer3DModel(
hidden_size=4,
depth=0,
num_heads=1,
caption_channels=4,
intermediate_dim=4,
output_dim=4,
audio_channel=4,
context_tokens=1,
)
hidden_states = torch.randn(1, 16, 5, 4, 4)
timestep = torch.zeros(1, 5)
encoder_hidden_states = torch.randn(1, 1, 4, 4)
audio_embs = torch.randn(2, 5, 5, 12, 4)
ref_target_masks = torch.zeros(3, 4, 4)
ref_target_masks[0, :, :2] = 1
ref_target_masks[1, :, 2:] = 1
ref_target_masks[2] = torch.where(ref_target_masks[0] + ref_target_masks[1] > 0, 0, 1)
output = model(
hidden_states=hidden_states,
timestep=timestep,
encoder_hidden_states=encoder_hidden_states,
num_cond_latents=1,
audio_embs=audio_embs,
ref_target_masks=ref_target_masks,
)
assert output.shape == hidden_states.shape
@@ -484,6 +484,182 @@ def test_shifted_sigma_schedule_matches_reference_values():
)
def test_base_schedule_overrides_the_uniform_sigma_positions():
from vllm_omni.diffusion.models.minimax_h3.time_request import (
minimax_h3_time_shift_sigmas,
)
base_schedule = [1.0, 0.7, 0.4, 0.15, 0.0]
video = minimax_h3_time_shift_sigmas(
num_steps=len(base_schedule),
shift_scale=12.0,
base_schedule=base_schedule,
)
audio = minimax_h3_time_shift_sigmas(
num_steps=len(base_schedule),
shift_scale=3.0,
base_schedule=base_schedule,
)
assert video == pytest.approx([1.0, 0.9655172, 0.8888889, 0.6792453, 0.0], abs=1e-6)
assert audio == pytest.approx([1.0, 0.875, 0.6666667, 0.3461539, 0.0], abs=1e-6)
assert video != pytest.approx(
minimax_h3_time_shift_sigmas(num_steps=len(base_schedule), shift_scale=12.0),
abs=1e-6,
)
@pytest.mark.parametrize(
"base_schedule",
[
[0.9, 0.5, 0.0],
[1.0, 0.5, 0.1],
[1.0, 0.5, 0.5, 0.0],
[1.0],
[],
[1.0, float("nan"), 0.4, 0.0],
[1.0, float("inf"), 0.4, 0.0],
],
)
def test_base_schedule_rejects_malformed_positions(base_schedule):
from vllm_omni.diffusion.models.minimax_h3.time_request import (
minimax_h3_time_shift_sigmas,
)
with pytest.raises(ValueError):
minimax_h3_time_shift_sigmas(
num_steps=max(len(base_schedule), 1),
shift_scale=12.0,
base_schedule=base_schedule,
)
def _distilled_pipeline(diffuse_calls, base_schedule_by_partition):
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
from vllm_omni.diffusion.sched import DMD2SigmaSchedule
schedules = {
partition: None if positions is None else DMD2SigmaSchedule.from_positions(positions)
for partition, positions in base_schedule_by_partition.items()
}
pipeline = object.__new__(MiniMaxH3Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.partition = "combined"
pipeline.supported_tasks = frozenset({"t2va", "fl2va", "ref2va"})
pipeline.default_video_shift = 12.0
pipeline.default_audio_shift = 3.0
pipeline.device = torch.device("cpu")
pipeline.od_config = SimpleNamespace()
pipeline._base_schedule_by_partition = schedules
pipeline._quality_policy = Mock()
pipeline._quality_policy.resolve.return_value = SimpleNamespace(cache_dit=None)
pipeline._cache_dit_runtime = SimpleNamespace(prepare=lambda spec: None)
pipeline.encode_prompt = Mock(
return_value=(
torch.ones(1, 2),
torch.ones(1, dtype=torch.long),
)
)
def diffuse(**kwargs):
diffuse_calls.append(kwargs)
return torch.zeros(1), torch.zeros(1)
pipeline.diffuse = diffuse
pipeline.decode = Mock(return_value=(torch.zeros(1), torch.zeros(1)))
return pipeline
def _t2va_batch(num_inference_steps=None):
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
sampling = OmniDiffusionSamplingParams(
quality="lossless",
width=1344,
height=768,
fps=24,
num_frames=124,
num_inference_steps=num_inference_steps,
extra_args={"task": "t2va", "aspect_ratio": "16:9"},
)
return DiffusionRequestBatch(
[
OmniDiffusionRequest(
prompt="distilled schedule",
sampling_params=sampling,
request_id="distilled",
)
]
)
def test_base_schedule_is_scoped_to_the_serving_partition():
"""A distilled FL2VA must not drag a regular Ref2VA onto its step count."""
distilled = [1.0, 0.7, 0.4, 0.15, 0.0]
pipeline = _distilled_pipeline([], {"fl2va": distilled, "ref2va": None})
assert pipeline._base_schedule_for_task("t2va").base_schedule == tuple(distilled)
assert pipeline._base_schedule_for_task("fl2va").base_schedule == tuple(distilled)
assert pipeline._base_schedule_for_task("ref2va") is None
def test_partially_constructed_pipeline_falls_back_to_the_uniform_schedule():
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
pipeline = object.__new__(MiniMaxH3Pipeline)
assert pipeline._base_schedule_for_task("t2va") is None
def test_distilled_forward_reports_denoising_steps_not_sigma_boundaries():
diffuse_calls = []
base_schedule = [1.0, 0.7, 0.4, 0.15, 0.0]
pipeline = _distilled_pipeline(diffuse_calls, {"fl2va": base_schedule, "ref2va": None})
pipeline.forward(_t2va_batch())
assert diffuse_calls[0]["base_schedule"] == tuple(base_schedule)
# Five boundaries describe four denoising steps.
assert diffuse_calls[0]["num_steps"] == 4
pipeline._quality_policy.resolve.assert_called_once_with(
quality="lossless",
num_inference_steps=4,
extra_args={"task": "t2va", "aspect_ratio": "16:9"},
)
def test_distilled_forward_accepts_the_matching_explicit_step_count():
diffuse_calls = []
pipeline = _distilled_pipeline(diffuse_calls, {"fl2va": [1.0, 0.7, 0.4, 0.15, 0.0], "ref2va": None})
pipeline.forward(_t2va_batch(num_inference_steps=4))
assert diffuse_calls[0]["num_steps"] == 4
def test_distilled_forward_rejects_a_mismatched_explicit_step_count():
from vllm_omni.errors import OmniClientError
pipeline = _distilled_pipeline([], {"fl2va": [1.0, 0.7, 0.4, 0.15, 0.0], "ref2va": None})
with pytest.raises(OmniClientError, match="must be 4 or omitted"):
pipeline.forward(_t2va_batch(num_inference_steps=50))
def test_absent_base_schedule_key_differs_from_an_empty_list():
from vllm_omni.diffusion.models.minimax_h3.pipeline_minimax_h3 import (
_read_base_schedule,
)
assert _read_base_schedule({}) is None
assert _read_base_schedule({"base_schedule": [1.0, 0.5, 0.0]}).base_schedule == (1.0, 0.5, 0.0)
with pytest.raises(ValueError):
_read_base_schedule({"base_schedule": []})
def test_cudnn_packed_attention_uses_python_length_without_padding_mask():
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
MiniMaxH3Attention,
@@ -492,6 +668,10 @@ def test_cudnn_packed_attention_uses_python_length_without_padding_mask():
class FakeBackend:
supports_prefix_kv_slicing = True
@classmethod
def supports_packed_mask_free(cls) -> bool:
return False
class FakeAttention(torch.nn.Module):
attn_backend = FakeBackend
@@ -526,6 +706,48 @@ def test_cudnn_packed_attention_uses_python_length_without_padding_mask():
assert attention.attention.metadata.extra["valid_kv_length"] == 5
def test_packed_attention_skips_mask_for_packed_mask_free_backend():
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
MiniMaxH3Attention,
)
class FakeBackend:
supports_prefix_kv_slicing = False
@classmethod
def supports_packed_mask_free(cls) -> bool:
return True
class FakeAttention(torch.nn.Module):
attn_backend = FakeBackend
def __init__(self):
super().__init__()
self.metadata = None
def forward(self, query, key, value, metadata):
self.metadata = metadata
return query
attention = object.__new__(MiniMaxH3Attention)
torch.nn.Module.__init__(attention)
attention.attention = FakeAttention()
q = torch.randn(8, 2, 4)
attention._run_packed_attention(
q,
q,
q,
cu_seqlens=torch.tensor([0, 5, 8], dtype=torch.int32),
max_seqlen=5,
packed_total=8,
)
assert attention.attention.metadata.attn_mask is None
assert attention.attention.metadata.extra["valid_kv_length"] == 5
assert attention.attention.metadata.extra["npu_attn_varlen"] is True
def test_packed_attention_keeps_padding_mask_for_other_backends():
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import (
MiniMaxH3Attention,
@@ -534,6 +756,10 @@ def test_packed_attention_keeps_padding_mask_for_other_backends():
class FakeBackend:
supports_prefix_kv_slicing = False
@classmethod
def supports_packed_mask_free(cls) -> bool:
return False
class FakeAttention(torch.nn.Module):
attn_backend = FakeBackend
@@ -1196,8 +1422,38 @@ def test_r7_r8_ref2va_video_segment_matrix(monkeypatch, tmp_path, case, start_ti
)
assert prepared[0]["duration_seconds"] == pytest.approx(expected_duration)
assert prepared[0]["audio_duration_seconds"] == pytest.approx(min(expected_duration, 209 / 24.0))
assert prepared[0]["start_time_seconds"] == pytest.approx(start_time or 0.0)
assert transcode_calls[0][1]["duration_seconds"] == pytest.approx(expected_duration)
assert transcode_calls[0][1]["target_frame_count"] == 209
def test_ref2va_qwen_sampling_decodes_prepared_video_once(monkeypatch):
from vllm_omni.diffusion.models.minimax_h3 import reference_video as reference_video_module
decoded = np.arange(25 * 2 * 2 * 3, dtype=np.uint8).reshape(25, 2, 2, 3)
load_calls = []
monkeypatch.setattr(
reference_video_module,
"_probe_video",
lambda _path: {"frame_count": len(decoded)},
)
monkeypatch.setattr(
reference_video_module,
"load_video_frames",
lambda path: load_calls.append(path) or decoded,
)
sampled = reference_video_module.sample_reference_video_frames("prepared.mp4")
assert load_calls == ["prepared.mp4"]
for frame, expected in zip(
sampled["frames"],
(decoded[0], decoded[12], decoded[24]),
strict=True,
):
np.testing.assert_array_equal(frame, expected)
assert sampled["block_timestamps"] == [0.25, 1.0]
def test_ref2va_two_video_recipe_tolerates_container_rounding(monkeypatch, tmp_path):
@@ -1437,6 +1693,47 @@ def test_g4_standalone_audio_duration_and_total_duration_contract():
)
def test_ref2va_audio_duration_validation_precedes_rank_branch(monkeypatch):
from vllm_omni.diffusion.models.minimax_h3 import pipeline_minimax_h3 as pipeline_module
pipeline = object.__new__(pipeline_module.MiniMaxH3Pipeline)
pipeline.device = torch.device("cpu")
monkeypatch.setattr(pipeline_module, "_dit_rank_world", lambda: (None, 1, 2))
waveform = torch.zeros(1, 10)
with pytest.raises(ValueError, match="max_duration_seconds must be positive"):
pipeline._encode_audio_conditions(
[(waveform, 10)],
max_duration_seconds=0,
)
def test_ref2va_standalone_audio_condition_is_bounded_to_output_duration():
from vllm_omni.diffusion.models.minimax_h3.pipeline_minimax_h3 import MiniMaxH3Pipeline
pipeline = object.__new__(MiniMaxH3Pipeline)
pipeline.device = torch.device("cpu")
observed = []
class FakeAudioVAE:
def encode_waveform(self, waveform, sample_rate):
observed.append((waveform.clone(), sample_rate))
return waveform, waveform.shape[-1]
pipeline.audio_vae = FakeAudioVAE()
waveform = torch.arange(40, dtype=torch.float32).reshape(1, 40)
encoded, lengths = pipeline._encode_audio_conditions(
[(waveform, 10)],
max_duration_seconds=2.5,
)
assert observed[0][0].shape == (1, 25)
assert observed[0][1] == 10
assert encoded.shape == (1, 25)
assert lengths == [25]
@pytest.mark.parametrize(
("field", "value", "message"),
[
@@ -515,8 +515,14 @@ def test_quantization_quality(config: QualityTestConfig):
generate_fn = _generate_video if config.task == "t2v" else _generate_image
# Run both arms eager: LPIPS must measure quantization only. With compile
# on, a compiler failure in one arm (e.g. inductor's CantSplit on fp8 FLUX)
# silently drops that arm to eager while the other stays compiled, and the
# metric then includes compile-state differences (fp8_ltx2 rose from ~0.09
# to 0.1291 that way in build 2954). Mirrors
# vllm_omni/quantization/tools/compare_diffusion_trajectory_similarity.py.
# --- BF16 baseline ---
bl_kwargs: dict = {"model": config.baseline_ref()}
bl_kwargs: dict = {"model": config.baseline_ref(), "enforce_eager": True}
if config.enable_cpu_offload:
bl_kwargs["enable_cpu_offload"] = True
omni_bl = Omni(**bl_kwargs)
@@ -528,7 +534,7 @@ def test_quantization_quality(config: QualityTestConfig):
# --- Quantized ---
quantization = config.quantization_ref()
qt_kwargs: dict = {"model": config.quantized_ref()}
qt_kwargs: dict = {"model": config.quantized_ref(), "enforce_eager": True}
if config.enable_cpu_offload:
qt_kwargs["enable_cpu_offload"] = True
if quantization is None:
Loaded 100 of 317 files, more files were not shown because too many files have changed in this diff. Show more