Compare commits

..
Author SHA1 Message Date
hsliu 67b50d99fc docs: address recipe review feedback
Signed-off-by: hsliu <liuhongsheng4@huawei.com>
2026-04-18 09:47:41 +08:00
Hongsheng LiuandCopilot 13d074e5db Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Signed-off-by: hsliu <liuhongsheng4@huawei.com>
2026-04-18 09:47:41 +08:00
hsliuustc0106 189824dfb9 draft vllm-omni recipes
Signed-off-by: hsliuustc0106 <liuhongsheng4@huawei.com>
Signed-off-by: hsliu <liuhongsheng4@huawei.com>
2026-04-18 09:47:41 +08:00
Samit 956f53b278 [Refactor] Use trajectory_* fields for Qwen-Image structured RL outputs (#2513)
Signed-off-by: samithuang <285365963@qq.com>
2026-04-09 03:06:40 -07:00
Ziming Huang d2aa9cf08a Revert "[Fix] Fix slow hasattr in CUDAGraphWrapper.__getattr__ (#1982)" (#2639)
Signed-off-by: ZeldaHuang <hzm414167@alibaba-inc.com>
2026-04-09 17:19:58 +08:00
2d980133fe [Bugfix] Fix precedence between caller runtime args and default stage configs (#2076)
Signed-off-by: xiaohajiayou <923390377@qq.com>
Co-authored-by: 汪志鹏 <wangzhipeng628@gmail.com>
Co-authored-by: SYLAR <125541396+lishunyang12@users.noreply.github.com>
2026-04-09 16:44:31 +08:00
e2b0ee4b87 [Model] Add Dynin-omni model in vllm-omni (#1759)
Signed-off-by: kdg6245@snu.ac.kr <kdg6245@snu.ac.kr>
Signed-off-by: Yejoon Lee (IPAI) <leeyejoon@snu.ac.kr>
Signed-off-by: aidas (arpa-kt) <aidaslab@gmail.com>
Signed-off-by: Dogeun Kim <82812668+DOGEUNNKIM@users.noreply.github.com>
Co-authored-by: Yejoon Lee (IPAI) <leeyejoon@snu.ac.kr>
Co-authored-by: aidas (arpa-kt) <aidaslab@gmail.com>
2026-04-09 16:37:34 +08:00
Samit a7bf4050de [Docs] Add multi-thread weight loading documentation (#2445)
Signed-off-by: samithuang <285365963@qq.com>
2026-04-09 15:54:23 +08:00
LiBai 9225039d17 [Bug] Lazy-import entrypoints to fix subprocess pynvml crash (#2187)
Signed-off-by: Meng Jianwen <mengjianwen@liblib.ai>
2026-04-09 15:35:22 +08:00
Ting FU 0e8e630c5b [Feat] /v1/images/generations api supports request cancel (#2621)
Signed-off-by: Semmer <semmer@live.cn>
2026-04-09 07:08:08 +00:00
Zhengyuan Su (苏政渊)andClaude Opus 4.6 92c788e5fd [Misc] Clean up method name in BAGEL. (#2501)
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:55:15 +08:00
汪志鹏 9d87229a30 [MagiHuman] Fix audio sample rate and fps propagation for online serving (#2554)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
2026-04-09 06:52:12 +00:00
pjh4993 ed7a448cd3 [Bugfix] Guard app.state access during server shutdown (#2587)
Signed-off-by: pjh4993 <pjh4993@naver.com>
2026-04-09 14:41:23 +08:00
wangyu 0edc356fc8 [CI]Refactor nightly test configuration in Buildkite, Add group for Omni and Diffusion models (#2582)
Signed-off-by: wangyu <410167048@qq.com>
2026-04-09 14:35:35 +08:00
Nick CaoandClaude 3bd8a5239e [Bugfix] Enforce --max-generated-image-size on /v1/images/generations (#2599)
Signed-off-by: Nick Cao <ncao@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-09 03:54:12 +00:00
zhumingjue138 e6f88f7c5a [CI] Reorganize the L1 L2 use cases and add markers (#2449)
Signed-off-by: zhumingjue <zhumingjue@huawei.com>
2026-04-09 10:31:21 +08:00
Zhengyuan Su (苏政渊)andClaude Opus 4.6 c3f1042061 [Feature] End-to-end LoRA support for BAGEL (#2494)
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:33:55 +00:00
Nick CaoandClaude 149b9f179a [Perf][Qwen3-TTS][Voxtral-TTS] Share CUDA graph memory pool across decoder capture sizes (#2386)
Signed-off-by: Nick Cao <ncao@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-08 20:40:17 -04:00
vveerrggandvveerrgg 2c6c07c4f6 [TTS] Add missing _generate_pcm_chunks for OmniOpenAIServingSpeech streaming (#2569)
Signed-off-by: Yueqian Lin <pandaleefree@gmail.com>
Signed-off-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
Co-authored-by: vveerrgg <vveerrgg@users.noreply.github.com>
2026-04-08 22:20:33 +00:00
Peiqi Yin fcda835f4d [Unit Test] Add unit tests for orchestrator (#2096)
Signed-off-by: yinpe <11810305@mail.sustech.edu.cn>
2026-04-08 20:58:14 +08:00
Dnoob 7e7efdd1ab [Bugfix] Fix benchmark Total input tokens for multimodal requests (#2540) (#2549)
Signed-off-by: Dnoob <dxpouo@gmail.com>
2026-04-08 11:00:53 +00:00
Jinheng aefa2ee45c [Fix] Align diffusion proc test mock with current output fields (#2584)
Signed-off-by: Jinheng Li <ahengljh@gmail.com>
2026-04-08 17:45:10 +08:00
R0CKSTAR fb3c6bd9b1 [Feat] Add MUSA flash attention support via mate package (#2451)
Signed-off-by: Xiaodong Ye <yeahdongcn@gmail.com>
2026-04-08 16:01:30 +08:00
c3c736d488 [Feature] Port Bagel RDMA flow to latest main (#2000)
Signed-off-by: Jinheng Li <ahengljh@gmail.com>
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
Co-authored-by: ahengljh <ahengljh@users.noreply.github.com>
Co-authored-by: natureofnature <wzliu@connect.hku.hk>
2026-04-08 14:55:35 +08:00
Lancer 8609bc8ed9 [Feat] image2image for Z-Image (#1580)
Signed-off-by: Lancer <maruixiang6688@gmail.com>
Signed-off-by: Lancer <402430575@qq.com>
2026-04-08 14:41:24 +08:00
Yangshen DengandClaude Opus 4.6 1cd5210440 [Diffusion] Refactor LTX2 to use unified CFG parallel framework (#2160)
Signed-off-by: Yangshen Deng <yangshen.d@outlook.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:44:04 +08:00
Y. FisherandCanlin Guo ec082add35 [Perf] Wan2.2 I2V optimization: convert datatype from FP32 to BF16 in vae (#2391)
Signed-off-by: KexiongYu <yukexiong1@huawei.com>
Signed-off-by: Canlin Guo <961750412@qq.com>
Co-authored-by: Canlin Guo <961750412@qq.com>
2026-04-08 03:26:39 +00:00
cb6a8739c1 [Feature] Add trajectory recording to BAGEL denoising loop (#2483)
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Signed-off-by: Zhengyuan Su (苏政渊) <su.zhengyuan@u.nus.edu>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Samit <285365963@qq.com>
2026-04-08 10:43:01 +08:00
skfandCanlin Guo 6433847249 [skipCI][Docs] Add expert_parallel.md (#2471)
Signed-off-by: skf1999 <13234016272@163.com>
Co-authored-by: Canlin Guo <canlinguosdu@gmail.com>
2026-04-08 02:35:14 +00:00
Yuanheng Zhao 8a55d3d01f [Feat] Enable Layerwise CPU offloading for SD3.5, Ovis-Image, Nextstep_1, LongCat-Image (#2339)
Signed-off-by: Yuanheng Zhao <jonathan.zhaoyh@gmail.com>
Signed-off-by: yuanheng <jonathan.zhaoyh@gmail.com>
2026-04-08 09:55:09 +08:00
Kevin H. Luu b246617fe5 [release] Fix lint issue (#2567)
Signed-off-by: khluu <khluu000@gmail.com>
2026-04-08 01:47:42 +00:00
Kevin H. Luu bc5e945544 [release] Fix release script (#2566)
Signed-off-by: khluu <khluu000@gmail.com>
2026-04-07 14:50:11 -07:00
pikaxingeandAlicia c9dbc0955b [Bugfix] Prevent Silent Stage Dropouts: fix coordinator reconnect bug, close/update race, and heartbeat stall (#1899)
Signed-off-by: pikaxinge <2392811793@qq.com>
Co-authored-by: Alicia <115451386+congw729@users.noreply.github.com>
2026-04-08 00:48:26 +08:00
Markus / Markandmarksverdhai feefdaee9f [Bugfix] Accept 'speaker' as alias for 'voice' in TTS speech API (#2424)
Signed-off-by: marksverdhei <marksverdhei@hotmail.com>
Signed-off-by: marksverdhai <249650165+marksverdhai@users.noreply.github.com>
Co-authored-by: marksverdhai <249650165+marksverdhai@users.noreply.github.com>
2026-04-07 15:07:01 +00:00
汪志鹏 408365fa0d [Bagel]Fused gate_proj and up_proj (#2546)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
2026-04-07 10:02:51 +00:00
汪志鹏 340cba7b04 [daVinci-MagiHuman][Doc][BugFix] Update model support for daVici-MagiHuman and fix media utils bug (#2542)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
2026-04-07 17:16:12 +08:00
skf 9584dd6e69 [HunyuanImage3] Align system_prompt support with official implementation (#2270)
Signed-off-by: skf1999 <13234016272@163.com>
2026-04-07 15:37:17 +08:00
wangyu 0998b30cbe [Bugfix] To resolve timeout error, update nightly test commands for diffusion model (#2532) 2026-04-07 15:32:51 +08:00
Bvicii badbe8eb0f [RFC] Offload blocking TTS/speech ops to thread pool to unblock event loop (#2511)
Signed-off-by: Bvicii <yizhanhuang2002@gmail.com>
2026-04-07 00:17:04 -04:00
erfgss 5d4c9ec4fa [bugfix]bugfix dreamid (#2125)
Signed-off-by: Chen Yang <2082464740@qq.com>
Signed-off-by: erfgss <97771661+erfgss@users.noreply.github.com>
2026-04-07 12:06:58 +08:00
R0CKSTARandCanlin Guo 0304c975d5 [Docs] Add installation guide for Moore Threads (MUSA) GPUs (#2359)
Signed-off-by: Xiaodong Ye <yeahdongcn@gmail.com>
Co-authored-by: Canlin Guo <canlinguosdu@gmail.com>
2026-04-07 11:27:30 +08:00
Alex Brooks 08e2e1fc8e [Bugfix] Fix DataType Handling in Default Diffusion Config (#2530)
Signed-off-by: Alex Brooks <albrooks@redhat.com>
2026-04-07 11:24:19 +08:00
R0CKSTAR 7a72f34ce4 Add supports_float64() to OmniPlatform and clean up MPS (#2488)
Signed-off-by: Xiaodong Ye <yeahdongcn@gmail.com>
2026-04-07 11:15:02 +08:00
wangyu 368de99f08 [Test] Test whether CI can be correctly skipped when the committed files only contain documentation. (#2534)
Signed-off-by: wangyu <410167048@qq.com>
2026-04-07 10:47:44 +08:00
Aliciaandwangyu 93a3fcf48e [CI] Add doc-only change detection to skip Buildkite CI. (#1284)
Signed-off-by: Alicia <115451386+congw729@users.noreply.github.com>
Signed-off-by: wangyu <410167048@qq.com>
Co-authored-by: wangyu <410167048@qq.com>
2026-04-07 09:57:38 +08:00
汪志鹏 8dd66ceb00 [BugFix] Continue decode if don't need transfer kv cache between two … (#2502)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
2026-04-07 13:30:01 +12:00
汪志鹏 5b2c4f909d [BugFix] Add bagel text2text/img2text think mode support (#2503)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
2026-04-07 13:29:26 +12:00
Zhengyuan Su (苏政渊) 54e964dc0a [Bugfix] Fix load_weights fallback for non-fused stacked_params_mapping entries (#2523) 2026-04-07 07:28:13 +08:00
Canlin GuoandHongsheng Liu e771842781 [Feature] Support vae tiling parallel encode (#2368)
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-04-06 14:47:45 +00:00
Zhengyuan Su (苏政渊)andClaude 486d77d797 [Feature] Enable LoRA adapter injection for BAGEL (#2490)
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-06 19:30:52 +08:00
LancerandDidan Deng 328de586aa [Feat] support for multi-block layerwise offloading, fix top-level parameters/buffers staying on CPU (#1486)
Signed-off-by: Lancer <maruixiang6688@gmail.com>
Signed-off-by: Lancer <402430575@qq.com>
Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
2026-04-06 11:49:35 +08:00
e23b2634d1 fix(qwen3_tts): align code predictor buffer dtype with model parameters (#2470)
Signed-off-by: willamhou <willamhou@ceresman.com>
Co-authored-by: willamhou <willamhou@ceresman.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-04-05 20:54:15 +00:00
Sy03 8b57c6205e [Perf][Fish Speech] Free unused DAC codec components to save VRAM (#2430)
Signed-off-by: Sy03 <1370724210@qq.com>
2026-04-05 20:49:18 +00:00
Sy03andYueqian Lin f6cfacdd16 [Perf][Qwen3-TTS] Free unused decoder in Talker SpeechTokenizer to VRAM (#2429)
Signed-off-by: Sy03 <1370724210@qq.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-04-05 19:04:37 +00:00
Sy03 025408f693 [Fix] [Qwen3-TTS] Qwen3-TTS streaming chunk-boundary artifacts (#2480)
Signed-off-by: Sy03 <1370724210@qq.com>
2026-04-05 14:40:38 -04:00
Hyoseop Song b2b2ab0c3c [CI/Build] Add Dockerfile.cuda for NVIDIA GPU users [Skip-CI] (#1439)
Signed-off-by: Hyoseop Song <crad_on25@naver.com>
Signed-off-by: Hyoseop Song  <crad_on25@naver.com>
2026-04-06 00:48:31 +08:00
Yuanheng Zhao 88f7ed9fb6 [Bugfix] Assign original prompt back to RequestOutput (#2498)
Signed-off-by: Yuanheng Zhao <jonathan.zhaoyh@gmail.com>
2026-04-05 10:46:11 +00:00
Zhengyuan Su (苏政渊) dd9ca6feef [Bugfix] Include uv.lock in .gitignore (#2493)
Signed-off-by: Zhengyuan Su (苏政渊) <su.zhengyuan@u.nus.edu>
2026-04-05 07:43:00 +00:00
Lancer 832952b2be [Feat] add diffusion pipeline profiler and progress bar support to FluxKontextPipeline et.al (#2489)
Signed-off-by: Lancer <maruixiang6688@gmail.com>
2026-04-05 15:16:11 +08:00
Yueqian Lin 0824edefd5 ci: remove CosyVoice3 post-merge test (#2492) 2026-04-05 15:05:52 +08:00
Yuanheng Zhao 094907eeee [Chore] Fix Bagel model import compatibility (#2491)
Signed-off-by: Yuanheng Zhao <jonathan.zhaoyh@gmail.com>
2026-04-05 06:47:08 +00:00
indevnandlinyueqian 6fc38e0467 [Model][Core] Enable async_chunk streaming pipeline for CosyVoice3 (#1703)
Signed-off-by: linyueqian <linyueqian@outlook.com>
Signed-off-by: indevn <indevn@outlook.com>
Co-authored-by: linyueqian <linyueqian@outlook.com>
2026-04-05 06:11:33 +00:00
Yueqian Lin d92439c155 [CosyVoice3] Fix vLLM 0.19.0 compatibility issues (#2486) 2026-04-05 08:45:49 +08:00
Yueqian Lin 0cf38197a0 [CI] Fix missing queue for Voxtral-TTS E2E test step (#2484)
Signed-off-by: linyueqian <linyueqian@outlook.com>
2026-04-04 17:00:05 +00:00
WeiQing Chenanddavid6666666 f2227d3c9a [Docs] Update WeChat QR code for community support (#2481)
Signed-off-by: david6666666 <david6666666@users.noreply.github.com>
Co-authored-by: david6666666 <david6666666@users.noreply.github.com>
2026-04-05 00:21:20 +08:00
汪志鹏andDidan Deng 0059ec878c [Model Support]: Magihuman support (#2301)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: 汪志鹏 <wangzhipeng628@gmail.com>
Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
2026-04-04 09:29:09 +00:00
Joshna-Medisetty 191b9a8dbf Voxtral TTS: drop hardcoded CUDA in audio tokenizer; add XPU stage config (#2428)
Signed-off-by: Joshna Medisetty <joshna.medisetty@intel.com>
Signed-off-by: Joshna-Medisetty <joshna.medisetty@intel.com>
2026-04-04 07:04:04 +00:00
Zhou Taichang 2804a85e58 [Rebase] Rebase to vllm v0.19.0 (#2475) 2026-04-04 15:03:32 +08:00
Yueqian Lin 4c031580cf [CosyVoice3] Add online serving support, fix stage config, and add CI tests (#2431)
Signed-off-by: linyueqian <linyueqian@outlook.com>
2026-04-04 14:55:15 +13:00
+11 f50c5a413f [Qwen3TTS] [TTS] [Feat] Refactor voice cache manager (#2108)
Signed-off-by: JuanPZuluaga <juanz9312@gmal.com>
Signed-off-by: yiliu30 <yi4.liu@intel.com>
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
Signed-off-by: Binh Tang <tangbinhna@gmail.com>
Signed-off-by: Binh Tang <binht@netflix.com>
Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
Signed-off-by: Rein Yang <ruiruyang2@gmail.com>
Signed-off-by: CHEN <116010019@link.cuhk.edu.cn>
Signed-off-by: vraiti <vraiti@redhat.com>
Signed-off-by: Songrui625 <songrui625@gmail.com>
Signed-off-by: Lidang Jiang <lidangjiang@gmail.com>
Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
Signed-off-by: Alex Brooks <albrooks@redhat.com>
Co-authored-by: JuanPZuluaga <juanz9312@gmal.com>
Co-authored-by: Yi Liu <yi4.liu@intel.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: Canlin Guo <canlinguosdu@gmail.com>
Co-authored-by: Binh Tang <tangbinhna@gmail.com>
Co-authored-by: Binh Tang <binht@netflix.com>
Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
Co-authored-by: rein yang <73573651+R2-Y@users.noreply.github.com>
Co-authored-by: zhumingjue138 <zhumingjue@huawei.com>
Co-authored-by: ChenWenjing <54166744+Shirley125@users.noreply.github.com>
Co-authored-by: vraiti <vraiti@redhat.com>
Co-authored-by: 汪志鹏 <wangzhipeng628@gmail.com>
Co-authored-by: Sy03 <1370724210@qq.com>
Co-authored-by: chickeyton <ngton2014@gmail.com>
Co-authored-by: Jerry Song <46962917+Songrui625@users.noreply.github.com>
Co-authored-by: Lidang Jiang <119769478+Lidang-Jiang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Alex Brooks <albrooks@redhat.com>
Co-authored-by: linyueqian <linyueqian@outlook.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-04-03 16:51:58 -04:00
Yueqian Lin 0e83ebe1d4 [OmniVoice] Add two-stage TTS serving support (#2463)
Signed-off-by: linyueqian <linyueqian@outlook.com>
2026-04-03 20:41:20 +00:00
Alex Brooks 10db95f9a9 [Bugfix] Fix Flux2 Dev Guidance (#2433)
Signed-off-by: Alex Brooks <albrooks@redhat.com>
2026-04-04 00:05:19 +08:00
Lidang JiangandClaude Opus 4.6 515d15ef87 [Cleanup] Replace bare print() with logger and use specific exception types (#2228)
Signed-off-by: Lidang Jiang <lidangjiang@gmail.com>
Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 23:59:07 +08:00
Jerry Song cd71567b06 [Model] Add two stages inference for model LTX-2 distilled. (#2260)
Signed-off-by: Songrui625 <songrui625@gmail.com>
2026-04-03 23:17:49 +08:00
Canlin Guo 6dc61c9a20 [Perf] Skip Wan2.2 cross attn Ulysses SP (#2459)
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
2026-04-03 16:23:36 +08:00
chickeyton 563f73b78a Refactor StageDiffusionClient and StageEngineCoreClient (#2006) 2026-04-03 08:05:19 +00:00
Sy03 7fb86d51cd [BugFix][FishSpeech] Fix structured voice clone prefill conditioning (#2446) 2026-04-03 06:56:33 +00:00
汪志鹏 fa275fd1be [BAGEL] [Feature]: Add thinking mode in Bagel multi-stage serving (#2447) 2026-04-03 11:47:57 +08:00
Canlin Guo 6be5d05a7c [Docs] Update docs to use vllm-ascend v0.18.0rc1 (#2453)
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
2026-04-03 09:54:47 +08:00
vraitiandCanlin Guo 6211413677 Update MRoPE config fallback logic (#2278)
Signed-off-by: vraiti <vraiti@redhat.com>
Co-authored-by: Canlin Guo <canlinguosdu@gmail.com>
2026-04-02 16:28:53 +00:00
ChenWenjingandHongsheng Liu 728cf6d023 [Feature] add session based audio streaming input (#2208)
Signed-off-by: CHEN <116010019@link.cuhk.edu.cn>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-04-02 22:51:41 +08:00
zhumingjue138 50bb47a629 [Test] Skip tests/e2e/online_serving/test_zimage_expansion.py due to issue #2435 (#2454) 2026-04-02 20:32:57 +08:00
rein yang ca02351a1e [skip ci][Bugfix] clean useless log (#2450)
Signed-off-by: Rein Yang <ruiruyang2@gmail.com>
2026-04-02 18:07:22 +08:00
458f402323 Add VACE support for WAN 2.1 conditional video generation (#1885)
Signed-off-by: Binh Tang <tangbinhna@gmail.com>
Signed-off-by: Binh Tang <binht@netflix.com>
Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
Co-authored-by: Binh Tang <binht@netflix.com>
Co-authored-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
2026-04-02 15:42:50 +08:00
Canlin Guo e2892ef6c1 [Perf] Optimize Wan2.2 rotary embedding (#2393)
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
2026-04-02 14:28:44 +08:00
Yi LiuandHongsheng Liu c1d2dcc3ac [AutoRound] Add offline quantized W4A16 model support (#1777)
Signed-off-by: yiliu30 <yi4.liu@intel.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
2026-04-02 13:57:53 +08:00
Alicia 900f6aa837 [CI] [skip ci] Rename & reset timout mins for nightly L4 tests. (#2251)
Signed-off-by: Alicia <115451386+congw729@users.noreply.github.com>
2026-04-02 11:45:00 +08:00
Jason d3daafbe41 [Feat] Add benchmarks for Qwen3-TTS Base/VoiceDesign Model (#2411)
Signed-off-by: Jiahui Sun <jhsun2020@gmail.com>
2026-04-02 03:44:04 +00:00
Didan Deng ebc9a8d875 [skip ci][Doc] Small fix of Doc (#2400)
Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
2026-04-02 10:08:58 +08:00
wangyu 9c2a576301 [Test] Add voice or language test case for Qwen3-omni and Qwen-tts (#1844)
Signed-off-by: yenuo26 <410167048@qq.com>
Signed-off-by: wangyu <410167048@qq.com>
Signed-off-by: wangyu <53896905+yenuo26@users.noreply.github.com>
2026-04-02 09:29:22 +08:00
Yuanheng Zhao 9595be59ae [skip ci][Doc] Update RFC template doc (#2141)
Signed-off-by: Yuanheng Zhao <jonathan.zhaoyh@gmail.com>
2026-04-02 09:28:48 +08:00
NATURE bbae904f1d [Bugfix] Fix delayed decoding bug for Bagel AR/DIT workflow (L3 test_bagel_img2img error) (#2422)
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
2026-04-01 15:01:39 +00:00
Lancer 70a62651b9 [Feat] support HSDP for Qwen-image series, Z-Image, GLM-Image (#2029)
Signed-off-by: Lancer <maruixiang6688@gmail.com>
2026-04-01 20:01:09 +08:00
TJian 4e4bbc42a6 [CI] Tune GPU resources for test (#2401)
Signed-off-by: tjtanaa <tunjian.tan@embeddedllm.com>
2026-04-01 19:37:34 +08:00
Roger Wang 6ef0e907af Add new committers to governance page (#2419) 2026-04-01 04:35:48 -07:00
348 changed files with 33115 additions and 3848 deletions

No files matched your search

+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.18.0"
VLLM_VERSION: "v0.19.0"
priority: 100
timeout_in_minutes: 60
soft_fail: true
+17 -2
View File
@@ -1,6 +1,21 @@
# Document 1: Buildkite loads only this block on first parse. The next step resolves docs-only skip-ci
# from git diff, then uploads document 2. When docs-only skip applies, image-build still runs if nightly-test
# / main NIGHTLY so upload-nightly is not skipped together with test-ready/test-merge.
#
# Document 2: appended after `---`; same file, read by upload_pipeline_with_skip_ci.sh (not evaluated as a second pipeline by Buildkite).
steps:
- label: ":github: Resolve skip-ci & upload pipeline"
key: upload-ci-pipeline
commands:
- "bash .buildkite/scripts/upload_pipeline_with_skip_ci.sh"
agents:
queue: "cpu_queue_premerge"
---
steps:
- label: ":docker: Build image"
key: image-build
if: __IMAGE_BUILD_IF__
commands:
- "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/q9t5s3a7"
- "docker build --progress=plain --file docker/Dockerfile.ci -t vllm-omni-ci ."
@@ -13,7 +28,7 @@ steps:
- label: "Upload Ready Pipeline"
depends_on: image-build
key: upload-ready-pipeline
if: build.branch != "main" && build.pull_request.labels includes "ready"
if: __UPLOAD_READY_IF__
commands:
- buildkite-agent pipeline upload .buildkite/test-ready.yml
agents:
@@ -23,7 +38,7 @@ steps:
- label: "Upload Merge Pipeline"
depends_on: image-build
key: upload-merge-pipeline
if: build.branch == "main" && build.env("NIGHTLY") != "1"
if: __UPLOAD_MERGE_IF__
commands:
- buildkite-agent pipeline upload .buildkite/test-merge.yml
agents:
@@ -19,7 +19,7 @@ has_new_python=$($PYTHON -c "print(1 if __import__('sys').version_info >= (3,12)
if [[ "$has_new_python" -eq 0 ]]; then
# use new python from docker
docker pull python:3-slim
PYTHON="docker run --rm -v $(pwd):/app -w /app python:3-slim python3"
PYTHON="docker run --rm --user $(id -u):$(id -g) -v $(pwd):/app -w /app python:3-slim python3"
fi
echo "Using python interpreter: $PYTHON"
@@ -36,7 +36,7 @@ mkdir -p "$INDICES_OUTPUT_DIR"
# HACK: we do not need regex module here, but it is required by pre-commit hook
# To avoid any external dependency, we simply replace it back to the stdlib re module
sed -i 's/import regex as re/import re/g' .buildkite/scripts/generate-nightly-index.py
sed -i.bak 's/import regex as re/import re/g' .buildkite/scripts/generate-nightly-index.py && rm -f .buildkite/scripts/generate-nightly-index.py.bak
# Generate indices -- the version is just the commit hash (not omni/{commit})
# because relative paths are computed between the index and wheel directories,
@@ -73,15 +73,16 @@ echo "Pure version (without variant): $pure_version"
# re-generate and copy to /omni/{version}/ only if it does not have "dev" in the version
if [[ "$version" != *"dev"* ]]; then
echo "Re-generating indices for /omni/$pure_version/"
s3_version="v$pure_version"
echo "Re-generating indices for /omni/$s3_version/"
rm -rf "${INDICES_OUTPUT_DIR:?}"
mkdir -p "$INDICES_OUTPUT_DIR"
# wheel-dir is overridden to be the commit directory, so that the indices point to the correct wheel path
$PYTHON .buildkite/scripts/generate-nightly-index.py \
--version "$pure_version" \
--version "$s3_version" \
--wheel-dir "$BUILDKITE_COMMIT" \
--current-objects "$obj_json" \
--output-dir "$INDICES_OUTPUT_DIR" \
--comment "version $pure_version"
aws s3 cp --recursive "$INDICES_OUTPUT_DIR/" "s3://$BUCKET/omni/$pure_version/"
aws s3 cp --recursive "$INDICES_OUTPUT_DIR/" "s3://$BUCKET/omni/$s3_version/"
fi
+1 -2
View File
@@ -4,6 +4,7 @@
import argparse
import json
import re
import sys
from dataclasses import asdict, dataclass
from datetime import datetime
@@ -11,8 +12,6 @@ from pathlib import Path
from typing import Any
from urllib.parse import quote
import regex as re
def normalize_package_name(name: str) -> str:
"""Normalize package name per PEP 503."""
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# Evaluate docs-only skip-ci and upload continuation steps from the same `.buildkite/pipeline.yml`
# (YAML document after the first `---`). Buildkite `if` is evaluated at upload time.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
PIPELINE_YML="${ROOT}/.buildkite/pipeline.yml"
# Prints a single digit to stdout: 1 = skip image CI, 0 = run. Logs go to stderr.
is_docs_only_change() {
local file_path
local has_any=0
while IFS= read -r file_path; do
[[ -z "${file_path}" ]] && continue
has_any=1
if [[ "${file_path}" == docs/* ]]; then
continue
fi
if [[ "${file_path}" == *.md ]]; then
continue
fi
if [[ "${file_path}" == "mkdocs.yaml" ]]; then
continue
fi
return 1
done
[[ "${has_any}" -eq 1 ]]
}
resolve_skip_ci() {
local is_pr_build=0
local files
local base_branch base_ref
if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" && -n "${BUILDKITE_PULL_REQUEST:-}" ]]; then
is_pr_build=1
fi
if [[ "${is_pr_build}" -eq 1 ]]; then
base_branch="${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-main}"
if ! git rev-parse --verify "origin/${base_branch}" >/dev/null 2>&1; then
echo "resolve_skip_ci: origin/${base_branch} not found locally; trying fetch" >&2
git fetch --depth=200 origin "${base_branch}" >/dev/null 2>&1 || true
fi
base_ref=""
if git rev-parse --verify "origin/${base_branch}" >/dev/null 2>&1; then
base_ref="origin/${base_branch}"
elif git rev-parse --verify "${base_branch}" >/dev/null 2>&1; then
base_ref="${base_branch}"
else
echo "resolve_skip_ci: cannot resolve PR base ${base_branch}; skip-ci=0" >&2
echo -n 0
return 0
fi
if ! files="$(git diff --name-only "${base_ref}...${BUILDKITE_COMMIT}" 2>/dev/null)"; then
echo "resolve_skip_ci: failed to compute PR changed files; skip-ci=0" >&2
echo -n 0
return 0
fi
elif [[ "${BUILDKITE_BRANCH:-}" == "main" ]]; then
if ! git rev-parse --verify "${BUILDKITE_COMMIT}^" >/dev/null 2>&1; then
echo "resolve_skip_ci: commit has no parent on main; skip-ci=0" >&2
echo -n 0
return 0
fi
if ! files="$(git diff --name-only "${BUILDKITE_COMMIT}^..${BUILDKITE_COMMIT}" 2>/dev/null)"; then
echo "resolve_skip_ci: failed to compute main changed files; skip-ci=0" >&2
echo -n 0
return 0
fi
else
echo "resolve_skip_ci: not PR/main build; skip-ci=0" >&2
echo -n 0
return 0
fi
if is_docs_only_change <<< "${files}"; then
echo "resolve_skip_ci: docs-only change detected; skip-ci=1" >&2
echo -n 1
return 0
fi
echo "resolve_skip_ci: non-doc changes detected; skip-ci=0" >&2
echo -n 0
}
SKIP_CI="$(resolve_skip_ci)"
if [[ ! -f "${PIPELINE_YML}" ]]; then
echo "upload_pipeline_with_skip_ci: missing ${PIPELINE_YML}" >&2
exit 1
fi
export ROOT SKIP_CI PIPELINE_YML
python3 <<'PY' | buildkite-agent pipeline upload
import os
import pathlib
path = pathlib.Path(os.environ["PIPELINE_YML"])
text = path.read_text(encoding="utf-8")
sep = "\n---\n"
if sep not in text:
raise SystemExit(
"upload_pipeline_with_skip_ci: .buildkite/pipeline.yml must contain a '\\n---\\n' separator "
"(document 1 = bootstrap, document 2 = uploaded steps)"
)
_, continuation = text.split(sep, 1)
skip = os.environ.get("SKIP_CI") == "1"
# When docs-only skip-ci: skip default CI image, but still build for L4 nightly (PR label nightly-test or
# main NIGHTLY=1), otherwise upload-nightly (depends_on image-build) would be skipped too.
nightly_only = (
'(build.pull_request.labels includes "nightly-test") '
'|| (build.branch == "main" && build.env("NIGHTLY") == "1")'
)
# Placeholder in pipeline.yml is `if: __IMAGE_BUILD_IF__` (valid YAML); replace value only.
if skip:
rep = f"'{nightly_only}'"
ready_rep = "'false'"
merge_rep = "'false'"
else:
rep = "'true'"
ready_rep = "'build.branch != \"main\" && build.pull_request.labels includes \"ready\"'"
merge_rep = "'build.branch == \"main\" && build.env(\"NIGHTLY\") != \"1\"'"
rendered = (
continuation
.replace("__IMAGE_BUILD_IF__", rep)
.replace("__UPLOAD_READY_IF__", ready_rep)
.replace("__UPLOAD_MERGE_IF__", merge_rep)
)
print(rendered, end="")
PY
+16 -16
View File
@@ -56,8 +56,8 @@ steps:
timeout_in_minutes: 20
depends_on: upload-merge-pipeline
commands:
- pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py
- pytest -s -v tests/e2e/offline_inference/test_diffusion_layerwise_offload.py
# Single pytest session for one combined summary at end of log.
- pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py tests/e2e/offline_inference/test_diffusion_layerwise_offload.py
agents:
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
plugins:
@@ -111,8 +111,7 @@ steps:
timeout_in_minutes: 20
depends_on: upload-merge-pipeline
commands:
- pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py
- pytest -s -v tests/diffusion/distributed/test_ulysses_uaa_perf.py
- pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py tests/diffusion/distributed/test_ulysses_uaa_perf.py
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
@@ -174,7 +173,7 @@ steps:
pytest -s -v tests/engine/test_async_omni_engine_abort.py
'
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
queue: "gpu_1_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
@@ -193,8 +192,7 @@ steps:
commands:
- export VLLM_LOGGING_LEVEL=DEBUG
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/offline_inference/test_qwen2_5_omni.py
- pytest -s -v tests/e2e/online_serving/test_qwen2_5_omni.py -m "advanced_model" --run-level "advanced_model"
- pytest -s -v tests/e2e/offline_inference/test_qwen2_5_omni.py tests/e2e/online_serving/test_qwen2_5_omni.py -m "advanced_model" --run-level "advanced_model"
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
@@ -216,7 +214,7 @@ steps:
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_WORKER_MULTIPROC_METHOD=spawn
export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
pytest -s -v tests/e2e/online_serving/test_qwen3_tts_customvoice.py -m "advanced_model" --run-level "advanced_model" && pytest -s -v tests/e2e/offline_inference/test_qwen3_tts_customvoice.py
pytest -s -v tests/e2e/online_serving/test_qwen3_tts_customvoice.py tests/e2e/offline_inference/test_qwen3_tts_customvoice.py -m "advanced_model" --run-level "advanced_model"
'
agents:
queue: "gpu_1_queue"
@@ -239,7 +237,7 @@ steps:
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_WORKER_MULTIPROC_METHOD=spawn
export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
pytest -s -v tests/e2e/online_serving/test_qwen3_tts_base.py -m "advanced_model" --run-level "advanced_model" && pytest -s -v tests/e2e/offline_inference/test_qwen3_tts_base.py
pytest -s -v tests/e2e/online_serving/test_qwen3_tts_base.py tests/e2e/offline_inference/test_qwen3_tts_base.py -m "advanced_model" --run-level "advanced_model"
'
agents:
queue: "gpu_1_queue"
@@ -259,9 +257,8 @@ steps:
depends_on: upload-merge-pipeline
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py
- pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py -m "advanced_model" --run-level "advanced_model"
- pytest -s -v tests/e2e/online_serving/test_mimo_audio.py -m "advanced_model" --run-level "advanced_model"
- export VLLM_TEST_CLEAN_GPU_MEMORY="1"
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py tests/e2e/online_serving/test_qwen3_omni.py tests/e2e/online_serving/test_mimo_audio.py -m "advanced_model" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
@@ -347,8 +344,7 @@ steps:
export VLLM_TEST_CLEAN_GPU_MEMORY=1
export VLLM_IMAGE_FETCH_TIMEOUT=60
pytest -s -v tests/e2e/offline_inference/test_bagel_text2img.py -m "advanced_model" --run-level "advanced_model" -k "shared_memory"
pytest -s -v tests/e2e/offline_inference/test_bagel_img2img.py -m "advanced_model" --run-level "advanced_model"
pytest -s -v tests/e2e/online_serving/test_bagel_online.py -m "advanced_model" --run-level "advanced_model"
pytest -s -v tests/e2e/offline_inference/test_bagel_img2img.py tests/e2e/online_serving/test_bagel_online.py -m "advanced_model" --run-level "advanced_model"
'
agents:
queue: "mithril-h100-pool"
@@ -392,8 +388,7 @@ steps:
timeout 20m bash -c '
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -s -v tests/e2e/online_serving/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
pytest -s -v tests/e2e/offline_inference/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
pytest -s -v tests/e2e/online_serving/test_voxtral_tts.py tests/e2e/offline_inference/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
'
agents:
queue: "mithril-h100-pool"
@@ -413,6 +408,11 @@ steps:
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
+367
View File
@@ -0,0 +1,367 @@
# Nightly diffusion GPU tests — appended to the main nightly build via
# buildkite-agent pipeline upload .buildkite/test-nightly-diffusion.yml
# from test-nightly.yml (step key: nightly-diffusion-model-test). Top-level groups are
# foldable in the Buildkite UI (Other / Wan / Qwen-Image).
steps:
- group: ":card_index_dividers: Other Model Test"
key: nightly-other-model-test-group
steps:
- label: ":full_moon: Diffusion · Other · Function Test with H100"
timeout_in_minutes: 120
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -k "not test_wan22_expansion and not test_wan_2_1_vace_expansion and not test_qwen_image" -m "advanced_model and diffusion and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Other · Function Test with L4"
timeout_in_minutes: 60
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and diffusion and L4" --run-level "advanced_model"
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Diffusion · Other · Doc Test"
timeout_in_minutes: 60
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_TEST_CLEAN_GPU_MEMORY="1"
- pytest -s -v tests/examples/online_serving/test_text_to_image.py tests/examples/offline_inference/test_text_to_image.py -m "advanced_model and example and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- group: ":card_index_dividers: Wan Series Model Test"
key: nightly-wan-model-test-group
steps:
- label: ":full_moon: Diffusion · Wan · Function Test"
timeout_in_minutes: 90
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py tests/e2e/online_serving/test_wan_2_1_vace_expansion.py -m "advanced_model" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Wan · Accuracy Test"
key: nightly-wan22-i2v-accuracy
timeout_in_minutes: 180
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py --run-level advanced_model
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- group: ":card_index_dividers: Qwen-Image Series Model Test"
key: nightly-qwen-image-edit-group
steps:
- label: ":full_moon: Diffusion · Qwen-Image · Function Test with H100"
timeout_in_minutes: 120
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_qwen_image*_expansion.py -m "advanced_model and diffusion and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Qwen-Image · GEBench Accuracy Test"
key: nightly-gebench-accuracy
timeout_in_minutes: 60
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/test_gebench_h100_smoke.py --run-level advanced_model --gebench-model Qwen/Qwen-Image-2512 --accuracy-judge-model QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ --accuracy-gpu 0 --gebench-port 8093 --accuracy-workers 1
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gebench_qwen-image-2512/summary*.json"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Qwen-Image · GEdit-Bench Accuracy Test"
key: nightly-gedit-bench-accuracy
timeout_in_minutes: 60
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/test_gedit_bench_h100_smoke.py --run-level advanced_model --gedit-model Qwen/Qwen-Image-Edit --accuracy-judge-model QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ --accuracy-gpu 0 --gedit-port 8093 --gedit-samples-per-group 20 --accuracy-workers 1
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gedit_scores_qwen-image-edit/qwen-image-edit_all_all_vie_score_*.csv"
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gedit_scores_qwen-image-edit/qwen-image-edit_all_all_summary_*.json"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: VLLM_HTTP_TIMEOUT_KEEP_ALIVE
value: "120"
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion · Qwen-Image · Perf Test"
key: nightly-qwen-image-performance
timeout_in_minutes: 180
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export CACHE_DIT_VERSION=1.3.0
- pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
- buildkite-agent artifact upload "tests/dfx/perf/results/benchmark_results_*.json"
- buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 4
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
+183 -395
View File
@@ -1,235 +1,199 @@
steps:
- label: ":full_moon: Omni Model Test with H100"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- |
set +e
pytest -s -v tests/e2e/online_serving/test_qwen3_omni_expansion.py -m "advanced_model" --run-level "advanced_model"
EXIT1=$$?
pytest -s -v tests/examples/online_serving/test_qwen3_omni.py -m "advanced_model" --run-level "advanced_model"
EXIT2=$$?
pytest -s -v tests/e2e/online_serving/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
EXIT3=$$?
pytest -s -v tests/e2e/offline_inference/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
EXIT4=$$?
exit $$((EXIT1 | EXIT2 | EXIT3 | EXIT4))
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
# Group: collapses under one heading in the Buildkite UI; child steps still run in parallel.
- group: ":card_index_dividers: Omni Model Test"
key: nightly-omni-test-group
steps:
- label: ":full_moon: Omni · Function Test with H100"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and H100 and omni" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
mountPath: /dev/shm
emptyDir:
medium: Memory
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Omni Model Test with L4"
timeout_in_minutes: 90
- label: ":full_moon: Omni · Function Test with L4"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and L4 and omni" --run-level "advanced_model"
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Omni · Doc Test with L4"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/examples/ -m "advanced_model and omni and L4" --run-level "advanced_model"
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Omni · Doc Test with H100"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/examples/ -m "advanced_model and omni and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Omni · Perf Test"
key: nightly-omni-performance
timeout_in_minutes: 180
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export BENCHMARK_DIR=tests/dfx/perf/results
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py
- buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
- buildkite-agent artifact upload "tests/dfx/perf/results/*.html"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
# Dynamically appends steps from test-nightly-diffusion.yml into this build (same mechanism as
# pipeline.yml → test-ready.yml / test-merge.yml / test-nightly.yml). Foldable groups stay in the
# uploaded YAML (Other / Wan / Qwen-Image).
- label: ":card_index_dividers: Diffusion Model Test"
key: nightly-diffusion-model-test
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/examples/ -m "advanced_model and L4 and omni" --run-level "advanced_model"
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and L4 and omni" --run-level "advanced_model"
- buildkite-agent pipeline upload .buildkite/test-nightly-diffusion.yml
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
queue: "cpu_queue_premerge"
- label: ":full_moon: Diffusion Model Test with H100"
- label: ":bar_chart: Testcase Statistics"
key: nightly-testcase-statistics
timeout_in_minutes: 120
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -k "not test_wan22_expansion" -m "advanced_model and diffusion and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion Model Wan22 completed Test with H100"
timeout_in_minutes: 90
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py -m "advanced_model" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion Model Test with L4"
timeout_in_minutes: 60
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/online_serving/test_*_expansion.py -m "advanced_model and diffusion and L4" --run-level "advanced_model"
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Documentation Example Code Test with H100"
timeout_in_minutes: 60
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export VLLM_TEST_CLEAN_GPU_MEMORY="1"
- pytest -s -v tests/examples/online_serving/test_text_to_image.py tests/examples/offline_inference/test_text_to_image.py -m "advanced_model and example and H100" --run-level "advanced_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Omni Model Perf Test & Test Case Statistics"
key: nightly-omni-performance
timeout_in_minutes: 180
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export BENCHMARK_DIR=tests/dfx/perf/results
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py
- buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
- python tools/nightly/buildkite_testcase_statistics.py -o tests/dfx/perf/results/buildkite_testcase_statistics.html
- buildkite-agent artifact upload "tests/dfx/perf/results/*.html"
agents:
@@ -266,189 +230,13 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: GEBench Accuracy Test with H100"
key: nightly-gebench-accuracy
timeout_in_minutes: 60
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/test_gebench_h100_smoke.py --run-level advanced_model --gebench-model Qwen/Qwen-Image-2512 --accuracy-judge-model QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ --accuracy-gpu 0 --gebench-port 8093 --accuracy-workers 1
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gebench_qwen-image-2512/summary*.json"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: GEdit-Bench Accuracy Test with H100"
key: nightly-gedit-bench-accuracy
timeout_in_minutes: 60
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/test_gedit_bench_h100_smoke.py --run-level advanced_model --gedit-model Qwen/Qwen-Image-Edit --accuracy-judge-model QuantTrio/Qwen3-VL-30B-A3B-Instruct-AWQ --accuracy-gpu 0 --gedit-port 8093 --gedit-samples-per-group 20 --accuracy-workers 1
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gedit_scores_qwen-image-edit/qwen-image-edit_all_all_vie_score_*.csv"
- buildkite-agent artifact upload "tests/e2e/accuracy/artifacts/gedit_scores_qwen-image-edit/qwen-image-edit_all_all_summary_*.json"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: VLLM_HTTP_TIMEOUT_KEEP_ALIVE
value: "120"
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Wan22 I2V Accuracy Test with H100"
key: nightly-wan22-i2v-accuracy
timeout_in_minutes: 180
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py --run-level advanced_model
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:c392ce21e9cf9ea65c52b866447793db10e0261c
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Qwen-Image Diffusion Perf Test with H100"
key: nightly-qwen-image-performance
timeout_in_minutes: 300
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export CACHE_DIT_VERSION=1.3.0
- pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
- buildkite-agent artifact upload "tests/dfx/perf/results/benchmark_results_*.json"
- buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 4
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
# No need to run this step for PRs with label nightly-test
- label: ":email: Nightly Collection & Email"
key: nightly-perf-distribution
depends_on:
- nightly-omni-performance
- nightly-qwen-image-performance
- nightly-testcase-statistics
if: build.env("NIGHTLY") == "1"
commands:
- pip install openpyxl
+87 -25
View File
@@ -16,11 +16,10 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Voxtral TTS CUDA Unit Test"
timeout_in_minutes: 10
- label: "CUDA Unit Test with single card"
depends_on: upload-ready-pipeline
commands:
- "timeout 10m pytest -s -v tests/model_executor/models/voxtral_tts/test_cuda_graph_acoustic_transformer.py"
- timeout 10m pytest -v -s -m 'core_model and cuda and L4 and not distributed_cuda' --ignore=tests/e2e --ignore=tests/engine/test_async_omni_engine_abort.py --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml
agents:
queue: "gpu_1_queue"
plugins:
@@ -33,6 +32,22 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "CUDA Unit Test with multi cards"
depends_on: upload-ready-pipeline
commands:
- timeout 10m pytest -v -s -m 'core_model and cuda and L4 and distributed_cuda' --ignore=tests/e2e --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml
agents:
queue: "gpu_4_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
environment:
- "HF_HOME=/fsx/hf_cache"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Diffusion Model Test"
depends_on: upload-ready-pipeline
commands:
@@ -152,24 +167,6 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Diffusion GPU Worker Test"
depends_on: upload-ready-pipeline
commands:
- timeout 20m pytest -s -v tests/diffusion/test_diffusion_worker.py
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Engine Test"
depends_on: upload-ready-pipeline
@@ -180,7 +177,7 @@ steps:
pytest -s -v tests/engine/test_async_omni_engine_abort.py
'
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
queue: "gpu_1_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
@@ -271,7 +268,7 @@ steps:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
@@ -320,6 +317,29 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "OmniVoice E2E Test"
timeout_in_minutes: 20
depends_on: upload-ready-pipeline
commands:
- |
timeout 20m bash -c '
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -s -v tests/e2e/online_serving/test_omnivoice.py -m "core_model" --run-level "core_model"
'
agents:
queue: "gpu_1_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Voxtral-TTS E2E Test"
timeout_in_minutes: 20
depends_on: upload-ready-pipeline
@@ -328,8 +348,7 @@ steps:
timeout 20m bash -c '
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -s -v tests/e2e/online_serving/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
pytest -s -v tests/e2e/offline_inference/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
pytest -s -v tests/e2e/online_serving/test_voxtral_tts.py -m "core_model" --run-level "core_model"
'
agents:
queue: "mithril-h100-pool"
@@ -526,3 +545,46 @@ steps:
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "CosyVoice3-TTS E2E Test"
timeout_in_minutes: 20
depends_on: upload-ready-pipeline
commands:
- |
timeout 20m bash -c '
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -s -v tests/e2e/online_serving/test_cosyvoice3_tts.py -m "core_model" --run-level "core_model"
'
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
+2 -2
View File
@@ -7,7 +7,7 @@ body:
- type: markdown
attributes:
value: >
#### Please take a look at previous [RFCs](https://github.com/vllm-project/vllm-omni/issues?q=label%3ARFC+sort%3Aupdated-desc) for reference.
#### Please take a look at previous [RFCs](https://github.com/vllm-project/vllm-omni/issues?q=in%3Atitle%20RFC%20sort%3Aupdated-desc) for reference.
- type: textarea
attributes:
label: Motivation.
@@ -21,7 +21,7 @@ body:
description: >
The proposed change of the RFC.
value: |
Please provide the detailed design document of the RFC using the [template](https://docs.google.com/document/d/12YxSsVeD1jvL-InClkeAEnZyWFDndz_65JmXvsamuV4/edit?tab=t.0#heading=h.4ef4szrsgspp).
Please provide the detailed design document of the RFC using the [template](https://docs.google.com/document/d/1jcgR3cDaUQH3VczD4ZcKaJAoYWHjCmnYzHkCNyz-9fk/edit?usp=sharing).
validations:
required: true
- type: textarea
+3
View File
@@ -83,6 +83,9 @@ target/
profile_default/
ipython_config.py
# uv
uv.lock
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
+3
View File
@@ -32,6 +32,9 @@ bash run_benchmark.sh --hf-only
# Use a different model (e.g. 1.7B)
MODEL=Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice bash run_benchmark.sh --async-only
# Use a Voice Clone model
MODEL=Qwen/Qwen3-TTS-12Hz-1.7B-Base TASK_TYPE=Base bash run_benchmark.sh --async-only
# Use bs16 config for higher throughput
STAGE_CONFIG=vllm_omni/configs/qwen3_tts_bs16.yaml bash run_benchmark.sh --async-only
+10 -2
View File
@@ -23,6 +23,9 @@
# # Use 1.7B model:
# MODEL=Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice bash run_benchmark.sh --async-only
#
# # Use Voice Clone model
# MODEL=Qwen/Qwen3-TTS-12Hz-1.7B-Base TASK_TYPE=Base bash run_benchmark.sh --async-only
#
# # Use batch_size=4 config:
# STAGE_CONFIG=vllm_omni/configs/qwen3_tts_bs4.yaml bash run_benchmark.sh --async-only
#
@@ -35,6 +38,7 @@
# GPU_MEM_TALKER - gpu_memory_utilization for talker stage (default: 0.3)
# GPU_MEM_CODE2WAV - gpu_memory_utilization for code2wav stage (default: 0.2)
# STAGE_CONFIG - Path to stage config YAML (default: configs/qwen3_tts_bs1.yaml)
# TASK_TYPE - Task type: CustomVoice, VoiceDesign, Base (default: CustomVoice)
set -euo pipefail
@@ -53,6 +57,7 @@ NUM_WARMUPS="${NUM_WARMUPS:-3}"
STAGE_CONFIG="${STAGE_CONFIG:-vllm_omni/configs/qwen3_tts_bs1.yaml}"
RESULT_DIR="${SCRIPT_DIR}/results"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
TASK_TYPE="${TASK_TYPE:-CustomVoice}"
# Parse args
RUN_ASYNC=true
@@ -77,6 +82,7 @@ echo " Concurrency: ${CONCURRENCY}"
echo " Port: ${PORT}"
echo " Stage config: ${STAGE_CONFIG}"
echo " Results: ${RESULT_DIR}"
echo " Task type: ${TASK_TYPE}"
echo "============================================================"
# Prepare stage config with correct GPU device and memory settings
@@ -195,7 +201,8 @@ run_bench() {
--max-concurrency ${conc_args} \
--num-warmups "${NUM_WARMUPS}" \
--config-name "${config_name}" \
--result-dir "${RESULT_DIR}"
--result-dir "${RESULT_DIR}" \
--task-type "${TASK_TYPE}"
stop_server
@@ -222,7 +229,8 @@ if [ "${RUN_HF}" = true ]; then
--num-warmups "${NUM_WARMUPS}" \
--gpu-device "${GPU_DEVICE}" \
--config-name "hf_transformers" \
--result-dir "${RESULT_DIR}"
--result-dir "${RESULT_DIR}" \
--task-type "${TASK_TYPE}"
# Allow GPU memory to settle
sleep 5
@@ -38,6 +38,10 @@ PROMPTS = [
"It was a dark and stormy night when the old lighthouse keeper heard a knock at the door.",
]
REF_AUDIO = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav"
REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
INSTRUCT = "Speak in an incredulous tone, but with a hint of panic beginning to creep into your voice."
@dataclass
class BenchmarkResult:
@@ -75,6 +79,29 @@ class BenchmarkResult:
per_request: list = field(default_factory=list)
def generate_audio(model, prompt: str, args):
if args.task_type == "Base":
return model.generate_voice_clone(
text=prompt,
language=args.language,
ref_audio=REF_AUDIO,
ref_text=REF_TEXT,
)
if args.task_type == "VoiceDesign":
return model.generate_voice_design(
text=prompt,
language=args.language,
instruct=INSTRUCT,
)
return model.generate_custom_voice(
text=prompt,
language=args.language,
speaker=args.voice,
)
def run_benchmark(args):
from qwen_tts import Qwen3TTSModel
@@ -95,11 +122,7 @@ def run_benchmark(args):
print(f"Warming up with {args.num_warmups} requests...")
for i in range(args.num_warmups):
p = PROMPTS[i % len(PROMPTS)]
wavs, sr = model.generate_custom_voice(
text=p,
language=args.language,
speaker=args.voice,
)
wavs, sr = generate_audio(model, p, args)
# Sync GPU
torch.cuda.synchronize(device)
print("Warmup done.")
@@ -124,11 +147,7 @@ def run_benchmark(args):
torch.cuda.synchronize(device)
st = time.perf_counter()
wavs, sr = model.generate_custom_voice(
text=prompt,
language=args.language,
speaker=args.voice,
)
wavs, sr = generate_audio(model, prompt, args)
torch.cuda.synchronize(device)
elapsed = time.perf_counter() - st
@@ -268,6 +287,7 @@ def parse_args():
parser.add_argument("--gpu-device", type=int, default=0)
parser.add_argument("--voice", type=str, default="Vivian")
parser.add_argument("--language", type=str, default="English")
parser.add_argument("--task-type", type=str, default="CustomVoice", choices=["CustomVoice", "VoiceDesign", "Base"])
parser.add_argument(
"--config-name", type=str, default="hf_transformers", help="Label for this config (used in filenames)"
)
@@ -37,6 +37,9 @@ PROMPTS = [
"Could you please turn down the music a little bit, I'm trying to concentrate on my work.",
"It was a dark and stormy night when the old lighthouse keeper heard a knock at the door.",
]
REF_AUDIO = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav"
REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
INSTRUCT = "Speak in an incredulous tone, but with a hint of panic beginning to creep into your voice."
@dataclass
@@ -93,22 +96,39 @@ def pcm_bytes_to_duration(num_bytes: int, sample_rate: int = 24000, sample_width
return num_samples / sample_rate
def create_payload(
prompt: str, task_type: str = "CustomVoice", voice: str = "vivian", language: str = "English"
) -> dict:
payload = {
"input": prompt,
"language": language,
"stream": True,
"response_format": "pcm",
"task_type": task_type,
}
if task_type == "Base":
payload["ref_audio"] = REF_AUDIO
payload["ref_text"] = REF_TEXT
elif task_type == "CustomVoice":
payload["voice"] = voice
elif task_type == "VoiceDesign":
payload["instructions"] = INSTRUCT
return payload
async def send_tts_request(
session: aiohttp.ClientSession,
api_url: str,
prompt: str,
task_type: str = "CustomVoice",
voice: str = "vivian",
language: str = "English",
pbar: tqdm | None = None,
) -> RequestResult:
"""Send a streaming TTS request and measure latency metrics."""
payload = {
"input": prompt,
"voice": voice,
"language": language,
"stream": True,
"response_format": "pcm",
}
payload = create_payload(prompt, task_type, voice, language)
result = RequestResult(prompt=prompt)
st = time.perf_counter()
@@ -153,6 +173,7 @@ async def run_benchmark(
num_prompts: int,
max_concurrency: int,
num_warmups: int = 3,
task_type: str = "CustomVoice",
voice: str = "vivian",
language: str = "English",
) -> BenchmarkResult:
@@ -175,7 +196,7 @@ async def run_benchmark(
warmup_tasks = []
for i in range(num_warmups):
prompt = PROMPTS[i % len(PROMPTS)]
warmup_tasks.append(send_tts_request(session, api_url, prompt, voice, language))
warmup_tasks.append(send_tts_request(session, api_url, prompt, task_type, voice, language))
await asyncio.gather(*warmup_tasks)
print(" Warmup done.")
@@ -189,7 +210,7 @@ async def run_benchmark(
async def limited_request(prompt):
async with semaphore:
return await send_tts_request(session, api_url, prompt, voice, language, pbar)
return await send_tts_request(session, api_url, prompt, task_type, voice, language, pbar)
start_time = time.perf_counter()
tasks = [asyncio.create_task(limited_request(p)) for p in request_prompts]
@@ -306,6 +327,7 @@ async def main(args):
num_prompts=args.num_prompts,
max_concurrency=concurrency,
num_warmups=args.num_warmups,
task_type=args.task_type,
voice=args.voice,
language=args.language,
)
@@ -334,6 +356,7 @@ def parse_args():
"--max-concurrency", type=int, nargs="+", default=[1, 4, 10], help="Concurrency levels to test"
)
parser.add_argument("--num-warmups", type=int, default=3)
parser.add_argument("--task-type", type=str, default="CustomVoice", choices=["CustomVoice", "VoiceDesign", "Base"])
parser.add_argument("--voice", type=str, default="vivian")
parser.add_argument("--language", type=str, default="English")
parser.add_argument(
+2 -4
View File
@@ -1,5 +1,5 @@
ARG VLLM_BASE_IMAGE=vllm/vllm-openai
ARG VLLM_BASE_TAG=v0.18.0
ARG VLLM_BASE_TAG=v0.19.0
FROM ${VLLM_BASE_IMAGE}:${VLLM_BASE_TAG}
ARG APP_DIR=/workspace/vllm-omni
WORKDIR ${APP_DIR}
@@ -11,9 +11,7 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install vllm-omni into the same uv-managed Python environment used by the base image.
# Use bash -c so that $(python3 -c ...) is expanded inside the container.
RUN uv pip install --system --no-cache-dir ".[dev]"
RUN uv pip install --system ".[dev]"
RUN ln -sf /usr/bin/python3 /usr/bin/python
+22
View File
@@ -0,0 +1,22 @@
ARG BASE_IMAGE=vllm/vllm-openai:v0.19.0
FROM ${BASE_IMAGE}
ARG COMMON_WORKDIR=/app
WORKDIR ${COMMON_WORKDIR}
# Step 1: Setup - Install system dependencies
RUN apt-get update && \
apt-get install -y ffmpeg git sox libsox-fmt-all jq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p ${COMMON_WORKDIR}/vllm-omni
# Step 2: Copy vllm-omni code and install
COPY . ${COMMON_WORKDIR}/vllm-omni
RUN cd ${COMMON_WORKDIR}/vllm-omni && uv pip install --python "$(python3 -c 'import sys; print(sys.executable)')" --no-cache-dir "."
RUN ln -sf /usr/bin/python3 /usr/bin/python
ENTRYPOINT []
+13 -13
View File
@@ -1,20 +1,20 @@
ARG VLLM_ASCEND_IMAGE=quay.io/ascend/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.17.0rc1
ARG VLLM_ASCEND_TAG=v0.18.0rc1
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
WORKDIR /vllm-workspace/vllm
RUN git fetch origin --tags && git checkout v0.18.0
# WORKDIR /vllm-workspace/vllm
# RUN git fetch origin --tags && git checkout v0.18.0
WORKDIR /vllm-workspace/vllm-ascend
RUN git fetch origin releases/v0.18.0 && git checkout d781902ce9dbda8ab1e11bb0f2f0c1bc508fee7a
# Install vllm-ascend
# Append `libascend_hal.so` path (devlib) to LD_LIBRARY_PATH
RUN export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
source /usr/local/Ascend/nnal/atb/set_env.sh && \
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
python3 -m pip install -v -e /vllm-workspace/vllm-ascend/ --extra-index https://download.pytorch.org/whl/cpu/ && \
python3 -m pip cache purge
# WORKDIR /vllm-workspace/vllm-ascend
# RUN git fetch origin releases/v0.18.0 && git checkout d781902ce9dbda8ab1e11bb0f2f0c1bc508fee7a
# # Install vllm-ascend
# # Append `libascend_hal.so` path (devlib) to LD_LIBRARY_PATH
# RUN export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi && \
# source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
# source /usr/local/Ascend/nnal/atb/set_env.sh && \
# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
# python3 -m pip install -v -e /vllm-workspace/vllm-ascend/ --extra-index https://download.pytorch.org/whl/cpu/ && \
# python3 -m pip cache purge
ARG APP_DIR=/vllm-workspace/vllm-omni
WORKDIR ${APP_DIR}
+13 -13
View File
@@ -1,20 +1,20 @@
ARG VLLM_ASCEND_IMAGE=quay.io/ascend/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.17.0rc1-a3
ARG VLLM_ASCEND_TAG=v0.18.0rc1-a3
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
WORKDIR /vllm-workspace/vllm
RUN git fetch origin --tags && git checkout v0.18.0
# WORKDIR /vllm-workspace/vllm
# RUN git fetch origin --tags && git checkout v0.18.0
WORKDIR /vllm-workspace/vllm-ascend
RUN git fetch origin releases/v0.18.0 && git checkout d781902ce9dbda8ab1e11bb0f2f0c1bc508fee7a
# Install vllm-ascend
# Append `libascend_hal.so` path (devlib) to LD_LIBRARY_PATH
RUN export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi && \
source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
source /usr/local/Ascend/nnal/atb/set_env.sh && \
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
python3 -m pip install -v -e /vllm-workspace/vllm-ascend/ --extra-index https://download.pytorch.org/whl/cpu/ && \
python3 -m pip cache purge
# WORKDIR /vllm-workspace/vllm-ascend
# RUN git fetch origin releases/v0.18.0 && git checkout d781902ce9dbda8ab1e11bb0f2f0c1bc508fee7a
# # Install vllm-ascend
# # Append `libascend_hal.so` path (devlib) to LD_LIBRARY_PATH
# RUN export PIP_EXTRA_INDEX_URL=https://mirrors.huaweicloud.com/ascend/repos/pypi && \
# source /usr/local/Ascend/ascend-toolkit/set_env.sh && \
# source /usr/local/Ascend/nnal/atb/set_env.sh && \
# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Ascend/ascend-toolkit/latest/`uname -i`-linux/devlib && \
# python3 -m pip install -v -e /vllm-workspace/vllm-ascend/ --extra-index https://download.pytorch.org/whl/cpu/ && \
# python3 -m pip cache purge
ARG APP_DIR=/vllm-workspace/vllm-omni
WORKDIR ${APP_DIR}
+1 -1
View File
@@ -1,4 +1,4 @@
ARG BASE_IMAGE=vllm/vllm-openai-rocm:v0.18.0
ARG BASE_IMAGE=vllm/vllm-openai-rocm:v0.19.0
FROM ${BASE_IMAGE} AS base
# Declare a variable to know if we want to use the nightly build or the stable build.
+1 -1
View File
@@ -76,7 +76,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.18.0
ARG VLLM_VERSION=v0.19.0
RUN git clone -b ${VLLM_VERSION} https://github.com/vllm-project/vllm
WORKDIR /workspace/vllm
+1
View File
@@ -98,6 +98,7 @@ nav:
- design/feature/ray_based_execution.md
- design/feature/omni_connectors/
- design/feature/cfg_parallel.md
- design/feature/expert_parallel.md
- design/feature/sequence_parallel.md
- design/feature/tensor_parallel.md
- design/feature/vae_parallel.md
-1
View File
@@ -5,7 +5,6 @@
Main entry points for vLLM-Omni inference and serving.
- [vllm_omni.entrypoints.async_omni.AsyncOmni][]
- [vllm_omni.entrypoints.async_omni_diffusion.AsyncOmniDiffusion][]
- [vllm_omni.entrypoints.cfg_companion_tracker.CfgCompanionTracker][]
- [vllm_omni.entrypoints.cli.benchmark.base.OmniBenchmarkSubcommandBase][]
- [vllm_omni.entrypoints.cli.benchmark.main.OmniBenchmarkSubcommand][]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 96 KiB

+1 -2
View File
@@ -168,7 +168,6 @@ vllm_omni/ tests/
│ └── arg_utils.py │ └── test_arg_utils.py ⬜
│
├── entrypoints/ → ├── entrypoints/
│ ├── async_omni_diffusion.py │ ├── test_async_omni_diffusion_config.py ✅
│ ├── stage_utils.py │ ├── test_stage_utils.py ✅
│ ├── cli/ │ ├── cli/ (benchmarks/test_serve_cli.py covers CLI serve)
│ │ └── ... │ │ └── test_*.py ⬜
@@ -272,7 +271,7 @@ Before entering specific testing levels, the project establishes two common spec
L1 and L2 level testing form the foundation of the quality assurance system. L1 level testing focuses on verifying the internal logic correctness of code units (e.g., functions, classes), ensuring each independent component behaves as designed.
L2 level testing builds upon L1 by introducing GPU resources and verifying that the end-to-end (E2E) process of the model in basic deployment scenarios is smooth. For example, it uses dummy models to confirm that core interfaces like the inference pipeline, output format, and streaming response work properly. The common goal of these two levels is to provide developers with rapid feedback, discovering and fixing issues early in the development cycle .
L2 level testing builds upon L1 by introducing GPU resources and verifying that the end-to-end (E2E) process of the model in basic deployment scenarios is smooth. For example, it uses dummy models to confirm that core interfaces like the inference pipeline, output format, and streaming response work properly. The common goal of these two levels is to provide developers with rapid feedback, discovering and fixing issues early in the development cycle.
-1
View File
@@ -73,7 +73,6 @@ vllm_omni/ tests/
│ └── arg_utils.py │ └── test_arg_utils.py ⬜
│
├── entrypoints/ → ├── entrypoints/
│ ├── async_omni_diffusion.py │ ├── test_async_omni_diffusion_config.py ✅
│ ├── stage_utils.py │ ├── test_stage_utils.py ✅
│ ├── cli/ │ ├── cli/ (benchmarks/test_serve_cli.py covers CLI serve)
│ │ └── ... │ │ └── test_*.py ⬜
@@ -802,7 +802,7 @@ omni = Omni(model="your-model", enable_layerwise_offload=True)
```python
class WanTransformer3DModel(nn.Module):
_layerwise_offload_blocks_attr = "blocks" # Attribute name containing transformer blocks
_layerwise_offload_blocks_attrs = ["blocks"] # Attribute name containing transformer blocks
def __init__(self):
self.blocks = nn.ModuleList([...]) # Transformer blocks
+221
View File
@@ -0,0 +1,221 @@
# Expert Parallel
This section describes how to add Expert Parallel (EP) to a diffusion transformer that uses Mixture-of-Experts (MoE) layers.
We use **HunyuanImage3.0** as the reference implementation.
---
## Table of Contents
- [Overview](#overview)
- [Step-by-Step Implementation](#step-by-step-implementation)
- [Testing](#testing)
- [Reference Implementations](#reference-implementations)
- [Summary](#summary)
---
## Overview
### What is Expert Parallel?
**Expert Parallel** is a parallelism strategy in Mixture-of-Experts (MoE) models that distributes different expert networks across distinct computational devices. Each device holds and computes only a subset of experts (local experts), with tokens dispatched to and gathered from remote devices via collective communication operations (e.g., All-to-All, All-Gather).
| Backend | Description |
|---------|-------------|
| `allgather_reducescatter` | Default backend based on allgather/reducescatter primitives, suitable for general EP+DP deployments.|
## Configuration
Enable EP by setting the `--enable-expert-parallel` flag. The EP size is automatically calculated as:
```text
EP_SIZE = TP_SIZE × SP_SIZE × CFG_SIZE × DP_SIZE
```
Where:
- `TP_SIZE`: Tensor parallel size
- `SP_SIZE`: Sequence parallel size
- `CFG_SIZE`: Classifier-free guidance parallel size
- `DP_SIZE`: Data parallel size
- `EP_SIZE`: Expert parallel size (computed automatically)
Note:
- Expert parallelism is only applicable to Mixture-of-Experts (MoE) models.
- The EP group is created **per pipeline stage**, meaning it includes all ranks that participate in model parallelism except pipeline parallelism.
- The underlying communication pattern for expert parallelism is **All-to-All** among the ranks in the EP group.
For example, consider a configuration with `TP=2`, `SP=1`, `CFG=2`, and `DP=4` (total 2×1×2×4 = 16 GPUs).
- Expert layers are handled by an EP group of size 16.
- Attention layers use tensor parallelism of size 2 within each of the 8 DP groups (because `DP×CFG×SP = 4×2×1 = 8` groups, each containing the 2 TP ranks). Inside each such group, the attention weights are sharded across the 2 GPUs.
## Step-by-Step Implementation
### Step 1: Configure Expert Parallelism Settings
Calculate local experts per rank:
```
ep_size = 8 # Expert Parallel size (typically equals TP size)
num_experts = 64
num_local_experts = num_experts // ep_size # 8 experts per card
# Check divisibility
assert num_experts % ep_size == 0, "Experts must be divisible by EP size"
```
### Step 2: Use Sparse MoE Block to enable EP routing.
Example:
```
from vllm.model_executor.layers.linear import ReplicatedLinear
class HunYuanSparseMoeBlock(nn.Module):
def __init__(
self,
config: PretrainedConfig,
layer_id: int = -1,
prefix: str = "",
):
super().__init__()
self.tp_size = get_tensor_model_parallel_world_size()
self.n_routed_experts = config.num_experts # 64
# Calculate local experts per rank (key for EP)
if self.tp_size > self.n_routed_experts:
raise ValueError(f"TP size {self.tp_size} > experts {self.n_routed_experts}")
# Routing gate (replicated on all ranks, computes scores for all tokens to all experts)
self.gate = ReplicatedLinear(
config.hidden_size,
config.num_experts,
bias=False,
quant_config=None,
prefix=f"{prefix}.gate",
)
# EP expert layer (factory loads platform-specific implementation)
self.experts = HunyuanFusedMoE(...)
```
**Key Points:**
- gate is **ReplicatedLinear** (replicated on all ranks)
- experts is created via **HunyuanFusedMoE factory**, which automatically handles EP dispatch
### Step 3: Initialize EP Runtime
Initialize the EP communication context before model loading.
```
from vllm.utils.import_utils import resolve_obj_by_qualname
# Call during __init__ or model loading
op_name = "hunyuan_fused_moe"
# Prepare EP runtime: establish communication groups, assign local expert indices, init _expert_map
current_omni_platform.prepare_diffusion_op_runtime(op_name)
# Factory automatically resolves platform implementation (GPU: FusedMoE / NPU: AscendFusedMoE)
impl = resolve_obj_by_qualname(
current_omni_platform.get_diffusion_model_impl_qualname(op_name)
)
```
### Step 4: Expert Weight Mapping & Loading
Each rank loads only the expert weights assigned to its local allocation.
```
# Get expert parameter mapping (different per rank)
expert_mapping = HunyuanFusedMoE.make_expert_params_mapping(
model=self,
ckpt_gate_proj_name="gate_proj",
ckpt_down_proj_name="down_proj",
ckpt_up_proj_name="up_proj",
num_experts=64,
num_redundant_experts=0,
)
# Returns: [(param_name, weight_name, expert_id, shard_id), ...]
# Note: Each rank only contains mappings for its local expert_ids
# Filter non-local experts during loading
for name, loaded_weight in weights:
if "mlp.experts" in name:
# Parse expert_id from weight name (implementation needed)
expert_id = parse_expert_id_from_name(name)
local_expert_start = (ep_rank) * num_local_experts
local_expert_end = (ep_rank + 1) * num_local_experts
if not (local_expert_start <= expert_id < local_expert_end):
continue # Skip non-local expert weights
```
### Step 5: Forward Pass with EP
Example (MoE Forward):
```
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
orig_shape = hidden_states.shape
hidden_states = hidden_states.view(-1, hidden_states.shape[-1])
# 1. Global routing computation (all tokens, all expert scores)
# hidden_states: [num_tokens, hidden_dim] (full tensor)
router_logits, _ = self.gate(hidden_states) # [num_tokens, num_experts]
# 2. EP dispatch and compute (HunyuanFusedMoE handles all_to_all internally)
# - Dispatch: Send tokens to target ranks based on router_logits
# - Local Compute: Each rank processes only its num_local_experts
# - Combine: Results returned to original token positions
final_hidden_states = self.experts(
hidden_states=hidden_states,
router_logits=router_logits,
)
# 3. Add shared expert output (not EP, computed on all ranks)
if self.shared_mlp is not None:
shared_out = self.shared_mlp(hidden_states)
final_hidden_states = final_hidden_states + shared_out
# 4. Tensor Parallel All-Reduce (synchronize across TP group)
if self.tp_size > 1:
final_hidden_states = self.experts.maybe_all_reduce_tensor_model_parallel(
final_hidden_states
)
return final_hidden_states.view(orig_shape)
```
## Testing
After adding Expert Parallel support, test via command line:
```bash
cd examples/offline_inference/text_to_image
python text_to_image.py \
--model Your-org/your-model \
--prompt "a cup of coffee on the table" \
--output "ep_enabled.png" \
--num-inference-steps 50 \
--guidance-scale 5.0 \
--tensor-parallel-size 8 \
--seed 1234 \
--enable-expert-parallel
```
vLLM‑Omni currently focuses on core diffusion model inference acceleration, so the Expert Parallel implementation includes only the basic multi‑GPU expert sharding functionality (enabled via --enable-expert-parallel). Advanced features such as communication backend selection (--all2all-backend), load balancing (--enable-eplb and its configuration), and multi‑node deployment belong to the extended capabilities of the main vLLM project and have not yet been integrated into Omni.
## Reference Implementations
Complete examples in the codebase:
| Model | Path | Pattern | Notes |
|-------|------|---------|-------|
| **HunyuanImage3.0** | `vllm_omni/diffusion/models/hunyuan_image_3/hunyuan_image_3_transformer.py` | Standard EP | Full implementation with validation |
| **EP Tests** | `vllm-omni/tests/e2e/offline_inference/test_expert_parallel.py` | E2E testing | EP correctness and performance |
| **Constraint Tests** | `vllm-omni/tests/diffusion/models/hunyuan_image_3/test_hunyuan_fused_moe.py` | Unit testing | Validation logic |
---
## Summary
Adding Expert Parallel support to diffusion model:
1. **Identify MoE layers** - Locate the router and expert networks in each transformer block.
2. **Validate EP constraints** – Ensure num_experts is divisible by expert_parallel_size.
3. **Test** - Run with enable-expert-parallel, check memory reduction, speedup, and output quality against single‑GPU baseline.
@@ -33,8 +33,8 @@ runtime:
zmq_port: 50051 # ZMQ base port (see "Port Offset Scheme" below)
protocol: "rdma" # "rdma" or "tcp"
device_name: "" # RDMA device (e.g., "mlx5_0"), empty for auto-detect
memory_pool_size: 2147483648 # 2GB memory pool
memory_pool_device: "cpu" # "cpu" for pinned memory, "cuda" for GPUDirect RDMA
memory_pool_size: 4294967296 # 4 GB (CPU); use 2147483648 (2 GB) for GPU
memory_pool_device: "cpu" # "cpu" for pinned memory (recommended), "cuda" for GPUDirect RDMA
```
Wire stages to the connector:
@@ -64,8 +64,8 @@ stage_args:
| Parameter | Default | Description |
|---|---|---|
| `memory_pool_size` | 1 GB | Total size of the RDMA-registered memory pool in bytes. |
| `memory_pool_device` | `"cpu"` | `"cpu"`: pinned host memory (recommended). `"cuda"`: GPU VRAM for GPUDirect RDMA (requires NIC-GPU direct PCIe connectivity). |
| `memory_pool_size` | 4 GB (CPU) / 2 GB (GPU) | Total size of the RDMA-registered memory pool in bytes. Recommended 4 GB for CPU pinned memory; 2 GB for GPU VRAM to conserve device memory. |
| `memory_pool_device` | `"cpu"` | `"cpu"`: pinned host memory (recommended, works on all topologies). `"cuda"`: GPU VRAM for GPUDirect RDMA (requires NIC-GPU direct PCIe connectivity, PIX topology). |
### Networking
@@ -107,10 +107,10 @@ receiver_connect = remote_side_channel_port + tp_rank
## Memory Pool Modes
| Mode | Config | Data Flow | Best For |
|---|---|---|---|
| CPU Pinned | `memory_pool_device: "cpu"` | GPU → CPU pool → RDMA → CPU pool → GPU | Most hardware topologies (recommended) |
| GPUDirect | `memory_pool_device: "cuda"` | GPU → GPU pool → RDMA (NIC reads GPU BAR1) → GPU pool | NIC-GPU direct PCIe (PIX topology) |
| Mode | Config | Recommended Pool Size | Data Flow | Best For |
|---|---|---|---|---|
| CPU Pinned | `memory_pool_device: "cpu"` | 4 GB | GPU → CPU pool → RDMA → CPU pool → GPU | Most hardware topologies (recommended) |
| GPUDirect | `memory_pool_device: "cuda"` | 2 GB | GPU → GPU pool → RDMA (NIC reads GPU BAR1) → GPU pool | NIC-GPU direct PCIe (PIX topology) |
> **Note**: GPUDirect RDMA requires the NIC and GPU to share a direct PCIe
> switch (PIX topology). On systems where they are connected via PXB or NODE,
+188 -18
View File
@@ -1,14 +1,15 @@
# VAE Patch Parallelism
This document describes how to add **VAE Patch Parallelism** support to a diffusion model.
We use **Qwen-Image** as the reference implementation.
We use **Qwen-Image** as the reference implementation for decode parallel, and **Wan2.2** for encode parallel.
---
## Table of Contents
- [Overview](#overview)
- [Step-by-Step Implementation](#step-by-step-implementation)
- [Step-by-Step Implementation (Decode)](#step-by-step-implementation-decode)
- [Encode Parallel Implementation](#encode-parallel-implementation)
- [Testing](#testing)
- [Reference Implementations](#reference-implementations)
- [Summary](#summary)
@@ -19,13 +20,13 @@ We use **Qwen-Image** as the reference implementation.
### What is Vae Patch parallel?
**VAE Patch Parallelism** is a decoding acceleration technique. Instead of decoding the entire latent tensor at once, the latent tensor is:
**VAE Patch Parallelism** is an acceleration technique for both **encoding** and **decoding**. Instead of processing the entire tensor at once, the tensor is:
+ Split into multiple spatial tiles
+ Distributed across multiple ranks
+ Decoded in parallel
+ Encoded/Decoded in parallel
+ Merged to reconstruct the final output
@@ -35,10 +36,17 @@ This approach:
+ Reduces peak memory usage per device
+ Accelerates decoding latency
+ Accelerates encoding/decoding latency
### When to Use Encode vs Decode Parallel
| Operation | Use Case | Example |
|-----------|----------|---------|
| **Decode Parallel** | Text-to-Image, Text-to-Video | Latent → Image/Video |
| **Encode Parallel** | Image-to-Video (I2V) | Image → Latent (for conditioning) |
### Architecture
We introduce **DistributedVaeExecutor** as the core component responsible for distributed VAE decoding.
We introduce **DistributedVaeExecutor** as the core component responsible for distributed VAE encoding/decoding.
The executor is model-agnostic and accepts three function parameters:
@@ -84,7 +92,7 @@ Therefore:
+ Merge must perform blending to avoid seams
## Step-by-Step Implementation
## Step-by-Step Implementation (Decode)
### Step 1: Implement DistributedAutoencoderKLQwenImage
`QwenImagePipeline` use `AutoencoderKLQwenImage` for vae, so implement a distributed version:
@@ -205,14 +213,14 @@ def tile_merge(self, coord_tensor_map: dict[tuple[int, ...], torch.Tensor], grid
We need to override tiled_decode, the main logic is:
+ check distributed is enabled
+ select split/exec/merge
+ Invoke self.distributed_decoder.execute to decode
+ Invoke self.distributed_executor.execute to decode
```
def tiled_decode(self, z: torch.Tensor, return_dict: bool = True):
if not self.is_distributed_enabled():
return super().tiled_decode(z, return_dict=return_dict)
logger.info("Decode run with distributed executor")
result = self.distributed_decoder.execute(
result = self.distributed_executor.execute(
z,
DistributedOperator(split=self.tile_split, exec=self.tile_exec, merge=self.tile_merge),
broadcast_result=True,
@@ -243,6 +251,166 @@ class YourModelPipeline(nn.Module):
+ ).to(self.device)
```
## Encode Parallel Implementation
For models that require VAE encoding (e.g., Image-to-Video), you can also parallelize the encode operation. We use **Wan2.2** as the reference implementation.
### Step 1: Implement encode_tile_split
Similar to decode, split the input tensor into tiles. Key considerations:
+ **Patchify handling**: If the model uses `patch_size`, scale tile parameters accordingly
+ **Temporal chunking**: Video VAEs may have temporal compression (e.g., 4x)
```python
def encode_tile_split(self, x: torch.Tensor) -> tuple[list[TileTask], GridSpec]:
_, _, num_frames, height, width = x.shape
encode_spatial_compression_ratio = self.spatial_compression_ratio
# Scale tile parameters for patchified coordinate system
tile_sample_min_height = self.tile_sample_min_height
tile_sample_min_width = self.tile_sample_min_width
tile_sample_stride_height = self.tile_sample_stride_height
tile_sample_stride_width = self.tile_sample_stride_width
if self.config.patch_size is not None:
# When input is patchified, scale tile parameters accordingly
encode_spatial_compression_ratio = self.spatial_compression_ratio // self.config.patch_size
tile_sample_min_height = tile_sample_min_height // self.config.patch_size
tile_sample_min_width = tile_sample_min_width // self.config.patch_size
tile_sample_stride_height = tile_sample_stride_height // self.config.patch_size
tile_sample_stride_width = tile_sample_stride_width // self.config.patch_size
latent_height = height // encode_spatial_compression_ratio
latent_width = width // encode_spatial_compression_ratio
tile_latent_min_height = tile_sample_min_height // encode_spatial_compression_ratio
tile_latent_min_width = tile_sample_min_width // encode_spatial_compression_ratio
tile_latent_stride_height = tile_sample_stride_height // encode_spatial_compression_ratio
tile_latent_stride_width = tile_sample_stride_width // encode_spatial_compression_ratio
blend_height = tile_latent_min_height - tile_latent_stride_height
blend_width = tile_latent_min_width - tile_latent_stride_width
tiletask_list = []
# Use temporal compression ratio from config instead of hardcoding
temporal_compression = self.config.scale_factor_temporal
for i in range(0, height, tile_sample_stride_height):
for j in range(0, width, tile_sample_stride_width):
time_list = []
frame_range = 1 + (num_frames - 1) // temporal_compression
for k in range(frame_range):
if k == 0:
tile = x[:, :, :1, i : i + tile_sample_min_height, j : j + tile_sample_min_width]
else:
tile = x[
:, :,
1 + temporal_compression * (k - 1) : 1 + temporal_compression * k,
i : i + tile_sample_min_height,
j : j + tile_sample_min_width,
]
time_list.append(tile)
tiletask_list.append(
TileTask(len(tiletask_list), (i // tile_sample_stride_height, j // tile_sample_stride_width),
time_list, workload=time_list[0].shape[3] * time_list[0].shape[4])
)
grid_spec = GridSpec(
split_dims=(3, 4),
grid_shape=(tiletask_list[-1].grid_coord[0] + 1, tiletask_list[-1].grid_coord[1] + 1),
tile_spec={
"latent_height": latent_height, "latent_width": latent_width,
"blend_height": blend_height, "blend_width": blend_width,
"tile_latent_stride_height": tile_latent_stride_height,
"tile_latent_stride_width": tile_latent_stride_width,
},
output_dtype=self.dtype,
)
return tiletask_list, grid_spec
```
### Step 2: Implement encode_tile_exec
```python
def encode_tile_exec(self, task: TileTask) -> torch.Tensor:
"""Encode a single sample tile into latent space."""
self.clear_cache()
time = []
for k, tile in enumerate(task.tensor):
self._enc_conv_idx = [0]
encoded = self.encoder(tile, feat_cache=self._enc_feat_map, feat_idx=self._enc_conv_idx)
encoded = self.quant_conv(encoded)
time.append(encoded)
result = torch.cat(time, dim=2)
self.clear_cache()
return result
```
### Step 3: Implement encode_tile_merge
```python
def encode_tile_merge(
self, coord_tensor_map: dict[tuple[int, ...], torch.Tensor], grid_spec: GridSpec
) -> torch.Tensor:
"""Merge encoded tiles into a full latent tensor."""
grid_h, grid_w = grid_spec.grid_shape
result_rows = []
for i in range(grid_h):
result_row = []
for j in range(grid_w):
tile = coord_tensor_map[(i, j)]
if i > 0:
tile = self.blend_v(coord_tensor_map[(i - 1, j)], tile, grid_spec.tile_spec["blend_height"])
if j > 0:
tile = self.blend_h(coord_tensor_map[(i, j - 1)], tile, grid_spec.tile_spec["blend_width"])
result_row.append(tile[:, :, :,
: grid_spec.tile_spec["tile_latent_stride_height"],
: grid_spec.tile_spec["tile_latent_stride_width"]])
result_rows.append(torch.cat(result_row, dim=-1))
enc = torch.cat(result_rows, dim=3)[
:, :, :, : grid_spec.tile_spec["latent_height"], : grid_spec.tile_spec["latent_width"]
]
return enc
```
### Step 4: Override tiled_encode method
Override `tiled_encode` instead of `encode`. The parent's `_encode()` handles patchify before calling `tiled_encode()`, so input `x` is already patchified.
```python
def tiled_encode(self, x: torch.Tensor) -> torch.Tensor:
"""
Encode using distributed VAE executor.
Note: x is already patchified by parent's _encode() before calling this method.
"""
if not self.is_distributed_enabled():
return super().tiled_encode(x)
self.clear_cache()
result = self.distributed_executor.execute(
x,
DistributedOperator(
split=self.encode_tile_split,
exec=self.encode_tile_exec,
merge=self.encode_tile_merge,
),
broadcast_result=True, # Latents needed by all ranks for diffusion
)
self.clear_cache()
return result
```
**Key differences from decode parallel:**
| Aspect | Decode Parallel | Encode Parallel |
|--------|-----------------|-----------------|
| `broadcast_result` | Often `False` (only rank 0 needs output) | `True` (all ranks need latents for diffusion) |
| Patchify | Applied in merge (unpatchify) | Handled by parent `_encode()` before `tiled_encode()` |
| Temporal chunking | Frame-by-frame | Chunk-based (e.g., 1 + 4n frames) |
## Testing
Verify numerical consistency between:
+ vae_patch_parallel_size = 1
@@ -272,18 +440,20 @@ When vae_patch_parallel_size is larger than the DiT world size, it will automati
Complete examples in the codebase:
| Model | Path | Notes |
|-------|------|-------|
| **Z-Image** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl.py` | Distributed AutoencoderKL |
| **Wan2.2** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_wan.py` | Distributed AutoencoderKLWan |
| **Qwen-Image** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_qwenimage.py` | Distributed AutoencoderKLQwenImage |
| Model | Path | Decode Parallel | Encode Parallel |
|-------|------|-----------------|-----------------|
| **Z-Image** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl.py` | ✅ | ❌ |
| **Wan2.2** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_wan.py` | ✅ | ✅ |
| **Qwen-Image** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_qwenimage.py` | ✅ | ❌ |
---
## Summary
Adding Vae Patch Parallel support to diffusion model:
Adding VAE Patch Parallel support to diffusion model:
1. **Implement Distributed Vae** - mainly copy from `diffusers` tiled_decode, and refactor into split/exec/merge
2. **Change vae model in pipeline to Distributed Vae**
3. **Test** - Verify with `tensor_parallel_size=N` quality
1. **Implement Distributed VAE** - Inherit from base VAE class and `DistributedVaeMixin`
2. **Decode Parallel** - Refactor `tiled_decode` into `tile_split`/`tile_exec`/`tile_merge`
3. **Encode Parallel** (optional) - Implement `encode_tile_split`/`encode_tile_exec`/`encode_tile_merge` for I2V models
4. **Change VAE model in pipeline** - Use the distributed version
5. **Test** - Verify numerical consistency with `vae_patch_parallel_size=1` vs `N`
@@ -69,7 +69,7 @@
[5] Orchestrator._orchestration_loop (loop)
-> poll stage output
- llm stage: await get_output_async()
- diffusion stage: get_diffusion_output_async()
- diffusion stage: get_diffusion_output_nowait()
-> (llm stage) output_processors[i].process_outputs(...)
-> _route_output(...)
-> if finished and not final_stage and non-async-chunk:
@@ -112,7 +112,7 @@ sequenceDiagram
ORCH->>S0: add_request_async
loop poll route forward
ORCH->>S0: get_output_async / get_diffusion_output_async
ORCH->>S0: get_output_async / get_diffusion_output_nowait
ORCH->>ORCH: _route_output
alt need forward to next stage
ORCH->>SN: add_request_async
@@ -6,4 +6,5 @@ vLLM-Omni supports the following hardware platforms:
- [NVIDIA CUDA](gpu.md)
- [AMD ROCm](gpu.md)
- [Intel XPU](gpu.md)
- [MThreads MUSA](gpu.md)
- [NPU](npu.md)
+20
View File
@@ -22,6 +22,10 @@ vLLM-Omni is a Python library that supports the following GPU variants. The libr
--8<-- "docs/getting_started/installation/gpu/xpu.inc.md:requirements"
=== "MThreads MUSA"
--8<-- "docs/getting_started/installation/gpu/musa.inc.md:requirements"
## Set up using Python
### Create a new Python environment
@@ -44,6 +48,10 @@ Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc
--8<-- "docs/getting_started/installation/gpu/xpu.inc.md:pre-built-wheels"
=== "MThreads MUSA"
--8<-- "docs/getting_started/installation/gpu/musa.inc.md:pre-built-wheels"
[](){ #build-from-source }
### Build wheel from source
@@ -60,6 +68,10 @@ Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc
--8<-- "docs/getting_started/installation/gpu/xpu.inc.md:build-wheel-from-source"
=== "MThreads MUSA"
--8<-- "docs/getting_started/installation/gpu/musa.inc.md:build-wheel-from-source"
## Set up using Docker
### Pre-built images
@@ -76,6 +88,10 @@ Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc
--8<-- "docs/getting_started/installation/gpu/xpu.inc.md:pre-built-images"
=== "MThreads MUSA"
--8<-- "docs/getting_started/installation/gpu/musa.inc.md:pre-built-images"
### Build your own docker image
=== "AMD ROCm"
@@ -85,3 +101,7 @@ Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc
=== "Intel XPU"
--8<-- "docs/getting_started/installation/gpu/xpu.inc.md:build-docker"
=== "MThreads MUSA"
--8<-- "docs/getting_started/installation/gpu/musa.inc.md:build-docker"
@@ -39,13 +39,13 @@ uv pip install 'vllm-omni[demo]'
# --8<-- [start:build-wheel-from-source]
#### Installation of vLLM
If you do not need to modify source code of vLLM, you can directly install the stable 0.18.0 release version of the library
If you do not need to modify source code of vLLM, you can directly install the stable 0.19.0 release version of the library
```bash
uv pip install vllm==0.18.0 --torch-backend=auto
uv pip install vllm==0.19.0 --torch-backend=auto
```
The 0.18.0 release of vLLM ships CUDA 12.9-compatible binaries by default. If you need a different CUDA variant or want to reuse an existing PyTorch installation, build vLLM from source instead.
The 0.19.0 release of vLLM ships CUDA 13.0-compatible binaries by default. If you need a different CUDA variant or want to reuse an existing PyTorch installation, build vLLM from source instead.
#### Installation of vLLM-Omni
Since vllm-omni is rapidly evolving, it's recommended to install it from source
@@ -66,11 +66,12 @@ If you want to check, modify or debug with source code of vLLM, install the libr
```bash
git clone https://github.com/vllm-project/vllm.git
cd vllm
git checkout v0.18.0
git checkout v0.19.0
```
Set up environment variables to get pre-built wheels. If there are internet problems, just download the whl file manually. And set `VLLM_PRECOMPILED_WHEEL_LOCATION` as your local absolute path of whl file.
```bash
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.18.0/vllm-0.18.0+cu129-cp38-abi3-manylinux_2_35_x86_64.whl
#For CUDA 13.0
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.19.0/vllm-0.19.0+cu130-cp38-abi3-manylinux_2_35_x86_64.whl
```
Install vllm with command below (If you have no existing PyTorch).
```bash
@@ -0,0 +1,65 @@
# --8<-- [start:requirements]
- GPU: Moore Threads GPU with MUSA SDK installed (validated on MTT S5000)
# --8<-- [end:requirements]
# --8<-- [start:set-up-using-python]
vLLM-Omni for MUSA requires building from source. Pre-built wheels are not currently available.
!!! note
MUSA platform requires vLLM-MUSA to be installed first.
# --8<-- [start:pre-built-wheels]
# --8<-- [end:pre-built-wheels]
# --8<-- [start:build-wheel-from-source]
#### Prerequisites
- **MUSA SDK**: Download from [MUSA SDK Download](https://developer.mthreads.com/sdk/download/musa)
- **torchada**: CUDA→MUSA compatibility layer for PyTorch (`pip install torchada`)
- **mthreads-ml-py**: MTML Python bindings (`pip install mthreads-ml-py`)
- **MATE**: MUSA AI Tensor Engine ([GitHub](https://github.com/MooreThreads/mate))
#### Installation of vLLM-MUSA
```bash
git clone https://github.com/MooreThreads/vllm-musa.git
cd vllm-musa
git checkout v0.18.0-dev
pip install . --no-build-isolation -v
```
#### Installation of vLLM-Omni
```bash
git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni
VLLM_OMNI_TARGET_DEVICE=musa pip install -e . --no-build-isolation
```
For Gradio demos:
```bash
pip install -e '.[demo]' --no-build-isolation
```
#### Environment Variables
```bash
export MUSA_VISIBLE_DEVICES=0,1
export VLLM_WORKER_MULTIPROC_METHOD=spawn
export VLLM_MUSA_CUSTOM_OP_USE_NATIVE=false
```
# --8<-- [end:build-wheel-from-source]
# --8<-- [start:build-docker]
# --8<-- [end:build-docker]
# --8<-- [start:pre-built-images]
# --8<-- [end:pre-built-images]
@@ -13,7 +13,7 @@ vLLM-Omni current recommends the steps in under setup through Docker Images.
vLLM-Omni is built based on vLLM. Please install it with command below.
```bash
uv pip install vllm==0.18.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.18.0/rocm700
uv pip install vllm==0.19.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.19.0/rocm700
```
#### Installation of vLLM-Omni
@@ -34,13 +34,13 @@ uv pip install onnxruntime-rocm sox
# --8<-- [start:build-wheel-from-source]
#### Installation of vLLM
If you do not need to modify source code of vLLM, you can directly install the stable 0.18.0 release version of the library
If you do not need to modify source code of vLLM, you can directly install the stable 0.19.0 release version of the library
```bash
uv pip install vllm==0.18.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.18.0/rocm700
uv pip install vllm==0.19.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.19.0/rocm700
```
The pre-built 0.18.0 vLLM wheel targets ROCm 7.0. If you need a different ROCm stack or want to reuse an existing PyTorch installation, build vLLM from source instead.
The pre-built 0.19.0 vLLM wheel targets ROCm 7.0. If you need a different ROCm stack or want to reuse an existing PyTorch installation, build vLLM from source instead.
#### Installation of vLLM-Omni
Since vllm-omni is rapidly evolving, it's recommended to install it from source
@@ -58,7 +58,7 @@ If you want to check, modify or debug with source code of vLLM, install the libr
```bash
git clone https://github.com/vllm-project/vllm.git
cd vllm
git checkout v0.18.0
git checkout v0.19.0
python3 -m pip install -r requirements/rocm.txt
python3 setup.py develop
```
@@ -130,7 +130,7 @@ docker run --rm \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--env "HF_TOKEN=$HF_TOKEN" \
-p 8091:8091 \
vllm/vllm-omni-rocm:v0.18.0 \
vllm/vllm-omni-rocm:v0.19.0 \
--model Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
@@ -149,7 +149,7 @@ docker run --rm -it \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--env "HF_TOKEN=$HF_TOKEN" \
--entrypoint bash \
vllm/vllm-omni-rocm:v0.18.0
vllm/vllm-omni-rocm:v0.19.0
```
# --8<-- [end:pre-built-images]
@@ -10,10 +10,10 @@ The recommended way to use vLLM-Omni on NPU is through the vllm-ascend pre-built
```bash
# Update the vllm-ascend image
# Atlas A2:
# export IMAGE=quay.io/ascend/vllm-ascend:v0.17.0rc1
# export IMAGE=quay.io/ascend/vllm-ascend:v0.18.0rc1
# Atlas A3:
# export IMAGE=quay.io/ascend/vllm-ascend:v0.17.0rc1-a3
export IMAGE=quay.io/ascend/vllm-ascend:v0.17.0rc1
# export IMAGE=quay.io/ascend/vllm-ascend:v0.18.0rc1-a3
export IMAGE=quay.io/ascend/vllm-ascend:v0.18.0rc1
docker run --rm \
--name vllm-omni-npu \
--shm-size=1g \
@@ -33,17 +33,6 @@ docker run --rm \
-p 8000:8000 \
-it $IMAGE bash
cd /vllm-workspace/vllm
git fetch origin --tags
git checkout v0.18.0
# Because vllm-ascend will release v0.18.0rc1 after vllm-omni 0.16.0,
# we have to pin vllm-ascend at the current commit.
cd /vllm-workspace/vllm-ascend
git pull origin main
git checkout d781902ce9dbda8ab1e11bb0f2f0c1bc508fee7a
pip install -v -e .
# Inside the container, install vLLM-Omni from source
cd /vllm-workspace
git clone -b v0.18.0 https://github.com/vllm-project/vllm-omni.git
@@ -68,15 +57,10 @@ You can also build vLLM-Omni from the latest main branch if you want to use the
```bash
# Pin vLLM version to 0.18.0
cd /vllm-workspace/vllm
git fetch origin --tags
git checkout v0.18.0
git clone -b v0.18.0 https://github.com/vllm-project/vllm.git
VLLM_TARGET_DEVICE=empty pip install -v -e .
# Because vllm-ascend has not yet entered continuous development and has not been officially released, we need to pin it to a specific commit. Please note that this commit may change over time.
cd /vllm-workspace/vllm-ascend
git pull origin main
git fetch origin --tags
git checkout d781902ce9dbda8ab1e11bb0f2f0c1bc508fee7a
git clone -b v0.18.0rc1 https://github.com/vllm-project/vllm-ascend.git
pip install -v -e .
# Install vLLM-Omni from the latest main branch
+2 -2
View File
@@ -19,10 +19,10 @@ uv venv --python 3.12 --seed
source .venv/bin/activate
# On CUDA
uv pip install vllm==0.18.0 --torch-backend=auto
uv pip install vllm==0.19.0 --torch-backend=auto
# On ROCm
uv pip install vllm==0.18.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.18.0/rocm700
uv pip install vllm==0.19.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.19.0/rocm700
git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni
+1
View File
@@ -120,6 +120,7 @@ def extract_omni_serve_subparser_init():
"_FlexibleArgumentParser": _FlexibleArgumentParser,
"FlexibleArgumentParser": _FlexibleArgumentParser,
"make_arg_parser": lambda parser: parser, # no-op for doc
"_ensure_vllm_platform": lambda: None, # no-op for doc
"VLLM_SUBCMD_PARSER_EPILOG": "",
"logger": logger,
"DummySubparsers": DummySubparsers,
+5
View File
@@ -31,9 +31,13 @@ th {
| `ZImagePipeline` | Z-Image | `Tongyi-MAI/Z-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `WanPipeline` | Wan2.1-T2V, Wan2.2-T2V, Wan2.2-TI2V | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers`, `Wan-AI/Wan2.1-T2V-14B-Diffusers`, `Wan-AI/Wan2.2-T2V-A14B-Diffusers`, `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `WanImageToVideoPipeline` | Wan2.2-I2V | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `Wan22VACEPipeline` | Wan2.1-VACE | `Wan-AI/Wan2.1-VACE-1.3B-diffusers`, `Wan-AI/Wan2.1-VACE-14B-diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `LTX2Pipeline` | LTX-2-T2V | `Lightricks/LTX-2` | ✅︎ | ✅︎ | | |
| `LTX2ImageToVideoPipeline` | LTX-2-I2V | `Lightricks/LTX-2` | ✅︎ | ✅︎ | | |
| `LTX2TwoStagesPipeline` | LTX-2-T2V | `rootonchair/LTX-2-19b-distilled` | ✅︎ | ✅︎ | | |
| `LTX2ImageToVideoTwoStagesPipeline` | LTX-2-I2V | `rootonchair/LTX-2-19b-distilled` | ✅︎ | ✅︎ | | |
| `HeliosPipeline`, `HeliosPyramidPipeline` | Helios | `BestWishYsh/Helios-Base`, `BestWishYsh/Helios-Mid`, `BestWishYsh/Helios-Distilled` | ✅︎ | ✅︎ | ✅︎ | |
| `MagiHumanPipeline` | MagiHuman | `SII-GAIR/daVinci-MagiHuman-Base-1080p` | ✅︎ | ✅︎ | | |
| `OvisImagePipeline` | Ovis-Image | `OvisAI/Ovis-Image` | ✅︎ | ✅︎ | | ✅︎ |
| `LongcatImagePipeline` | LongCat-Image | `meituan-longcat/LongCat-Image` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `LongCatImageEditPipeline` | LongCat-Image-Edit | `meituan-longcat/LongCat-Image-Edit` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
@@ -56,5 +60,6 @@ th {
| `HunyuanVideo15Pipeline` | HunyuanVideo-1.5-T2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | ✅︎ | ✅︎ | | |
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | |
| `VoxtralTTSForConditionalGeneration` | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✅︎ | ✅︎ | | |
|`DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | |
✅︎ indicates the model is supported on that backend. Empty cells mean not listed as supported on that backend.
+24 -3
View File
@@ -118,6 +118,7 @@ Content-Type: application/json
| `instructions` | string | "" | Voice style/emotion instructions |
| `max_new_tokens` | integer | 2048 | Maximum tokens to generate |
| `initial_codec_chunk_frames` | integer | null | Per-request initial chunk size override for TTFA tuning. When null, IC is computed dynamically based on server load. |
| `stream` | bool | false | Stream raw PCM chunks as they are decoded (requires `response_format="pcm"`) |
**Supported languages:** Auto, Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian
@@ -143,9 +144,23 @@ Lists available voices for the loaded model.
```json
{
"voices": ["aiden", "dylan", "eric", "ono_anna", "ryan", "serena", "sohee", "uncle_fu", "vivian"]
"voices": ["aiden", "dylan", "eric", "ono_anna", "ryan", "serena", "sohee", "uncle_fu", "vivian", "custom_voice_1"],
"uploaded_voices": [
{
"name": "custom_voice_1",
"consent": "user_consent_id",
"created_at": 1738660000,
"file_size": 1024000,
"mime_type": "audio/wav",
"ref_text": "The exact transcript of the audio sample.",
"speaker_description": "warm narrator"
}
]
}
```
`uploaded_voices` is always present (empty list when no custom voices have been uploaded). Fields `ref_text` and `speaker_description` are omitted per-entry when not provided at upload time.
```
POST /v1/audio/voices
Content-Type: multipart/form-data
@@ -161,6 +176,7 @@ Upload a new voice sample for voice cloning in Base task TTS requests.
| `consent` | string | Yes | Consent recording ID |
| `name` | string | Yes | Name for the new voice |
| `ref_text` | string | No | Transcript of the audio. When provided, enables in-context voice cloning (higher quality). Without it, only the speaker embedding is extracted. |
| `speaker_description` | string | No | Free-form description of the voice (e.g. "warm narrator", "energetic presenter"). Stored as metadata and returned in `GET /v1/audio/voices`. |
**Response Example:**
@@ -172,11 +188,15 @@ Upload a new voice sample for voice cloning in Base task TTS requests.
"consent": "user_consent_id",
"created_at": 1738660000,
"mime_type": "audio/wav",
"file_size": 1024000
"file_size": 1024000,
"ref_text": "The exact transcript of the audio sample.",
"speaker_description": "warm narrator"
}
}
```
Fields `ref_text` and `speaker_description` are omitted when not provided at upload time.
**Usage Example:**
```bash
@@ -184,7 +204,8 @@ curl -X POST http://localhost:8091/v1/audio/voices \
-F "audio_sample=@/path/to/voice_sample.wav" \
-F "consent=user_consent_id" \
-F "name=custom_voice_1" \
-F "ref_text=The exact transcript of the audio sample."
-F "ref_text=The exact transcript of the audio sample." \
-F "speaker_description=warm narrator"
```
## Streaming Text Input (WebSocket)
@@ -164,7 +164,7 @@ cache_config={
**Performance Tips**:
- Default `Fn_compute_blocks=1` works well for most cases. Some models (e.g., [FLUX.2-klein](https://github.com/wtomin/vllm-omni/blob/main/vllm_omni/diffusion/cache/cache_dit_backend.py#L363)) use a larger value for `Fn_compute_blocks` for a balanced performance.
- Default `Fn_compute_blocks=1` works well for most cases. Some models (e.g., FLUX.2-klein) use a larger value for `Fn_compute_blocks` for a balanced performance.
- Increase `residual_diff_threshold` (e.g., 0.12-0.15) for faster inference with slight quality trade-off, or decrease from default 0.24 for higher quality.
- Default `max_warmup_steps=4` is optimized for few-step models. Increase to 6-8 for more steps if needed.
@@ -91,12 +91,19 @@ Models must define the blocks attribute name for layerwise offloading:
```python
class WanTransformer3DModel(nn.Module):
_layerwise_offload_blocks_attr = "blocks" # Attribute name containing transformer blocks
_layerwise_offload_blocks_attrs = ["blocks"] # Attribute names containing transformer blocks
def __init__(self):
self.blocks = nn.ModuleList([...]) # Transformer blocks
```
For models with multiple block types:
```python
class Flux2Transformer2DModel(nn.Module):
_layerwise_offload_blocks_attrs = ["transformer_blocks", "single_transformer_blocks"]
```
### Limitations
- Cold start latency increases because of
1) components are loaded to CPU first at the very first during initialization,
@@ -132,12 +139,16 @@ Factory function `get_offload_backend()` selects the appropriate backend based o
## Supported Models
| Architecture | Example Models | DiT Class | Model-Level Offload | Layerwise Offload | Blocks Attr (Layerwise specific) |
|--------------|----------------|-----------|---------------------|-------------------|-------------|
| Wan22Pipeline | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
| Wan22I2VPipeline | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
| Architecture | Example Models | DiT Class | Model-Level Offload | Layerwise Offload | Blocks Attrs (Layerwise specific) |
|--------------|----------------|-----------|---------------------|-------------------|-----------------------------------|
| LongCatImagePipeline | `meituan-longcat/LongCat-Image` | `LongCatImageTransformer2DModel` | - | ✓ | `"transformer_blocks"`, `"single_transformer_blocks"` |
| NextStep11Pipeline | `stepfun-ai/NextStep-1.1` | `NextStepModel` | - | ✓ | `"layers"` |
| OvisImagePipeline | `AIDC-AI/Ovis-Image-7B` | `OvisImageTransformer2DModel` | - | ✓ | `"transformer"` |
| QwenImagePipeline | `Qwen/Qwen-Image` | `QwenImageTransformer2DModel` | ✓ | ✓ | `"transformer_blocks"` |
| StableDiffusion3Pipeline | `stabilityai/stable-diffusion-3.5-medium` | `SD3Transformer2DModel` | - | ✓ | `"transformer_blocks"` |
| Wan22I2VPipeline | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
| Wan22Pipeline | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
**Notes:**
- Model-Level Offloading is expected to be supported by all common diffusion models (DiT and encoders) naturally
- Layerwise Offloading requires DiT class to define `_layerwise_offload_blocks_attr` pointing to transformer blocks
- Layerwise Offloading requires DiT class to define `_layerwise_offload_blocks_attrs` pointing to transformer blocks
@@ -0,0 +1,91 @@
# AutoRound Quantization
## Overview
[AutoRound](https://github.com/intel/auto-round) is an advanced quantization toolkit designed for Large Language Models (LLMs), Vision-Language Models (VLMs), and diffusion models. It achieves high accuracy at ultra-low bit widths (2–4 bits) with minimal tuning by leveraging sign-gradient descent, while providing broad hardware compatibility with multi-datatype support.
The quantization config is auto-detected from the checkpoint's `config.json` (`quantization_config.quant_method = "auto-round"`). No extra CLI flags are needed.
### Supported Schemes
| Scheme | Bits | Status |
|--------|------|--------|
| W4A16 | 4 | ✅ Supported |
| W8A16 | 8 | Planned |
W4A16 is the first supported scheme. Additional schemes will be added in future releases.
## Configuration
1. **Python API**: point `model` at a pre-quantized checkpoint. The quantization is detected automatically.
```python
from vllm_omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
omni = Omni(model="vllm-project-org/FLUX.1-dev-AutoRound-w4a16")
outputs = omni.generate(
"A cat sitting on a windowsill",
OmniDiffusionSamplingParams(num_inference_steps=28),
)
outputs[0].save_images("output.png")
```
2. **CLI**: pass the quantized model path directly.
```bash
python examples/offline_inference/text_to_image/text_to_image.py \
--model vllm-project-org/FLUX.1-dev-AutoRound-w4a16 \
--prompt "A cat sitting on a windowsill" \
--num-inference-steps 28 \
--output outputs/flux_w4a16.png
```
No `--quantization` flag is needed — the quantization method is read from the checkpoint.
## How It Works
The checkpoint's `config.json` contains:
```json
{
"quantization_config": {
"quant_method": "auto-round",
"bits": 4,
"group_size": 128,
"sym": true,
"packing_format": "auto_round:auto_gptq",
"block_name_to_quantize": "transformer_blocks,single_transformer_blocks"
}
}
```
At load time:
1. `TransformerConfig.from_dict()` parses the `quantization_config` section and builds a vLLM `INCConfig` via `build_quant_config("auto-round", ...)`.
2. `OmniDiffusionConfig.set_tf_model_config()` propagates the detected config to the engine.
3. The appropriate compute kernel (e.g. GPTQ-Marlin for W4A16) is selected automatically based on the checkpoint's bit-width and packing format.
## Supported Models
| Model | HF Checkpoint | Scheme | Group Size | Backend |
|-------|--------------|--------|------------|---------|
| FLUX.1-dev | `vllm-project-org/FLUX.1-dev-AutoRound-w4a16` | W4A16 | 128 | GPTQ-Marlin |
## Creating a Quantized Checkpoint
Use [AutoRound](https://github.com/intel/auto-round) to quantize a BF16 model. The `--scheme` flag selects the quantization scheme:
```bash
# W4A16 (4-bit weight, 16-bit activation)
auto-round \
--model black-forest-labs/FLUX.1-dev \
--scheme W4A16 \
--batch_size 1 \
--disable_opt_rtn \
--dataset coco2014 \
--iters 0
```
The output directory can be used directly as the `model` argument. See the [AutoRound documentation](https://github.com/intel/auto-round) for all available schemes and options.
@@ -11,6 +11,7 @@ vLLM-Omni provides a unified quantization framework that supports both diffusion
| FP8 | [FP8](fp8.md) | FP8 W8A8, dynamic or static | Z-Image, Qwen-Image, Flux, Bagel | SM 89 (Ada) |
| Int8 | [Int8](int8.md) | Int8 W8A8 | Z-Image, Qwen-Image | SM 89 (Ada) / Ascend NPU |
| GGUF | [GGUF](gguf.md) | GGUF format, dequant+GEMM for N-D tensors | Z-Image, Flux | SM 60 |
| AutoRound | [AutoRound](autoround.md) | W4A16 (pre-quantized) | Flux | SM 80 (Ampere) |
### Multi-stage Omni Models (Pre-quantized Checkpoints)
@@ -102,6 +103,9 @@ config = build_quant_config("fp8")
# Dict with parameters
config = build_quant_config({"method": "fp8", "activation_scheme": "static"})
# AutoRound / INC (auto-detected from checkpoint, or explicit)
config = build_quant_config("auto-round", bits=4, group_size=128)
# Per-component dict
config = build_quant_config({
"transformer": {"method": "fp8"},
+82 -17
View File
@@ -12,7 +12,7 @@
vLLM-Omni supports various advanced features for diffusion models:
- Acceleration: **cache methods**, **parallelism methods**
- Acceleration: **cache methods**, **parallelism methods**, **startup optimizations**
- Memory optimization: **cpu offloading**, **quantization**
- Extensions: **LoRA inference**
- Execution modes: **step execution**
@@ -44,6 +44,12 @@ Parallelism methods distribute computation across GPUs without quality loss (mat
| **[HSDP](diffusion/parallelism/hsdp.md)** | Weight sharding via FSDP2, redistributed on-demand at runtime | Very large models (14B+) on limited VRAM, combinable with SP |
| **[Expert Parallelism](diffusion/parallelism/expert_parallel.md)** | Shards MoE expert MLP blocks across devices | MoE diffusion models (e.g., HunyuanImage3.0) |
#### Startup Optimization
| 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 |
**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.
### Memory Optimization
@@ -105,34 +111,36 @@ The following tables show which models support each feature:
| **FLUX.2-klein** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |
| **FLUX.1-Kontext-dev** | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **FLUX.2-dev** | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
| **LongCat-Image** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **LongCat-Image-Edit** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
| **LongCat-Image** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **LongCat-Image-Edit** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **MagiHuman** | ❌ | ❌ | ❌ | ❓ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **MammothModa2(T2I)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Nextstep_1(T2I)** | ❓ | ❓ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Nextstep_1(T2I)** | ❓ | ❓ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **OmniGen2** | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Ovis-Image** | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Qwen-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| **Qwen-Image-2512** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| **Qwen-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| **Qwen-Image-Edit-2509** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| **Qwen-Image-Layered** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| **Stable-Diffusion3.5** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Z-Image** | ✅ | ✅ | ✅ | ❓ | ✅ (TP=2 only) | ❌ | ❌ | ✅ | ✅ | ❌ |
| **Ovis-Image** | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **Qwen-Image** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ✅ | ✅ |
| **Qwen-Image-2512** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ✅ | ✅ |
| **Qwen-Image-Edit** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **Qwen-Image-Edit-2509** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ✅ | ❌ | ❌ |
| **Qwen-Image-Layered** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **Stable-Diffusion3.5** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ (decode) | ❌ | ❌ |
| **Z-Image** | ✅ | ✅ | ✅ | ❓ | ✅ (TP=2 only) | ✅ | ❌ | ✅ (decode) | ✅ | ❌ |
> Notes:
> 1. Nextstep_1(T2I) does not support cache acceleration methods such as TeaCache or Cache-DiT.
> 2. `Tongyi-MAI/Z-Image-Turbo` is a distilled model with minimal NFEs; CFG-Parallel is not necessary.
> 2. `Tongyi-MAI/Z-Image-Turbo` and `SII-GAIR/daVinci-MagiHuman-Base-1080p` are distilled models with minimal NFEs; CFG-Parallel is not necessary.
### VideoGen
| Model | ⚡TeaCache | ⚡Cache-DiT | 🔀SP (Ulysses & Ring) | 🔀CFG-Parallel | 🔀Tensor-Parallel | 🔀HSDP | 💾CPU Offload (Layerwise) | 💾VAE-Patch-Parallel | 💾Quantization | 🔄Step Execution |
|-------|:----------:|:-----------:|:---------------------:|:--------------:|:-----------------:|:------:|:------------------------:|:--------------------:|:--------------:|:----------------:|
| **Wan2.2** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| **Wan2.2** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (encode/decode) | ❌ | ❌ |
| **Wan2.1-VACE** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Helios** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ (decode) | ✅ | ❌ |
| **DreamID-Omni** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
### AudioGen
@@ -177,6 +185,59 @@ The following tables show which models support each feature:
6. Step Execution is not compatible with cache backends (TeaCache, Cache-DiT) or LoRA.
## 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** |
## Learn More
**Cache Acceleration:**
@@ -202,6 +263,10 @@ The following tables show which models support each feature:
- **[Step Execution Guide](diffusion/step_execution.md)** - Per-step denoise execution with mid-request abort support
**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
@@ -159,7 +159,7 @@ curl -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Hello, how are you?",
"speaker": "vivian",
"voice": "vivian",
"language": "English"
}' --output output.wav
@@ -168,7 +168,7 @@ curl -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "I am so excited!",
"speaker": "vivian",
"voice": "vivian",
"instructions": "Speak with great enthusiasm"
}' --output excited.wav
@@ -185,7 +185,7 @@ client = OpenAI(base_url="http://localhost:8091/v1", api_key="none")
response = client.audio.speech.create(
model="Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
speaker="vivian",
voice="vivian",
input="Hello, how are you?",
)
@@ -201,7 +201,7 @@ response = httpx.post(
"http://localhost:8091/v1/audio/speech",
json={
"input": "Hello, how are you?",
"speaker": "vivian",
"voice": "vivian",
"language": "English",
},
timeout=300.0,
@@ -237,12 +237,16 @@ List all available voices/speakers from the loaded model, including both built-i
"consent": "user_consent_id",
"created_at": 1738660000,
"file_size": 1024000,
"mime_type": "audio/wav"
"mime_type": "audio/wav",
"ref_text": "The exact transcript of the audio sample.",
"speaker_description": "warm narrator"
}
]
}
```
Fields `ref_text` and `speaker_description` are omitted per-entry when not provided at upload time.
#### POST /v1/audio/voices
Upload a new voice sample for voice cloning in Base task TTS requests.
@@ -252,6 +256,7 @@ Upload a new voice sample for voice cloning in Base task TTS requests.
- `consent` (required): Consent recording ID
- `name` (required): Name for the new voice
- `ref_text` (optional): Transcript of the audio. Enables in-context voice cloning (higher quality).
- `speaker_description` (optional): Free-form description of the voice (e.g. "warm narrator", "energetic presenter"). Stored as metadata.
**Response Example:**
```json
@@ -262,18 +267,23 @@ Upload a new voice sample for voice cloning in Base task TTS requests.
"consent": "user_consent_id",
"created_at": 1738660000,
"mime_type": "audio/wav",
"file_size": 1024000
"file_size": 1024000,
"ref_text": "The exact transcript of the audio sample.",
"speaker_description": "warm narrator"
}
}
```
Fields `ref_text` and `speaker_description` are omitted when not provided at upload time.
**Usage Example:**
```bash
curl -X POST http://localhost:8000/v1/audio/voices \
curl -X POST http://localhost:8091/v1/audio/voices \
-F "audio_sample=@/path/to/voice_sample.wav" \
-F "consent=user_consent_id" \
-F "name=custom_voice_1" \
-F "ref_text=The exact transcript of the audio sample."
-F "ref_text=The exact transcript of the audio sample." \
-F "speaker_description=warm narrator"
```
### Endpoint
@@ -290,7 +300,7 @@ This endpoint follows the [OpenAI Audio Speech API](https://platform.openai.com/
```json
{
"input": "Text to synthesize",
"speaker": "vivian",
"voice": "vivian",
"response_format": "wav",
"task_type": "CustomVoice",
"language": "Auto",
@@ -310,7 +320,7 @@ Returns binary audio data with appropriate `Content-Type` header (e.g., `audio/w
### Voice and language (summary)
- **Speaker**: Use the `speaker` request field to select the speaker (e.g., `vivian`, `ryan`, `aiden`). List available speakers with `GET /v1/audio/voices`.
- **Speaker**: Use the `voice` request field to select the speaker (e.g., `vivian`, `ryan`, `aiden`). List available speakers with `GET /v1/audio/voices`.
- **Language**: Use the `language` field for the codec language tag (`Auto`, `Chinese`, `English`, etc.). Default is `Auto` for automatic detection.
- **CustomVoice**: Requires a valid `voice` from the model’s speaker set. **VoiceDesign**: Use `instructions` to describe the voice. **Base**: Use `ref_audio` and `ref_text` for voice cloning.
@@ -322,7 +332,7 @@ Returns binary audio data with appropriate `Content-Type` header (e.g., `audio/w
| ----------------- | ------ | -------------- | ----------------------------------------------------------- |
| `input` | string | **required** | Text to synthesize |
| `model` | string | server's model | Model to use (optional, should match server if specified) |
| `speaker` | string | "vivian" | Speaker name (e.g., vivian, ryan, aiden) |
| `voice` | string | "vivian" | Speaker name (e.g., vivian, ryan, aiden) |
| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, aac, opus |
| `speed` | float | 1.0 | Playback speed (0.25-4.0, not supported with `stream=true`) |
@@ -357,7 +367,7 @@ curl -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Hello, how are you?",
"speaker": "vivian",
"voice": "vivian",
"language": "English",
"stream": true,
"response_format": "pcm"
+51 -6
View File
@@ -2,6 +2,10 @@ import argparse
import os
from vllm_omni.inputs.data import OmniPromptType
from vllm_omni.model_executor.stage_input_processors.bagel import (
GEN_THINK_SYSTEM_PROMPT,
VLM_THINK_SYSTEM_PROMPT,
)
def parse_args():
@@ -65,6 +69,17 @@ def parse_args():
help="CFG parallel size: 1=batched (single GPU), 2=parallel with 2 branches (text CFG only), 3=parallel (3 GPUs).",
)
parser.add_argument("--seed", type=int, default=None, help="Random seed for generation.")
parser.add_argument(
"--cfg-interval",
type=float,
nargs=2,
default=None,
help="CFG interval [start, end] (default: pipeline default)",
)
parser.add_argument(
"--cfg-renorm-type", type=str, default=None, help="CFG renorm type: global, text_channel, channel"
)
parser.add_argument("--cfg-renorm-min", type=float, default=None, help="CFG renorm min")
parser.add_argument(
"--enable-diffusion-pipeline-profiler",
action="store_true",
@@ -76,6 +91,12 @@ def parse_args():
default=None,
help="Quantization method (e.g. 'fp8').",
)
parser.add_argument(
"--think",
action="store_true",
default=False,
help="Enable thinking mode: AR stage decodes <think>...</think> planning tokens before image generation.",
)
args = parser.parse_args()
return args
@@ -110,8 +131,12 @@ def main():
from vllm_omni.entrypoints.omni import Omni
omni_kwargs = {}
if args.stage_configs_path:
omni_kwargs["stage_configs_path"] = args.stage_configs_path
stage_configs_path = args.stage_configs_path
if args.think and stage_configs_path is None:
stage_configs_path = "vllm_omni/model_executor/stage_configs/bagel_think.yaml"
print(f"[Info] Think mode enabled, using stage config: {stage_configs_path}")
if stage_configs_path:
omni_kwargs["stage_configs_path"] = stage_configs_path
omni_kwargs.update(
{
@@ -136,7 +161,8 @@ def main():
if not args.image_path or not os.path.exists(args.image_path):
raise ValueError(f"img2img requires --image-path pointing to an existing file, got: {args.image_path}")
loaded_image = Image.open(args.image_path).convert("RGB")
final_prompt_text = f"<|fim_middle|><|im_start|>{p}<|im_end|>"
think_prefix = f"<|im_start|>{GEN_THINK_SYSTEM_PROMPT}<|im_end|>" if args.think else ""
final_prompt_text = f"{think_prefix}<|fim_middle|><|im_start|>{p}<|im_end|>"
prompt_dict = {
"prompt": final_prompt_text,
"multi_modal_data": {"img2img": loaded_image},
@@ -148,7 +174,10 @@ def main():
elif args.modality == "img2text":
if args.image_path:
loaded_image = Image.open(args.image_path).convert("RGB")
final_prompt_text = f"<|im_start|>user\n<|image_pad|>\n{p}<|im_end|>\n<|im_start|>assistant\n"
think_prefix = f"<|im_start|>system\n{VLM_THINK_SYSTEM_PROMPT}<|im_end|>\n" if args.think else ""
final_prompt_text = (
f"{think_prefix}<|im_start|>user\n<|image_pad|>\n{p}<|im_end|>\n<|im_start|>assistant\n"
)
prompt_dict = {
"prompt": final_prompt_text,
"multi_modal_data": {"image": loaded_image},
@@ -156,11 +185,13 @@ def main():
}
formatted_prompts.append(prompt_dict)
elif args.modality == "text2text":
final_prompt_text = f"<|im_start|>user\n{p}<|im_end|>\n<|im_start|>assistant\n"
think_prefix = f"<|im_start|>{VLM_THINK_SYSTEM_PROMPT}<|im_end|>" if args.think else ""
final_prompt_text = f"{think_prefix}<|im_start|>{p}<|im_end|><|im_start|>"
prompt_dict = {"prompt": final_prompt_text, "modalities": ["text"]}
formatted_prompts.append(prompt_dict)
else:
final_prompt_text = f"<|im_start|>{p}<|im_end|>"
think_prefix = f"<|im_start|>{GEN_THINK_SYSTEM_PROMPT}<|im_end|>" if args.think else ""
final_prompt_text = f"{think_prefix}<|im_start|>{p}<|im_end|>"
prompt_dict = {"prompt": final_prompt_text, "modalities": ["image"]}
if args.negative_prompt is not None:
prompt_dict["negative_prompt"] = args.negative_prompt
@@ -178,6 +209,12 @@ def main():
"cfg_text_scale": args.cfg_text_scale,
"cfg_img_scale": args.cfg_img_scale,
}
if args.cfg_interval is not None:
extra["cfg_interval"] = tuple(args.cfg_interval)
if args.cfg_renorm_type is not None:
extra["cfg_renorm_type"] = args.cfg_renorm_type
if args.cfg_renorm_min is not None:
extra["cfg_renorm_min"] = args.cfg_renorm_min
if args.negative_prompt is not None:
extra["negative_prompt"] = args.negative_prompt
diffusion_params.extra_args = extra # type: ignore
@@ -186,6 +223,13 @@ def main():
img_idx = 0
for req_output in omni_outputs:
if args.think:
ro = getattr(req_output, "request_output", None)
if ro and getattr(ro, "outputs", None):
txt = "".join(getattr(o, "text", "") or "" for o in ro.outputs)
if txt:
print(txt)
images = getattr(req_output, "images", None)
if not images:
@@ -194,6 +238,7 @@ def main():
for j, img in enumerate(images):
save_path = os.path.join(args.output, f"output_{img_idx}_{j}.png")
img.save(save_path)
print(f"[Output] Saved image to {save_path}")
img_idx += 1
print(omni_outputs)
@@ -0,0 +1,110 @@
# Dynin-Omni Offline End2End Example
This folder contains a unified offline inference entrypoint:
- `end2end.py`
## 1. Environment Setup
Run from repository root:
```bash
cd <REPO_ROOT>
```
If needed, install this repo in editable mode:
```bash
pip install -e .
```
## 2. Extra Dependencies (EMOVA)
Install the following packages for EMOVA-related components:
```bash
pip install \
"phonemizer==3.3.0" \
"Unidecode==1.4.0" \
"hydra-core==1.3.2" \
"pytorch-lightning==1.1.0" \
"wget==3.2" \
"wrapt==2.1.1" \
"onnx==1.20.1" \
"frozendict==2.4.7" \
"inflect==7.5.0" \
"braceexpand==0.1.7" \
"webdataset==1.0.2" \
"torch-stft==0.1.4" \
"editdistance==0.8.1"
```
## 3. Hardware and VRAM Requirements
This example uses a 3-stage pipeline on one GPU by default
([`dynin_omni.yaml`](../../../vllm_omni/model_executor/stage_configs/dynin_omni.yaml)):
- Stage-0 (`token2text`): `gpu_memory_utilization: 0.5`
- Stage-1 (`token2image`): `gpu_memory_utilization: 0.1`
- Stage-2 (`token2audio`): `gpu_memory_utilization: 0.1`
### Requested GPU Memory Budget from `gpu_memory_utilization`
| Stage | Utilization | A100 80GB | H200 141GB |
| :-- | :-- | :-- | :-- |
| Stage-0 (token2text) | 0.5 | ~40.0 GB | ~70.5 GB |
| Stage-1 (token2image) | 0.1 | ~8.0 GB | ~14.1 GB |
| Stage-2 (token2audio) | 0.1 | ~8.0 GB | ~14.1 GB |
| Total requested budget | 0.7 | ~56.0 GB | ~98.7 GB |
### Observed Runtime Signal (from your log)
- Stage-0 reported: `Model loading took 15.12 GiB memory` (weights footprint signal).
- Stages 1/2 can still add runtime memory depending on task path and backend allocations.
- Keep extra headroom for CUDA/PyTorch overhead and temporary allocations.
### GPU Compatibility
- Confirmed target GPUs for this setup: **NVIDIA H200**, **NVIDIA A100**.
- CI/e2e coverage in this repo also includes CUDA **L4** markers for Dynin tests.
## 4. End2End Run Examples
```bash
# t2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2t --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
# i2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task i2t --model snu-aidas/Dynin-Omni --image <IMAGE_PATH> --text "Please describe this image in detail."
# s2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task s2t --model snu-aidas/Dynin-Omni --audio <AUDIO_PATH> --text "Transcribe the given audio."
# t2i
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2i --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
# v2t
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task v2t --model snu-aidas/Dynin-Omni --video <VIDEO_PATH> --text "Describe this video in detail."
# i2i
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task i2i --model snu-aidas/Dynin-Omni --image <IMAGE_PATH> --text <INSTRUCTION_TEXT>
# t2s
python <REPO_ROOT>/examples/offline_inference/dynin_omni/end2end.py \
--task t2s --model snu-aidas/Dynin-Omni --text <INSTRUCTION_TEXT>
```
## 5. Notes
- Outputs are saved under task-specific directories in `/tmp` by default.
- You can override output path with `--output-dir`.
- If you want to force local config resolution, pass `--dynin-config-path <PATH_TO_DYNIN_OMNI_YAML>`.
- If you see the warning
`max_num_batched_tokens (32768) exceeds max_num_seqs * max_model_len (4096)`,
reduce `max_num_batched_tokens` in stage config (for example, `4096` in CI config).
File diff suppressed because it is too large. Load diff
@@ -51,5 +51,6 @@ Key arguments:
- `--vae-use-tiling`: enable VAE tiling for memory optimization.
- `--cfg-parallel-size`: set it to 2 to enable CFG Parallel. See more examples in [`user_guide`](../../../docs/user_guide/diffusion/parallelism_acceleration.md#cfg-parallel).
- `--enable-cpu-offload`: enable CPU offloading for diffusion models.
- `--strength`: **Z-Image only** - controls the denoising start timestep for I2I (default: 0.6). Range: [0.0, 1.0]. Lower values preserve more of the original image; higher values allow more creative changes.
> ℹ️ If you encounter OOM errors, try using `--vae-use-slicing` and `--vae-use-tiling` to reduce memory usage.
@@ -146,7 +146,7 @@ def parse_args() -> argparse.Namespace:
"--audio-sample-rate",
type=int,
default=24000,
help="Sample rate for audio output when saved (default: 24000 for LTX2).",
help="Sample rate for audio output when saved (default: 24000).",
)
parser.add_argument(
"--cache-backend",
@@ -471,15 +471,9 @@ def main():
video_array = _ensure_frame_list(video_array)
use_ltx2_export = is_ltx2
encode_video = None
if use_ltx2_export:
try:
from diffusers.pipelines.ltx2.export_utils import encode_video
except ImportError:
encode_video = None
if audio is not None:
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
if use_ltx2_export and encode_video is not None:
if isinstance(video_array, list):
frames_np = np.stack(video_array, axis=0)
elif isinstance(video_array, np.ndarray):
@@ -490,29 +484,24 @@ def main():
if frames_np.ndim == 4 and frames_np.shape[-1] == 4:
frames_np = frames_np[..., :3]
frames_np = np.clip(frames_np, 0.0, 1.0)
frames_u8 = (frames_np * 255).round().clip(0, 255).astype("uint8")
video_tensor = torch.from_numpy(frames_u8)
frames_u8 = (np.clip(frames_np, 0.0, 1.0) * 255).round().clip(0, 255).astype("uint8")
audio_out = None
if audio is not None:
if isinstance(audio, list):
audio = audio[0] if audio else None
if isinstance(audio, np.ndarray):
audio = torch.from_numpy(audio)
if isinstance(audio, torch.Tensor):
audio_out = audio
if audio_out.dim() > 1:
audio_out = audio_out[0]
audio_out = audio_out.float().cpu()
audio_np = audio
if isinstance(audio_np, list):
audio_np = audio_np[0] if audio_np else None
if isinstance(audio_np, torch.Tensor):
audio_np = audio_np.detach().cpu().float().numpy()
if isinstance(audio_np, np.ndarray):
audio_np = np.squeeze(audio_np).astype(np.float32)
encode_video(
video_tensor,
fps=fps,
audio=audio_out,
audio_sample_rate=args.audio_sample_rate if audio_out is not None else None,
output_path=str(output_path),
video_bytes = mux_video_audio_bytes(
frames_u8,
audio_np,
fps=float(fps),
audio_sample_rate=args.audio_sample_rate,
)
with open(str(output_path), "wb") as f:
f.write(video_bytes)
else:
export_to_video(video_array, str(output_path), fps=fps)
print(f"Saved generated video to {output_path}")
@@ -0,0 +1,72 @@
# MagiHuman Generation
MagiHuman is an advanced, omni-modality model that generates both high-quality video and lip-synced audio from a text prompt.
Because MagiHuman is a very large model featuring a powerful DiT MoE backbone and a ~9B parameter T5Gemma text encoder, it natively supports **Tensor Parallelism (TP)** in vLLM-Omni to run efficiently across multi-GPU setups, reducing device memory bottlenecks.
## Setup
### Install MagiCompiler (recommended)
MagiHuman relies on [MagiCompiler](https://github.com/SandAI-org/MagiCompiler) for custom-op registration used by the DiT attention kernels. While the pipeline can fall back to stub implementations, installing MagiCompiler is **strongly recommended** for correct behaviour.
```bash
# Clone the repo
git clone https://github.com/SandAI-org/MagiCompiler.git
cd MagiCompiler
# System dependencies (optional, for FX graph visualization; Debian/Ubuntu)
sudo apt update && sudo apt install -y graphviz
# Python dependencies
pip install -r requirements.txt
# Install MagiCompiler
pip install . # end users (recommended)
# pip install -e . # developers (editable install)
```
### Hardware requirements
Ensure your hardware has enough VRAM. For a standard node with 80GB GPUs, running with `--tensor-parallel-size 4` is recommended to shard both the MoE weights and the T5Gemma text encoder across 4 GPUs, reducing the per-GPU peak VRAM overhead significantly (by roughly ~13.5GB per GPU compared to single-device inference).
Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) for further details on allocating memory.
## Run Examples
Get into the example folder:
```bash
cd examples/offline_inference/magi_human
```
### End-to-End Generation (Text to Video+Audio)
Generate a video with synchronized speech natively generated by the model.
```bash
python end2end.py \
--model /proj-tango-pvc/users/zhipeng.wang/workspace/models/daVinci-MagiHuman \
--prompt "A young woman with long, wavy golden blonde hair..." \
--tensor-parallel-size 4 \
--output output_magihuman.mp4
```
## Common Parameters
| Parameter | Default | Description |
|-----------|---------|-------------|
| `--model` | *(Required)* | Local model path or HuggingFace ID |
| `--prompt` | *(built-in demo prompt)* | Highly detailed text prompt dictating visual look and dialogue text |
| `--tensor-parallel-size` | `4` | Tensor parallelism size (Number of GPUs) |
| `--height` | `256` | Initial resolution height |
| `--width` | `448` | Initial resolution width |
| `--num-inference-steps` | `8` | Denoising steps |
| `--seed` | `52` | Random seed |
| `--output` | `output_magihuman.mp4` | Output video with audio path |
## Example materials
??? abstract "end2end.py"
``````py
--8<-- "examples/offline_inference/magi_human/end2end.py"
``````
@@ -0,0 +1,122 @@
import argparse
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
def parse_args():
parser = argparse.ArgumentParser(description="End-to-end inference script for MagiHuman.")
parser.add_argument("--model", type=str, required=True, help="Path or ID of the MagiHuman model.")
parser.add_argument(
"--prompt",
type=str,
default="",
help="Text prompt containing visual description, dialogue, and background sound.",
)
parser.add_argument(
"--tensor-parallel-size", "-tp", type=int, default=4, help="Tensor parallel size (number of GPUs)."
)
parser.add_argument(
"--output", type=str, default="output_magihuman.mp4", help="Path to save the generated mp4 file."
)
parser.add_argument("--height", type=int, default=256, help="Video height.")
parser.add_argument("--width", type=int, default=448, help="Video width.")
parser.add_argument("--num-inference-steps", type=int, default=8, help="Number of denoising steps.")
parser.add_argument("--seed", type=int, default=52, help="Random seed for generation.")
return parser.parse_args()
def main():
args = parse_args()
print(f"Initializing MagiHuman pipeline with TP={args.tensor_parallel_size}...")
omni = Omni(
model=args.model,
init_timeout=1200,
tensor_parallel_size=args.tensor_parallel_size,
devices=list(range(args.tensor_parallel_size)),
)
prompt = args.prompt
if not prompt:
prompt = (
"A young woman with long, wavy golden blonde hair and bright blue eyes, "
"wearing a fitted ivory silk blouse with a delicate lace collar, sits "
"stationary in front of a softly lit, blurred warm-toned interior. Her "
"overall disposition is warm, composed, and gently confident. The camera "
"holds a static medium close-up, framing her from the shoulders up, "
"with shallow depth of field keeping her face in sharp focus. Soft "
"directional key light falls from the upper left, casting a gentle "
"highlight along her cheekbone and nose bridge. She draws a quiet breath, "
"the levator labii superiors relaxing as her lips part. She speaks in "
"clear, warm, unhurried American English: "
"\"The most beautiful things in life aren't things at all — "
"they're moments, feelings, and the people who make you feel truly alive.\" "
"Her jaw descends smoothly on each stressed syllable; the orbicularis oris "
"shapes each vowel with precision. A faint, genuine smile engages the "
"zygomaticus major, lifting her lip corners fractionally. Her brows rest "
"in a soft, neutral arch throughout. She maintains steady, forward-facing "
"eye contact. Head position remains level; no torso displacement occurs.\n\n"
"Dialogue:\n"
"<Young blonde woman, American English>: "
"\"The most beautiful things in life aren't things at all — "
"they're moments, feelings, and the people who make you feel truly alive.\"\n\n"
"Background Sound:\n"
"<Soft, warm indoor ambience with a faint distant piano melody>"
)
sampling_params = OmniDiffusionSamplingParams(
height=args.height,
width=args.width,
num_inference_steps=args.num_inference_steps,
seed=args.seed,
extra_args={
"seconds": 5,
"sr_height": 1080,
"sr_width": 1920,
"sr_num_inference_steps": 5,
},
)
print(f"Generating with prompt: {prompt[:80]}...")
outputs = omni.generate(
prompts=[prompt],
sampling_params_list=[sampling_params],
)
print(f"Generation complete. Output type: {type(outputs)}")
if outputs:
first = outputs[0]
if hasattr(first, "images") and first.images:
video_frames = first.images[0]
print(f"Video frames: shape={video_frames.shape}, dtype={video_frames.dtype}")
audio_waveform = None
mm = first.multimodal_output or {}
if mm:
audio_waveform = mm.get("audio")
if audio_waveform is not None:
print(f"Audio waveform: shape={audio_waveform.shape}, dtype={audio_waveform.dtype}")
output_fps = float(mm.get("fps", 25))
output_sr = int(mm.get("audio_sample_rate", 24000))
print(f"Using fps={output_fps}, audio_sample_rate={output_sr} from model output")
video_bytes = mux_video_audio_bytes(
video_frames,
audio_waveform,
fps=output_fps,
audio_sample_rate=output_sr,
)
with open(args.output, "wb") as f:
f.write(video_bytes)
print(f"Saved MP4 ({len(video_bytes)} bytes) to {args.output}")
print("SUCCESS: MagiHuman pipeline generation completed.")
else:
print("WARNING: No outputs returned.")
if __name__ == "__main__":
main()
@@ -0,0 +1,73 @@
# OmniVoice
This directory contains an offline demo for running OmniVoice TTS models with vLLM Omni. It generates speech from text and saves WAV files locally.
## Model Overview
[OmniVoice](https://huggingface.co/k2-fsa/OmniVoice) is a zero-shot multilingual TTS model supporting 600+ languages. It uses a diffusion language model (Qwen3-0.6B backbone) with iterative masked unmasking to generate speech.
Three inference modes are supported:
- **Auto Voice**: Generate speech without any reference — the model picks a voice automatically.
- **Voice Clone**: Clone a voice from a reference audio + transcription.
- **Voice Design**: Control voice style via natural language instruction (e.g., "female, low pitch, british accent").
## Setup
Ensure the model is downloaded:
```bash
huggingface-cli download k2-fsa/OmniVoice
```
> **Note:** Voice cloning requires `transformers>=5.3.0` for `HiggsAudioV2TokenizerModel`. Auto voice and voice design modes work with `transformers>=4.57.0`.
## Quick Start
Auto voice (text only):
```bash
python end2end.py --model k2-fsa/OmniVoice --text "Hello, this is a test."
```
Voice design (with style instruction):
```bash
python end2end.py --model k2-fsa/OmniVoice \
--text "Hello, this is a test." \
--instruct "female, low pitch, british accent"
```
Voice clone (with reference audio):
```bash
python end2end.py --model k2-fsa/OmniVoice \
--text "Hello, this is a test." \
--ref-audio ref.wav \
--ref-text "This is the reference transcription."
```
## Language Support
Specify a language for improved quality:
```bash
python end2end.py --model k2-fsa/OmniVoice \
--text "你好,这是一个测试。" \
--lang zh
```
## Architecture
OmniVoice uses a two-stage pipeline:
- **Stage 0 (Generator)**: Qwen3-0.6B transformer with 32-step iterative unmasking and classifier-free guidance. Generates 8-codebook audio tokens from text.
- **Stage 1 (Decoder)**: HiggsAudioV2 RVQ quantizer + DAC acoustic decoder. Converts tokens to 24kHz waveform.
Both stages use `GPUGenerationWorker` with `OmniGenerationScheduler`.
## Notes
- Output audio is saved to `output.wav` by default. Use `--output` to change the path.
- The model estimates duration from text automatically via `RuleDurationEstimator`.
- Use `--stage-init-timeout` to increase the stage initialization timeout for first-time model downloads.
@@ -0,0 +1,164 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""End-to-end OmniVoice TTS inference via vLLM-Omni.
Supports:
- Auto voice mode: text only → generated speech
- Voice cloning mode: text + reference audio → cloned voice speech
Usage:
# Auto voice
python end2end.py --model k2-fsa/OmniVoice --text "Hello world"
# Voice cloning
python end2end.py --model k2-fsa/OmniVoice --text "Hello" \
--ref-audio ref.wav --ref-text "reference transcription"
"""
import argparse
import os
import numpy as np
import soundfile as sf
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
def run_e2e():
parser = argparse.ArgumentParser(description="OmniVoice E2E TTS inference")
parser.add_argument(
"--model",
type=str,
default="k2-fsa/OmniVoice",
help="Model name or path (HuggingFace or local)",
)
parser.add_argument(
"--stage-config",
type=str,
default="vllm_omni/model_executor/stage_configs/omnivoice.yaml",
)
parser.add_argument(
"--text",
type=str,
default="Hello, this is a test of the OmniVoice text to speech system.",
)
parser.add_argument(
"--ref-audio",
type=str,
default=None,
help="Reference audio for voice cloning (WAV file)",
)
parser.add_argument(
"--ref-text",
type=str,
default=None,
help="Transcription of reference audio",
)
parser.add_argument(
"--lang",
type=str,
default=None,
help="Language code (e.g., 'en', 'zh')",
)
parser.add_argument(
"--instruct",
type=str,
default=None,
help="Voice design instruction (e.g., 'female, low pitch, british accent')",
)
parser.add_argument(
"--output",
type=str,
default="output.wav",
help="Output audio file path",
)
parser.add_argument(
"--stage-init-timeout",
type=int,
default=600,
help="Stage initialization timeout in seconds",
)
args = parser.parse_args()
if not os.path.exists(args.stage_config):
raise FileNotFoundError(f"Stage config not found: {args.stage_config}")
print(f"Initializing OmniVoice with model={args.model}")
omni = Omni(
model=args.model,
stage_configs_path=args.stage_config,
trust_remote_code=True,
log_stats=True,
)
print("Model initialized. Preparing inputs...")
# Build prompt
mm_processor_kwargs = {}
multi_modal_data = {}
if args.ref_audio:
if not os.path.exists(args.ref_audio):
raise FileNotFoundError(f"Reference audio not found: {args.ref_audio}")
import librosa
audio_signal, sr = librosa.load(args.ref_audio, sr=None)
multi_modal_data["audio"] = (audio_signal.astype(np.float32), sr)
mm_processor_kwargs["ref_text"] = args.ref_text or ""
mm_processor_kwargs["sample_rate"] = sr
if args.lang:
mm_processor_kwargs["lang"] = args.lang
if args.instruct:
mm_processor_kwargs["instruct"] = args.instruct
prompts = {"prompt": args.text}
if multi_modal_data:
prompts["multi_modal_data"] = multi_modal_data
if mm_processor_kwargs:
prompts["mm_processor_kwargs"] = mm_processor_kwargs
sampling_params_list = [OmniDiffusionSamplingParams()]
print(f"Generating speech for: {args.text}")
outputs = list(omni.generate(prompts, sampling_params_list=sampling_params_list))
print(f"Received {len(outputs)} outputs.")
for i, output in enumerate(outputs):
try:
ro = output.request_output
if ro is None:
print("No request_output found.")
continue
mm = getattr(ro, "multimodal_output", None)
if not mm and ro.outputs:
mm = getattr(ro.outputs[0], "multimodal_output", None)
if mm:
print(f"Multimodal output keys: {mm.keys()}")
if "audio" in mm:
audio_out = mm["audio"]
sr = mm.get("sr", 24000)
if isinstance(audio_out, np.ndarray):
audio_np = audio_out
else:
audio_np = audio_out.cpu().numpy().squeeze()
out_path = args.output if i == 0 else f"output_{i}.wav"
sf.write(out_path, audio_np, sr)
print(f"Saved audio to {out_path} ({sr}Hz, {len(audio_np) / sr:.2f}s)")
else:
print("No multimodal output found.")
except Exception as e:
print(f"Error inspecting output: {e}")
omni.close()
print("Done.")
if __name__ == "__main__":
run_e2e()
@@ -33,6 +33,7 @@ This folder provides several entrypoints for experimenting with text-to-image di
| `black-forest-labs/FLUX.2-klein-4B` | 1024 x 1024 | 72.7 | 14.9 |
| `black-forest-labs/FLUX.2-klein-9B` | 1024 x 1024 | 37.1 | 32.3 |
| `black-forest-labs/FLUX.2-dev` | 1024 x 1024 | 65.7 | >80 (CPU offload required) |
| `HunyuanImage-3.0` | 1024 x 1024 | 80.0 (TP≥3) | 160 |
!!! info
*Peak VRAM: based on basic single-card usage, batch size =1, without any acceleration/optimization features. FLUX.2-dev requires `--enable-cpu-offload` on a single 80 GiB GPU.
@@ -90,6 +91,8 @@ python text_to_image.py \
| `--enable-cpu-offload` | flag | off | Enable CPU offloading for diffusion models |
| `--lora-path` | str | — | Path to PEFT LoRA adapter folder |
| `--lora-scale` | float | `1.0` | Scale factor for LoRA weights |
| `--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.|
**NextStep-1.1 specific arguments:**
@@ -242,6 +242,19 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Enable logging of diffusion pipeline stats.",
)
parser.add_argument(
"--use-system-prompt",
type=str,
default=None,
choices=["None", "dynamic", "en_vanilla", "en_recaption", "en_think_recaption", "en_unified", "custom"],
help="System prompt preset for generation. Recommended: en_unified.",
)
parser.add_argument(
"--system-prompt",
type=str,
default=None,
help=("Custom system prompt. Used when --use-system-prompt is custom. "),
)
return parser.parse_args()
@@ -363,7 +376,7 @@ def main():
f"vae_patch_parallel_size={args.vae_patch_parallel_size}, "
f"enable_expert_parallel={args.enable_expert_parallel}."
)
print(f" CPU offload: {args.enable_cpu_offload}")
print(f" CPU offload: {args.enable_cpu_offload}; CPU Layerwise Offload: {args.enable_layerwise_offload}")
print(f" Image size: {args.width}x{args.height}")
if args.lora_path:
print(f" LoRA: scale={args.lora_scale}")
@@ -382,13 +395,13 @@ def main():
)
generation_start = time.perf_counter()
extra_args = {
"timesteps_shift": args.timesteps_shift,
"cfg_schedule": args.cfg_schedule,
"use_norm": args.use_norm,
"use_system_prompt": args.use_system_prompt,
"system_prompt": args.system_prompt,
}
if lora_request:
extra_args["lora_request"] = lora_request
extra_args["lora_scale"] = args.lora_scale
@@ -56,8 +56,13 @@ def parse_args() -> argparse.Namespace:
"Examples: Wan-AI/Wan2.2-T2V-A14B-Diffusers, "
"hunyuanvideo-community/HunyuanVideo-1.5-480p_t2v",
)
parser.add_argument(
"--model-class-name",
default=None,
help="Override model class name (e.g., LTX2TwoStagesVideoPipeline).",
)
parser.add_argument("--prompt", default="A serene lakeside sunrise with mist over the water.", help="Text prompt.")
parser.add_argument("--negative-prompt", default="", help="Negative prompt (Wan2.2 only).")
parser.add_argument("--negative-prompt", default="", help="Negative prompt.")
parser.add_argument("--seed", type=int, default=42, help="Random seed.")
parser.add_argument("--guidance-scale", type=float, default=None, help="CFG scale. Default: model-specific.")
parser.add_argument(
@@ -155,7 +160,7 @@ def parse_args() -> argparse.Namespace:
"--audio-sample-rate",
type=int,
default=24000,
help="Sample rate for audio output when saved (default: 24000 for LTX2).",
help="Sample rate for audio output when saved (default: 24000).",
)
parser.add_argument(
"--vae-patch-parallel-size",
@@ -185,6 +190,7 @@ def parse_args() -> argparse.Namespace:
def main():
args = parse_args()
model_class_name = args.model_class_name
preset = _detect_preset(args.model)
for key, default_val in preset.items():
@@ -229,6 +235,7 @@ def main():
enable_cpu_offload=args.enable_cpu_offload,
parallel_config=parallel_config,
enforce_eager=args.enforce_eager,
model_class_name=model_class_name,
cache_backend=args.cache_backend,
cache_config=cache_config,
enable_diffusion_pipeline_profiler=args.enable_diffusion_pipeline_profiler,
@@ -423,17 +430,8 @@ def main():
video_array = _ensure_frame_list(video_array)
use_ltx2_export = False
if args.model and "ltx" in str(args.model).lower():
use_ltx2_export = True
if audio is not None:
use_ltx2_export = True
if use_ltx2_export:
try:
from diffusers.pipelines.ltx2.export_utils import encode_video
except ImportError:
raise ImportError("diffusers is required for LTX2 encode_video.")
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
if isinstance(video_array, list):
frames_np = np.stack(video_array, axis=0)
@@ -442,28 +440,24 @@ def main():
else:
frames_np = np.asarray(video_array)
frames_u8 = (frames_np * 255).round().clip(0, 255).astype("uint8")
video_tensor = torch.from_numpy(frames_u8)
frames_u8 = (np.clip(frames_np, 0.0, 1.0) * 255).round().clip(0, 255).astype("uint8")
audio_out = None
if audio is not None:
if isinstance(audio, list):
audio = audio[0] if audio else None
if isinstance(audio, np.ndarray):
audio = torch.from_numpy(audio)
if isinstance(audio, torch.Tensor):
audio_out = audio
if audio_out.dim() > 1:
audio_out = audio_out[0]
audio_out = audio_out.float().cpu()
audio_np = audio
if isinstance(audio_np, list):
audio_np = audio_np[0] if audio_np else None
if isinstance(audio_np, torch.Tensor):
audio_np = audio_np.detach().cpu().float().numpy()
if isinstance(audio_np, np.ndarray):
audio_np = np.squeeze(audio_np).astype(np.float32)
encode_video(
video_tensor,
fps=args.fps,
audio=audio_out,
audio_sample_rate=args.audio_sample_rate if audio_out is not None else None,
output_path=str(output_path),
video_bytes = mux_video_audio_bytes(
frames_u8,
audio_np,
fps=float(args.fps),
audio_sample_rate=args.audio_sample_rate,
)
with open(str(output_path), "wb") as f:
f.write(video_bytes)
else:
export_to_video(video_array, str(output_path), fps=args.fps)
print(f"Saved generated video to {output_path}")
@@ -0,0 +1,88 @@
# VACE Video Generation
[VACE](https://github.com/ali-vilab/VACE) (Video All-in-one Creation Engine) supports multiple video tasks through a single model.
| Model | Architecture | Model Weights (bf16) | HuggingFace |
|-------|-------------|----------------------|-------------|
| Wan2.1-VACE (1.3B) | Wan2.1 | ~10 GB | [Wan-AI/Wan2.1-VACE-1.3B-diffusers](https://huggingface.co/Wan-AI/Wan2.1-VACE-1.3B-diffusers) |
| Wan2.1-VACE (14B) | Wan2.1 | ~38 GB | [Wan-AI/Wan2.1-VACE-14B-diffusers](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) |
## Text-to-Video (T2V)
```bash
python vace_video_generation.py \
--mode t2v \
--prompt "A sleek robot stands in a vast warehouse filled with boxes" \
--height 480 --width 832 --num-frames 81 \
--num-inference-steps 30 --guidance-scale 5.0 --flow-shift 5.0 \
--output t2v_output.mp4
```
## Image-to-Video (I2V)
First frame is kept, remaining frames are generated:
```bash
python vace_video_generation.py \
--mode i2v \
--image astronaut.jpg \
--prompt "An astronaut emerging from a cracked egg on the moon" \
--height 480 --width 832 --num-frames 81 \
--output i2v_output.mp4
```
## First-Last-Frame Interpolation (FLF2V)
```bash
python vace_video_generation.py \
--mode flf2v \
--image first_frame.jpg --last-image last_frame.jpg \
--prompt "A bird takes off from a branch and lands on another" \
--height 512 --width 512 --num-frames 81 \
--output flf2v_output.mp4
```
## Inpainting
Center vertical stripe is masked and regenerated:
```bash
python vace_video_generation.py \
--mode inpaint \
--image scene.jpg \
--prompt "Shrek walks out of a building" \
--height 480 --width 832 --num-frames 81 \
--output inpaint_output.mp4
```
## Reference Image-guided (R2V)
```bash
python vace_video_generation.py \
--mode r2v \
--image reference.jpg \
--prompt "Camera slowly zooms out from the character" \
--height 480 --width 832 --num-frames 81 \
--output r2v_output.mp4
```
## Key Arguments
- `--mode`: VACE task mode (`t2v`, `i2v`, `flf2v`, `inpaint`, `r2v`).
- `--model`: Model ID (default: `Wan-AI/Wan2.1-VACE-1.3B-diffusers`).
- `--image`: Input image for I2V, inpainting, and R2V modes.
- `--last-image`: Last frame image for FLF2V mode.
- `--prompt`: Text description of desired video.
- `--height/--width`: Output resolution (default 480x832). Dimensions should be multiples of 16.
- `--num-frames`: Number of frames (default 81).
- `--guidance-scale`: CFG scale (default 5.0).
- `--flow-shift`: Scheduler flow shift (default 5.0).
- `--num-inference-steps`: Number of denoising steps (default 30).
- `--fps`: Frames per second for the saved MP4 (default 16).
- `--output`: Path to save the generated video.
- `--vae-use-tiling`: Enable VAE tiling for memory optimization.
- `--ulysses-degree`: Ulysses sequence parallelism degree for multi-GPU.
- `--cfg-parallel-size`: CFG parallel size for multi-GPU.
- `--tensor-parallel-size`: Tensor parallel size.
> If you encounter OOM errors, try `--vae-use-tiling` or multi-GPU parallelism options.
@@ -0,0 +1,209 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""VACE video generation example.
VACE (Video All-in-one Creation Engine) supports multiple video tasks:
- T2V: Text-to-Video
- I2V: Image-to-Video (first frame conditioning)
- V2LF: Video-to-Last-Frame
- FLF2V: First-Last-Frame interpolation
- Inpainting: Masked region generation
- R2V: Reference image-guided generation
Usage examples:
# T2V (text-to-video)
python vace_video_generation.py --mode t2v --prompt "A robot in a warehouse"
# I2V (image-to-video, first frame kept)
python vace_video_generation.py --mode i2v --image input.jpg --prompt "..."
# FLF2V (first-last frame interpolation)
python vace_video_generation.py --mode flf2v --image first.jpg --last-image last.jpg
# R2V (reference image guided)
python vace_video_generation.py --mode r2v --image ref.jpg --prompt "..."
"""
import argparse
import time
from pathlib import Path
import numpy as np
import PIL.Image
import torch
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.platforms import current_omni_platform
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="VACE video generation.")
parser.add_argument(
"--model",
default="Wan-AI/Wan2.1-VACE-14B-diffusers",
help="VACE model ID or local path.",
)
parser.add_argument(
"--mode",
default="t2v",
choices=["t2v", "i2v", "v2lf", "flf2v", "inpaint", "r2v"],
help="Generation mode.",
)
parser.add_argument("--prompt", default="A cat walking in a garden", help="Text prompt.")
parser.add_argument("--negative-prompt", default="", help="Negative prompt.")
parser.add_argument("--image", type=str, default=None, help="Input image path (for I2V, R2V, FLF2V, inpaint).")
parser.add_argument("--last-image", type=str, default=None, help="Last frame image path (for FLF2V).")
parser.add_argument("--video-dir", type=str, default=None, help="Directory of video frames (for inpaint).")
parser.add_argument("--seed", type=int, default=42, help="Random seed.")
parser.add_argument("--guidance-scale", type=float, default=5.0, help="CFG guidance scale.")
parser.add_argument("--height", type=int, default=480, help="Video height.")
parser.add_argument("--width", type=int, default=832, help="Video width.")
parser.add_argument("--num-frames", type=int, default=81, help="Number of frames.")
parser.add_argument("--num-inference-steps", type=int, default=30, help="Sampling steps.")
parser.add_argument("--flow-shift", type=float, default=5.0, help="Scheduler flow_shift.")
parser.add_argument("--output", type=str, default="vace_output.mp4", help="Output video path.")
parser.add_argument("--fps", type=int, default=16, help="Output video FPS.")
parser.add_argument("--vae-use-tiling", action="store_true", default=True, help="Enable VAE tiling.")
parser.add_argument("--enforce-eager", action="store_true", help="Disable torch.compile.")
parser.add_argument("--ulysses-degree", type=int, default=1, help="Ulysses SP degree.")
parser.add_argument("--ring-degree", type=int, default=1, help="Ring attention degree.")
parser.add_argument("--cfg-parallel-size", type=int, default=1, choices=[1, 2], help="CFG parallel size.")
return parser.parse_args()
def build_prompts(args):
"""Build prompt dict with multi_modal_data based on mode."""
h, w, nf = args.height, args.width, args.num_frames
gray = PIL.Image.new("RGB", (w, h), (128, 128, 128))
mask_black = PIL.Image.new("L", (w, h), 0)
mask_white = PIL.Image.new("L", (w, h), 255)
prompt_data = {
"prompt": args.prompt,
"negative_prompt": args.negative_prompt,
}
if args.mode == "t2v":
return prompt_data
if args.mode == "r2v":
assert args.image, "--image required for R2V mode"
ref_img = PIL.Image.open(args.image).convert("RGB").resize((w, h))
prompt_data["multi_modal_data"] = {"reference_images": [ref_img]}
return prompt_data
if args.mode == "i2v":
assert args.image, "--image required for I2V mode"
img = PIL.Image.open(args.image).convert("RGB").resize((w, h))
prompt_data["multi_modal_data"] = {
"video": [img] + [gray] * (nf - 1),
"mask": [mask_black] + [mask_white] * (nf - 1),
}
return prompt_data
if args.mode == "v2lf":
assert args.image, "--image required for V2LF mode"
img = PIL.Image.open(args.image).convert("RGB").resize((w, h))
prompt_data["multi_modal_data"] = {
"video": [gray] * (nf - 1) + [img],
"mask": [mask_white] * (nf - 1) + [mask_black],
}
return prompt_data
if args.mode == "flf2v":
assert args.image and args.last_image, "--image and --last-image required for FLF2V"
first = PIL.Image.open(args.image).convert("RGB").resize((w, h))
last = PIL.Image.open(args.last_image).convert("RGB").resize((w, h))
prompt_data["multi_modal_data"] = {
"video": [first] + [gray] * (nf - 2) + [last],
"mask": [mask_black] + [mask_white] * (nf - 2) + [mask_black],
}
return prompt_data
if args.mode == "inpaint":
assert args.image, "--image required for inpaint mode"
img = PIL.Image.open(args.image).convert("RGB").resize((w, h))
d = 80
frames, masks = [], []
for _ in range(nf):
base = np.array(img).copy()
mask = PIL.Image.new("L", (w, h), 0)
stripe = PIL.Image.new("L", (2 * d, h), 255)
mask.paste(stripe, (w // 2 - d, 0))
base[np.array(mask) > 128] = 128
frames.append(PIL.Image.fromarray(base))
masks.append(mask)
prompt_data["multi_modal_data"] = {"video": frames, "mask": masks}
return prompt_data
raise ValueError(f"Unknown mode: {args.mode}")
def main():
args = parse_args()
generator = torch.Generator(device=current_omni_platform.device_type).manual_seed(args.seed)
parallel_config = DiffusionParallelConfig(
ulysses_degree=args.ulysses_degree,
ring_degree=args.ring_degree,
cfg_parallel_size=args.cfg_parallel_size,
)
omni = Omni(
model=args.model,
vae_use_tiling=args.vae_use_tiling,
flow_shift=args.flow_shift,
enforce_eager=args.enforce_eager,
parallel_config=parallel_config,
)
prompt_data = build_prompts(args)
print(f"\n{'=' * 60}")
print(f"VACE {args.mode.upper()} Generation")
print(f" Model: {args.model}")
print(f" Size: {args.width}x{args.height}, {args.num_frames} frames, {args.num_inference_steps} steps")
print(f"{'=' * 60}\n")
start = time.perf_counter()
outputs = omni.generate(
prompt_data,
OmniDiffusionSamplingParams(
height=args.height,
width=args.width,
num_frames=args.num_frames,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
generator=generator,
),
)
elapsed = time.perf_counter() - start
video = outputs[0].images
if isinstance(video, list):
video = video[0]
if isinstance(video, torch.Tensor):
video = video.cpu().numpy()
if video.ndim == 5:
video = video[0]
print(f"Output shape: {video.shape}, Time: {elapsed:.1f}s")
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
from diffusers.utils import export_to_video
if np.issubdtype(video.dtype, np.integer):
video = video.astype(np.float32) / 255.0
export_to_video(list(video), str(output_path), fps=args.fps)
print(f"Saved to {output_path}")
omni.close()
if __name__ == "__main__":
main()
@@ -82,7 +82,6 @@ def main(output_dir: str):
data = {
"_class_name": "DreamIDOmniPipeline",
"fusion": "DreamID-Omni/dreamid_omni.safetensors",
}
with open(os.path.join(output_dir, "model_index.json"), "w", encoding="utf-8") as f:
@@ -90,6 +89,12 @@ def main(output_dir: str):
print(f"model_index.json created at {os.path.join(output_dir, 'model_index.json')}")
transformer_dir = os.path.join(output_dir, "transformer")
os.makedirs(transformer_dir, exist_ok=True)
with open(os.path.join(transformer_dir, "config.json"), "w", encoding="utf-8") as f:
json.dump({"fusion": "DreamID-Omni/dreamid_omni.safetensors"}, f)
print(f"transformer/config.json created at {os.path.join(transformer_dir, 'config.json')}")
# now we download the dependency code
download_dependency()
@@ -24,7 +24,9 @@ dreamid_omni/
│ ├── models_t5_umt5-xxl-enc-bf16.pth
│ ├── Wan2.2_VAE.pth
│
├── model_index.json # create by download_dreamid_omni.py
├── model_index.json
└── transformer/
└── config.json # create by download_dreamid_omni.py
```
### Run the Inference
@@ -132,8 +132,8 @@ def main() -> None:
if not outputs:
raise RuntimeError("No output returned from DreamID-Omni.")
output = outputs[0].request_output
generated_video = output[0].images[0][0]
generated_audio = output[0].images[0][1]
generated_video = output.images[0][0]
generated_audio = output.images[0][1]
try:
from dreamid_omni.utils.io_utils import save_video
except Exception as e:
@@ -0,0 +1,97 @@
# Dynin-Omni Online Serving Example
## Installation
Please refer to [README.md](../../../README.md).
## Launch the Server
First, find the `transformers_modules` path:
```bash
python - <<'PY'
from transformers.utils.hub import HF_MODULES_CACHE
print(HF_MODULES_CACHE)
PY
```
Then export it for both `PYTHONPATH` and `HF_MODULES_CACHE`:
```bash
export PYTHONPATH=<transformers_modules_path>:$PYTHONPATH
export HF_MODULES_CACHE=<transformers_modules_path>
```
Run from repository root:
```bash
vllm-omni serve snu-aidas/Dynin-Omni \
--omni \
--port 8091 \
--stage-configs-path "$(pwd)/vllm_omni/model_executor/stage_configs/dynin_omni.yaml"
```
If `vllm-omni` is not in PATH, run:
```bash
PYTHONPATH="$(pwd)" python -m vllm_omni.entrypoints.cli.main serve snu-aidas/Dynin-Omni \
--omni \
--port 8091 \
--stage-configs-path "$(pwd)/vllm_omni/model_executor/stage_configs/dynin_omni.yaml"
```
Wait until the server logs show both `All stages initialized successfully` and
`Application startup complete.` before sending requests.
## Send Requests via Python Client
Move to the example directory:
```bash
cd examples/online_serving/dynin_omni
```
### Text -> Image
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type t2i \
--prompt "A realistic indoor living room with natural daylight."
```
### Image -> Image
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type i2i \
--image-path ../../offline_inference/dynin_omni/data/image/sofa_under_water.jpg \
--prompt "Transform this surreal underwater setting into a realistic indoor living room while preserving the sofa layout."
```
### Text -> Speech
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type t2s \
--prompt "Hello. This is Dynin-omni."
```
## CLI Arguments
- `--query-type` (`t2i|t2s|i2i`)
- `--model` (default: `snu-aidas/Dynin-Omni`)
- `--host` / `--port` (OpenAI-compatible vLLM endpoint)
- `--prompt` (custom text)
- `--image-path` (required for `i2i`)
- `--modalities` (optional output modalities override)
- `--output-dir` (default: `/tmp/dynin_online_outputs`)
## Notes
- This client currently supports only `t2i`, `t2s`, and `i2i`.
- `t2t` is intentionally not exposed in this online example.
- This example intentionally uses the OpenAI-compatible chat completion endpoint.
- Task routing for non-text outputs relies on Dynin task trigger tokens (`<|t2i|>`, `<|i2i|>`, `<|t2s|>`) injected by the client.
- Outputs are saved under `/tmp/dynin_online_outputs` by default.
- Dynin stage-0 warmup can take a while on first startup; do not send requests before startup completes.
- Dynin itself can execute text-returning tasks such as `t2t`, `s2t`, `i2t`, and `v2t`, but this online serving example currently runs stage-0 in `generation` mode. In that path, the generation worker does not surface the final text as `output.text`, so OpenAI chat responses for those text-output tasks may complete internally but still return empty text.
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import argparse
import base64
import json
import mimetypes
import os
import time
from pathlib import Path
from typing import Any
DEFAULT_MODEL = "snu-aidas/Dynin-Omni"
DEFAULT_OUTPUT_DIR = "/tmp/dynin_online_outputs"
QUERY_CHOICES = ("t2i", "t2s", "i2i")
DEFAULT_PROMPT_BY_QUERY = {
"t2i": "A high quality detailed living room interior photo.",
"t2s": "Please read this sentence naturally: Hello from Dynin-Omni online serving.",
"i2i": "Transform this image into a realistic indoor living room while preserving layout.",
}
DEFAULT_MODALITIES_BY_QUERY = {
"t2i": ["image"],
"t2s": ["audio"],
"i2i": ["image"],
}
OFFLINE_PARITY_STAGE_COUNT = 3
OFFLINE_PARITY_STAGE_SAMPLING = {
"max_tokens": 1,
"temperature": 0.0,
"top_p": 1.0,
"detokenize": False,
}
def _infer_mime_type(path: Path) -> str:
mime_type, _ = mimetypes.guess_type(str(path))
return mime_type or "application/octet-stream"
def _encode_file_as_data_url(path: Path) -> str:
mime_type = _infer_mime_type(path)
raw = path.read_bytes()
encoded = base64.b64encode(raw).decode("utf-8")
return f"data:{mime_type};base64,{encoded}"
def _to_image_url(path_or_url: str) -> str:
value = str(path_or_url)
if value.startswith(("http://", "https://", "data:image/")):
return value
path = Path(value).expanduser().resolve()
if not path.exists():
raise FileNotFoundError(f"Image file not found: {path}")
return _encode_file_as_data_url(path)
def _build_user_content(query_type: str, prompt: str, image_path: str | None) -> list[dict[str, Any]]:
if query_type == "t2i":
return [{"type": "text", "text": f"<|t2i|> {prompt}"}]
if query_type == "t2s":
return [{"type": "text", "text": f"<|t2s|> {prompt}"}]
if query_type == "i2i":
if not image_path:
raise ValueError("--image-path is required for query type i2i")
return [
{"type": "text", "text": f"<|i2i|> {prompt}"},
{"type": "image_url", "image_url": {"url": _to_image_url(image_path)}},
]
raise ValueError(f"Unsupported query_type: {query_type}")
def _collect_text_from_content(content: Any) -> list[str]:
texts: list[str] = []
if isinstance(content, str):
stripped = content.strip()
if stripped:
texts.append(stripped)
return texts
if isinstance(content, dict):
for key in ("text", "content", "value", "output_text"):
text_value = content.get(key)
if isinstance(text_value, str) and text_value.strip():
texts.append(text_value.strip())
return texts
if isinstance(content, list):
for item in content:
texts.extend(_collect_text_from_content(item))
return texts
content_text = getattr(content, "text", None)
if isinstance(content_text, str) and content_text.strip():
texts.append(content_text.strip())
content_value = getattr(content, "content", None)
if isinstance(content_value, str) and content_value.strip():
texts.append(content_value.strip())
output_text = getattr(content, "output_text", None)
if isinstance(output_text, str) and output_text.strip():
texts.append(output_text.strip())
return texts
def _extract_text_outputs(chat_completion: Any) -> list[str]:
texts: list[str] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
content = getattr(message, "content", None)
texts.extend(_collect_text_from_content(content))
reasoning_content = getattr(message, "reasoning_content", None)
if isinstance(reasoning_content, str) and reasoning_content.strip():
texts.append(reasoning_content.strip())
choice_text = getattr(choice, "text", None)
if isinstance(choice_text, str) and choice_text.strip():
texts.append(choice_text.strip())
top_level_output_text = getattr(chat_completion, "output_text", None)
if isinstance(top_level_output_text, str) and top_level_output_text.strip():
texts.append(top_level_output_text.strip())
return texts
def _extract_image_data_urls(chat_completion: Any) -> list[str]:
urls: list[str] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
content = getattr(message, "content", None)
if not isinstance(content, list):
continue
for item in content:
if not isinstance(item, dict):
continue
if item.get("type") != "image_url":
continue
image_url = (item.get("image_url") or {}).get("url")
if isinstance(image_url, str) and image_url.startswith("data:image"):
urls.append(image_url)
return urls
def _extract_audio_payloads(chat_completion: Any) -> list[bytes]:
payloads: list[bytes] = []
for choice in getattr(chat_completion, "choices", []) or []:
message = getattr(choice, "message", None)
if message is None:
continue
message_audio = getattr(message, "audio", None)
if message_audio is None:
continue
data_b64 = getattr(message_audio, "data", None)
if isinstance(data_b64, str) and data_b64:
try:
payloads.append(base64.b64decode(data_b64))
except Exception:
continue
return payloads
def _decode_data_url(data_url: str) -> tuple[bytes, str]:
header, data = data_url.split(",", 1)
mime_type = "image/png"
if ";" in header and ":" in header:
mime_type = header.split(":", 1)[1].split(";", 1)[0]
return base64.b64decode(data), mime_type
def _image_extension_from_mime(mime_type: str) -> str:
if mime_type == "image/jpeg":
return ".jpg"
if mime_type == "image/webp":
return ".webp"
if mime_type == "image/gif":
return ".gif"
return ".png"
def _save_outputs(
*,
query_type: str,
chat_completion: Any,
output_dir: Path,
) -> None:
output_dir.mkdir(parents=True, exist_ok=True)
stamp = time.strftime("%Y%m%d_%H%M%S")
text_outputs = _extract_text_outputs(chat_completion)
image_data_urls = _extract_image_data_urls(chat_completion)
audio_payloads = _extract_audio_payloads(chat_completion)
if text_outputs:
text_path = output_dir / f"{query_type}_{stamp}.txt"
text_path.write_text("\n\n".join(text_outputs) + "\n", encoding="utf-8")
print(f"[dynin-online] text saved: {text_path}")
print(text_outputs[0])
for idx, image_url in enumerate(image_data_urls):
image_bytes, mime_type = _decode_data_url(image_url)
ext = _image_extension_from_mime(mime_type)
image_path = output_dir / f"{query_type}_{stamp}_{idx}{ext}"
image_path.write_bytes(image_bytes)
print(f"[dynin-online] image saved: {image_path}")
for idx, audio_bytes in enumerate(audio_payloads):
audio_path = output_dir / f"{query_type}_{stamp}_{idx}.wav"
audio_path.write_bytes(audio_bytes)
print(f"[dynin-online] audio saved: {audio_path}")
if not text_outputs and not image_data_urls and not audio_payloads:
print("[dynin-online] no output extracted from response")
raw_path = output_dir / f"{query_type}_{stamp}_raw_response.json"
try:
if hasattr(chat_completion, "model_dump_json"):
serialized = chat_completion.model_dump_json(indent=2)
else:
if hasattr(chat_completion, "model_dump"):
raw_payload: Any = chat_completion.model_dump(mode="json")
else:
raw_payload = chat_completion
try:
serialized = json.dumps(raw_payload, ensure_ascii=False, indent=2)
except Exception:
serialized = json.dumps({"repr": repr(raw_payload)}, ensure_ascii=False, indent=2)
raw_path.write_text(serialized + "\n", encoding="utf-8")
print(f"[dynin-online] raw response saved: {raw_path}")
except Exception:
pass
def _build_offline_parity_sampling_params_list() -> list[dict[str, Any]]:
return [dict(OFFLINE_PARITY_STAGE_SAMPLING) for _ in range(OFFLINE_PARITY_STAGE_COUNT)]
def run_request(args: argparse.Namespace) -> None:
from openai import OpenAI
client = OpenAI(
api_key="EMPTY",
base_url=f"http://{args.host}:{args.port}/v1",
)
prompt = args.prompt.strip() if args.prompt else DEFAULT_PROMPT_BY_QUERY[args.query_type]
user_content = _build_user_content(
query_type=args.query_type,
prompt=prompt,
image_path=args.image_path,
)
if args.modalities:
modalities = [item.strip() for item in args.modalities.split(",") if item.strip()]
else:
modalities = DEFAULT_MODALITIES_BY_QUERY[args.query_type]
extra_body = {
"sampling_params_list": _build_offline_parity_sampling_params_list(),
}
chat_completion = client.chat.completions.create(
model=args.model,
messages=[{"role": "user", "content": user_content}],
modalities=modalities,
extra_body=extra_body,
)
_save_outputs(
query_type=args.query_type,
chat_completion=chat_completion,
output_dir=Path(args.output_dir).expanduser(),
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Dynin-Omni online chat completion client")
parser.add_argument(
"--query-type",
"-q",
type=str,
default="t2i",
choices=QUERY_CHOICES,
help="Dynin query type",
)
parser.add_argument(
"--model",
"-m",
type=str,
default=DEFAULT_MODEL,
help="Model name/path",
)
parser.add_argument(
"--host",
type=str,
default="localhost",
help="Host/IP of the vLLM Omni API server",
)
parser.add_argument(
"--port",
type=int,
default=8091,
help="Port of the vLLM Omni API server",
)
parser.add_argument(
"--prompt",
"-p",
type=str,
default="",
help="Custom prompt text",
)
parser.add_argument(
"--image-path",
"-i",
type=str,
default=None,
help="Image path/URL for i2i",
)
parser.add_argument(
"--modalities",
type=str,
default="",
help="Comma-separated output modalities override (e.g., text,image,audio)",
)
parser.add_argument(
"--output-dir",
"-o",
type=str,
default=DEFAULT_OUTPUT_DIR,
help="Directory to save outputs",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
os.environ.setdefault("HF_HUB_DISABLE_XET", "1")
run_request(args)
if __name__ == "__main__":
main()
@@ -314,6 +314,7 @@ count, use `size` and `n` rather than `height`, `width`, or
| `seed` | int | None | Random seed (reproducible) |
| `negative_prompt` | str | None | Negative prompt |
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
| `strength` | float | 0.6 | **Z-Image only** - Denoising start timestep for I2I. Range: [0.0, 1.0]. Lower preserves more of original image. |
| `layers` | int | 4 | Number of layers (Qwen-Image-Layered) |
| `resolution` | int | 640 | Resolution, 640 or 1024 (Qwen-Image-Layered) |
+131
View File
@@ -0,0 +1,131 @@
# OmniVoice
## Model Overview
| Model | Description |
|-------|-------------|
| `k2-fsa/OmniVoice` | Zero-shot multilingual TTS (600+ languages) with diffusion language model (Qwen3-0.6B backbone) |
> **Note:** Requires `transformers>=5.3.0` for voice cloning (HiggsAudioV2 tokenizer). Auto voice and voice design work with `transformers>=4.57.0`.
## Launch the Server
```bash
vllm serve k2-fsa/OmniVoice \
--omni \
--port 8091 \
--trust-remote-code
```
Or use the convenience script:
```bash
./run_server.sh
```
## Send TTS Request
### Using curl
```bash
# Basic TTS (auto voice)
curl -X POST http://localhost:8091/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Hello, how are you?",
"voice": "default",
"response_format": "wav"
}' --output output.wav
```
### Using Python
```python
import httpx
response = httpx.post(
"http://localhost:8091/v1/audio/speech",
json={
"input": "Hello, how are you?",
"voice": "default",
"response_format": "wav",
},
timeout=300.0,
)
with open("output.wav", "wb") as f:
f.write(response.content)
```
### Using OpenAI SDK
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8091/v1", api_key="none")
response = client.audio.speech.create(
model="k2-fsa/OmniVoice",
voice="default",
input="Hello, how are you?",
)
response.stream_to_file("output.wav")
```
### Using the CLI Client
```bash
cd examples/online_serving/omnivoice
# Basic TTS
python speech_client.py --text "Hello, how are you?"
# Specify language for improved quality
python speech_client.py --text "Bonjour, comment allez-vous?" --language French
```
The CLI client supports:
- `--api-base`: API base URL (default: `http://localhost:8091`)
- `--model` (or `-m`): Model name (default: `k2-fsa/OmniVoice`)
- `--text`: Text to synthesize (required)
- `--response-format`: Audio format: wav, mp3, flac, pcm, aac, opus (default: wav)
- `--language`: Language hint (default: Auto)
- `--output` (or `-o`): Output file path (default: `omnivoice_output.wav`)
## Inference Modes
OmniVoice supports three inference modes. Currently, **auto voice** is supported through the online Speech API. Voice cloning and voice design are available via offline inference (see `examples/offline_inference/omnivoice/`).
| Mode | Description | Online API | Offline |
|------|-------------|:----------:|:-------:|
| Auto Voice | Generate speech without reference | Yes | Yes |
| Voice Clone | Clone from reference audio + transcript | - | Yes |
| Voice Design | Control style via natural language instruction | - | Yes |
## Architecture
OmniVoice uses a single-stage diffusion pipeline:
- **Stage 0 (Generator)**: Qwen3-0.6B transformer with 32-step iterative masked unmasking and classifier-free guidance. Generates 8-codebook audio tokens from text, then decodes to 24kHz waveform via HiggsAudioV2 RVQ quantizer + DAC acoustic decoder.
## API Parameters
OmniVoice uses the standard `/v1/audio/speech` endpoint. See the [Speech API reference](https://docs.vllm.ai/projects/vllm-omni/en/latest/serving/speech_api/) for full documentation.
Key parameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `input` | string | **required** | Text to synthesize |
| `voice` | string | "default" | Voice name |
| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, aac, opus |
| `speed` | float | 1.0 | Playback speed (0.25-4.0) |
## Troubleshooting
1. **TTS model did not produce audio output**: Ensure the model is fully downloaded (`huggingface-cli download k2-fsa/OmniVoice`)
2. **Connection refused**: Make sure the server is running on the correct port
3. **Out of memory**: Reduce `--gpu-memory-utilization` (default stage config uses 0.5)
4. **Slow first request**: The model performs warmup on first inference; subsequent requests are faster
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Launch vLLM-Omni server for OmniVoice TTS
#
# Usage:
# ./run_server.sh
# CUDA_VISIBLE_DEVICES=0 ./run_server.sh
set -e
MODEL="${MODEL:-k2-fsa/OmniVoice}"
PORT="${PORT:-8091}"
echo "Starting OmniVoice server with model: $MODEL"
vllm serve "$MODEL" \
--host 0.0.0.0 \
--port "$PORT" \
--trust-remote-code \
--omni
@@ -0,0 +1,84 @@
"""Client for OmniVoice TTS via /v1/audio/speech endpoint.
Examples:
# Basic TTS (auto voice)
python speech_client.py --text "Hello, how are you?"
# Specify language
python speech_client.py --text "Bonjour, comment allez-vous?" --language French
"""
import argparse
import httpx
DEFAULT_API_BASE = "http://localhost:8091"
DEFAULT_API_KEY = "EMPTY"
def run_tts(args) -> None:
"""Generate speech via /v1/audio/speech API."""
payload = {
"model": args.model,
"input": args.text,
"voice": "default",
"response_format": args.response_format,
}
if args.language:
payload["language"] = args.language
print(f"Model: {args.model}")
print(f"Text: {args.text}")
if args.language:
print(f"Language: {args.language}")
print("Generating audio...")
api_url = f"{args.api_base}/v1/audio/speech"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {args.api_key}",
}
with httpx.Client(timeout=300.0) as client:
response = client.post(api_url, json=payload, headers=headers)
if response.status_code != 200:
print(f"Error: {response.status_code}")
print(response.text)
return
try:
text = response.content.decode("utf-8")
if text.startswith('{"error"'):
print(f"Error: {text}")
return
except UnicodeDecodeError:
pass
output_path = args.output or "omnivoice_output.wav"
with open(output_path, "wb") as f:
f.write(response.content)
print(f"Audio saved to: {output_path}")
def main():
parser = argparse.ArgumentParser(description="OmniVoice TTS client")
parser.add_argument("--api-base", default=DEFAULT_API_BASE, help="API base URL")
parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="API key")
parser.add_argument("--model", "-m", default="k2-fsa/OmniVoice", help="Model name")
parser.add_argument("--text", required=True, help="Text to synthesize")
parser.add_argument("--language", default=None, help="Language hint (e.g., English, Chinese, French)")
parser.add_argument(
"--response-format",
default="wav",
choices=["wav", "mp3", "flac", "pcm", "aac", "opus"],
help="Audio format (default: wav)",
)
parser.add_argument("--output", "-o", default=None, help="Output file path")
args = parser.parse_args()
run_tts(args)
if __name__ == "__main__":
main()
@@ -36,6 +36,45 @@ cd examples/online_serving/qwen3_omni
python examples/online_serving/openai_chat_completion_client_for_multimodal_generation.py --model Qwen/Qwen3-Omni-30B-A3B-Instruct --query-type use_image --port 8091 --host "localhost"
```
#### Realtime WebSocket client (`openai_realtime_client.py`)
[`openai_realtime_client.py`](./openai_realtime_client.py) connects to **`ws://<host>:<port>/v1/realtime`**, uploads a local audio file as **PCM16 mono @ 16 kHz** chunks (OpenAI-style `input_audio_buffer.append` / `commit`), and prints **streaming transcription** (`transcription.delta` / `transcription.done`).
**Dependencies:**
```bash
pip install websockets librosa numpy
```
(ffmpeg may be required by `librosa` for some formats; see the FAQ below.)
**From this directory** (`examples/online_serving/qwen3_omni`):
```bash
python openai_realtime_client.py \
--host localhost \
--port 8091 \
--model Qwen/Qwen3-Omni-30B-A3B-Instruct \
--audio_path /path/to/your.wav
```
If `--audio_path` is omitted, the script uses a bundled default clip (`mary_had_lamb` via vLLM assets).
**Arguments:**
| Flag | Default | Description |
|------|---------|-------------|
| `--host` | `localhost` | API server host |
| `--port` | `8000` | API server port (match your `vllm serve` port, e.g. `8091`) |
| `--model` | `Qwen/Qwen3-Omni-30B-A3B-Instruct` | Must match the served model (also sent in `session.update`) |
| `--audio_path` | *(optional)* | Path to input audio; resampled to 16 kHz mono inside the client |
Ensure the vLLM-Omni server is running with realtime support for this endpoint, for example:
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
The Python client supports the following command-line arguments:
- `--query-type` (or `-q`): Query type (default: `use_video`). Options: `text`, `use_audio`, `use_image`, `use_video`
@@ -0,0 +1,146 @@
"""
This script demonstrates how to use the vLLM-Omni Realtime WebSocket API to perform
audio transcription by uploading an audio file.
Before running this script, you must start the vLLM-Omni server with a realtime-capable
model, for example:
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni
Requirements:
- vllm with audio support
- websockets
- librosa
- numpy
The script:
1. Connects to the Realtime WebSocket endpoint
2. Converts an audio file to PCM16 @ 16kHz
3. Sends audio chunks to the server
4. Receives and prints transcription as it streams
"""
import argparse
import asyncio
import base64
import json
import librosa
import numpy as np
import websockets
from vllm.assets.audio import AudioAsset
def audio_to_pcm16_base64(audio_path: str) -> str:
"""
Load an audio file and convert it to base64-encoded PCM16 @ 16kHz.
"""
# Load audio and resample to 16kHz mono
audio, _ = librosa.load(audio_path, sr=16000, mono=True)
# Convert to PCM16
pcm16 = (audio * 32767).astype(np.int16)
# Encode as base64
return base64.b64encode(pcm16.tobytes()).decode("utf-8")
async def realtime_transcribe(audio_path: str, host: str, port: int, model: str):
"""
Connect to the Realtime API and transcribe an audio file.
"""
uri = f"ws://{host}:{port}/v1/realtime"
async with websockets.connect(uri) as ws:
# Wait for session.created
response = json.loads(await ws.recv())
if response["type"] == "session.created":
print(f"Session created: {response['id']}")
else:
print(f"Unexpected response: {response}")
return
# Validate model
await ws.send(json.dumps({"type": "session.update", "model": model}))
# Signal ready to start
await ws.send(json.dumps({"type": "input_audio_buffer.commit"}))
# Convert audio file to base64 PCM16
print(f"Loading audio from: {audio_path}")
audio_base64 = audio_to_pcm16_base64(audio_path)
# Send audio in chunks (4KB of raw audio = ~8KB base64)
chunk_size = 4096
audio_bytes = base64.b64decode(audio_base64)
total_chunks = (len(audio_bytes) + chunk_size - 1) // chunk_size
print(f"Sending {total_chunks} audio chunks...")
for i in range(0, len(audio_bytes), chunk_size):
chunk = audio_bytes[i : i + chunk_size]
await ws.send(
json.dumps(
{
"type": "input_audio_buffer.append",
"audio": base64.b64encode(chunk).decode("utf-8"),
}
)
)
# Signal all audio is sent
await ws.send(json.dumps({"type": "input_audio_buffer.commit", "final": True}))
print("Audio sent. Waiting for transcription...\n")
# Receive transcription
print("Transcription: ", end="", flush=True)
while True:
response = json.loads(await ws.recv())
if response["type"] == "transcription.delta":
print(response["delta"], end="", flush=True)
elif response["type"] == "transcription.done":
print(f"\n\nFinal transcription: {response['text']}")
if response.get("usage"):
print(f"Usage: {response['usage']}")
break
elif response["type"] == "error":
print(f"\nError: {response['error']}")
break
def main(args):
if args.audio_path:
audio_path = args.audio_path
else:
# Use default audio asset
audio_path = str(AudioAsset("mary_had_lamb").get_local_path())
print(f"No audio path provided, using default: {audio_path}")
asyncio.run(realtime_transcribe(audio_path, args.host, args.port, args.model))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Realtime WebSocket Transcription Client")
parser.add_argument(
"--model",
type=str,
default="Qwen/Qwen3-Omni-30B-A3B-Instruct",
help="Model that is served and should be pinged.",
)
parser.add_argument(
"--audio_path",
type=str,
default=None,
help="Path to the audio file to transcribe.",
)
parser.add_argument(
"--host",
type=str,
default="localhost",
help="vLLM-Omni server host (default: localhost)",
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="vLLM-Omni server port (default: 8000)",
)
args = parser.parse_args()
main(args)
+8 -2
View File
@@ -233,6 +233,7 @@ Upload a new voice sample for voice cloning in Base task TTS requests.
- `consent` (required): Consent recording ID
- `name` (required): Name for the new voice
- `ref_text` (optional): Transcript of the audio. Enables in-context voice cloning (higher quality).
- `speaker_description` (optional): Free-form description of the voice (e.g. "warm narrator", "energetic presenter").
**Response Example:**
```json
@@ -243,18 +244,23 @@ Upload a new voice sample for voice cloning in Base task TTS requests.
"consent": "user_consent_id",
"created_at": 1738660000,
"mime_type": "audio/wav",
"file_size": 1024000
"file_size": 1024000,
"ref_text": "The exact transcript of the audio sample.",
"speaker_description": "warm narrator"
}
}
```
Fields `ref_text` and `speaker_description` are omitted when not provided at upload time.
**Usage Example:**
```bash
curl -X POST http://localhost:8000/v1/audio/voices \
-F "audio_sample=@/path/to/voice_sample.wav" \
-F "consent=user_consent_id" \
-F "name=custom_voice_1" \
-F "ref_text=The exact transcript of the audio sample."
-F "ref_text=The exact transcript of the audio sample." \
-F "speaker_description=warm narrator"
```
### Endpoint
@@ -71,7 +71,7 @@ def run_tts_generation(args) -> None:
payload = {
"model": args.model,
"input": args.text,
"speaker": args.speaker,
"voice": args.speaker,
"response_format": args.response_format,
}
@@ -231,6 +231,8 @@ count, use `size` and `n` rather than `height`, `width`, or
| `seed` | int | None | Random seed (reproducible) |
| `negative_prompt` | str | None | Negative prompt |
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
| `use_system_prompt` | str | None | System prompt preset: `en_unified`, `en_vanilla`, `en_recaption`, `en_think_recaption`, `dynamic`, `None`, or custom text string. 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. |
## Response Format
@@ -28,6 +28,8 @@ def generate_image(
lora_name: str | None = None,
lora_scale: float | None = None,
lora_int_id: int | None = None,
use_system_prompt: str | None = None,
system_prompt: str | None = None,
) -> bytes | None:
"""Generate an image using the images generation API.
@@ -45,6 +47,8 @@ def generate_image(
lora_name: LoRA name (optional, defaults to path stem)
lora_scale: LoRA scale factor (default: 1.0)
lora_int_id: LoRA integer ID (optional, derived from path if not provided)
use_system_prompt: System prompt for generation.
system_prompt: Custom system prompt.
Returns:
Image bytes or None if failed
@@ -70,7 +74,10 @@ def generate_image(
payload["negative_prompt"] = negative_prompt
if seed is not None:
payload["seed"] = seed
if use_system_prompt is not None:
payload["use_system_prompt"] = use_system_prompt
if system_prompt is not None:
payload["system_prompt"] = system_prompt
# Add LoRA if provided
if lora_path:
lora_body: dict = {
@@ -128,9 +135,21 @@ def main():
default=None,
help="LoRA integer id (cache key). If omitted, the server derives a stable id from lora_path.",
)
parser.add_argument(
"--use-system-prompt",
type=str,
default=None,
help=(
"System prompt for generation. Use predefined types: 'en_unified', 'en_vanilla', 'en_recaption', 'en_think_recaption', 'dynamic', or 'None'; Or provide custom text string directly. Recommended en_unified. "
),
)
parser.add_argument(
"--system-prompt",
type=str,
default=None,
help=("Custom system prompt. Used when --use-system-prompt is custom. "),
)
args = parser.parse_args()
print(f"Generating image for: {args.prompt}")
image_bytes = generate_image(
@@ -146,6 +165,8 @@ def main():
lora_name=args.lora_name,
lora_scale=args.lora_scale if args.lora_path else None,
lora_int_id=args.lora_int_id if args.lora_path else None,
use_system_prompt=args.use_system_prompt,
system_prompt=args.system_prompt,
)
if image_bytes:
+1
View File
@@ -223,3 +223,4 @@ extend-ignore-identifiers-re = [
ue = "ue"
semantics = "semantics"
fullset = "fullset"
Vai = "Vai"
+90
View File
@@ -0,0 +1,90 @@
# Qwen3-Omni for multimodal chat on 1x A100 80GB
## Summary
- Vendor: Qwen
- Model: `Qwen/Qwen3-Omni-30B-A3B-Instruct`
- Task: Multimodal chat with text, image, audio, or video input
- Mode: Online serving with the OpenAI-compatible API
- Maintainer: Community
## When to use this recipe
Use this recipe when you want a known-good starting point for serving
`Qwen/Qwen3-Omni-30B-A3B-Instruct` with vLLM-Omni on a single 80 GB A100 and
validate the deployment with the existing multimodal client examples in this
repository.
## References
- Upstream or canonical docs:
[`docs/user_guide/examples/online_serving/qwen3_omni.md`](../../docs/user_guide/examples/online_serving/qwen3_omni.md)
- Related example under `examples/`:
[`examples/online_serving/qwen3_omni/README.md`](../../examples/online_serving/qwen3_omni/README.md)
- Related issue or discussion:
[RFC: add recipes folder](https://github.com/vllm-project/vllm-omni/issues/2645)
## Hardware Support
This recipe currently documents one tested-style reference configuration for
CUDA GPU serving. Add more sections for other hardware as community validation
lands.
## GPU
### 1x A100 80GB
#### Environment
- OS: Linux
- Python: 3.10+
- Driver / runtime: NVIDIA CUDA environment with an A100 80 GB GPU
- vLLM version: Match the repository requirements for your checkout
- vLLM-Omni version or commit: Use the commit you are deploying from
#### Command
Start the server from the repository root:
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
To enable async chunking, use the bundled stage config:
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct \
--omni \
--port 8091 \
--stage-configs-path vllm_omni/model_executor/stage_configs/qwen3_omni_moe_async_chunk.yaml
```
#### Verification
Run one of the existing example clients after the server is ready:
```bash
python examples/online_serving/openai_chat_completion_client_for_multimodal_generation.py \
--model Qwen/Qwen3-Omni-30B-A3B-Instruct \
--query-type use_image \
--port 8091 \
--host localhost
```
For a quick API smoke test, request text-only output:
```bash
curl http://localhost:8091/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"messages": [{"role": "user", "content": "Describe vLLM in brief."}],
"modalities": ["text"]
}'
```
#### Notes
- Memory usage: Size depends on runtime options and output modalities; leave headroom for multimodal workloads.
- Key flags: `--omni` is required; `--stage-configs-path` is optional for custom or async-chunk stage configs.
- Known limitations: This starter recipe is intentionally narrow and focuses on the single-GPU online-serving path already documented in the repo examples.
+35
View File
@@ -0,0 +1,35 @@
# Community Recipes
This directory contains community-maintained recipes for answering a
practical user question:
> How do I run model X on hardware Y for task Z?
Add recipes for this repository under this in-repo `recipes/` directory. To
keep naming and layout consistent, organize recipes by model vendor in a way
that is aligned with
[`vllm-project/recipes`](https://github.com/vllm-project/recipes), but treat
that external repository as a reference for structure rather than the place to
add files for this repo. Use one Markdown file per model family by default.
Example layout:
```text
recipes/
Qwen/
Qwen3-Omni.md
Qwen3-TTS.md
Tencent-Hunyuan/
HunyuanVideo.md
```
## Available Recipes
- [`Qwen/Qwen3-Omni.md`](./Qwen/Qwen3-Omni.md): online serving recipe for
multimodal chat on `1x A100 80GB`
Within a single recipe file, include different hardware support sections such
as `GPU`, `ROCm`, and `NPU`, and add concrete tested configurations like
`1x A100 80GB` or `2x L40S` inside those sections when applicable.
See [TEMPLATE.md](./TEMPLATE.md) for the recommended format.
+82
View File
@@ -0,0 +1,82 @@
# Recipe Title
> Example: Qwen3-Omni for speech chat on 1x A100 80GB
## Summary
- Vendor:
- Model:
- Task:
- Mode:
- Maintainer:
## When to use this recipe
Briefly describe the concrete scenario this recipe covers.
## References
- Upstream or canonical docs:
- Related example under `examples/`:
- Related issue or discussion:
## Hardware Support
Add one section per platform, such as `GPU`, `ROCm`, or `NPU`. Under each
platform section, document one or more tested hardware configurations.
## GPU
### 1x A100 80GB
#### Environment
- OS:
- Python:
- Driver / runtime:
- vLLM version:
- vLLM-Omni version or commit:
#### Command
```bash
# Add the exact command(s) here
```
#### Verification
```bash
# Add a quick validation command or expected output here
```
#### Notes
- Memory usage:
- Key flags:
- Known limitations:
### 2x L40S
Repeat the same structure for other hardware setups as needed.
## ROCm
### Example hardware configuration
Repeat the same nested structure for ROCm setups as needed:
- `#### Environment`
- `#### Command`
- `#### Verification`
- `#### Notes`
## NPU
### Example hardware configuration
Repeat the same nested structure for NPU setups as needed:
- `#### Environment`
- `#### Command`
- `#### Verification`
- `#### Notes`
+1
View File
@@ -1,4 +1,5 @@
# Common dependencies for all platforms
av>=14.0.0
omegaconf>=2.3.0
librosa>=0.11.0
resampy>=0.4.3
+67
View File
@@ -0,0 +1,67 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Unit tests for metrics.py
"""
import pytest
from vllm.benchmarks.serve import TaskType
from vllm_omni.benchmarks.metrics.metrics import calculate_metrics
from vllm_omni.benchmarks.patch.patch import MixRequestFuncOutput
pytestmark = [pytest.mark.core_model, pytest.mark.benchmark, pytest.mark.cpu]
def _make_output(prompt_len: int, output_tokens: int = 10) -> MixRequestFuncOutput:
"""Build a minimal successful MixRequestFuncOutput for metrics aggregation."""
output = MixRequestFuncOutput()
output.success = True
output.prompt_len = prompt_len
output.output_tokens = output_tokens
output.generated_text = "x" * output_tokens
output.ttft = 0.1
output.text_latency = 1.0
output.latency = 1.0
output.start_time = 0.0
output.itl = [0.1] * max(output_tokens - 1, 0)
output.audio_ttfp = 0.0
output.audio_rtf = 0.0
output.audio_duration = 0.0
output.audio_frames = 0
output.input_audio_duration = 0.0
output.error = ""
return output
# ============================================================================
# total_input Tests
# ============================================================================
def test_total_input_aggregated_from_output_prompt_len():
"""Test that total_input sums outputs[i].prompt_len, not input_requests[i].prompt_len."""
outputs = [_make_output(4992), _make_output(3000)]
metrics, _ = calculate_metrics(
input_requests=[],
outputs=outputs,
dur_s=10.0,
tokenizer=None,
selected_percentiles=[99.0],
goodput_config_dict={},
task_type=TaskType.GENERATION,
selected_percentile_metrics=[],
max_concurrency=None,
request_rate=float("inf"),
benchmark_duration=10.0,
)
assert metrics.total_input == 7992, (
"total_input should aggregate from outputs[i].prompt_len to reflect the true multimodal input token count"
)
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
+54
View File
@@ -574,5 +574,59 @@ class TestTextLatencyAttribute:
)
# ============================================================================
# prompt_len Tests
# ============================================================================
@pytest.mark.asyncio
async def test_prompt_len_assigned_from_usage(mocker: MockerFixture):
# Arrange: request claims prompt_len=100, but server reports 4992 (multimodal).
request_input = RequestFuncInput(
model="test-model",
model_name="test-model",
prompt="test prompt",
api_url="http://test.com/v1/chat/completions",
prompt_len=100,
output_len=20,
)
chunks = [
create_sse_chunk(
{
"choices": [{"delta": {"content": "Hello"}}],
"modality": "text",
}
),
create_sse_chunk(
{
"choices": [{"delta": {"content": " world"}}],
"modality": "text",
}
),
# Final usage chunk emitted because stream_options.include_usage=True.
create_sse_chunk(
{
"choices": [],
"usage": {"prompt_tokens": 4992, "completion_tokens": 2, "total_tokens": 4994},
}
),
b"data: [DONE]\n\n",
]
mock_response = MockResponse(200, chunks)
mock_session = mocker.AsyncMock()
mock_session.post = mocker.MagicMock(return_value=mock_response)
# Act
output = await async_request_openai_chat_omni_completions(request_input, mock_session)
# Assert
assert output.success is True
assert output.prompt_len == 4992, (
"prompt_len should be overridden by usage.prompt_tokens to reflect the true multimodal input token count"
)
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
+88 -50
View File
@@ -16,6 +16,7 @@ if "VLLM_TARGET_DEVICE" not in os.environ:
os.environ["VLLM_TARGET_DEVICE"] = "cpu"
import concurrent.futures
import contextlib
import gc
import multiprocessing
import socket
@@ -52,6 +53,7 @@ from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
PromptAudioInput = list[tuple[Any, int]] | tuple[Any, int] | None
PromptImageInput = list[Any] | Any | None
PromptVideoInput = list[Any] | Any | None
@@ -337,10 +339,10 @@ def log_test_name_before_test(request):
def _run_pre_test_cleanup(enable_force=False):
if os.getenv("VLLM_TEST_CLEAN_GPU_MEMORY", "0") != "1" and not enable_force:
print("GPU cleanup disabled")
print("\nPre-test GPU cleanup skipped(Default off is typical when one worker/instance runs many tests.)\n")
return
print("Pre-test GPU status:")
print("\nPre-test GPU status:")
num_gpus = torch.cuda.device_count()
if num_gpus > 0:
@@ -1087,6 +1089,22 @@ def _merge_base64_audio_to_segment(base64_list: list[str]):
return merged
@contextlib.contextmanager
def _serialize_whisper_small_model_download():
"""Serialize Whisper ``small`` cache writes across processes (Linux; ``fcntl``)."""
import fcntl
lock_path = Path.home() / ".cache" / "whisper" / ".small_model_download.lock"
lock_path.parent.mkdir(parents=True, exist_ok=True)
f = open(lock_path, "a+b")
try:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
yield
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
f.close()
def _whisper_transcribe_in_current_process(output_path: str) -> str:
import whisper
@@ -1107,7 +1125,8 @@ def _whisper_transcribe_in_current_process(output_path: str) -> str:
else:
use_accelerator = False
device = "cpu"
model = whisper.load_model("small", device=device)
with _serialize_whisper_small_model_download():
model = whisper.load_model("small", device=device)
try:
text = model.transcribe(
output_path,
@@ -1126,8 +1145,7 @@ def _whisper_transcribe_in_current_process(output_path: str) -> str:
def convert_audio_file_to_text(output_path: str) -> str:
"""Convert an audio file to text in an isolated subprocess."""
# Import locally to avoid impacting test module import time.
"""Convert an audio file to text in an isolated subprocess (spawn)."""
ctx = multiprocessing.get_context("spawn")
with concurrent.futures.ProcessPoolExecutor(max_workers=1, mp_context=ctx) as executor:
future = executor.submit(_whisper_transcribe_in_current_process, output_path)
@@ -1146,18 +1164,6 @@ def convert_audio_bytes_to_text(raw_bytes: bytes) -> str:
return text
def merge_base64_and_convert_to_text(base64_list):
"""
Merge a list of base64 encoded audio data and convert to text.
"""
merged_audio = _merge_base64_audio_to_segment(base64_list)
output_path = f"./test_{uuid.uuid4().hex}.wav"
merged_audio.export(output_path, format="wav")
print(f"audio data is saved: {output_path}")
text = convert_audio_file_to_text(output_path)
return text
def modify_stage_config(
yaml_path: str,
updates: dict[str, Any] = None,
@@ -1742,7 +1748,7 @@ def _estimate_voice_gender_from_audio(audio_bytes: bytes) -> str:
label = str(top.get("label", "")).lower()
conf = float(top.get("score", 0.0))
if conf < 0.6:
if conf < 0.5:
gender = "unknown"
# Some models use non-English labels (e.g., Russian). Normalize to 'male'/'female'.
elif ("female" in label) or ("жен" in label):
@@ -1771,6 +1777,34 @@ def _estimate_voice_gender_from_audio(audio_bytes: bytes) -> str:
return "unknown"
_PRESET_VOICE_GENDER_MAP: dict[str, str] = {
"serena": "female",
"uncle_fu": "male",
"chelsie": "female",
"clone": "female",
"ethan": "male",
}
def _assert_preset_voice_gender_from_audio(
audio_bytes: bytes | None,
voice_name: str | None,
) -> None:
"""If ``voice_name`` matches a known preset, assert classifier gender matches (skip when unknown)."""
if not voice_name or not audio_bytes:
return
key = str(voice_name).lower()
expected_gender = _PRESET_VOICE_GENDER_MAP.get(key)
if expected_gender is None:
return
estimated_gender = _estimate_voice_gender_from_audio(audio_bytes)
print(f"Preset voice gender check: preset={key!r}, estimated={estimated_gender!r}, expected={expected_gender!r}")
if estimated_gender != "unknown":
assert estimated_gender == expected_gender, (
f"{voice_name!r} is expected {expected_gender}, but estimated gender is {estimated_gender!r}"
)
# Threshold aligned with _compute_pcm_hnr_db docstring (clean clone vs distorted).
_MIN_PCM_SPEECH_HNR_DB = 1.0
@@ -1837,6 +1871,12 @@ def assert_omni_response(response: OmniResponse, request_config: dict[str, Any],
if "audio" in modalities:
assert response.audio_content is not None, "No audio output is generated"
print(f"audio content is: {response.audio_content}")
speaker = request_config.get("speaker")
if speaker:
_assert_preset_voice_gender_from_audio(
response.audio_bytes,
speaker,
)
if "text" in modalities:
assert response.text_content is not None, "No text output is generated"
@@ -1849,12 +1889,14 @@ def assert_omni_response(response: OmniResponse, request_config: dict[str, Any],
keywords = keywords_dict.get(word_type)
if "text" in modalities:
if keywords:
assert any(keyword in response.text_content.lower() for keyword in keywords), (
text_lower = response.text_content.lower()
assert any(str(kw).lower() in text_lower for kw in keywords), (
"The output does not contain any of the keywords."
)
else:
if keywords:
assert any(keyword in response.audio_content.lower() for keyword in keywords), (
audio_lower = response.audio_content.lower()
assert any(str(kw).lower() in audio_lower for kw in keywords), (
"The output does not contain any of the keywords."
)
@@ -1908,24 +1950,12 @@ def assert_audio_speech_response(
f"Transcript doesn't match input: similarity={similarity:.2f}, transcript='{transcript}'"
)
# Voice gender consistency check:
# Voice gender consistency check (preset names in ``_PRESET_VOICE_GENDER_MAP``).
# When the estimator returns 'unknown', we treat it as inconclusive and do NOT fail the test.
voice = (request_config.get("voice") or "").lower()
if voice and response.audio_bytes:
estimated_gender = _estimate_voice_gender_from_audio(response.audio_bytes)
voice_gender_map = {
# adjust this mapping to your actual voice names
"serena": "female",
"uncle_fu": "male",
"clone": "female",
}
expected_gender = voice_gender_map.get(voice)
if expected_gender is not None:
print(f"Estimated voice gender from audio: {estimated_gender} (voice='{voice}')")
if estimated_gender != "unknown":
assert estimated_gender == expected_gender, (
f"Voice '{voice}' is expected {expected_gender}, but estimated gender is '{estimated_gender}'"
)
_assert_preset_voice_gender_from_audio(
response.audio_bytes,
request_config.get("voice"),
)
def assert_diffusion_response(response: DiffusionResponse, request_config: dict[str, Any], run_level: str = None):
@@ -2041,7 +2071,11 @@ class OpenAIClientHandler:
if audio_data or text_content:
if audio_data:
audio_content = merge_base64_and_convert_to_text(audio_data)
merged_seg = _merge_base64_audio_to_segment(audio_data)
wav_buf = BytesIO()
merged_seg.export(wav_buf, format="wav")
result.audio_bytes = wav_buf.getvalue()
audio_content = convert_audio_bytes_to_text(result.audio_bytes)
if audio_content and text_content:
similarity = cosine_similarity_text(audio_content.lower(), text_content.lower())
@@ -2096,7 +2130,8 @@ class OpenAIClientHandler:
if audio_data or text_content:
if audio_data:
audio_content = convert_audio_to_text(audio_data)
result.audio_bytes = base64.b64decode(audio_data)
audio_content = convert_audio_bytes_to_text(result.audio_bytes)
if audio_content and text_content:
similarity = cosine_similarity_text(audio_content.lower(), text_content.lower())
@@ -2265,8 +2300,9 @@ class OpenAIClientHandler:
request_config: Request configuration dictionary containing parameters like model, messages, stream.
Optional ``use_audio_in_video`` (bool): when true, sets
``extra_body["mm_processor_kwargs"] = {"use_audio_in_video": True}`` for Qwen-Omni video+audio
extraction (merged with any existing ``extra_body`` / ``mm_processor_kwargs``).
Optional ``extra_body`` (dict): passed through to ``chat.completions.create`` after merge.
extraction.
Optional top-level ``speaker`` (str): Qwen3-Omni preset TTS speaker name; sent as
``extra_body["speaker"]`` to ``chat.completions.create``.
request_num: Number of requests, defaults to 1 (single request)
Returns:
@@ -2278,9 +2314,8 @@ class OpenAIClientHandler:
modalities = request_config.get("modalities", ["text", "audio"])
extra_body: dict[str, Any] = {}
raw_extra = request_config.get("extra_body")
if raw_extra:
extra_body.update(raw_extra)
if "speaker" in request_config:
extra_body["speaker"] = request_config["speaker"]
if request_config.get("use_audio_in_video"):
mm = dict(extra_body.get("mm_processor_kwargs") or {})
mm["use_audio_in_video"] = True
@@ -2312,12 +2347,15 @@ class OpenAIClientHandler:
# Send concurrent requests: run create + process in worker so e2e_latency includes full round-trip.
def _one_omni_request():
start = time.perf_counter()
chat_completion = self.client.chat.completions.create(
model=request_config.get("model"),
messages=request_config.get("messages"),
modalities=modalities,
stream=stream,
)
worker_kwargs: dict[str, Any] = {
"model": request_config.get("model"),
"messages": request_config.get("messages"),
"modalities": modalities,
"stream": stream,
}
if extra_body_arg is not None:
worker_kwargs["extra_body"] = extra_body_arg
chat_completion = self.client.chat.completions.create(**worker_kwargs)
if stream:
response = self._process_stream_omni_response(chat_completion)
else:
@@ -9,6 +9,10 @@ those requests are permanently orphaned.
import unittest
from collections import deque
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class FakeAdapter:
"""Minimal mock of OmniChunkTransferAdapter tracking restore calls."""
+1 -1
View File
@@ -88,7 +88,7 @@ runtime:
connector_get_max_wait_first_chunk: 3000
connector_get_max_wait: 300
codec_chunk_frames: 25
codec_left_context_frames: 25
codec_left_context_frames: 72
edges:
- from: 0
@@ -0,0 +1,217 @@
[
{
"test_name": "test_ltx2_baseline_eager",
"description": "Single-device baseline with enforce-eager (no torch.compile)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
},
{
"test_name": "test_ltx2_torch_compile",
"description": "Single-device with torch.compile (default, no enforce-eager)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
},
{
"test_name": "test_ltx2_cfg2_eager",
"description": "CFG-parallel=2 with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cfg-parallel-size": 2,
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
},
{
"test_name": "test_ltx2_cfg2_compile",
"description": "CFG-parallel=2 with torch.compile",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cfg-parallel-size": 2,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
},
{
"test_name": "test_ltx2_cache_dit_eager",
"description": "CacheDiT with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cache-backend": "cache_dit",
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
}
]
}
]
+7 -1
View File
@@ -21,12 +21,13 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
import torch
from tests.utils import hardware_test
from vllm_omni.diffusion.cache.teacache.extractors import extract_flux2_klein_context
from vllm_omni.diffusion.models.flux2_klein.flux2_klein_transformer import (
Flux2Transformer2DModel,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
pytestmark = [pytest.mark.core_model]
@pytest.fixture(scope="function", autouse=True)
@@ -113,6 +114,7 @@ class TestFlux2KleinExtractor(BaseExtractorTest):
def get_sample_inputs(self, sample_inputs):
return sample_inputs
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_modulated_input_shape(self, flux2_klein_module, sample_inputs):
"""Test that modulated_input has correct shape matching the model's inner_dim.
@@ -126,16 +128,19 @@ class TestFlux2KleinExtractor(BaseExtractorTest):
inner_dim = flux2_klein_module.inner_dim
assert context.modulated_input.shape == (batch_size, img_seq_len, inner_dim)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_run_transformer_blocks_callable(self, flux2_klein_module, sample_inputs):
"""Test that run_transformer_blocks is callable."""
context = extract_flux2_klein_context(flux2_klein_module, **sample_inputs)
assert callable(context.run_transformer_blocks)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_postprocess_callable(self, flux2_klein_module, sample_inputs):
"""Test that postprocess is callable."""
context = extract_flux2_klein_context(flux2_klein_module, **sample_inputs)
assert callable(context.postprocess)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_extra_states_contains_full_transformer(self, flux2_klein_module, sample_inputs):
"""Test that extra_states contains run_flux2_full_transformer_with_single."""
context = extract_flux2_klein_context(flux2_klein_module, **sample_inputs)
@@ -154,6 +159,7 @@ class TestFlux2KleinExtractor(BaseExtractorTest):
assert context is not None
assert context.temb is not None
@pytest.mark.cpu
def test_invalid_module_raises_error(self):
"""Test that invalid module without transformer_blocks raises ValueError."""
invalid_module = Mock()
@@ -0,0 +1,43 @@
import pytest
import torch
from vllm_omni.diffusion.distributed.autoencoders import autoencoder_kl_wan as wan_vae_module
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import OmniAutoencoderKLWan
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _DummyOmniAutoencoderKLWan(OmniAutoencoderKLWan):
def __init__(self, *, dtype: torch.dtype):
torch.nn.Module.__init__(self)
self.register_parameter("dummy_weight", torch.nn.Parameter(torch.ones(1, dtype=dtype)))
def test_wan_vae_execution_context_handles_fp32():
model = _DummyOmniAutoencoderKLWan(dtype=torch.float32)
with model._execution_context():
output = model.dummy_weight + 1
assert output.dtype == torch.float32
def test_wan_vae_execution_context_handles_bf16():
model = _DummyOmniAutoencoderKLWan(dtype=torch.bfloat16)
with model._execution_context():
output = model.dummy_weight + 1
assert output.dtype == torch.bfloat16
def test_wan_vae_execution_context_uses_platform_autocast(mocker):
sentinel = object()
platform = mocker.Mock()
platform.create_autocast_context.return_value = sentinel
mocker.patch.object(wan_vae_module, "current_omni_platform", platform)
model = _DummyOmniAutoencoderKLWan(dtype=torch.bfloat16)
assert model._execution_context() is sentinel
platform.create_autocast_context.assert_called_once_with(
device_type=model.dummy_weight.device.type,
dtype=torch.bfloat16,
enabled=True,
)
@@ -0,0 +1,273 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for DistributedAutoencoderKLWan encode parallel (CPU-only)."""
import pytest
import torch
pytestmark = [pytest.mark.cpu, pytest.mark.core_model]
class _DummyConfig:
def __init__(self, patch_size=None, scale_factor_temporal=4):
self.patch_size = patch_size
self.scale_factor_temporal = scale_factor_temporal
class _DummyWanVae:
"""Minimal mock of DistributedAutoencoderKLWan for testing encode_tile_split."""
def __init__(
self,
config=None,
spatial_compression_ratio=8,
tile_sample_min_height=256,
tile_sample_min_width=256,
tile_sample_stride_height=192,
tile_sample_stride_width=192,
):
self.config = config or _DummyConfig()
self.spatial_compression_ratio = spatial_compression_ratio
self.tile_sample_min_height = tile_sample_min_height
self.tile_sample_min_width = tile_sample_min_width
self.tile_sample_stride_height = tile_sample_stride_height
self.tile_sample_stride_width = tile_sample_stride_width
self.dtype = torch.float32
# Mock caches
self._enc_feat_map = None
self._enc_conv_idx = [0]
def clear_cache(self):
self._enc_feat_map = None
self._enc_conv_idx = [0]
def encoder(self, x, feat_cache=None, feat_idx=None): # noqa: ARG002
# Simple mock: just return the input
return x
def quant_conv(self, x):
return x
def blend_v(self, _a, b, _blend_extent):
return b
def blend_h(self, _a, b, _blend_extent):
return b
def _import_encode_tile_split():
"""Import the encode_tile_split method from the module."""
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import (
DistributedAutoencoderKLWan,
)
return DistributedAutoencoderKLWan.encode_tile_split
def _import_encode_tile_exec():
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import (
DistributedAutoencoderKLWan,
)
return DistributedAutoencoderKLWan.encode_tile_exec
def _import_encode_tile_merge():
from vllm_omni.diffusion.distributed.autoencoders.autoencoder_kl_wan import (
DistributedAutoencoderKLWan,
)
return DistributedAutoencoderKLWan.encode_tile_merge
class TestEncodeTileSplit:
"""Tests for encode_tile_split method."""
def test_basic_split_without_patch_size(self):
"""Test basic tile splitting without patch_size."""
encode_tile_split = _import_encode_tile_split()
vae = _DummyWanVae(
config=_DummyConfig(patch_size=None, scale_factor_temporal=4),
spatial_compression_ratio=8,
tile_sample_min_height=256,
tile_sample_min_width=256,
tile_sample_stride_height=192,
tile_sample_stride_width=192,
)
# Input: (B, C, T, H, W) = (1, 3, 5, 256, 256)
x = torch.randn(1, 3, 5, 256, 256)
tiletask_list, grid_spec = encode_tile_split(vae, x)
# With stride 192 and input size 256, we should get:
# Height: ceil(256/192) = 2 positions (0, 192) but 192+256 > 256, so only 1
# Actually for i in range(0, 256, 192): i = 0, 192 but 192 is out of bounds
# So we get 1x1 grid
assert len(tiletask_list) >= 1
assert grid_spec.grid_shape[0] >= 1
assert grid_spec.grid_shape[1] >= 1
# Check temporal chunking: 5 frames -> 1 + (5-1)//4 = 2 chunks
first_task = tiletask_list[0]
assert len(first_task.tensor) == 2 # 2 temporal chunks
def test_split_with_patch_size_scales_coordinates(self):
"""Test that patch_size properly scales tile coordinates."""
encode_tile_split = _import_encode_tile_split()
# Without patch_size
vae_no_patch = _DummyWanVae(
config=_DummyConfig(patch_size=None, scale_factor_temporal=4),
spatial_compression_ratio=8,
tile_sample_min_height=256,
tile_sample_min_width=256,
tile_sample_stride_height=128,
tile_sample_stride_width=128,
)
# With patch_size=2 (simulating patchified input)
vae_with_patch = _DummyWanVae(
config=_DummyConfig(patch_size=2, scale_factor_temporal=4),
spatial_compression_ratio=8,
tile_sample_min_height=256,
tile_sample_min_width=256,
tile_sample_stride_height=128,
tile_sample_stride_width=128,
)
# Same patchified input size
x = torch.randn(1, 3, 5, 256, 256)
tasks_no_patch, _ = encode_tile_split(vae_no_patch, x)
tasks_with_patch, _ = encode_tile_split(vae_with_patch, x)
# With patch_size=2, stride becomes 128//2=64, so more tiles
assert len(tasks_with_patch) >= len(tasks_no_patch)
def test_temporal_compression_from_config(self):
"""Test that temporal compression ratio is read from config."""
encode_tile_split = _import_encode_tile_split()
# temporal_compression=4 (default)
vae_4x = _DummyWanVae(
config=_DummyConfig(scale_factor_temporal=4),
tile_sample_min_height=512,
tile_sample_min_width=512,
tile_sample_stride_height=512,
tile_sample_stride_width=512,
)
# temporal_compression=2
vae_2x = _DummyWanVae(
config=_DummyConfig(scale_factor_temporal=2),
tile_sample_min_height=512,
tile_sample_min_width=512,
tile_sample_stride_height=512,
tile_sample_stride_width=512,
)
# 9 frames input
x = torch.randn(1, 3, 9, 512, 512)
tasks_4x, _ = encode_tile_split(vae_4x, x)
tasks_2x, _ = encode_tile_split(vae_2x, x)
# With 4x compression: 1 + (9-1)//4 = 3 chunks
assert len(tasks_4x[0].tensor) == 3
# With 2x compression: 1 + (9-1)//2 = 5 chunks
assert len(tasks_2x[0].tensor) == 5
def test_grid_spec_latent_dimensions(self):
"""Test that grid_spec contains correct latent dimensions."""
encode_tile_split = _import_encode_tile_split()
vae = _DummyWanVae(
config=_DummyConfig(patch_size=None),
spatial_compression_ratio=8,
tile_sample_min_height=512,
tile_sample_min_width=512,
tile_sample_stride_height=512,
tile_sample_stride_width=512,
)
# Input: 512x512 with compression 8 -> 64x64 latent
x = torch.randn(1, 3, 5, 512, 512)
_, grid_spec = encode_tile_split(vae, x)
assert grid_spec.tile_spec["latent_height"] == 64
assert grid_spec.tile_spec["latent_width"] == 64
class TestEncodeTileExec:
"""Tests for encode_tile_exec method."""
def test_basic_exec(self):
"""Test basic tile execution."""
encode_tile_exec = _import_encode_tile_exec()
vae = _DummyWanVae()
from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor import (
TileTask,
)
# Create a simple task with 2 temporal chunks
tile1 = torch.randn(1, 3, 1, 32, 32)
tile2 = torch.randn(1, 3, 4, 32, 32)
task = TileTask(tile_id=0, grid_coord=(0, 0), tensor=[tile1, tile2])
result = encode_tile_exec(vae, task)
# Result should concatenate temporal dimension
assert result.shape[2] == 5 # 1 + 4 frames
class TestEncodeTileMerge:
"""Tests for encode_tile_merge method."""
def test_basic_merge(self):
"""Test basic tile merging."""
encode_tile_merge = _import_encode_tile_merge()
vae = _DummyWanVae()
from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor import (
GridSpec,
)
# Create 2x2 grid of tiles
tile_00 = torch.ones(1, 16, 2, 32, 32) * 0
tile_01 = torch.ones(1, 16, 2, 32, 32) * 1
tile_10 = torch.ones(1, 16, 2, 32, 32) * 2
tile_11 = torch.ones(1, 16, 2, 32, 32) * 3
coord_tensor_map = {
(0, 0): tile_00,
(0, 1): tile_01,
(1, 0): tile_10,
(1, 1): tile_11,
}
grid_spec = GridSpec(
split_dims=(3, 4),
grid_shape=(2, 2),
tile_spec={
"latent_height": 48,
"latent_width": 48,
"blend_height": 8,
"blend_width": 8,
"tile_latent_stride_height": 24,
"tile_latent_stride_width": 24,
},
)
result = encode_tile_merge(vae, coord_tensor_map, grid_spec)
# Output should be (1, 16, 2, 48, 48)
assert result.shape == (1, 16, 2, 48, 48)
@@ -11,6 +11,8 @@ from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor impor
TileTask,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class E2EOperator:
"""tiles with (2, 3) -- (H,W)"""
@@ -59,9 +61,9 @@ class E2EOperator:
class DummyMixin(DistributedVaeMixin):
def __init__(self):
self.use_tiling = True
self.distributed_decoder = MagicMock()
self.distributed_decoder.parallel_size = 2
self.distributed_decoder.group = None
self.distributed_executor = MagicMock()
self.distributed_executor.parallel_size = 2
self.distributed_executor.group = None
@pytest.fixture(autouse=True)
@@ -17,6 +17,7 @@ import pytest
import torch
import torch.distributed as dist
from tests.utils import hardware_test
from vllm_omni.diffusion.attention.parallel.ulysses import (
_all_gather_int,
_ulysses_all_to_all_any_o,
@@ -69,6 +70,8 @@ PERF_CASES: list[_PerfCase] = [
@pytest.mark.parametrize("case", PERF_CASES)
@pytest.mark.core_model
@hardware_test(res={"cuda": "L4"}, num_cards=4)
def test_ulysses_advanced_uaa_comm_overhead(case: _PerfCase) -> None:
available_gpus = current_omni_platform.get_device_count()
if available_gpus < case.world_size:
View File
Whitespace-only changes.
+237
View File
@@ -0,0 +1,237 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for shared AdaLayerNorm layers used by FLUX and other models."""
import os
import pytest
import torch
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@pytest.fixture(autouse=True)
def _init_distributed():
"""Initialize the minimal distributed environment required by
ReplicatedLinear (tensor-parallel group must exist)."""
from vllm.distributed.parallel_state import (
cleanup_dist_env_and_memory,
init_distributed_environment,
initialize_model_parallel,
)
os.environ.setdefault("MASTER_ADDR", "localhost")
os.environ.setdefault("MASTER_PORT", "29501")
init_distributed_environment(
world_size=1,
rank=0,
local_rank=0,
distributed_init_method="env://",
)
initialize_model_parallel()
yield
cleanup_dist_env_and_memory()
@pytest.fixture(autouse=True)
def _force_default_gemm(monkeypatch):
"""Force CPU-compatible GEMM dispatch for tests using CPU tensors.
vLLM's dispatch_unquantized_gemm() selects the backend by platform
(e.g. rocm_unquantized_gemm on AMD machines), not by tensor device.
CPU test tensors crash with NotImplementedError on ROCm. Monkeypatch
the dispatcher to always return the default (torch.nn.functional.linear)
implementation which works on any device."""
from vllm.model_executor.layers.utils import default_unquantized_gemm
monkeypatch.setattr(
"vllm.model_executor.layers.linear.dispatch_unquantized_gemm",
lambda: default_unquantized_gemm,
)
def test_adalayernorm_import_from_shared_module():
"""Verify imports work from the shared adalayernorm module."""
from vllm_omni.diffusion.layers.adalayernorm import ( # noqa: F401
AdaLayerNormContinuous,
AdaLayerNormZero,
AdaLayerNormZeroSingle,
)
def test_adalayernorm_zero_forward_shape():
"""AdaLayerNormZero produces correct output shapes (x, gate, shift, scale, gate)."""
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNormZero
dim = 64
batch = 2
seq_len = 4
norm = AdaLayerNormZero(dim)
x = torch.randn(batch, seq_len, dim)
emb = torch.randn(batch, dim)
out_x, gate_msa, shift_mlp, scale_mlp, gate_mlp = norm(x, emb)
assert out_x.shape == (batch, seq_len, dim)
assert gate_msa.shape == (batch, dim)
assert shift_mlp.shape == (batch, dim)
assert scale_mlp.shape == (batch, dim)
assert gate_mlp.shape == (batch, dim)
def test_adalayernorm_zero_single_forward_shape():
"""AdaLayerNormZeroSingle produces (x, gate) with correct shapes."""
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNormZeroSingle
dim = 64
batch = 2
seq_len = 4
norm = AdaLayerNormZeroSingle(dim)
x = torch.randn(batch, seq_len, dim)
emb = torch.randn(batch, dim)
out_x, gate = norm(x, emb)
assert out_x.shape == (batch, seq_len, dim)
assert gate.shape == (batch, dim)
def test_adalayernorm_continuous_forward_shape():
"""AdaLayerNormContinuous produces correct output shape."""
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNormContinuous
dim = 64
cond_dim = 64
batch = 2
seq_len = 4
norm = AdaLayerNormContinuous(dim, cond_dim)
x = torch.randn(batch, seq_len, dim)
conditioning = torch.randn(batch, cond_dim)
out = norm(x, conditioning)
assert out.shape == (batch, seq_len, dim)
def test_adalayernorm_zero_accepts_quant_config():
"""Constructor accepts quant_config=None and prefix='test' without error."""
from vllm_omni.diffusion.layers.adalayernorm import (
AdaLayerNormContinuous,
AdaLayerNormZero,
AdaLayerNormZeroSingle,
)
# Should not raise with quant_config=None and prefix
AdaLayerNormZero(64, quant_config=None, prefix="test.norm1")
AdaLayerNormZeroSingle(64, quant_config=None, prefix="test.norm")
AdaLayerNormContinuous(64, 64, quant_config=None, prefix="test.norm_out")
def test_adalayernorm_uses_replicated_linear():
"""Verify .linear is a ReplicatedLinear instance (not nn.Linear)."""
from vllm.model_executor.layers.linear import ReplicatedLinear
from vllm_omni.diffusion.layers.adalayernorm import (
AdaLayerNormContinuous,
AdaLayerNormZero,
AdaLayerNormZeroSingle,
)
norm_zero = AdaLayerNormZero(64)
assert isinstance(norm_zero.linear, ReplicatedLinear)
norm_zero_single = AdaLayerNormZeroSingle(64)
assert isinstance(norm_zero_single.linear, ReplicatedLinear)
norm_continuous = AdaLayerNormContinuous(64, 64)
assert isinstance(norm_continuous.linear, ReplicatedLinear)
# ── Numerical equivalence tests against diffusers originals ──
def _copy_weights(src_linear, dst_replicated_linear):
"""Copy weights from nn.Linear to ReplicatedLinear for comparison."""
dst_replicated_linear.weight.data.copy_(src_linear.weight.data)
if src_linear.bias is not None and dst_replicated_linear.bias is not None:
dst_replicated_linear.bias.data.copy_(src_linear.bias.data)
def test_adalayernorm_zero_matches_diffusers():
"""Verify AdaLayerNormZero produces identical output to diffusers original."""
from diffusers.models.normalization import (
AdaLayerNormZero as DiffusersAdaLayerNormZero,
)
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNormZero
dim = 64
torch.manual_seed(42)
ours = AdaLayerNormZero(dim)
ref = DiffusersAdaLayerNormZero(dim)
# Copy weights: nn.Linear -> ReplicatedLinear
_copy_weights(ref.linear, ours.linear)
x = torch.randn(2, 4, dim)
emb = torch.randn(2, dim)
out_ours = ours(x, emb)
out_ref = ref(x, emb=emb)
for o, r in zip(out_ours, out_ref):
torch.testing.assert_close(o, r, atol=1e-5, rtol=1e-5)
def test_adalayernorm_zero_single_matches_diffusers():
"""Verify AdaLayerNormZeroSingle produces identical output to diffusers original."""
from diffusers.models.normalization import (
AdaLayerNormZeroSingle as DiffusersAdaLayerNormZeroSingle,
)
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNormZeroSingle
dim = 64
torch.manual_seed(42)
ours = AdaLayerNormZeroSingle(dim)
ref = DiffusersAdaLayerNormZeroSingle(dim)
_copy_weights(ref.linear, ours.linear)
x = torch.randn(2, 4, dim)
emb = torch.randn(2, dim)
out_ours = ours(x, emb)
out_ref = ref(x, emb=emb)
for o, r in zip(out_ours, out_ref):
torch.testing.assert_close(o, r, atol=1e-5, rtol=1e-5)
def test_adalayernorm_continuous_matches_diffusers():
"""Verify AdaLayerNormContinuous produces identical output to diffusers original."""
from diffusers.models.normalization import (
AdaLayerNormContinuous as DiffusersAdaLayerNormContinuous,
)
from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNormContinuous
dim = 64
cond_dim = 64
torch.manual_seed(42)
# Match constructor args: diffusers defaults elementwise_affine=True, eps=1e-5
ours = AdaLayerNormContinuous(dim, cond_dim, elementwise_affine=False, eps=1e-6)
ref = DiffusersAdaLayerNormContinuous(dim, cond_dim, elementwise_affine=False, eps=1e-6)
_copy_weights(ref.linear, ours.linear)
x = torch.randn(2, 4, dim)
cond = torch.randn(2, cond_dim)
out_ours = ours(x, cond)
out_ref = ref(x, cond)
torch.testing.assert_close(out_ours, out_ref, atol=1e-5, rtol=1e-5)
+56
View File
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Shared test helpers for diffusion LoRA tests."""
from __future__ import annotations
import torch
from vllm.model_executor.layers.linear import LinearBase
class FakeLinearBase(LinearBase):
"""Minimal LinearBase stub for LoRA layer discovery."""
def __init__(self):
torch.nn.Module.__init__(self)
class DummyBaseLayerWithLoRA(torch.nn.Module):
"""Fake LoRA wrapper that records set/reset/create calls."""
def __init__(self, base_layer: torch.nn.Module):
super().__init__()
self.base_layer = base_layer
self.set_calls: list[
tuple[list[torch.Tensor | None] | torch.Tensor, list[torch.Tensor | None] | torch.Tensor]
] = []
self.reset_calls: int = 0
self.create_calls: int = 0
def set_lora(self, index: int, lora_a, lora_b):
assert index == 0
self.set_calls.append((lora_a, lora_b))
def reset_lora(self, index: int):
assert index == 0
self.reset_calls += 1
def create_lora_weights(self, max_loras, lora_config, model_config):
self.create_calls += 1
def fake_replace_submodule(
root: torch.nn.Module,
module_name: str,
submodule: torch.nn.Module,
replace_calls: list[str] | None = None,
) -> None:
"""Replace a submodule by traversing dotted paths correctly."""
if replace_calls is not None:
replace_calls.append(module_name)
parts = module_name.split(".")
parent = root
for attr in parts[:-1]:
parent = getattr(parent, attr)
setattr(parent, parts[-1], submodule)
+50 -30
View File
@@ -7,8 +7,12 @@ import pytest
import torch
from vllm.lora.lora_weights import LoRALayerWeights
from vllm.lora.utils import get_supported_lora_modules
from vllm.model_executor.layers.linear import LinearBase
from tests.diffusion.lora.conftest import (
DummyBaseLayerWithLoRA,
FakeLinearBase,
fake_replace_submodule,
)
from vllm_omni.diffusion.lora.manager import DiffusionLoRAManager
from vllm_omni.lora.request import LoRARequest
@@ -33,35 +37,9 @@ class _DummyLoRALayer:
self.reset_calls += 1
class _FakeLinearBase(LinearBase):
def __init__(self):
torch.nn.Module.__init__(self)
class _DummyBaseLayerWithLoRA(torch.nn.Module):
def __init__(self, base_layer: torch.nn.Module):
super().__init__()
self.base_layer = base_layer
self.set_calls: list[
tuple[list[torch.Tensor | None] | torch.Tensor, list[torch.Tensor | None] | torch.Tensor]
] = []
self.reset_calls: int = 0
self.create_calls: int = 0
def set_lora(self, index: int, lora_a, lora_b):
assert index == 0
self.set_calls.append((lora_a, lora_b))
def reset_lora(self, index: int):
assert index == 0
self.reset_calls += 1
def create_lora_weights(self, max_loras, lora_config, model_config):
# Needs to be callable for scale test when rank changes, but not
# actually used since we mock everything and check everything based
# on set calls.
self.create_calls += 1
# Aliases for backward compatibility within this file
_FakeLinearBase = FakeLinearBase
_DummyBaseLayerWithLoRA = DummyBaseLayerWithLoRA
class _DummyPipeline(torch.nn.Module):
@@ -555,3 +533,45 @@ def test_lora_manager_max_rank_validation(monkeypatch, rank):
req1 = _dummy_lora_request(1)
with pytest.raises(ValueError):
manager.add_adapter(req1)
def test_lora_manager_discovers_bagel_component(monkeypatch):
"""Verify that _replace_layers_with_lora finds layers under 'bagel'."""
import vllm_omni.diffusion.lora.manager as manager_mod
monkeypatch.setattr(manager_mod, "BaseLayerWithLoRA", _DummyBaseLayerWithLoRA)
def _fake_from_layer_diffusion(*, layer: torch.nn.Module, **_kwargs):
if isinstance(layer, _FakeLinearBase):
return _DummyBaseLayerWithLoRA(layer)
return layer
replace_calls: list[str] = []
monkeypatch.setattr(manager_mod, "from_layer_diffusion", _fake_from_layer_diffusion)
monkeypatch.setattr(
manager_mod,
"replace_submodule",
lambda root, name, sub: fake_replace_submodule(root, name, sub, replace_calls),
)
# Pipeline with a 'bagel' component (no 'transformer')
pipeline = torch.nn.Module()
pipeline.bagel = torch.nn.Module()
pipeline.bagel.language_model = torch.nn.Module()
pipeline.bagel.language_model.qkv_proj = _FakeLinearBase()
manager = DiffusionLoRAManager(
pipeline=pipeline,
device=torch.device("cpu"),
dtype=torch.bfloat16,
max_cached_adapters=1,
)
peft_helper = type("_PH", (), {"r": 1})()
manager._replace_layers_with_lora(peft_helper)
assert "language_model.qkv_proj" in replace_calls
assert "bagel.language_model.qkv_proj" in manager._lora_modules
# Verify the module was actually replaced in the tree (not just recorded)
assert isinstance(pipeline.bagel.language_model.qkv_proj, _DummyBaseLayerWithLoRA)
@@ -0,0 +1,248 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for BAGEL LoRA support across Stage 0 (Thinker) and Stage 1 (DiT)."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
import torch
from safetensors.torch import save_file
from tests.diffusion.lora.conftest import (
DummyBaseLayerWithLoRA,
FakeLinearBase,
fake_replace_submodule,
)
from vllm_omni.diffusion.lora.manager import DiffusionLoRAManager
from vllm_omni.lora.request import LoRARequest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_FakeLinearBase = FakeLinearBase
# ---------------------------------------------------------------------------
# Stage 0 (Thinker / AR) -- packed_modules_mapping on the AR model class
# ---------------------------------------------------------------------------
class TestStage0ThinkerLoRA:
"""Validate that OmniBagelForConditionalGeneration declares correct LoRA metadata."""
def test_omni_bagel_supports_lora(self):
from vllm_omni.model_executor.models.bagel.bagel import (
OmniBagelForConditionalGeneration,
)
assert getattr(OmniBagelForConditionalGeneration, "supports_lora", False) is True
def test_omni_bagel_packed_modules_mapping_complete(self):
from vllm_omni.model_executor.models.bagel.bagel import (
OmniBagelForConditionalGeneration,
)
mapping = OmniBagelForConditionalGeneration.packed_modules_mapping
# Standard Qwen2 projections
assert mapping["qkv_proj"] == ["q_proj", "k_proj", "v_proj"]
assert mapping["gate_up_proj"] == ["gate_proj", "up_proj"]
# MoE generation-mode projections
assert mapping["qkv_proj_moe_gen"] == [
"q_proj_moe_gen",
"k_proj_moe_gen",
"v_proj_moe_gen",
]
assert mapping["mlp_moe_gen.gate_up_proj"] == [
"mlp_moe_gen.gate_proj",
"mlp_moe_gen.up_proj",
]
# ---------------------------------------------------------------------------
# Stage 1 (DiT / Diffusion) -- DiffusionLoRAManager with bagel component
# ---------------------------------------------------------------------------
class TestStage1DiTLoRA:
"""Validate DiffusionLoRAManager discovers BAGEL's packed modules."""
def test_diffusion_lora_manager_discovers_bagel_packed_modules(self):
"""Manager should derive packed→sublayer mapping from stacked_params_mapping."""
pipeline = torch.nn.Module()
pipeline.bagel = torch.nn.Module()
# Simulate a submodule that exposes stacked_params_mapping
# (as Bagel does after load_weights())
language_model = torch.nn.Module()
language_model.stacked_params_mapping = [
(".qkv_proj_moe_gen", ".q_proj_moe_gen", "q"),
(".qkv_proj_moe_gen", ".k_proj_moe_gen", "k"),
(".qkv_proj_moe_gen", ".v_proj_moe_gen", "v"),
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
(".gate_up_proj", ".gate_proj", 0),
(".gate_up_proj", ".up_proj", 1),
]
pipeline.bagel.language_model = language_model
manager = DiffusionLoRAManager(
pipeline=pipeline,
device=torch.device("cpu"),
dtype=torch.bfloat16,
max_cached_adapters=1,
)
mapping = manager._packed_modules_mapping
assert mapping["qkv_proj"] == ["q_proj", "k_proj", "v_proj"]
assert mapping["qkv_proj_moe_gen"] == [
"q_proj_moe_gen",
"k_proj_moe_gen",
"v_proj_moe_gen",
]
assert mapping["gate_up_proj"] == ["gate_proj", "up_proj"]
def test_diffusion_lora_manager_replaces_bagel_packed_layer_via_sublayer_target(self, monkeypatch):
"""Targeting sublayer 'q_proj' should replace the fused 'qkv_proj' under bagel."""
import vllm_omni.diffusion.lora.manager as manager_mod
monkeypatch.setattr(manager_mod, "BaseLayerWithLoRA", DummyBaseLayerWithLoRA)
def _fake_from_layer_diffusion(*, layer, **_kwargs):
return DummyBaseLayerWithLoRA(layer)
replace_calls: list[str] = []
monkeypatch.setattr(manager_mod, "from_layer_diffusion", _fake_from_layer_diffusion)
monkeypatch.setattr(
manager_mod,
"replace_submodule",
lambda root, name, sub: fake_replace_submodule(root, name, sub, replace_calls),
)
# Build pipeline with bagel component
pipeline = torch.nn.Module()
pipeline.bagel = torch.nn.Module()
lm = torch.nn.Module()
lm.stacked_params_mapping = [
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
]
lm.attn = torch.nn.Module()
lm.attn.qkv_proj = _FakeLinearBase()
pipeline.bagel.language_model = lm
manager = DiffusionLoRAManager(
pipeline=pipeline,
device=torch.device("cpu"),
dtype=torch.bfloat16,
max_cached_adapters=1,
)
# Treat qkv_proj as 3-slice packed layer
monkeypatch.setattr(manager, "_get_packed_modules_list", lambda _module: ["q", "k", "v"])
# Target sublayer "q_proj" -- manager should replace the packed "qkv_proj"
peft_helper = type("_PH", (), {"r": 1, "target_modules": ["q_proj"]})()
manager._replace_layers_with_lora(peft_helper)
assert "language_model.attn.qkv_proj" in replace_calls
assert "bagel.language_model.attn.qkv_proj" in manager._lora_modules
# Verify the module was actually replaced in the tree (not just recorded)
assert isinstance(pipeline.bagel.language_model.attn.qkv_proj, DummyBaseLayerWithLoRA)
# ---------------------------------------------------------------------------
# Round-trip: synthetic checkpoint → set_active_adapter → verify weights
# ---------------------------------------------------------------------------
def _write_synthetic_lora(
adapter_dir: Path,
module_name: str,
rank: int,
in_dim: int,
out_dim: int,
) -> str:
"""Write a minimal LoRA adapter (safetensors + config) to *adapter_dir*."""
adapter_dir.mkdir(parents=True, exist_ok=True)
lora_a = torch.ones((rank, in_dim), dtype=torch.float32)
lora_b = torch.ones((out_dim, rank), dtype=torch.float32) * 2.0
save_file(
{
f"base_model.model.{module_name}.lora_A.weight": lora_a,
f"base_model.model.{module_name}.lora_B.weight": lora_b,
},
str(adapter_dir / "adapter_model.safetensors"),
)
(adapter_dir / "adapter_config.json").write_text(
json.dumps({"r": rank, "lora_alpha": rank, "target_modules": [module_name]}),
encoding="utf-8",
)
return str(adapter_dir)
class TestBagelLoRARoundTrip:
"""End-to-end: synthetic checkpoint → load → activate → verify weights in fused layer."""
def test_set_active_adapter_loads_and_activates_bagel_lora(self, tmp_path, monkeypatch):
"""Full round-trip through set_active_adapter for a bagel component module."""
import vllm_omni.diffusion.lora.manager as manager_mod
monkeypatch.setattr(manager_mod, "BaseLayerWithLoRA", DummyBaseLayerWithLoRA)
# Build pipeline with bagel.language_model.foo (simple non-packed layer)
pipeline = torch.nn.Module()
pipeline.bagel = torch.nn.Module()
lm = torch.nn.Module()
lm.foo = _FakeLinearBase()
pipeline.bagel.language_model = lm
def _fake_from_layer(*, layer, **_kwargs):
if isinstance(layer, FakeLinearBase):
return DummyBaseLayerWithLoRA(layer)
return layer
monkeypatch.setattr(manager_mod, "from_layer_diffusion", _fake_from_layer)
monkeypatch.setattr(
manager_mod,
"replace_submodule",
lambda root, name, sub: fake_replace_submodule(root, name, sub),
)
manager = DiffusionLoRAManager(
pipeline=pipeline,
device=torch.device("cpu"),
dtype=torch.bfloat16,
max_cached_adapters=1,
)
# Write synthetic adapter targeting bagel.language_model.foo
module_name = "bagel.language_model.foo"
rank = 2
in_dim = 4
out_dim = 4
lora_dir = _write_synthetic_lora(tmp_path / "lora", module_name, rank, in_dim, out_dim)
lora_request = LoRARequest(
lora_name="test_bagel",
lora_int_id=42,
lora_path=lora_dir,
)
# Full round-trip: load from disk → replace layer → activate weights
manager.set_active_adapter(lora_request, lora_scale=0.5)
# Verify the layer was replaced and weights were set
replaced_layer = pipeline.bagel.language_model.foo
assert isinstance(replaced_layer, DummyBaseLayerWithLoRA), "Layer should be wrapped with LoRA"
assert len(replaced_layer.set_calls) == 1, "set_lora should have been called once"
lora_a, lora_b = replaced_layer.set_calls[0]
# A weights should be ones (as written)
assert torch.all(lora_a == 1.0), f"lora_a should be all ones, got {lora_a}"
# B weights should be 2.0 * scale(0.5) = 1.0
assert torch.allclose(lora_b, torch.ones_like(lora_b)), f"lora_b should be 2.0 * 0.5 = 1.0, got {lora_b}"
@@ -0,0 +1,236 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for BAGEL trajectory recording in the denoising loop."""
import types
from dataclasses import dataclass
from unittest.mock import MagicMock, patch
import pytest
import torch
from vllm_omni.diffusion.models.bagel.bagel_transformer import (
Bagel,
NaiveCache,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
NUM_TOKENS = 8
HIDDEN_DIM = 16
NUM_TIMESTEPS = 5
# generate_image uses timesteps[:-1], so actual steps = NUM_TIMESTEPS - 1
EXPECTED_STEPS = NUM_TIMESTEPS - 1
def _make_mock_bagel():
"""Create a mock Bagel with forward returning constant velocity."""
mock = MagicMock(spec=Bagel)
mock._sp_size = 1
# forward returns a small constant velocity so x_t changes each step
def fake_forward(self, x_t, **kwargs):
return torch.ones_like(x_t) * 0.1
mock.forward = types.MethodType(fake_forward, mock)
# _merge_naive_caches is called in the batched CFG path
mock._merge_naive_caches = types.MethodType(lambda self, caches: NaiveCache(1), mock)
# Bind the real generate_image to our mock
mock.generate_image = types.MethodType(Bagel.generate_image, mock)
return mock
def _make_generate_args(num_tokens=NUM_TOKENS, hidden_dim=HIDDEN_DIM, cfg=False):
"""Tensor arguments for generate_image.
Args:
cfg: If True, enable batched CFG path (cfg_text_scale > 1.0).
"""
seq_len = num_tokens + 2 # packed_seqlens includes 2 extra tokens
base = dict(
packed_text_ids=torch.zeros(2, dtype=torch.long),
packed_text_indexes=torch.tensor([0, 1], dtype=torch.long),
packed_init_noises=torch.randn(num_tokens, hidden_dim),
packed_vae_position_ids=torch.arange(num_tokens, dtype=torch.long),
packed_vae_token_indexes=torch.arange(2, seq_len, dtype=torch.long),
packed_seqlens=torch.tensor([seq_len], dtype=torch.int),
packed_position_ids=torch.arange(seq_len, dtype=torch.long),
packed_indexes=torch.arange(seq_len, dtype=torch.long),
past_key_values=NaiveCache(1),
key_values_lens=torch.tensor([0], dtype=torch.int),
packed_key_value_indexes=torch.zeros(0, dtype=torch.long),
num_timesteps=NUM_TIMESTEPS,
timestep_shift=1.0,
cfg_text_scale=1.0,
cfg_img_scale=1.0,
)
if cfg:
base |= dict(
cfg_text_scale=4.0,
cfg_text_packed_query_indexes=torch.arange(seq_len, dtype=torch.long),
cfg_text_packed_position_ids=torch.arange(seq_len, dtype=torch.long),
cfg_text_past_key_values=NaiveCache(1),
cfg_text_key_values_lens=torch.tensor([0], dtype=torch.int),
cfg_text_packed_key_value_indexes=torch.zeros(0, dtype=torch.long),
)
return base
@pytest.fixture(params=[False, True], ids=["no_cfg", "batched_cfg"])
def bagel_and_args(request):
"""Mock Bagel instance and generate_image arguments.
Parametrized over CFG mode so every test runs on both the no-CFG
and batched-CFG code paths.
"""
cfg = request.param
with patch(
"vllm_omni.diffusion.models.bagel.bagel_transformer.get_classifier_free_guidance_world_size",
return_value=1,
):
yield _make_mock_bagel(), _make_generate_args(cfg=cfg)
class TestTrajectoryRecording:
"""Tests for trajectory latent/timestep recording in generate_image."""
def test_trajectory_disabled_returns_none(self, bagel_and_args):
bagel, args = bagel_and_args
unpacked, trajectory_latents, trajectory_timesteps, trajectory_log_probs = bagel.generate_image(
**args, return_trajectory_latents=False
)
assert isinstance(unpacked, (list, tuple))
assert len(unpacked) == 1 # one sequence
assert trajectory_latents is None
assert trajectory_timesteps is None
assert trajectory_log_probs is None
def test_trajectory_enabled_returns_correct_count(self, bagel_and_args):
bagel, args = bagel_and_args
_, trajectory_latents, trajectory_timesteps, trajectory_log_probs = bagel.generate_image(
**args, return_trajectory_latents=True
)
assert trajectory_latents is not None
assert trajectory_timesteps is not None
assert len(trajectory_latents) == EXPECTED_STEPS
assert len(trajectory_timesteps) == EXPECTED_STEPS
# log_probs is None without a scheduler (default ODE path)
assert trajectory_log_probs is None
def test_trajectory_latents_shape_matches_input(self, bagel_and_args):
bagel, args = bagel_and_args
expected_shape = args["packed_init_noises"].shape
_, trajectory_latents, *_ = bagel.generate_image(**args, return_trajectory_latents=True)
for i, lat in enumerate(trajectory_latents):
assert lat.shape == expected_shape, f"Step {i}: expected {expected_shape}, got {lat.shape}"
def test_trajectory_latents_are_distinct(self, bagel_and_args):
bagel, args = bagel_and_args
_, trajectory_latents, *_ = bagel.generate_image(**args, return_trajectory_latents=True)
for i in range(1, len(trajectory_latents)):
assert not torch.equal(trajectory_latents[i], trajectory_latents[i - 1]), (
f"Steps {i - 1} and {i} should differ"
)
def test_trajectory_timesteps_are_decreasing(self, bagel_and_args):
bagel, args = bagel_and_args
_, _, trajectory_timesteps, _ = bagel.generate_image(**args, return_trajectory_latents=True)
for i in range(1, len(trajectory_timesteps)):
assert trajectory_timesteps[i] < trajectory_timesteps[i - 1], (
f"Timestep {i} ({trajectory_timesteps[i]:.4f}) should be less than "
f"timestep {i - 1} ({trajectory_timesteps[i - 1]:.4f})"
)
def test_trajectory_final_latent_matches_output(self, bagel_and_args):
bagel, args = bagel_and_args
unpacked, trajectory_latents, *_ = bagel.generate_image(**args, return_trajectory_latents=True)
# Reconstruct the full final latent from unpacked pieces
final_latent = torch.cat(unpacked, dim=0)
assert torch.allclose(trajectory_latents[-1], final_latent, atol=1e-6), (
"Last trajectory latent should match the final output"
)
# ---------------------------------------------------------------------------
# Mock scheduler for log-prob tests
# ---------------------------------------------------------------------------
@dataclass
class _MockStepOutput:
prev_sample: torch.Tensor
log_prob: torch.Tensor
class _MockScheduler:
"""Minimal scheduler: Euler step + constant log-prob per step."""
def step(self, model_output, sigma, sample, dt, **kwargs):
prev_sample = sample - model_output * dt
log_prob = torch.tensor(-1.0)
return _MockStepOutput(prev_sample=prev_sample, log_prob=log_prob)
class TestTrajectoryLogProbs:
"""Tests for log-prob recording when a scheduler is provided."""
@pytest.fixture()
def bagel_scheduler_args(self):
with patch(
"vllm_omni.diffusion.models.bagel.bagel_transformer.get_classifier_free_guidance_world_size",
return_value=1,
):
yield _make_mock_bagel(), _make_generate_args(), _MockScheduler()
def test_log_probs_recorded_with_scheduler(self, bagel_scheduler_args):
bagel, args, scheduler = bagel_scheduler_args
_, _, _, trajectory_log_probs = bagel.generate_image(
**args, return_trajectory_latents=True, scheduler=scheduler
)
assert trajectory_log_probs is not None
assert len(trajectory_log_probs) == EXPECTED_STEPS
def test_log_probs_are_finite(self, bagel_scheduler_args):
bagel, args, scheduler = bagel_scheduler_args
_, _, _, trajectory_log_probs = bagel.generate_image(
**args, return_trajectory_latents=True, scheduler=scheduler
)
for i, lp in enumerate(trajectory_log_probs):
assert torch.isfinite(lp).all(), f"Step {i}: log_prob is not finite"
def test_log_probs_none_without_scheduler(self, bagel_scheduler_args):
bagel, args, _ = bagel_scheduler_args
_, _, _, trajectory_log_probs = bagel.generate_image(**args, return_trajectory_latents=True, scheduler=None)
assert trajectory_log_probs is None
def test_scheduler_updates_latents(self, bagel_scheduler_args):
"""Verify the scheduler's prev_sample is used (not the raw Euler step)."""
bagel, args, scheduler = bagel_scheduler_args
_, traj_with_sched, *_ = bagel.generate_image(**args, return_trajectory_latents=True, scheduler=scheduler)
_, traj_without, *_ = bagel.generate_image(**args, return_trajectory_latents=True, scheduler=None)
# Mock scheduler does the same Euler step, so latents should match
for i in range(len(traj_with_sched)):
assert torch.allclose(traj_with_sched[i], traj_without[i], atol=1e-5), (
f"Step {i}: scheduler and ODE paths should produce same latents"
)
Whitespace-only changes.
@@ -0,0 +1,134 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests that FLUX transformer blocks correctly propagate `quant_config` and
`prefix` through all sub-layers.
The tests instantiate blocks with a known prefix and verify that all quantization-
aware sub-layers (AdaLayerNorm, FeedForward, Attention projections) receive the
prefix rooted at the block prefix. This is critical for quantized weight loading
to match checkpoint keys to the correct model parameters.
"""
import os
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
# Standard dimensions for a minimal FLUX block
_DIM = 64
_HEADS = 2
_HEAD_DIM = _DIM // _HEADS
@pytest.fixture(autouse=True)
def _init_distributed():
"""Initialize the minimal distributed environment required by
vLLM parallel linear layers (tensor-parallel group must exist)."""
from vllm.distributed.parallel_state import (
cleanup_dist_env_and_memory,
init_distributed_environment,
initialize_model_parallel,
)
os.environ.setdefault("MASTER_ADDR", "localhost")
os.environ.setdefault("MASTER_PORT", "29502")
init_distributed_environment(
world_size=1,
rank=0,
local_rank=0,
distributed_init_method="env://",
)
initialize_model_parallel()
yield
cleanup_dist_env_and_memory()
def _param_names(module) -> set[str]:
"""Return the set of all parameter names in a module."""
return {name for name, _ in module.named_parameters()}
def test_flux_transformer_block_passes_prefix():
"""FluxTransformerBlock propagates prefix to norm1, norm1_context, attn, ff, ff_context."""
from vllm_omni.diffusion.models.flux.flux_transformer import FluxTransformerBlock
prefix = "transformer_blocks.0"
block = FluxTransformerBlock(
dim=_DIM,
num_attention_heads=_HEADS,
attention_head_dim=_HEAD_DIM,
quant_config=None,
prefix=prefix,
)
params = _param_names(block)
# norm1 and norm1_context (AdaLayerNormZero) should have linear weights
assert any(name.startswith("norm1.linear.") for name in params), (
f"norm1.linear.* not found in params: {sorted(params)}"
)
assert any(name.startswith("norm1_context.linear.") for name in params), (
f"norm1_context.linear.* not found in params: {sorted(params)}"
)
# attn should have QKV projections
assert any(name.startswith("attn.to_qkv.") for name in params), (
f"attn.to_qkv.* not found in params: {sorted(params)}"
)
# ff and ff_context should have net layers
assert any(name.startswith("ff.net.") for name in params), f"ff.net.* not found in params: {sorted(params)}"
assert any(name.startswith("ff_context.net.") for name in params), (
f"ff_context.net.* not found in params: {sorted(params)}"
)
def test_flux_single_transformer_block_passes_prefix():
"""FluxSingleTransformerBlock propagates prefix to norm, proj_mlp, attn."""
from vllm_omni.diffusion.models.flux.flux_transformer import FluxSingleTransformerBlock
prefix = "single_transformer_blocks.0"
block = FluxSingleTransformerBlock(
dim=_DIM,
num_attention_heads=_HEADS,
attention_head_dim=_HEAD_DIM,
quant_config=None,
prefix=prefix,
)
params = _param_names(block)
# norm (AdaLayerNormZeroSingle) should have linear weights
assert any(name.startswith("norm.linear.") for name in params), (
f"norm.linear.* not found in params: {sorted(params)}"
)
# proj_mlp (ReplicatedLinear) should have weight
assert any(name.startswith("proj_mlp.") for name in params), f"proj_mlp.* not found in params: {sorted(params)}"
# attn should have QKV projection
assert any(name.startswith("attn.to_qkv.") for name in params), (
f"attn.to_qkv.* not found in params: {sorted(params)}"
)
def test_flux_feedforward_passes_prefix():
"""FeedForward propagates prefix to net.0 (GELU proj) and net.2 (output proj)."""
from vllm_omni.diffusion.models.flux.flux_transformer import FeedForward
prefix = "transformer_blocks.0.ff"
ff = FeedForward(
dim=_DIM,
dim_out=_DIM,
quant_config=None,
prefix=prefix,
)
params = _param_names(ff)
# net.0 is ColumnParallelApproxGELU which wraps a ColumnParallelLinear
assert any("net.0" in name for name in params), f"net.0 not found in params: {sorted(params)}"
# net.2 is RowParallelLinear
assert any("net.2" in name for name in params), f"net.2 not found in params: {sorted(params)}"
Loaded 100 of 348 files, more files were not shown because too many files have changed in this diff. Show more