Compare commits

...
Author SHA1 Message Date
Zhou Taichang 181eab754e Merge branch 'main' into dev/rebase-v0170 2026-03-10 09:50:31 +08:00
Ekagra Ranjan 7a560492a2 Add online serving to Stable Audio Diffusion and introduce v1/audio/generate endpoint (#1255) 2026-03-09 13:36:46 -04:00
Sy03 761eff930a [Fix][Qwen3-TTS] Preserve ref_code decoder context for Base ICL (#1731) 2026-03-09 13:36:27 -04:00
Ricardo Noriega 112a9aa237 [Feature] Add Helm Chart to deploy vLLM-Omni on Kubernetes (#1337)
Signed-off-by: Ricardo Noriega De Soto <rnoriega@redhat.com>
2026-03-09 16:02:23 +00:00
Taichang Zhou e942aa2640 revert changes for the docker file which will be updated after dockers are available
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-09 23:52:36 +08:00
Taichang Zhou c406622577 update installation doc
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-09 23:44:02 +08:00
Taichang Zhou b1073a6e7f Merge branch 'dev/rebase-v0170' of https://github.com/vllm-project/vllm-omni into dev/rebase-v0170 2026-03-09 23:40:54 +08:00
Taichang Zhou 0940c5a05e Merge remote-tracking branch 'remotes/origin/main' into dev/rebase-v0170
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-09 23:40:32 +08:00
Didan Deng e79ad8d59a [Doc] CLI Args Naming Style Correction (#1750)
Signed-off-by: Didan Deng <33117903+wtomin@users.noreply.github.com>
2026-03-09 23:35:45 +08:00
Yuanheng Zhao 82f63a4403 [BugFix] Enable CPU offloading and Cache-DiT together on Diffusion Model (#1723)
Signed-off-by: Yuanheng Zhao <jonathan.zhaoyh@gmail.com>
Signed-off-by: yuanheng <jonathan.zhaoyh@gmail.com>
2026-03-09 22:25:41 +08:00
汪志鹏 7271116973 [Bagel]: Support multistage img2img (#1669)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: 汪志鹏 <wangzhipeng628@gmail.com>
2026-03-09 22:23:50 +08:00
wangyu 5628d41242 [Test] Solving the Issue of Whisper Model's GPU Memory Not Being Successfully Cleared and the Occasional Accuracy Problem of the Qwen3-omni Model Test (#1744)
Signed-off-by: yenuo26 <410167048@qq.com>
2026-03-09 21:52:56 +08:00
tzhouam a9b4367808 Update GPU memory utilization and max number of batched tokens in qwen3_omni.yaml to optimize resource allocation during model execution.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-09 08:22:26 +00:00
tzhouam 79626586e1 Update GPU memory utilization in qwen3_omni.yaml from 0.6 to 0.45 to prevent out-of-memory errors during model execution.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-09 08:02:25 +00:00
tzhouam e6d4cdd71a Refactor model mode initialization and enhance error handling in AsyncOmni. Update worker type retrieval in omni_stage and add audio token handling in Qwen3OmniMoe classes for improved robustness.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-09 06:17:08 +00:00
Hongsheng Liu 9cdbcec44b Merge branch 'main' into dev/rebase-v0170 2026-03-08 09:18:57 +08:00
Taichang Zhou c8b695f181 smaller talker gpu util to avoid oom
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-08 01:48:42 +08:00
Taichang Zhou f405240513 Change pytest fixture scope from "module" to "class" for omni_server and omni_server_no_async_chunk in test_qwen3_tts.py to improve test isolation and resource management.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 14:50:59 +08:00
Taichang Zhou c630cf39c1 Enhance Dockerfile.ci by adding system dependencies installation and copying project files into the container. This improves the build process for the CI environment.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 14:02:48 +08:00
Taichang Zhou 3facc89dd1 Update base image in Dockerfile.rocm to v0.17.0 for improved compatibility and features.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:59:23 +08:00
Taichang Zhou 68bad871bc Fix model detection logic in is_diffusion_model function to ensure config_dict is checked for None before accessing its fields. This improves robustness against missing model_index.json files.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:57:44 +08:00
Taichang Zhou f945d2a30d Refactor Dockerfile.ci to streamline installation process and update vllm-omni installation method. Removed commented-out installation commands and ensured Python symlink is created.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:57:23 +08:00
Taichang Zhou 315f9401db Refactor imports in metrics.py and patch.py to use TokenizerLike from vllm.tokenizers instead of PreTrainedTokenizerBase from transformers.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:33:10 +08:00
Taichang Zhou fd4607f215 rebase 2026/03/07
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:30:58 +08:00
Taichang Zhou 0ecfff0694 comment out extra installation
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:29:12 +08:00
Taichang Zhou 4a43c6b017 Merge branch 'dev/rebase-v0170' of https://github.com/vllm-project/vllm-omni into dev/rebase-v0170 2026-03-07 13:26:46 +08:00
Taichang Zhou 0c2464db20 revert unnecessary changes
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:26:32 +08:00
Zhou TaichangandCyrus Leung 5f1e41970c Update vllm_omni/benchmarks/patch/patch.py
Co-authored-by: Cyrus Leung <cyrus.tl.leung@gmail.com>
Signed-off-by: Zhou Taichang <tzhouam@connect.ust.hk>
2026-03-07 13:23:47 +08:00
Zhou TaichangandCyrus Leung 4ead1c8913 Update vllm_omni/benchmarks/metrics/metrics.py
Co-authored-by: Cyrus Leung <cyrus.tl.leung@gmail.com>
Signed-off-by: Zhou Taichang <tzhouam@connect.ust.hk>
2026-03-07 13:23:38 +08:00
Taichang Zhou 2187db0e36 Remove obsolete RFC documents for automated rebase agent, output processor refactor, model runner clearance, long function decomposition, and model runner V2 integration. These files are no longer relevant to the current project direction.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:22:39 +08:00
Taichang Zhou 5511bc14c7 Merge origin/main into dev/rebase-v0170
- Resolved conflict in vllm_omni/engine/output_processor.py: use tracing_enabled for vLLM v0.17.0 API
- Resolved conflict in vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_code_predictor_vllm.py: keep correct _CodePredictorDecoderLayer.forward()
- Update Dockerfile.ci vLLM wheel commit to b31e9326a (releases/v0.17.0)

Made-with: Cursor

Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:18:33 +08:00
Taichang Zhou 3a6b2e27be [Update] Bump VLLM base tag to v0.17.0 in CI Dockerfile
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-03-07 13:14:10 +08:00
tzhouam 8c41492307 Merge remote-tracking branch 'remotes/origin/main' into dev/rebase-v0169 2026-03-06 03:02:50 +00:00
tzhouam 44ac28c386 trigger RTD rebuild
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-06 03:02:07 +00:00
tzhouam 1ccffce2bc rebase to updated commit
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-06 02:32:26 +00:00
tzhouam 5be08baed3 Merge remote-tracking branch 'remotes/origin/main' into dev/rebase-v0170
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-05 08:53:26 +00:00
tzhouam 395eab8984 Merge remote-tracking branch 'remotes/origin/main' into dev/rebase-v0169 2026-03-05 02:29:14 +00:00
tzhouam 36a507e071 Merge remote-tracking branch 'remotes/origin/main' into dev/rebase-v0170
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-04 08:26:56 +00:00
tzhouam ae818f4b45 Fix Qwen3Omni VisionTransformer compatibility with updated vLLM Qwen2_5_VisionAttention
The upstream vLLM Qwen2_5_VisionAttention.forward() now requires a
`sequence_lengths` argument, but Qwen3_VisionBlock.forward() does not
pass it, causing a TypeError at init time.

Instead of patching the vLLM package directly, introduce a
Qwen3Omni_VisionTransformer subclass in vllm-omni that overrides
forward() to properly compute and propagate sequence_lengths via
MMEncoderAttention (following qwen3_vl.py's pattern). Both thinker
and talker now import the fixed subclass.

Also bump the vLLM wheel to e196028dbaa41b.

Made-with: Cursor

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-04 02:51:02 +00:00
tzhouam 4914466f0b Update Dockerfile.ci to install vLLM wheel version 3dd142adb7f7fa3b0b8345ee046ba36a12d9e530 and remove ExtractHiddenStatesProposer references from GPU model runners for cleaner code.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 15:09:26 +00:00
tzhouam 2398a7c1d7 Merge origin/main into dev/rebase-v0170
Resolve conflict in omni_generation_scheduler.py: keep both
num_cached_tokens tracking and additional_information caching.

Made-with: Cursor

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 14:31:05 +00:00
tzhouam 16824aa295 Add max_query_len parameter to _LocalPredictorKVCache for improved query handling
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 14:04:07 +00:00
tzhouam 2da9d28f1b Enhance reproducibility in Ulysses attention tests by replacing current_omni_platform.seed_everything with torch's manual_seed and adding support for CUDA devices. Introduce get_supported_tasks method in AsyncOmni class to dynamically determine available tasks based on stage capabilities.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 09:45:51 +00:00
tzhouam 4d8a76e721 Reintroduce installation of flashinfer-jit-cache in Dockerfile.ci to enhance dependency management for the CI environment, ensuring compatibility with the latest requirements.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 09:23:09 +00:00
tzhouam a17078974f Remove flashinfer-jit-cache installation from Dockerfile.ci to streamline dependency management and avoid potential conflicts.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 07:00:18 +00:00
tzhouam 8dcf1061f1 Update Dockerfile.ci to install additional dependencies including flashinfer-jit-cache, nvidia-cublas-cu12, and numpy, ensuring a more comprehensive environment setup for development.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 06:47:10 +00:00
tzhouam 98926707f5 rebase 2026/3/3
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 03:50:34 +00:00
tzhouam 3e539fcb6a Merge remote-tracking branch 'origin/main' into dev/vllm-align
Made-with: Cursor

# Conflicts:
#	vllm_omni/entrypoints/openai/serving_chat.py
#	vllm_omni/worker/gpu_model_runner.py

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 02:57:27 +00:00
tzhouam e0497550c4 Update Dockerfile.ci to install a specific version of flashinfer-cubin (0.6.4) directly, simplifying the installation process and ensuring compatibility with the current setup.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 02:51:35 +00:00
tzhouam 029bb56e3a Update Dockerfile.ci to use importlib.metadata for retrieving the current version of flashinfer, enhancing compatibility with the latest Python standards.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 02:28:24 +00:00
tzhouam 65169baa9d Update Dockerfile.ci to create a symbolic link for Python 3, ensuring compatibility with Python scripts during the build process.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-03 01:58:35 +00:00
tzhouam 908e3fe761 Update Dockerfile.ci to dynamically install the current version of flashinfer-cubin, ensuring compatibility with the latest changes in the flashinfer package.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-02 07:00:59 +00:00
tzhouam 8fb247a481 Update Dockerfile.ci to install specific version of flashinfer-cubin (0.6.4) and ensure vllm installation remains intact.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-02 06:48:58 +00:00
tzhouam be4226bae0 Remove unnecessary uninstallation of nvidia-cublas-cu12 and streamline vllm installation in Dockerfile.ci
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-02 06:38:24 +00:00
tzhouam 473d3ecd29 Update vLLM wheel version and adjust GPU memory utilization in CI configuration
- Updated the vLLM wheel commit hash in Dockerfile.ci to a60985b07eaf0fefa4c353ad2309b745707591ed.
- Modified the mock shared memory handle name in test_basic_connectors.py for clarity.
- Increased GPU memory utilization to 0.9 and adjusted max_num_batched_tokens to 4096 in qwen2_5_omni_ci.yaml to enable testing on H800.
- Refactored GPU model runner classes to remove deprecated activate_lora parameter and ensure compatibility with num_active_loras.
- Enhanced request handling in the OmniGPUModelRunner class to improve streaming request updates.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-02 03:08:45 +00:00
tzhouam 26744bcb35 Merge origin/main into dev/vllm-align
Resolved conflicts:
- vllm_omni/inputs/preprocess.py: took origin/main's elif mm_processor_kwargs branch, fixed undefined mm_uuids reference
- vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_code_predictor_vllm.py: took origin/main's NPU support

Made-with: Cursor

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-03-02 02:10:32 +00:00
tzhouam 8d0c46487e Merge origin/main into dev/vllm-align
Resolve conflicts:
- docker/Dockerfile.ci: Keep wheel install section, update hash to 6042e66cd5
- vllm_omni/platforms/npu/worker/npu_model_runner.py: Accept main's refactoring

Made-with: Cursor

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-26 08:19:53 +00:00
tzhouam a252918008 [Update] Upgrade vLLM wheel version and enhance metrics handling
- Updated the vLLM wheel to commit 24650715105a26b41c88f4164777b5e1a0f3200b in Dockerfile.ci.
- Modified the `calculate_metrics` function to handle `tokenizer` as optional (can be None) in metrics.py.
- Adjusted the `benchmark` function to accept `tokenizer` as optional in patch.py.
- Enhanced condition checks in OmniARScheduler to ensure valid token generation in omni_ar_scheduler.py.
- Updated input processing to include supported tasks in input_processor.py.
- Cleaned up unnecessary code in output_processor.py.
- Added support for SSL ciphers in the API server configuration in api_server.py.
- Enabled speculative decoding in GPU model runners for improved performance in gpu_generation_model_runner.py and gpu_model_runner.py.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-25 10:02:23 +00:00
tzhouam 22e01c416b Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-02-25 08:15:55 +00:00
tzhouam 81b9e40bbf Enhance signature compatibility checks in OmniLLM tests
- Improved the test_overridden_method_signatures_compatible function to better handle missing parameters in method signatures.
- Added logic to account for VAR_KEYWORD parameters, ensuring more accurate detection of signature mismatches.

Signed-off-by: [Your Name] <your.email@example.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-24 07:56:47 +00:00
tzhouam 04ccb062fc Add signature compatibility tests for OmniLLM class
- Introduced tests to ensure method signatures in OmniLLM match those of the base LLM class, preventing runtime TypeErrors due to parameter mismatches.
- Updated the _run_engine method in OmniLLM to include an optional output_type parameter.

Signed-off-by: [Your Name] <your.email@example.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-24 06:58:39 +00:00
tzhouam 861698f88f Update Dockerfile for vllm wheel version and enhance SHM connector tests
- Updated Dockerfile to use a new vllm wheel version.
- Modified test_put_get_inline to clarify that it always uses SHM and updated metadata assertions.
- Added handling for speculative configuration in GPU model runners to delay clearing connector metadata.
- Refactored payload decoding in OmniGPUModelRunner for better clarity and maintainability.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-24 05:45:48 +00:00
tzhouam e22ae62396 Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-02-24 03:23:20 +00:00
tzhouam 9070f15f52 debug qwen tts
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-24 03:17:11 +00:00
tzhouam bf69cd94bb Rebase 2026/02/23
Update Dockerfile for vllm wheel version and enhance input processing in Omni models. Added decoding for byte messages in async requests, included new metrics in benchmarks, and improved pause state handling in the scheduler. Refined input and output processing methods for better multimodal support and error handling.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-23 03:36:01 +00:00
tzhouam 292da4b0d8 Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-02-23 02:09:09 +00:00
tzhouam 9869ea007e Update vLLM wheel version in Dockerfile and enhance input processing for omni models. Added functionality to reinject omni-specific fields into processed prompts and improved handling of multimodal audio extraction in serving_chat.py. Adjusted SharedMemoryConnector logic for payload size checks and refined input preprocessing methods for better compatibility with multimodal data.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-20 08:34:53 +00:00
tzhouam 663976b8ca Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-02-20 01:38:44 +00:00
tzhouam b867bacd24 rebase 2025/02/16
Qwen3 Omni offline/online passed

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-16 03:21:21 +00:00
tzhouam 93eeb1a4c8 Merge remote-tracking branch 'origin/main' into dev/rebase-v0.16.0 2026-02-15 01:24:23 +00:00
tzhouam 3051fb10b5 reverse change in SamplingParams
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 09:59:03 +00:00
tzhouam 15099119dd Merge remote-tracking branch 'remotes/origin/main' into dev/rebase-v0.16.0 2026-02-13 09:55:18 +00:00
tzhouam f24273dc03 Update vLLM version to 0.16.0 in quickstart and CUDA installation documentation, and adjust parameters in multimodal generation script for improved performance. Refactor request logging in serving_chat.py for better input tracking.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 09:50:11 +00:00
tzhouam f50cf07c74 Update Dockerfile.ci to allow the installation of 'pplx_kernels' to continue even if the compatible wheel is not found, enhancing build resilience.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 07:22:07 +00:00
tzhouam bc0a2598ed Update Dockerfile.ci to replace 'uv pip' with 'pip' for 'pplx_kernels' installation, ensuring consistency in package management across the CI environment.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 07:11:40 +00:00
tzhouam 963deaf5ad Refactor Dockerfile.ci to streamline package installations by using 'uv pip' consistently for both 'pplx_kernels' and VLLM dependencies, ensuring a more uniform CI environment setup.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 06:55:10 +00:00
tzhouam 3abec0173c Update Dockerfile.ci to force-reinstall the 'pplx_kernels' package, ensuring the latest version is used in the CI environment.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 06:41:41 +00:00
tzhouam 8a4eee9bed Enhance configuration and debugging: Added asyncio mode to pytest settings, increased timeout for Qwen3-TTS E2E test, and improved logging in GPU model runner for better traceability of input modalities. Updated Dockerfile.ci to streamline VLLM installation process.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 06:39:28 +00:00
tzhouam 31e9fdbb57 Refactor chat_utils to remove unused multi-modal classes and add async audio extraction from video. Update README to reflect new entry point for audio extraction. Enhance serving_chat to process audio from video URLs when specified.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 03:27:16 +00:00
tzhouam 39a045f439 debug Qwen 3 Omni cuda graph
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 02:57:05 +00:00
tzhouam 7e9cae56fc Refactor Dockerfile.ci to use precompiled VLLM installation and remove unused parse.py file. Update serving_chat.py to streamline rendering logic and enhance compatibility with new API. Modify OmniInputPreprocessor to directly handle SingletonDictPrompt, improving input processing efficiency.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 02:28:25 +00:00
tzhouam 31c5465e00 Update .gitignore to exclude .wav output files and remove audio_0.wav
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-13 01:52:06 +00:00
Hongsheng Liu 622c6f5f14 Merge branch 'main' into dev/rebase-v0.16.0 2026-02-13 06:05:57 +08:00
Taichang Zhou a6d633c319 debug
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-12 23:46:20 +08:00
tzhouam ec3814f587 debug
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 14:20:57 +00:00
tzhouam 59a7602d6e debug
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 13:55:08 +00:00
Taichang Zhou 85a7b558ab rebase to v0.16.0
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-12 21:30:54 +08:00
Taichang Zhou 3dc19bc305 0.16.0 docker not released yet
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-12 21:25:53 +08:00
Taichang Zhou 7458ba44b7 merge main
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-12 21:21:03 +08:00
Taichang ZhouandCursor e2ced80d59 Merge origin/main into dev/rebase-v0.16.0.
Resolve arg_utils merge conflict and align CI base image tag to v0.16.0 for the vLLM rebase track.

Co-authored-by: Cursor <cursoragent@cursor.com>

Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-12 21:18:03 +08:00
Taichang Zhou 1705edcce9 Refactor model path in test_qwen3_tts for improved clarity and maintainability
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-12 21:12:53 +08:00
tzhouam f8959bcc6e Temp fix for metrics error in test_qwen3_tts
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 10:02:19 +00:00
tzhouam db40563b88 Update Dockerfile.ci to pin nvidia-cublas-cu12 and add numpy installation, ensuring proper dependency management for the build environment.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 09:52:51 +00:00
tzhouam 86e20769fd Add SSL context support in benchmark function for improved connection handling
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 08:52:27 +00:00
tzhouam 8c1df5c704 update flashinfer
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 08:52:06 +00:00
tzhouam 3e89c99001 Merge remote-tracking branch 'remotes/origin/main' into dev/vllm-align 2026-02-12 07:16:25 +00:00
tzhouam c751f0d684 Update Dockerfile.ci to install a new version of vllm from a different extra index URL. Refactor import statement in serving_chat.py to use the correct BaseRenderer module path.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 06:37:17 +00:00
tzhouamandCursor 50f2b1e854 Merge origin/main into dev/vllm-align
Co-authored-by: Cursor <cursoragent@cursor.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 03:41:38 +00:00
tzhouam 0099eebb3f Refactor OmniEngineArgs and AsyncOmniEngineArgs to improve compatibility with partial payloads by using getattr for various attributes. Update GPUGenerationModelRunner and OmniGPUModelRunner to replace the activate_lora parameter with num_active_loras for better clarity and backward compatibility. This change enhances the handling of LoRA activation during model runs.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-12 03:33:50 +00:00
Taichang Zhou cd2ffbf2a0 update dockerfile.ci
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-11 23:39:15 +08:00
Taichang Zhou 9fbffc6137 Update requirements/cuda.txt to clarify the pinning of nvidia-cublas-cu12 version 12.9.1.4 due to dependency resolution issues with torch. This comment aids in understanding the force-reinstall strategy in Dockerfile.ci.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-11 22:44:11 +08:00
Taichang Zhou 514bd46f2e Uninstall old nvidia-cublas-cu12 version before reinstalling the pinned version to ensure consistent dependency management in Dockerfile.ci.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-11 22:34:05 +08:00
Taichang Zhou 169d94dab9 Refactor Dockerfile.ci to use bash for Python executable resolution during vllm-omni installation and pin nvidia-cublas-cu12 version to 12.9.1.4 for consistent dependency management.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-11 21:31:09 +08:00
Taichang Zhou 268a58e4e8 Update pyproject.toml to remove redundant transformers package installation and adjust Dockerfile.ci to install the current package in editable mode. Also, update fa3-fwd package version in requirements/cuda.txt to 0.0.2 for consistency.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-11 21:18:53 +08:00
Taichang Zhou eb05967451 Pin nvidia-cublas-cu12 to version 12.9.1.4 in Dockerfile.ci to ensure consistent dependency management in the CI environment.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-11 20:31:57 +08:00
tzhouam 05784d3dd0 Update pyproject.toml to include transformers package and remove redundant installation step in Dockerfile.ci for cleaner CI setup.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-11 06:27:51 +00:00
tzhouam b7f297c8c3 Add transformers package installation in Dockerfile.ci to ensure compatibility with the CI environment.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-11 06:15:00 +00:00
tzhouam 379f61d038 Enhance Dockerfile.ci by adding a debug step to print key dependency versions for CI troubleshooting, while commenting out the flashinfer version sync logic for clarity.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-11 06:01:01 +00:00
tzhouam 31b6584373 Refine flashinfer version retrieval in Dockerfile.ci to enhance reliability and maintain consistency in package installations
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-11 04:56:50 +00:00
tzhouam 50cea0b29c Refactor flashinfer version retrieval in Dockerfile.ci for improved accuracy and consistency in package installations
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-11 04:34:52 +00:00
tzhouam e897640b43 Update Dockerfile.ci to enhance flashinfer version retrieval logic and ensure package installation consistency
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-11 04:11:11 +00:00
tzhouam 3663e803d1 rebase 2026/02/11
vllm: 4a1550d22d7058e129d0e1257e726b3bf4a77025

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-11 02:43:49 +00:00
tzhouam 279b6b5050 Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-02-11 01:44:54 +00:00
tzhouam af53491cac Update sampling parameters in openai_chat_completion_client_for_multimodal_generation.py for improved model performance, adjusting temperature, top_k, and max_tokens settings.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-10 09:54:32 +00:00
tzhouam c3d35ed5fe pass local t2i test
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-10 06:20:26 +00:00
tzhouam 3c5b53358c Update Dockerfile.ci to ensure consistent formatting of flashinfer version extraction commands for improved readability and maintainability.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-10 03:53:55 +00:00
tzhouam 58d09ed656 reabse 2026/02/10
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-10 03:36:09 +00:00
tzhouam 064595a97c Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-02-10 02:22:49 +00:00
tzhouam e0f540fe98 Enhance Dockerfile.ci to synchronize flashinfer runtime package versions with installed flashinfer-python, ensuring compatibility across components. Additionally, improve error handling in ring_globals.py for flashinfer import failures, logging warnings for fallback scenarios.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-10 02:17:00 +00:00
tzhouam 670240ccd4 Enhance Dockerfile.ci to allow pre-release vLLM installations by adding --prerelease=allow flag for compatibility with nightly/commit wheels.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-09 10:01:39 +00:00
tzhouam c9fb2dc7c7 Update vLLM commit reference in Dockerfile.ci to f97ca671766c5201404e9fc812e35bf2c4e95a01
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-09 08:09:21 +00:00
tzhouam 7e4a604d4d reabse 2025/2/9
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-09 03:07:43 +00:00
tzhouamandCursor 03c6a58f69 Merge origin/main into dev/vllm-align; resolve omni_stage.py conflicts
Co-authored-by: Cursor <cursoragent@cursor.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-09 01:52:31 +00:00
tzhouam 7f54fd2f54 Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-02-06 02:26:06 +00:00
tzhouam 41e22e2770 [Refactor] Clean up code by removing unnecessary lines and adding missing imports in async_omni_llm.py, omni_stage.py, omni.py, and api_server.py
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-05 11:02:03 +00:00
tzhouam 24b04f58a0 rebase 2026/2/5 2026-02-05 10:52:28 +00:00
tzhouam 0144a424ed Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-02-05 01:44:34 +00:00
tzhouam 560a5aa57c Merge branch 'dev/vllm-align' of https://github.com/vllm-project/vllm-omni into dev/vllm-align 2026-02-03 05:59:37 +00:00
tzhouam 02c6062908 Merge branch 'main' into dev/vllm-align
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-03 05:57:38 +00:00
Taichang Zhou 87c7b7e519 Refactor comments and formatting in Qwen3 TTS configuration and modeling files for improved readability.
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-02-02 21:52:10 +08:00
Taichang Zhou 22ca74432b Merge branch 'main' into dev/vllm-align 2026-02-02 21:35:45 +08:00
Taichang Zhou 354777d5e7 Merge branch 'main' of https://github.com/vllm-project/vllm-omni 2026-02-02 21:34:42 +08:00
汪志鹏 b6c846cc95 Add stage_init_timeout=300 (#1153)
Signed-off-by: princepride <wangzhipeng628@gmail.com>
2026-02-02 17:58:48 +08:00
tzhouam 9194c1dd84 Update base image in Dockerfile.rocm from v0.14.0 to v0.15.0
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-02 08:41:35 +00:00
tzhouam ce9105267f Enhance error handling in TTS generation and improve audio output compatibility by supporting both "audio" and "model_outputs" keys in response.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-02 08:40:56 +00:00
tzhouam 39f7cade27 Fix audio sample rate access in end2end.py for correct output retrieval
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-02 08:38:24 +00:00
tzhouam 08fa85a1ad Update audio tensor access in offline inference tests and increase image fetch timeout for CI
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-02 06:10:54 +00:00
tzhouam 6568a78a26 Remove unused tokenizer argument from OmniInputProcessor class docstring
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-02 03:12:43 +00:00
tzhouam ff8b3dbbc8 update qwen 2.5 Omni
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-02 03:01:11 +00:00
tzhouam 1297a46040 Merge remote-tracking branch 'origin/main' into dev/vllm-align
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-02-02 01:54:12 +00:00
Taichang Zhou e033498600 Merge branch 'main' of https://github.com/vllm-project/vllm-omni 2026-01-31 10:59:36 +08:00
tzhouam 7aea8e8d91 Add OmniMRotaryEmbedding for multimodal position computation
- Introduced OmniMRotaryEmbedding to extend MRotaryEmbedding with methods for computing input positions across various multimodal scenarios, including image, video, and audio inputs.
- Updated patch.py to replace the original MRotaryEmbedding with OmniMRotaryEmbedding in the module patching logic.
- Created new files for the OmniMRotaryEmbedding implementation and its associated methods.

Signed-off-by: [Your Name] <[Your Email]>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-30 10:03:02 +00:00
tzhouam 3208afd82b Refactor OmniRequestOutput to encapsulate multimodal output
- Changed the multimodal_output field to a private attribute _multimodal_output for better encapsulation.
- Added a property method to access multimodal_output, allowing for a clearer interface and improved handling of request outputs.

Signed-off-by: [Your Name] <[Your Email]>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-30 10:02:22 +00:00
tzhouam 2b009b3cef Fix audio tensor extraction in end2end.py files across multiple examples
- Updated the audio tensor extraction logic to access the correct output structure in Qwen2.5 Omni, Qwen3 Omni, and Qwen3 TTS implementations.
- Ensured consistency in handling audio outputs for improved functionality.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-30 10:00:14 +00:00
tzhouam 4faf190e28 Merge remote-tracking branch 'origin/main' into dev/vllm-align 2026-01-30 03:49:04 +00:00
tzhouam ca57f510c7 Refactor OmniInputPreprocessor to remove unused embed processing logic
- Eliminated the _process_embeds method and related imports from preprocess.py, streamlining the input preprocessing workflow.
- Updated the main processing logic to focus solely on token inputs, enhancing clarity and maintainability.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-30 03:02:38 +00:00
tzhouam 4b895459e4 Merge branch 'main' into dev/vllm-align
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-30 02:53:03 +00:00
tzhouam 62dbf07fb6 Refactor OpenAI serving modules for improved functionality and organization
- Reintroduced the load_chat_template import in api_server.py for better template handling.
- Updated the logic in serving_chat.py to streamline the resolution of chat template content formats, enhancing clarity and functionality.
- Adjusted the application of chat templates to ensure proper handling of tools and chat templates.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-30 02:44:13 +00:00
tzhouam 104e105c93 Refactor multimodal processing imports and update configuration handling in Qwen3OmniMoeThinker
- Changed the import path for multimodal processing to improve module organization.
- Removed the unused multimodal configuration parameter from the Qwen3OmniMoeThinker initialization, streamlining the class definition.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-30 02:43:41 +00:00
tzhouam ef21879c3a Refactor GPU model runner to streamline multimodal configuration handling
- Removed the unused import of supports_mm_encoder_only from interfaces.
- Updated the dummy run check to utilize the multimodal configuration directly, enhancing clarity and future extensibility.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-30 02:43:26 +00:00
tzhouam 45880a30f0 Refactor chat_utils and OpenAI modules for improved functionality and organization
- Updated import statements in chat_utils.py to replace the internal content format type with the public one for better clarity.
- Refactored api_server.py to streamline the chat template loading process by replacing the previous method with a direct function call.
- Enhanced serving_chat.py by reintroducing safe_apply_chat_template functions for better template handling.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-29 09:00:43 +00:00
tzhouam 2ac33489b5 Update Dockerfile.ci to install vllm package using the correct URL format
- Modified the pip install command for vllm to use the correct URL format for direct installation from the specified commit hash, ensuring a more reliable installation process.
- Removed the fallback option for pre-release installation to streamline the installation command.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-29 08:25:51 +00:00
tzhouam 63fd163efb Refactor import statement in OpenAI API server for improved module organization
- Updated the import path for AnthropicServingMessages in api_server.py to enhance clarity and maintain consistency with the new module structure.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-29 08:23:17 +00:00
tzhouam fea328364a Update Dockerfile.ci to install vllm package directly from wheel file
- Changed the pip install command for vllm to download the wheel file directly instead of using an extra index URL, ensuring a more reliable installation process.
- Added a fallback option to install the pre-release version of vllm if the direct installation fails.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-29 08:05:57 +00:00
tzhouam 478c036a01 [Refactor] Update OpenAI API server and related modules for improved structure and functionality
- Refactored import statements in api_server.py to enhance module organization, including changes to the routing and server setup.
- Updated the method for removing routes to ensure compatibility with the new app structure.
- Adjusted the handling of chat completion requests to improve error responses and streamline processing.
- Modified serving modules to align with the new import paths and enhance code clarity.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-29 15:28:51 +08:00
tzhouam 8407818368 Update simple_test.sh to force reinstall vllm package with extra index URL
- Modified the pip install command for vllm to include --upgrade, --force-reinstall flags, and an extra index URL, ensuring the latest version is installed from the specified source during testing.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 17:34:22 +08:00
tzhouam 6e7cb01afe Update Dockerfile.ci to force reinstall vllm package during CI build
- Modified the pip install command for vllm to include --upgrade and --force-reinstall flags, ensuring the latest version is installed during the CI process.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 17:20:29 +08:00
tzhouam acb28f3d1d Remove commented-out import and related code for MRotaryEmbedding in patch.py to clean up the codebase.
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 16:52:10 +08:00
tzhouam bf51318176 debug
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 16:40:06 +08:00
tzhouam bd8c0443cf build image from commit id
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 16:21:29 +08:00
tzhouam 554d14fd28 debug docker file
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:57:13 +08:00
tzhouam 49b9d0d6b1 debug docker build
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:47:30 +08:00
tzhouam 2c2d06d920 [Enhancement] Update GPUARWorker and GPUGenerationWorker to enforce v1 model runner
- Added a warning in GPUARWorker and GPUGenerationWorker to indicate that the v2 model runner does not include omni hooks, forcing the use of the v1 model runner.
- Updated import statements in gpu_ar_model_runner.py and gpu_generation_model_runner.py to include necessary functions from parallel_state.
- Removed unused imports from both model runner files to streamline the code.

Signed-off-by: [Your Name] <your.email@example.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:43:27 +08:00
tzhouam 31d127edcd [Refactor] Remove unused MRotaryEmbedding and vision position ID functions
- Deleted the MRotaryEmbedding class and its associated methods from mrope.py to streamline the codebase.
- Removed the get_llm_pos_ids_for_vision function from vision.py as it is no longer needed.
- Updated import statements in qwen2_5_omni.py to reflect the removal of the vision module.

Signed-off-by: [Your Name] <your.email@example.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:42:35 +08:00
tzhouam 52d0122fb5 [Enhancement] Refactor OmniInputProcessor and related classes for improved prompt handling
- Updated OmniInputProcessor to streamline input processing, including enhanced handling of prompt embeddings and additional information.
- Introduced OmniTextPrompt and OmniEmbedsInputs to support new input structures with optional metadata.
- Modified input preprocessing methods to accommodate new prompt types and ensure compatibility with multimodal inputs.
- Improved error handling and type checking for data parallel configurations.

Signed-off-by: [Your Name] <your.email@example.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:42:05 +08:00
tzhouam b26a9735e6 [Enhancement] Update type hint for stage_connector_spec in OmniEngineArgs and AsyncOmniEngineArgs
- Changed type hint for stage_connector_spec from 'any' to 'Any' for improved type checking and consistency.
- Ensured compatibility with typing standards in the arg_utils module.

Signed-off-by: [Your Name] <your.email@example.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:41:20 +08:00
tzhouam d7cfe195ea [Enhancement] Refactor OmniARScheduler and OmniGenerationScheduler to include OmniSchedulerOutput
- Updated OmniARScheduler to wrap output in OmniSchedulerOutput, including transfer metadata for finished requests needing KV cache transfer.
- Enhanced OmniGenerationScheduler to handle routed experts and finish reasons more effectively, ensuring proper output structure.
- Introduced performance statistics handling in both schedulers for improved monitoring.

Signed-off-by: [Your Name] <your.email@example.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:40:59 +08:00
tzhouam e1c6639f18 [Enhancement] Add mm_encoder_only parameter and deprecation warning for convert option
- Introduced mm_encoder_only parameter to OmniModelConfig.
- Added a deprecation warning for the `--convert mm_encoder_only` option, advising users to switch to `--mm-encoder-only`.

Signed-off-by: [Your Name] <your.email@example.com>

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:40:29 +08:00
tzhouam ce7430b562 [Enhancement] Update OmniRequest to support torch.Tensor for prompt_embeds and add decoding method
- Modified the constructor to accept torch.Tensor as an option for prompt_embeds.
- Introduced a new static method _maybe_decode_prompt_embeds to handle the conversion from PromptEmbedsPayload to torch.Tensor.
- Preserved the original prompt_embeds payload for reference.

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 15:40:17 +08:00
tzhouam ed8b03224b update the docker setup to vllm nightly
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
2026-01-28 11:15:54 +08:00
Taichang Zhou af8501dcc7 debug Qwen TTS
Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
2026-01-22 22:46:06 +08:00
78 changed files with 4984 additions and 413 deletions

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ repos:
# list of supported hooks: https://pre-commit.com/hooks.html
- id: check-yaml
args: ["--unsafe"]
exclude: "examples/online_serving/chart-helm/templates/"
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
+1 -1
View File
@@ -7,7 +7,7 @@ COPY . .
# Install system dependencies
RUN apt-get update && \
apt-get install -y ffmpeg git sox libsox-fmt-all jq && \
apt-get install -y espeak-ng ffmpeg git sox libsox-fmt-all jq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
+1 -1
View File
@@ -19,7 +19,7 @@ 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 install -y espeak-ng ffmpeg git sox libsox-fmt-all jq && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
+1
View File
@@ -14,6 +14,7 @@ RUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRO
RUN apt clean && apt-get update -y && \
apt-get install -y --no-install-recommends --fix-missing \
curl \
espeak-ng \
ffmpeg \
git \
libsndfile1 \
+1 -1
View File
@@ -115,7 +115,7 @@ python image_edit.py \
--image cherry_blossom.jpg \
--prompt "Let this mascot dance under the moon, surrounded by floating stars" \
--output output_image_edit.png \
--num_inference_steps 10
--num-inference-steps 10
```
## Advanced Usage
@@ -34,13 +34,13 @@ uv pip install vllm-omni
# --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.16.0 release version of the library
If you do not need to modify source code of vLLM, you can directly install the stable 0.17.0 release version of the library
```bash
uv pip install vllm==0.16.0 --torch-backend=auto
uv pip install vllm==0.17.0 --torch-backend=auto
```
The release 0.16.0 of vLLM is based on PyTorch 2.9.0 which requires CUDA 12.9 environment.
The release 0.17.0 of vLLM is based on PyTorch 2.10.0 which requires CUDA 12.9 environment.
#### Installation of vLLM-Omni
Since vllm-omni is rapidly evolving, it's recommended to install it from source
@@ -56,11 +56,11 @@ 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.16.0
git checkout v0.17.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.16.0/vllm-0.16.0-cp38-abi3-manylinux_2_31_x86_64.whl
export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.17.0/vllm-0.17.0-cp38-abi3-manylinux_2_31_x86_64.whl
```
Install vllm with command below (If you have no existing PyTorch).
```bash
@@ -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.16.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.16.0/rocm700
uv pip install vllm==0.17.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.17.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.16.0 release version of the library
If you do not need to modify source code of vLLM, you can directly install the stable 0.17.0 release version of the library
```bash
uv pip install vllm==0.16.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.16.0/rocm700
uv pip install vllm==0.17.0+rocm700 --extra-index-url https://wheels.vllm.ai/rocm/0.17.0/rocm700
```
The release 0.16.0 of vLLM requires ROCm 7.0 environment.
The release 0.17.0 of vLLM requires ROCm 7.0 environment.
#### 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.16.0
git checkout v0.17.0
python3 -m pip install -r requirements/rocm.txt
python3 setup.py develop
```
+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.16.0 --torch-backend=auto
uv pip install vllm==0.17.0 --torch-backend=auto
# On ROCm
uv pip install vllm==0.16.0 --extra-index-url https://wheels.vllm.ai/rocm/0.16.0/rocm700
uv pip install vllm==0.17.0 --extra-index-url https://wheels.vllm.ai/rocm/0.17.0/rocm700
git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni
+338
View File
@@ -0,0 +1,338 @@
# Audio Generate API
vLLM-Omni provides an API for text-to-audio generation using diffusion-based models such as Stable Audio.
Unlike the [Speech API](speech_api.md) which targets text-to-speech synthesis, the Audio Generate API is designed for general-purpose audio generation from text descriptions (sound effects, music, ambient soundscapes, etc.).
Each server instance runs a single model (specified at startup via `vllm-omni serve <model> --omni`).
## Quick Start
### Start the Server
```bash
vllm-omni serve stabilityai/stable-audio-open-1.0 \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.9 \
--trust-remote-code \
--enforce-eager \
--omni
```
### Generate Audio
**Using curl:**
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "The sound of a cat purring",
"audio_length": 10.0
}' --output cat.wav
```
**Using Python:**
```python
import httpx
response = httpx.post(
"http://localhost:8000/v1/audio/generate",
json={
"input": "The sound of a cat purring",
"audio_length": 10.0,
},
timeout=300.0,
)
with open("cat.wav", "wb") as f:
f.write(response.content)
```
## API Reference
### Endpoint
```
POST /v1/audio/generate
Content-Type: application/json
```
### Request Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `input` | string | **required** | Text prompt describing the audio to generate |
| `model` | string | server's model | Model to use (optional, should match server if specified) |
| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, aac, opus |
| `speed` | float | 1.0 | Playback speed (0.25 - 4.0) |
#### Diffusion Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `audio_length` | float | null | Audio duration in seconds (default value is the max ~47s for `stable-audio-open-1.0`) |
| `audio_start` | float | 0.0 | Audio start time in seconds |
| `negative_prompt` | string | null | Text describing what to avoid in generation |
| `guidance_scale` | float | model default | Classifier-free guidance scale (higher = more adherence to prompt) |
| `num_inference_steps` | int | model default | Number of denoising steps (higher = better quality, slower) |
| `seed` | int | null | Random seed for reproducible generation |
### Response Format
Returns binary audio data with the appropriate `Content-Type` header:
| `response_format` | Content-Type |
|--------------------|--------------|
| `wav` | `audio/wav` |
| `mp3` | `audio/mpeg` |
| `flac` | `audio/flac` |
| `pcm` | `audio/pcm` |
| `aac` | `audio/aac` |
| `opus` | `audio/opus` |
## Examples
### Basic Generation
Generate audio with only a text prompt (model defaults for all other parameters):
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "The sound of ocean waves crashing on a beach"
}' --output ocean.wav
```
### Custom Duration
Specify an explicit audio length in seconds:
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "A dog barking",
"audio_length": 5.0
}' --output dog_5s.wav
```
### High Quality with Negative Prompt
Use a negative prompt to steer generation away from undesired characteristics, and increase inference steps for higher quality:
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "A piano playing a gentle melody",
"audio_length": 10.0,
"negative_prompt": "Low quality, distorted, noisy",
"guidance_scale": 8.0,
"num_inference_steps": 150
}' --output piano_hq.wav
```
### Reproducible Generation
Set a `seed` to get deterministic results across runs:
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "Thunder and rain sounds",
"audio_length": 15.0,
"seed": 42
}' --output thunder.wav
```
### Full Control
Combine all parameters for precise control over generation:
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "Thunder and rain sounds",
"audio_length": 15.0,
"negative_prompt": "Low quality",
"guidance_scale": 7.0,
"num_inference_steps": 100,
"seed": 42
}' --output thunder_rain.wav
```
### Quick Generation (Fewer Steps)
For faster generation with slightly lower quality:
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "Birds chirping in a forest",
"audio_length": 8.0,
"num_inference_steps": 50
}' --output birds_quick.wav
```
### Python Client
```python
import httpx
response = httpx.post(
"http://localhost:8000/v1/audio/generate",
json={
"input": "Thunder and rain",
"audio_length": 15.0,
"negative_prompt": "Low quality",
"guidance_scale": 7.0,
"num_inference_steps": 100,
"seed": 42,
"response_format": "wav",
},
timeout=300.0,
)
with open("thunder.wav", "wb") as f:
f.write(response.content)
```
## Parameter Tuning Guide
### `guidance_scale`
Controls how closely the generated audio follows the text prompt.
| Range | Behaviour |
|-------|-----------|
| 3 - 5 | More creative / varied output |
| 7 (default) | Balanced adherence |
| 10+ | Strict adherence to the prompt |
### `num_inference_steps`
Controls the number of denoising steps in the diffusion process.
| Steps | Quality | Speed | Use Case |
|-------|---------|-------|----------|
| 50 | Good | Fast | Quick previews |
| 100 | Very Good | Medium | General purpose |
| 150+ | Excellent | Slow | Final / critical audio |
### `audio_length`
Duration of the generated audio clip. For `stable-audio-open-1.0`, the maximum is approximately 47 seconds. If omitted, the model uses its own default length.
### `negative_prompt`
Describes characteristics to avoid. Common negative prompts include:
- `"Low quality, distorted, noisy"`
- `"Silence, static"`
- `"Music"` (when generating sound effects only)
## Supported Models
| Model | Description |
|-------|-------------|
| `stabilityai/stable-audio-open-1.0` | Open-source audio generation model, up to ~47 seconds, 44.1 kHz stereo |
## Error Responses
### 400 Bad Request
Invalid or missing parameters:
```json
{
"error": {
"message": "Audio generation model did not produce audio output.",
"type": "BadRequestError",
"param": null,
"code": 400
}
}
```
### 404 Not Found
Model mismatch:
```json
{
"error": {
"message": "The model `xxx` does not exist.",
"type": "NotFoundError",
"param": "model",
"code": 404
}
}
```
### 422 Unprocessable Entity
Pydantic validation failure (e.g. invalid `response_format`, `speed` out of range):
```json
{
"detail": [
{
"type": "literal_error",
"msg": "Input should be 'wav', 'pcm', 'flac', 'mp3', 'aac' or 'opus'",
...
}
]
}
```
## Troubleshooting
### "Audio generation model did not produce audio output"
The model finished but returned no audio data. Verify the server started successfully and the model loaded without errors.
### Server Not Responding
```bash
# Check if the server is healthy
curl http://localhost:8000/health
```
### Audio Quality Issues
- Increase `num_inference_steps` (e.g. 150).
- Add a negative prompt: `"Low quality, distorted, noisy"`.
- Increase `guidance_scale` for stronger prompt adherence.
### Generation Timeout
- Reduce `num_inference_steps`.
- Reduce `audio_length`.
- Check GPU memory with `nvidia-smi`.
### Out of Memory
- Lower `--gpu-memory-utilization` (e.g. 0.8).
- Reduce `audio_length`.
## Development
Enable debug logging:
```bash
vllm-omni serve stabilityai/stable-audio-open-1.0 \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.9 \
--trust-remote-code \
--enforce-eager \
--omni \
--uvicorn-log-level debug
```
@@ -136,9 +136,9 @@ python examples/offline_inference/text_to_image/text_to_image.py \
--height 768 \
--width 1360 \
--seed 42 \
--cfg_scale 4.0 \
--num_images_per_prompt 1 \
--num_inference_steps 4 \
--cfg-scale 4.0 \
--num-images-per-prompt 1 \
--num-inference-steps 4 \
--output outputs/flux2_klein_4b.png
```
@@ -152,9 +152,9 @@ python examples/offline_inference/text_to_image/text_to_image.py \
--height 768 \
--width 1360 \
--seed 42 \
--cfg_scale 4.0 \
--num_images_per_prompt 1 \
--num_inference_steps 4 \
--cfg-scale 4.0 \
--num-images-per-prompt 1 \
--num-inference-steps 4 \
--output outputs/flux2_klein_4b_gguf.png
```
@@ -0,0 +1,193 @@
# Text-To-Audio
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/stable_audio>.
This example demonstrates how to deploy Stable Audio models for online text-to-audio generation using vLLM-Omni.
## Supported Models
| Model | Description |
|-------|-------------|
| `stabilityai/stable-audio-open-1.0` | Open-source audio generation, up to ~47 seconds, 44.1 kHz stereo |
## Start Server
### Basic Start
```bash
vllm-omni serve stabilityai/stable-audio-open-1.0 \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.9 \
--trust-remote-code \
--enforce-eager \
--omni
```
## API Calls
### Method 1: Using curl
```bash
# Run all curl examples
bash curl_examples.sh
# Or execute directly
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "The sound of a cat purring",
"audio_length": 10.0
}' --output cat.wav
```
### Method 2: Using Python Client
```bash
cd examples/online_serving/stable_audio
# Simple generation
python stable_audio_client.py \
--text "The sound of a cat purring"
# With custom duration
python stable_audio_client.py \
--text "A dog barking" \
--audio_length 5.0
# With all parameters
python stable_audio_client.py \
--text "Thunder and rain" \
--audio_length 15.0 \
--negative_prompt "Low quality" \
--guidance_scale 7.0 \
--num_inference_steps 100 \
--seed 42 \
--output thunder.wav
```
The Python client supports the following command-line arguments:
- `--api_url`: API endpoint URL (default: `http://localhost:8000/v1/audio/generate`)
- `--text`: Text prompt for audio generation (default: `"The sound of a cat purring"`)
- `--audio_length`: Audio length in seconds (default: `10.0`, max ~47s for `stable-audio-open-1.0`)
- `--audio_start`: Audio start time in seconds (default: `0.0`)
- `--negative_prompt`: Negative prompt for classifier-free guidance (default: `"Low quality"`)
- `--guidance_scale`: Guidance scale for diffusion (default: `7.0`)
- `--num_inference_steps`: Number of inference steps (default: `100`)
- `--seed`: Random seed for reproducibility (default: `None`)
- `--response_format`: Audio output format (default: `wav`). Options: `wav`, `mp3`, `flac`, `pcm`
- `--output`: Output file path (default: `stable_audio_output.wav`)
### Method 3: Using Python httpx
```python
import httpx
response = httpx.post(
"http://localhost:8000/v1/audio/generate",
json={
"input": "The sound of ocean waves crashing on a beach",
"audio_length": 10.0,
"negative_prompt": "Low quality, distorted",
"guidance_scale": 7.0,
"num_inference_steps": 100,
},
timeout=300.0,
)
with open("ocean.wav", "wb") as f:
f.write(response.content)
```
## Request Format
### Simple Generation
```json
{
"input": "The sound of ocean waves"
}
```
### Generation with Parameters
```json
{
"input": "A piano playing a gentle melody",
"audio_length": 10.0,
"negative_prompt": "Low quality, distorted, noisy",
"guidance_scale": 8.0,
"num_inference_steps": 150,
"seed": 42,
"response_format": "wav"
}
```
## API Reference
### Endpoint
```
POST /v1/audio/generate
Content-Type: application/json
```
### Generation Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `input` | string | **required** | Text prompt describing the audio to generate |
| `model` | string | server's model | Model to use (optional, should match server if specified) |
| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, aac, opus |
| `speed` | float | 1.0 | Playback speed (0.25 - 4.0) |
| `audio_length` | float | null | Audio duration in seconds (max ~47s for `stable-audio-open-1.0`) |
| `audio_start` | float | 0.0 | Audio start time in seconds |
| `negative_prompt` | string | null | Text describing what to avoid in generation |
| `guidance_scale` | float | model default | Classifier-free guidance scale (higher = more adherence to prompt) |
| `num_inference_steps` | int | model default | Number of denoising steps (higher = better quality, slower) |
| `seed` | int | null | Random seed for reproducible generation |
### Response Format
Returns binary audio data with appropriate `Content-Type` header (e.g., `audio/wav`).
## Tuning Tips
1. **Audio Length**: Keep under 47 seconds for `stable-audio-open-1.0`.
2. **Quality vs Speed**:
- 50 steps: Fast, decent quality (quick previews)
- 100 steps: Good balance (general purpose)
- 150+ steps: High quality, slower (final / critical audio)
3. **Guidance Scale**:
- Lower (3 - 5): More creative / varied output
- Default (7): Good balance
- Higher (10+): Strict adherence to the prompt
4. **Negative Prompts**: Use to avoid unwanted characteristics such as `"Low quality"`, `"distorted"`, `"noisy"`.
5. **Seeds**: Set a fixed seed to get deterministic, reproducible results.
## File Description
| File | Description |
|------|-------------|
| `curl_examples.sh` | Curl examples covering common use cases |
| `stable_audio_client.py` | Python client with full CLI argument support |
## Troubleshooting
1. **Audio generation model did not produce audio output**: Verify the server started successfully and the model loaded without errors.
2. **Connection refused**: Make sure the server is running on the correct port.
3. **Generation timeout**: Reduce `num_inference_steps` or `audio_length`, and check GPU memory with `nvidia-smi`.
4. **Out of memory**: Lower `--gpu-memory-utilization` or reduce `audio_length`.
5. **Audio quality issues**: Increase `num_inference_steps`, add a negative prompt, or raise `guidance_scale`.
## Example materials
??? abstract "stable_audio_client.py"
``````py
--8<-- "examples/online_serving/stable_audio/stable_audio_client.py"
``````
??? abstract "curl_examples.sh"
``````sh
--8<-- "examples/online_serving/stable_audio/curl_examples.sh"
``````
+69 -97
View File
@@ -1,8 +1,7 @@
import argparse
import os
from typing import cast
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniPromptType
from vllm_omni.inputs.data import OmniPromptType
def parse_args():
@@ -58,6 +57,7 @@ def parse_args():
choices=[1, 2, 3],
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.")
args = parser.parse_args()
return args
@@ -88,108 +88,80 @@ def main():
from PIL import Image
if args.modality == "img2img":
from vllm_omni.entrypoints.omni_diffusion import OmniDiffusion
from vllm_omni.entrypoints.omni import Omni
print(f"[Info] Running in {args.modality} mode (Stage 1 only, cfg_parallel_size={args.cfg_parallel_size})")
omni_kwargs = {}
if args.stage_configs_path:
omni_kwargs["stage_configs_path"] = args.stage_configs_path
client = OmniDiffusion(
model=model_name,
parallel_config={"cfg_parallel_size": args.cfg_parallel_size},
)
omni_kwargs.update(
{
"log_stats": args.log_stats,
"init_sleep_seconds": args.init_sleep_seconds,
"batch_timeout": args.batch_timeout,
"init_timeout": args.init_timeout,
"shm_threshold_bytes": args.shm_threshold_bytes,
"worker_backend": args.worker_backend,
"ray_address": args.ray_address,
}
)
if args.image_path:
if os.path.exists(args.image_path):
loaded_image = Image.open(args.image_path).convert("RGB")
prompts = [
{
"prompt": cast(str, p),
"multi_modal_data": {"image": loaded_image},
}
for p in prompts
]
else:
print(f"[Warning] Image path {args.image_path} does not exist.")
omni = Omni(model=model_name, **omni_kwargs)
result = client.generate(
prompts,
OmniDiffusionSamplingParams(
seed=52,
need_kv_receive=False,
num_inference_steps=args.steps,
extra_args={
"cfg_text_scale": args.cfg_text_scale,
"cfg_img_scale": args.cfg_img_scale,
},
),
)
# Ensure result is a list for iteration
if not isinstance(result, list):
omni_outputs = [result]
else:
omni_outputs = result
else:
from vllm_omni.entrypoints.omni import Omni
omni_kwargs = {}
if args.stage_configs_path:
omni_kwargs["stage_configs_path"] = args.stage_configs_path
omni_kwargs.update(
{
"log_stats": args.log_stats,
"init_sleep_seconds": args.init_sleep_seconds,
"batch_timeout": args.batch_timeout,
"init_timeout": args.init_timeout,
"shm_threshold_bytes": args.shm_threshold_bytes,
"worker_backend": args.worker_backend,
"ray_address": args.ray_address,
formatted_prompts = []
for p in prompts:
if args.modality == "img2img":
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|>"
prompt_dict = {
"prompt": final_prompt_text,
"multi_modal_data": {"img2img": loaded_image},
"modalities": ["img2img"],
}
)
omni = Omni(model=model_name, **omni_kwargs)
formatted_prompts = []
for p in args.prompts:
if 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"
prompt_dict = {
"prompt": final_prompt_text,
"multi_modal_data": {"image": loaded_image},
"modalities": ["text"],
}
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"
prompt_dict = {"prompt": final_prompt_text, "modalities": ["text"]}
formatted_prompts.append(prompt_dict)
else:
# text2img
final_prompt_text = f"<|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
formatted_prompts.append(prompt_dict)
params_list = omni.default_sampling_params_list
if args.modality == "text2img":
params_list[0].max_tokens = 1 # type: ignore # The first stage is a SamplingParam (vllm)
if len(params_list) > 1:
diffusion_params = params_list[1]
diffusion_params.num_inference_steps = args.steps # type: ignore
extra = {
"cfg_text_scale": args.cfg_text_scale,
"cfg_img_scale": args.cfg_img_scale,
if args.negative_prompt is not None:
prompt_dict["negative_prompt"] = args.negative_prompt
formatted_prompts.append(prompt_dict)
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"
prompt_dict = {
"prompt": final_prompt_text,
"multi_modal_data": {"image": loaded_image},
"modalities": ["text"],
}
if args.negative_prompt is not None:
extra["negative_prompt"] = args.negative_prompt
diffusion_params.extra_args = extra # type: ignore
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"
prompt_dict = {"prompt": final_prompt_text, "modalities": ["text"]}
formatted_prompts.append(prompt_dict)
else:
final_prompt_text = f"<|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
formatted_prompts.append(prompt_dict)
omni_outputs = list(omni.generate(prompts=formatted_prompts, sampling_params_list=params_list))
params_list = omni.default_sampling_params_list
if args.modality in ("text2img", "img2img"):
params_list[0].max_tokens = 1 # type: ignore
if len(params_list) > 1:
diffusion_params = params_list[1]
diffusion_params.num_inference_steps = args.steps # type: ignore
diffusion_params.cfg_parallel_size = args.cfg_parallel_size # type: ignore
if args.seed is not None:
diffusion_params.seed = args.seed # type: ignore
extra = {
"cfg_text_scale": args.cfg_text_scale,
"cfg_img_scale": args.cfg_img_scale,
}
if args.negative_prompt is not None:
extra["negative_prompt"] = args.negative_prompt
diffusion_params.extra_args = extra # type: ignore
omni_outputs = list(omni.generate(prompts=formatted_prompts, sampling_params_list=params_list))
for i, req_output in enumerate(omni_outputs):
images = getattr(req_output, "images", None)
@@ -29,8 +29,8 @@ Accelerated with cache_dit:
python image_edit.py \
--image input.png \
--prompt "Edit description" \
--cache_backend cache_dit \
--cache_dit_enable_taylorseer
--cache-backend cache_dit \
--cache-dit-enable-taylorseer
Layered RGBA output:
python image_edit.py \
@@ -66,38 +66,38 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--model", default="Qwen/Qwen-Image-Edit", help="Model name or local path.")
parser.add_argument("--image", type=str, nargs="+", required=True, help="Input image file(s).")
parser.add_argument("--prompt", type=str, required=True, help="Edit description prompt.")
parser.add_argument("--negative_prompt", type=str, default=None)
parser.add_argument("--negative-prompt", type=str, default=None)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--cfg_scale", type=float, default=4.0)
parser.add_argument("--guidance_scale", type=float, default=1.0)
parser.add_argument("--cfg-scale", type=float, default=4.0)
parser.add_argument("--guidance-scale", type=float, default=1.0)
parser.add_argument("--output", type=str, default="output.png")
parser.add_argument("--num_outputs_per_prompt", type=int, default=1)
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--cache_backend", type=str, default=None, choices=["cache_dit", "tea_cache"])
parser.add_argument("--ulysses_degree", type=int, default=1)
parser.add_argument("--ring_degree", type=int, default=1)
parser.add_argument("--tensor_parallel_size", type=int, default=1)
parser.add_argument("--num-outputs-per-prompt", type=int, default=1)
parser.add_argument("--num-inference-steps", type=int, default=50)
parser.add_argument("--cache-backend", type=str, default=None, choices=["cache_dit", "tea_cache"])
parser.add_argument("--ulysses-degree", type=int, default=1)
parser.add_argument("--ring-degree", type=int, default=1)
parser.add_argument("--tensor-parallel-size", type=int, default=1)
parser.add_argument("--layers", type=int, default=4)
parser.add_argument("--resolution", type=int, default=640)
parser.add_argument("--color-format", type=str, default="RGB")
# Acceleration + Optimization Options
parser.add_argument("--cache_dit_fn_compute_blocks", type=int, default=1)
parser.add_argument("--cache_dit_bn_compute_blocks", type=int, default=0)
parser.add_argument("--cache_dit_max_warmup_steps", type=int, default=4)
parser.add_argument("--cache_dit_residual_diff_threshold", type=float, default=0.24)
parser.add_argument("--cache_dit_max_continuous_cached_steps", type=int, default=3)
parser.add_argument("--cache_dit_enable_taylorseer", action="store_true", default=False)
parser.add_argument("--cache_dit_taylorseer_order", type=int, default=1)
parser.add_argument("--cache-dit-fn-compute-blocks", type=int, default=1)
parser.add_argument("--cache-dit-bn-compute-blocks", type=int, default=0)
parser.add_argument("--cache-dit-max-warmup-steps", type=int, default=4)
parser.add_argument("--cache-dit-residual-diff-threshold", type=float, default=0.24)
parser.add_argument("--cache-dit-max-continuous-cached-steps", type=int, default=3)
parser.add_argument("--cache-dit-enable-taylorseer", action="store_true", default=False)
parser.add_argument("--cache-dit-taylorseer-order", type=int, default=1)
parser.add_argument(
"--cache_dit_scm_steps_mask_policy", type=str, default=None, choices=[None, "slow", "medium", "fast", "ultra"]
"--cache-dit-scm-steps-mask-policy", type=str, default=None, choices=[None, "slow", "medium", "fast", "ultra"]
)
parser.add_argument("--cache_dit_scm_steps_policy", type=str, default="dynamic", choices=["dynamic", "static"])
parser.add_argument("--tea_cache_rel_l1_thresh", type=float, default=0.2)
parser.add_argument("--cfg_parallel_size", type=int, default=1, choices=[1, 2])
parser.add_argument("--enforce_eager", action="store_true")
parser.add_argument("--vae_use_slicing", action="store_true")
parser.add_argument("--vae_use_tiling", action="store_true")
parser.add_argument("--cache-dit-scm-steps-policy", type=str, default="dynamic", choices=["dynamic", "static"])
parser.add_argument("--tea-cache-rel-l1-thresh", type=float, default=0.2)
parser.add_argument("--cfg-parallel-size", type=int, default=1, choices=[1, 2])
parser.add_argument("--enforce-eager", action="store_true")
parser.add_argument("--vae-use-slicing", action="store_true")
parser.add_argument("--vae-use-tiling", action="store_true")
parser.add_argument("--enable-cpu-offload", action="store_true")
return parser.parse_args()
@@ -3,4 +3,4 @@ python image_edit.py \
--image cherry_blossom.jpg \
--prompt "Let this mascot dance under the moon, surrounded by floating stars and poetic bubbles such as 'Be Kind'" \
--output output_image_edit.png \
--num_inference_steps 10
--num-inference-steps 10
@@ -22,7 +22,7 @@ Usage:
python image_to_video.py --model /path/to/LTX-2 \
--model-class-name LTX2ImageToVideoPipeline \
--image input.jpg --prompt "A cinematic dolly shot of a boat" \
--num-frames 121 --num_inference_steps 40 --guidance_scale 4.0 \
--num-frames 121 --num-inference-steps 40 --guidance-scale 4.0 \
--frame-rate 24 --fps 24 --output ltx2_i2v.mp4
"""
@@ -0,0 +1,6 @@
*.png
.git/
ct.yaml
lintconf.yaml
values.schema.json
/workflows
@@ -0,0 +1,15 @@
apiVersion: v2
name: chart-vllm-omni
description: A Helm chart for deploying vLLM-Omni on Kubernetes for omni-modality model serving
# Application chart that can be packaged and deployed
type: application
# Chart version - increment on each change to the chart
version: 0.1.0
# vllm-omni application version — keep in sync with image.tag in values.yaml
appVersion: "0.16.0"
maintainers:
- name: vllm-omni-team
@@ -0,0 +1,144 @@
# vLLM-Omni Helm Chart
Helm chart for deploying [vLLM-Omni](https://github.com/vllm-project/vllm-omni) on Kubernetes. vLLM-Omni extends vLLM with omni-modality model serving, supporting text-to-image, multimodal chat, text-to-speech, and more.
## Prerequisites
- Kubernetes 1.24+
- Helm 3.x
- NVIDIA GPU nodes with [NVIDIA Device Plugin](https://github.com/NVIDIA/k8s-device-plugin)
## Quick Start
```bash
helm install my-release ./chart-helm \
--set model=Tongyi-MAI/Z-Image-Turbo
```
## Configuration
### Model Selection
Set the `model` value to any supported HuggingFace model ID:
| Model | Type | GPUs | Notes |
|-------|------|------|-------|
| `Tongyi-MAI/Z-Image-Turbo` | text-to-image | 1 | Small, fast (default) |
| `stabilityai/stable-diffusion-3.5-medium` | text-to-image | 1 | ~6GB VRAM |
| `Qwen/Qwen-Image` | text-to-image | 1 | Large, ~40GB+ VRAM |
| `Qwen/Qwen2.5-Omni-7B` | multimodal | 2 | Text + audio + image + video |
| `Qwen/Qwen3-Omni-7B-Chat` | multimodal | 2 | Latest omni model |
| `Qwen/Qwen3-TTS` | text-to-speech | 1 | TTS |
### HuggingFace Token
For gated models that require authentication:
```bash
helm install my-release ./chart-helm \
--set model=Qwen/Qwen2.5-Omni-7B \
--set hfToken=hf_xxxxx \
--set resources.requests."nvidia\.com/gpu"=2 \
--set resources.limits."nvidia\.com/gpu"=2
```
### Omni-Specific Flags
Enable VAE memory optimizations for diffusion models:
```bash
helm install my-release ./chart-helm \
--set model=Qwen/Qwen-Image \
--set omniArgs.vaeUseSlicing=true \
--set omniArgs.vaeUseTiling=true
```
Enable CPU offloading:
```bash
helm install my-release ./chart-helm \
--set model=Qwen/Qwen-Image \
--set omniArgs.enableCpuOffload=true
```
Pass additional raw CLI flags:
```bash
helm install my-release ./chart-helm \
--set model=Qwen/Qwen-Image \
--set omniArgs.extraArgs[0]="--enable-layerwise-offload"
```
### Model Cache
By default, a PersistentVolumeClaim is created for the HuggingFace model cache to avoid re-downloading models on pod restarts:
```yaml
modelCache:
enabled: true
storageSize: "50Gi"
storageClassName: ""
```
To use an ephemeral volume instead:
```bash
helm install my-release ./chart-helm \
--set modelCache.enabled=false
```
### Custom Command Override
To fully override the container command:
```bash
helm install my-release ./chart-helm \
--set image.command[0]=vllm \
--set image.command[1]=serve \
--set image.command[2]=my-model \
--set image.command[3]=--omni \
--set image.command[4]=--host \
--set image.command[5]=0.0.0.0
```
## API Endpoints
Once deployed, vLLM-Omni exposes the following OpenAI-compatible endpoints:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Health check |
| `/v1/models` | GET | List available models |
| `/v1/chat/completions` | POST | Chat completions (text/multimodal) |
| `/v1/images/generations` | POST | Image generation |
| `/v1/images/edits` | POST | Image editing |
| `/v1/audio/speech` | POST | Text-to-speech |
## Files
| File | Description |
|------|-------------|
| `Chart.yaml` | Chart metadata (name, version, maintainers) |
| `values.yaml` | Default configuration values |
| `values.schema.json` | JSON schema for validating values |
| `templates/_helpers.tpl` | Helper templates for common configurations |
| `templates/deployment.yaml` | Kubernetes Deployment |
| `templates/service.yaml` | Kubernetes Service (ClusterIP) |
| `templates/secrets.yaml` | Secrets (generic + HuggingFace token) |
| `templates/pvc.yaml` | PersistentVolumeClaim for model cache |
| `templates/configmap.yaml` | Optional ConfigMap |
| `templates/hpa.yaml` | HorizontalPodAutoscaler |
| `templates/poddisruptionbudget.yaml` | PodDisruptionBudget |
| `templates/custom-objects.yaml` | Custom Kubernetes objects |
## Running Tests
This chart includes unit tests using [helm-unittest](https://github.com/helm-unittest/helm-unittest). Install the plugin and run tests:
```bash
# Install plugin
helm plugin install https://github.com/helm-unittest/helm-unittest
# Run tests
helm unittest .
```
@@ -0,0 +1,3 @@
chart-dirs:
- charts
validate-maintainers: false
@@ -0,0 +1,42 @@
---
rules:
braces:
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
brackets:
min-spaces-inside: 0
max-spaces-inside: 0
min-spaces-inside-empty: -1
max-spaces-inside-empty: -1
colons:
max-spaces-before: 0
max-spaces-after: 1
commas:
max-spaces-before: 0
min-spaces-after: 1
max-spaces-after: 1
comments:
require-starting-space: true
min-spaces-from-content: 2
document-end: disable
document-start: disable
empty-lines:
max: 2
max-start: 0
max-end: 0
hyphens:
max-spaces-after: 1
indentation:
spaces: consistent
indent-sequences: whatever
check-multi-line-strings: false
key-duplicates: enable
line-length: disable
new-line-at-end-of-file: disable
new-lines:
type: unix
trailing-spaces: enable
truthy:
level: warning
@@ -0,0 +1,203 @@
{{/*
Define the vllm-omni serve command from model + omniArgs.
If image.command is set, uses that as a full override.
*/}}
{{- define "chart.omni-command" -}}
{{- if .Values.image.command }}
{{- toYaml .Values.image.command }}
{{- else }}
- "vllm"
- "serve"
- {{ .Values.model | quote }}
- "--omni"
- "--host"
- "0.0.0.0"
- "--port"
- {{ include "chart.container-port" . | quote }}
{{- if .Values.omniArgs.vaeUseSlicing }}
- "--vae-use-slicing"
{{- end }}
{{- if .Values.omniArgs.vaeUseTiling }}
- "--vae-use-tiling"
{{- end }}
{{- if .Values.omniArgs.enableCpuOffload }}
- "--enable-cpu-offload"
{{- end }}
{{- if .Values.omniArgs.numGpus }}
- "--num-gpus"
- {{ .Values.omniArgs.numGpus | quote }}
{{- end }}
{{- if .Values.omniArgs.stageConfigsPath }}
- "--stage-configs-path"
- {{ .Values.omniArgs.stageConfigsPath | quote }}
{{- end }}
{{- if and .Values.omniArgs.cacheBackend (ne .Values.omniArgs.cacheBackend "none") }}
- "--cache-backend"
- {{ .Values.omniArgs.cacheBackend | quote }}
{{- end }}
{{- if .Values.omniArgs.defaultSamplingParams }}
- "--default-sampling-params"
- {{ .Values.omniArgs.defaultSamplingParams | quote }}
{{- end }}
{{- if and .Values.omniArgs.workerBackend (ne .Values.omniArgs.workerBackend "multi_process") }}
- "--worker-backend"
- {{ .Values.omniArgs.workerBackend | quote }}
{{- end }}
{{- range .Values.omniArgs.extraArgs }}
- {{ . | quote }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Define HuggingFace environment variables
*/}}
{{- define "chart.hf-env" -}}
- name: HF_HOME
value: "/cache/huggingface"
- name: HOME
value: "/cache"
{{- if .Values.hfToken }}
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-hf-token
key: token
{{- end }}
{{- end }}
{{/*
Define ports for the pods
*/}}
{{- define "chart.container-port" -}}
{{- default "8000" .Values.containerPort }}
{{- end }}
{{/*
Define service name
*/}}
{{- define "chart.service-name" -}}
{{- if .Values.serviceName -}}
{{ .Values.serviceName | lower | trim }}
{{- else -}}
{{ printf "%s-service" .Release.Name }}
{{- end -}}
{{- end }}
{{/*
Define service port
*/}}
{{- define "chart.service-port" -}}
{{- if .Values.servicePort }}
{{- .Values.servicePort }}
{{- else }}
{{- include "chart.container-port" . }}
{{- end }}
{{- end }}
{{/*
Define service port name
*/}}
{{- define "chart.service-port-name" -}}
"service-port"
{{- end }}
{{/*
Define container port name
*/}}
{{- define "chart.container-port-name" -}}
"container-port"
{{- end }}
{{/*
Define deployment strategy
*/}}
{{- define "chart.strategy" -}}
strategy:
{{- if not .Values.deploymentStrategy }}
type: Recreate
{{- else }}
{{ toYaml .Values.deploymentStrategy | indent 2 }}
{{- end }}
{{- end }}
{{/*
Define additional ports
*/}}
{{- define "chart.extraPorts" }}
{{- with .Values.extraPorts }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
Define chart external ConfigMaps and Secrets
*/}}
{{- define "chart.externalConfigs" -}}
{{- with .Values.externalConfigs -}}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
Define startup, liveness and readiness probes
*/}}
{{- define "chart.probes" -}}
{{- if .Values.startupProbe }}
startupProbe:
{{- with .Values.startupProbe }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- if .Values.readinessProbe }}
readinessProbe:
{{- with .Values.readinessProbe }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- if .Values.livenessProbe }}
livenessProbe:
{{- with .Values.livenessProbe }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Define resources
*/}}
{{- define "chart.resources" -}}
requests:
memory: {{ required "Value 'resources.requests.memory' must be defined !" .Values.resources.requests.memory | quote }}
cpu: {{ required "Value 'resources.requests.cpu' must be defined !" .Values.resources.requests.cpu | quote }}
{{- if and (gt (int (index .Values.resources.requests "nvidia.com/gpu")) 0) (gt (int (index .Values.resources.limits "nvidia.com/gpu")) 0) }}
nvidia.com/gpu: {{ required "Value 'resources.requests.nvidia.com/gpu' must be defined !" (index .Values.resources.requests "nvidia.com/gpu") }}
{{- end }}
limits:
memory: {{ required "Value 'resources.limits.memory' must be defined !" .Values.resources.limits.memory | quote }}
cpu: {{ required "Value 'resources.limits.cpu' must be defined !" .Values.resources.limits.cpu | quote }}
{{- if and (gt (int (index .Values.resources.requests "nvidia.com/gpu")) 0) (gt (int (index .Values.resources.limits "nvidia.com/gpu")) 0) }}
nvidia.com/gpu: {{ required "Value 'resources.limits.nvidia.com/gpu' must be defined !" (index .Values.resources.limits "nvidia.com/gpu") }}
{{- end }}
{{- end }}
{{/*
Define user for the main container
*/}}
{{- define "chart.user" }}
{{- if .Values.image.runAsUser }}
runAsUser:
{{- with .Values.image.runAsUser }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Define chart labels
*/}}
{{- define "chart.labels" -}}
{{- with .Values.labels -}}
{{ toYaml . }}
{{- end }}
{{- end }}
@@ -0,0 +1,11 @@
{{- if .Values.configs -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ .Release.Name }}-configs"
namespace: {{ .Release.Namespace }}
data:
{{- with .Values.configs }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end -}}
@@ -0,0 +1,6 @@
{{- if .Values.customObjects }}
{{- range .Values.customObjects }}
{{- tpl (. | toYaml) $ }}
---
{{- end }}
{{- end }}
@@ -0,0 +1,106 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ .Release.Name }}-deployment-vllm-omni"
namespace: {{ .Release.Namespace }}
labels:
{{- include "chart.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
{{- include "chart.strategy" . | nindent 2 }}
selector:
matchLabels:
{{- include "chart.labels" . | nindent 6 }}
progressDeadlineSeconds: 1800
template:
metadata:
labels:
{{- include "chart.labels" . | nindent 8 }}
spec:
containers:
- name: "vllm-omni"
image: "{{ required "Required value 'image.repository' must be defined !" .Values.image.repository }}:{{ required "Required value 'image.tag' must be defined !" .Values.image.tag }}"
command:
{{- include "chart.omni-command" . | nindent 12 }}
securityContext:
{{- if .Values.image.securityContext }}
{{- with .Values.image.securityContext }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- else }}
runAsNonRoot: false
{{- include "chart.user" . | indent 12 }}
{{- end }}
imagePullPolicy: IfNotPresent
env:
{{- include "chart.hf-env" . | nindent 12 }}
{{- if .Values.image.env }}
{{- toYaml .Values.image.env | nindent 12 }}
{{- end }}
{{- if or .Values.externalConfigs .Values.configs .Values.secrets }}
envFrom:
{{- if .Values.configs }}
- configMapRef:
name: "{{ .Release.Name }}-configs"
{{- end }}
{{- if .Values.secrets }}
- secretRef:
name: "{{ .Release.Name }}-secrets"
{{- end }}
{{- include "chart.externalConfigs" . | nindent 12 }}
{{- end }}
ports:
- name: {{ include "chart.container-port-name" . }}
containerPort: {{ include "chart.container-port" . }}
{{- include "chart.extraPorts" . | nindent 12 }}
{{- include "chart.probes" . | indent 10 }}
resources: {{- include "chart.resources" . | nindent 12 }}
volumeMounts:
- name: shm
mountPath: /dev/shm
- name: model-cache
mountPath: /cache
{{- with .Values.extraContainers }}
{{ toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.extraInit.initContainers }}
initContainers:
{{- toYaml .Values.extraInit.initContainers | nindent 8 }}
{{- end }}
volumes:
- name: shm
emptyDir:
medium: Memory
sizeLimit: {{ .Values.shmSize }}
- name: model-cache
{{- if .Values.modelCache.enabled }}
persistentVolumeClaim:
claimName: {{ .Release.Name }}-model-cache
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.gpuModels }}
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.product
operator: In
values:
{{- toYaml .Values.gpuModels | nindent 20 }}
{{- end }}
@@ -0,0 +1,31 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: "{{ .Release.Name }}-hpa"
namespace: {{ .Release.Namespace }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: "{{ .Release.Name }}-deployment-vllm-omni"
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
@@ -0,0 +1,32 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: "{{ .Release.Name }}-ingress"
namespace: {{ .Release.Namespace }}
labels:
{{- include "chart.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.ingressClassName }}
ingressClassName: {{ .Values.ingress.ingressClassName }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: "{{ include "chart.service-name" . }}"
port:
number: {{ include "chart.service-port" . }}
{{- end }}
@@ -0,0 +1,12 @@
{{- if gt (int .Values.replicaCount) 1 }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: "{{ .Release.Name }}-pdb"
namespace: {{ .Release.Namespace }}
spec:
maxUnavailable: {{ default 1 .Values.maxUnavailablePodDisruptionBudget }}
selector:
matchLabels:
{{- include "chart.labels" . | nindent 6 }}
{{- end }}
@@ -0,0 +1,16 @@
{{- if .Values.modelCache.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "{{ .Release.Name }}-model-cache"
namespace: {{ .Release.Namespace }}
spec:
accessModes:
{{- toYaml .Values.modelCache.accessModes | nindent 4 }}
{{- if .Values.modelCache.storageClassName }}
storageClassName: {{ .Values.modelCache.storageClassName }}
{{- end }}
resources:
requests:
storage: {{ .Values.modelCache.storageSize }}
{{- end }}
@@ -0,0 +1,23 @@
{{- if .Values.secrets }}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Release.Name }}-secrets"
namespace: {{ .Release.Namespace }}
type: Opaque
data:
{{- range $key, $val := .Values.secrets }}
{{ $key }}: {{ $val | b64enc | quote }}
{{- end }}
---
{{- end }}
{{- if .Values.hfToken }}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Release.Name }}-hf-token"
namespace: {{ .Release.Namespace }}
type: Opaque
data:
token: {{ .Values.hfToken | b64enc | quote }}
{{- end }}
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: "{{ include "chart.service-name" . }}"
namespace: {{ .Release.Namespace }}
spec:
type: ClusterIP
ports:
- name: {{ include "chart.service-port-name" . }}
port: {{ include "chart.service-port" . }}
targetPort: {{ include "chart.container-port-name" . }}
protocol: TCP
selector:
{{- include "chart.labels" . | nindent 4 }}
@@ -0,0 +1,181 @@
suite: test deployment
templates:
- deployment.yaml
tests:
- it: should create deployment with default omni command
asserts:
- hasDocuments:
count: 1
- isKind:
of: Deployment
- equal:
path: spec.template.spec.containers[0].name
value: vllm-omni
- equal:
path: spec.template.spec.containers[0].command[0]
value: vllm
- equal:
path: spec.template.spec.containers[0].command[1]
value: serve
- equal:
path: spec.template.spec.containers[0].command[2]
value: Tongyi-MAI/Z-Image-Turbo
- equal:
path: spec.template.spec.containers[0].command[3]
value: "--omni"
- equal:
path: spec.template.spec.containers[0].command[4]
value: "--host"
- equal:
path: spec.template.spec.containers[0].command[5]
value: "0.0.0.0"
- equal:
path: spec.template.spec.containers[0].command[6]
value: "--port"
- equal:
path: spec.template.spec.containers[0].command[7]
value: "8000"
- it: should use custom model when set
set:
model: "Qwen/Qwen2.5-Omni-7B"
asserts:
- equal:
path: spec.template.spec.containers[0].command[2]
value: Qwen/Qwen2.5-Omni-7B
- it: should include omniArgs flags in command
set:
omniArgs:
vaeUseSlicing: true
vaeUseTiling: true
enableCpuOffload: true
numGpus: 2
cacheBackend: "tea_cache"
workerBackend: "ray"
extraArgs:
- "--enable-layerwise-offload"
asserts:
- contains:
path: spec.template.spec.containers[0].command
content: "--vae-use-slicing"
- contains:
path: spec.template.spec.containers[0].command
content: "--vae-use-tiling"
- contains:
path: spec.template.spec.containers[0].command
content: "--enable-cpu-offload"
- contains:
path: spec.template.spec.containers[0].command
content: "--num-gpus"
- contains:
path: spec.template.spec.containers[0].command
content: "--cache-backend"
- contains:
path: spec.template.spec.containers[0].command
content: "--worker-backend"
- contains:
path: spec.template.spec.containers[0].command
content: "--enable-layerwise-offload"
- it: should use full command override when image.command is set
set:
image:
command:
- "custom-binary"
- "--flag"
asserts:
- equal:
path: spec.template.spec.containers[0].command[0]
value: custom-binary
- equal:
path: spec.template.spec.containers[0].command[1]
value: "--flag"
- it: should mount shm and model-cache volumes
asserts:
- contains:
path: spec.template.spec.containers[0].volumeMounts
content:
name: shm
mountPath: /dev/shm
- contains:
path: spec.template.spec.containers[0].volumeMounts
content:
name: model-cache
mountPath: /cache
- contains:
path: spec.template.spec.volumes
content:
name: shm
emptyDir:
medium: Memory
sizeLimit: 8Gi
- it: should use PVC for model-cache when modelCache is enabled
set:
modelCache:
enabled: true
storageSize: "50Gi"
accessModes:
- ReadWriteOnce
asserts:
- contains:
path: spec.template.spec.volumes
content:
name: model-cache
persistentVolumeClaim:
claimName: RELEASE-NAME-model-cache
- it: should use emptyDir for model-cache when modelCache is disabled
set:
modelCache:
enabled: false
storageSize: "50Gi"
accessModes:
- ReadWriteOnce
asserts:
- contains:
path: spec.template.spec.volumes
content:
name: model-cache
emptyDir: {}
- it: should set HF_HOME environment variable
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: HF_HOME
value: /cache/huggingface
- it: should set HF_TOKEN env when hfToken is provided
set:
hfToken: "hf_test_token_123"
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: HF_TOKEN
valueFrom:
secretKeyRef:
name: RELEASE-NAME-hf-token
key: token
- it: should create custom init containers when specified
set:
extraInit:
initContainers:
- name: my-init
image: busybox:latest
command: ["echo", "init"]
asserts:
- lengthEqual:
path: spec.template.spec.initContainers
count: 1
- equal:
path: spec.template.spec.initContainers[0].name
value: my-init
- equal:
path: spec.template.spec.initContainers[0].image
value: busybox:latest
@@ -0,0 +1,86 @@
suite: test ingress
templates:
- ingress.yaml
tests:
- it: should not create Ingress when disabled
set:
ingress:
enabled: false
asserts:
- hasDocuments:
count: 0
- it: should create Ingress when enabled
set:
ingress:
enabled: true
host: "vllm-omni.example.com"
asserts:
- hasDocuments:
count: 1
- isKind:
of: Ingress
- equal:
path: spec.rules[0].host
value: vllm-omni.example.com
- equal:
path: spec.rules[0].http.paths[0].path
value: /
- equal:
path: spec.rules[0].http.paths[0].pathType
value: Prefix
- equal:
path: spec.rules[0].http.paths[0].backend.service.name
value: RELEASE-NAME-service
- equal:
path: spec.rules[0].http.paths[0].backend.service.port.number
value: 80
- it: should set ingressClassName when specified
set:
ingress:
enabled: true
host: "vllm-omni.example.com"
ingressClassName: "nginx"
asserts:
- equal:
path: spec.ingressClassName
value: nginx
- it: should not set ingressClassName when empty
set:
ingress:
enabled: true
host: "vllm-omni.example.com"
ingressClassName: ""
asserts:
- notExists:
path: spec.ingressClassName
- it: should include annotations when specified
set:
ingress:
enabled: true
host: "vllm-omni.example.com"
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
asserts:
- isNotEmpty:
path: metadata.annotations
- it: should include TLS configuration when specified
set:
ingress:
enabled: true
host: "vllm-omni.example.com"
tls:
- secretName: vllm-omni-tls
hosts:
- vllm-omni.example.com
asserts:
- equal:
path: spec.tls[0].secretName
value: vllm-omni-tls
- equal:
path: spec.tls[0].hosts[0]
value: vllm-omni.example.com
@@ -0,0 +1,73 @@
suite: test pvc
templates:
- pvc.yaml
tests:
- it: should create PVC when modelCache is enabled
set:
modelCache:
enabled: true
storageSize: "50Gi"
storageClassName: ""
accessModes:
- ReadWriteOnce
asserts:
- hasDocuments:
count: 1
- isKind:
of: PersistentVolumeClaim
- equal:
path: spec.accessModes[0]
value: ReadWriteOnce
- equal:
path: spec.resources.requests.storage
value: 50Gi
- it: should not create PVC when modelCache is disabled
set:
modelCache:
enabled: false
storageSize: "50Gi"
accessModes:
- ReadWriteOnce
asserts:
- hasDocuments:
count: 0
- it: should use custom storage size
set:
modelCache:
enabled: true
storageSize: "100Gi"
storageClassName: ""
accessModes:
- ReadWriteOnce
asserts:
- equal:
path: spec.resources.requests.storage
value: 100Gi
- it: should set storageClassName when specified
set:
modelCache:
enabled: true
storageSize: "50Gi"
storageClassName: "fast-ssd"
accessModes:
- ReadWriteOnce
asserts:
- equal:
path: spec.storageClassName
value: fast-ssd
- it: should support ReadWriteMany access mode
set:
modelCache:
enabled: true
storageSize: "50Gi"
storageClassName: ""
accessModes:
- ReadWriteMany
asserts:
- equal:
path: spec.accessModes[0]
value: ReadWriteMany
@@ -0,0 +1,48 @@
suite: test secrets
templates:
- secrets.yaml
tests:
- it: should create HF token secret when hfToken is provided
set:
hfToken: "hf_test_token_123"
asserts:
- hasDocuments:
count: 1
- isKind:
of: Secret
- equal:
path: metadata.name
value: RELEASE-NAME-hf-token
- exists:
path: data.token
- it: should not create any secrets when hfToken is empty and no secrets defined
set:
hfToken: ""
secrets: {}
asserts:
- hasDocuments:
count: 0
- it: should create both generic and HF token secrets
set:
hfToken: "hf_test_token_123"
secrets:
mykey: myvalue
asserts:
- hasDocuments:
count: 2
- it: should create only generic secrets when no hfToken
set:
hfToken: ""
secrets:
mykey: myvalue
asserts:
- hasDocuments:
count: 1
- isKind:
of: Secret
- equal:
path: metadata.name
value: RELEASE-NAME-secrets
@@ -0,0 +1,197 @@
# -- Default values for chart vllm-omni
# -- Declare variables to be passed into your templates.
# -- HuggingFace model ID to serve
model: "Tongyi-MAI/Z-Image-Turbo"
# -- Image configuration
image:
# -- Image repository
repository: "vllm/vllm-omni"
# -- Image tag
tag: "v0.16.0"
# -- Override the container command entirely. If empty, the chart constructs
# -- it automatically from model + omniArgs.
command: []
# -- Optional environment variables for the container
env: []
# -- Security context override
securityContext: {}
# -- Container port
containerPort: 8000
# -- Service name (auto-generated from release name if empty)
serviceName:
# -- Service port
servicePort: 80
# -- Additional ports configuration
extraPorts: []
# -- Number of replicas
replicaCount: 1
# -- Deployment strategy configuration
deploymentStrategy: {}
# -- Resource configuration
resources:
requests:
# -- Number of CPUs
cpu: 4
# -- CPU memory
memory: 24Gi
# -- Number of GPUs
nvidia.com/gpu: 1
limits:
# -- Number of CPUs
cpu: 4
# -- CPU memory
memory: 24Gi
# -- Number of GPUs
nvidia.com/gpu: 1
# -- GPU model types for node affinity scheduling (optional)
# -- Leave empty to schedule on any GPU node. Set to restrict to specific GPU types.
# -- Example: ["NVIDIA-A100-SXM4-40GB"]
gpuModels: []
# -- vllm-omni specific CLI arguments
omniArgs:
# -- Additional raw CLI flags appended to the serve command (list of strings)
extraArgs: []
# -- Enable VAE slicing for memory optimization
vaeUseSlicing: false
# -- Enable VAE tiling for memory optimization
vaeUseTiling: false
# -- Enable CPU offloading for diffusion models
enableCpuOffload: false
# -- Number of GPUs for diffusion inference (empty = auto)
numGpus:
# -- Path to stage configs file (empty = auto-detected from model)
stageConfigsPath:
# -- Cache backend: none, tea_cache, or cache_dit
cacheBackend: "none"
# -- Default sampling params as a JSON string
defaultSamplingParams:
# -- Worker backend: multi_process or ray
workerBackend: "multi_process"
# -- HuggingFace token for gated models (optional)
hfToken: ""
# -- Shared memory size for PyTorch multiprocessing
shmSize: "8Gi"
# -- Model cache configuration (HuggingFace downloads)
modelCache:
# -- Use a PersistentVolumeClaim for model cache (recommended for production)
enabled: true
# -- Storage size for the PVC
storageSize: "50Gi"
# -- Storage class name (empty = cluster default)
storageClassName: ""
# -- Access modes
accessModes:
- ReadWriteOnce
# -- Autoscaling configuration
autoscaling:
# -- Enable autoscaling
enabled: false
# -- Minimum replicas
minReplicas: 1
# -- Maximum replicas
maxReplicas: 10
# -- Target CPU utilization for autoscaling
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
# -- ConfigMap data (key-value pairs injected as environment variables)
configs: {}
# -- Secrets data (key-value pairs, base64-encoded automatically)
secrets: {}
# -- External ConfigMaps/Secrets references
externalConfigs: []
# -- Custom Kubernetes objects
customObjects: []
# -- PodDisruptionBudget max unavailable
maxUnavailablePodDisruptionBudget: ""
# -- Additional init containers
extraInit:
initContainers: []
# -- Additional sidecar containers
extraContainers: []
# -- Startup probe configuration
# -- Protects slow-starting containers. Liveness and readiness probes
# -- do not start until the startup probe succeeds.
# -- failureThreshold * periodSeconds = max startup time (40 * 30 = 1200s = 20 min)
startupProbe:
# -- HTTP check configuration
httpGet:
# -- Path to check
path: /health
# -- Port to check (uses named port to stay in sync with containerPort)
port: "container-port"
# -- Failures before killing the container
failureThreshold: 40
# -- How often to perform the probe
periodSeconds: 30
# -- Readiness probe configuration
readinessProbe:
# -- How often to perform the probe
periodSeconds: 10
# -- Failures before marking unready
failureThreshold: 3
# -- HTTP check configuration
httpGet:
# -- Path to check
path: /health
# -- Port to check (uses named port to stay in sync with containerPort)
port: "container-port"
# -- Liveness probe configuration
livenessProbe:
# -- Failures before restarting
failureThreshold: 3
# -- How often to perform the probe
periodSeconds: 15
# -- HTTP check configuration
httpGet:
# -- Path to check
path: /health
# -- Port to check (uses named port to stay in sync with containerPort)
port: "container-port"
# -- Ingress configuration
ingress:
# -- Enable Ingress
enabled: false
# -- Ingress class name (e.g., nginx, traefik)
ingressClassName: ""
# -- Hostname for the Ingress rule
host: "vllm-omni.example.com"
# -- Additional annotations
annotations: {}
# -- TLS configuration
tls: []
# - secretName: vllm-omni-tls
# hosts:
# - vllm-omni.example.com
# -- Node selector for pod scheduling
nodeSelector: {}
# -- Tolerations for pod scheduling
tolerations: []
# -- Labels applied to all resources
labels:
app: "vllm-omni"
@@ -0,0 +1,234 @@
# Stable Audio Online Serving
Generate audio from text prompts using Stable Audio models via an OpenAI-compatible API endpoint.
## Features
- **OpenAI-compatible API**: Use `/v1/audio/generate` endpoint
- **Flexible control**: Adjust audio length, guidance scale, inference steps
- **Quality control**: Use negative prompts to avoid unwanted characteristics
- **Reproducible**: Set random seed for deterministic generation
## Quick Start
### 1. Start the Server
```bash
vllm-omni serve stabilityai/stable-audio-open-1.0 \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.9 \
--trust-remote-code \
--enforce-eager \
--omni
```
### 2. Generate Audio
#### Using curl
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "The sound of a cat purring",
"audio_length": 10.0
}' --output cat.wav
```
#### Using Python Client
```bash
python stable_audio_client.py \
--text "The sound of a cat purring" \
--audio_length 10.0 \
--output cat.wav
```
#### Using Bash Script
```bash
bash curl_examples.sh
```
## API Reference
### Endpoint
```
POST /v1/audio/generate
```
### Request Body
```json
{
"input": "Text description of the audio",
"audio_length": 10.0,
"audio_start": 0.0,
"negative_prompt": "Low quality",
"guidance_scale": 7.0,
"num_inference_steps": 100,
"seed": 42,
"response_format": "wav"
}
```
### Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `input` | string | **required** | Text prompt describing the audio to generate |
| `audio_length` | float | ~47s | Audio duration in seconds (max ~47s for stable-audio-open-1.0) |
| `audio_start` | float | 0.0 | Audio start time in seconds |
| `negative_prompt` | string | null | Text describing what to avoid in generation |
| `guidance_scale` | float | 7.0 | Classifier-free guidance scale (higher = more adherence to prompt) |
| `num_inference_steps` | int | 50 | Number of denoising steps (higher = better quality, slower) |
| `seed` | int | null | Random seed for reproducibility |
| `response_format` | string | "wav" | Output format: wav, mp3, flac, pcm |
### Response
Returns audio data in the requested format (default: WAV).
## Usage Examples
### Basic Generation
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "The sound of ocean waves"
}' --output ocean.wav
```
### Custom Duration
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "A dog barking",
"audio_length": 5.0
}' --output dog_5s.wav
```
### High Quality with Negative Prompt
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "A piano playing a gentle melody",
"audio_length": 10.0,
"negative_prompt": "Low quality, distorted, noisy",
"guidance_scale": 8.0,
"num_inference_steps": 150
}' --output piano_hq.wav
```
### Reproducible Generation
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "Thunder and rain sounds",
"audio_length": 15.0,
"seed": 42
}' --output thunder.wav
```
### Quick Generation (Fewer Steps)
For faster generation with slightly lower quality:
```bash
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "Birds chirping in a forest",
"audio_length": 8.0,
"num_inference_steps": 50
}' --output birds_quick.wav
```
## Python Client Examples
### Simple Generation
```bash
python stable_audio_client.py \
--text "The sound of a cat purring"
```
### Custom Parameters
```bash
python stable_audio_client.py \
--text "Thunder and rain" \
--audio_length 15.0 \
--negative_prompt "Low quality" \
--guidance_scale 7.0 \
--num_inference_steps 100 \
--seed 42 \
--output thunder.wav
```
### Different Output Format
```bash
python stable_audio_client.py \
--text "Guitar playing" \
--response_format mp3 \
--output guitar.mp3
```
## Tips
1. **Audio Length**: Keep under 47 seconds for `stable-audio-open-1.0`
2. **Quality vs Speed**:
- 50 steps: Fast, decent quality
- 100 steps: Good balance (default)
- 150+ steps: High quality, slower
3. **Guidance Scale**:
- Lower (3-5): More creative/varied
- Default (7): Good balance
- Higher (10+): More literal to prompt
4. **Negative Prompts**: Use to avoid "Low quality", "distorted", "noisy", etc.
5. **Seeds**: Use same seed for reproducible results
## Performance
| Inference Steps | Quality | Speed | Use Case |
|----------------|---------|-------|----------|
| 50 | Good | Fast | Quick previews |
| 100 (default) | Very Good | Medium | Production |
| 150+ | Excellent | Slow | Final/critical audio |
## Troubleshooting
### Server not responding
- Check if server is running: `curl http://localhost:8000/health`
- Check server logs for errors
### Audio quality issues
- Increase `num_inference_steps` (e.g., 150)
- Add negative prompts: `"Low quality, distorted, noisy"`
- Increase `guidance_scale` for more prompt adherence
### Generation timeout
- Reduce `num_inference_steps`
- Reduce `audio_length`
- Check GPU memory with `nvidia-smi`
### Wrong audio length
- Ensure `audio_length` is within model limits (~47s max)
- Adjust `audio_start` if trimming is needed
## See Also
- [Offline Inference Example](../../offline_inference/text_to_audio/README.md)
- [Stable Audio Model Card](https://huggingface.co/stabilityai/stable-audio-open-1.0)
- [vLLM-Omni Documentation](https://github.com/vllm-project/vllm-omni)
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# Examples for using Stable Audio with curl via /v1/audio/generate endpoint
# Example 1: Simple request with default parameters
echo "Example 1: Simple request with default parameters"
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "The sound audience clapping and cheering in a stadium"
}' --output stadium.wav
# Example 2: Request with custom audio_length
echo "Example 2: Custom audio length (5 seconds)"
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "The sound of a dog barking",
"audio_length": 5.0
}' --output dog_5s.wav
# Example 3: Request with negative prompt for quality control
echo "Example 3: With negative prompt"
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "A piano playing a gentle melody",
"audio_length": 10.0,
"negative_prompt": "Low quality, distorted, noisy"
}' --output piano.wav
# Example 4: Full control with all parameters
echo "Example 4: Full control (custom length, guidance, steps, seed)"
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "Thunder and rain sounds",
"audio_length": 15.0,
"negative_prompt": "Low quality",
"guidance_scale": 7.0,
"num_inference_steps": 100,
"seed": 42
}' --output thunder_rain.wav
# Example 5: Quick generation with fewer steps (faster but lower quality)
echo "Example 5: Quick generation (fewer steps)"
curl -X POST http://localhost:8000/v1/audio/generate \
-H "Content-Type: application/json" \
-d '{
"input": "Ocean waves crashing on a beach",
"audio_length": 8.0,
"num_inference_steps": 50
}' --output ocean.wav
echo "All examples completed!"
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""
OpenAI-compatible client for Stable Audio via /v1/audio/generate endpoint.
This script demonstrates how to use the OpenAI-compatible speech API
to generate audio from text using Stable Audio models.
Examples:
# Simple generation
python stable_audio_client.py --text "The sound of a cat purring"
# With custom duration
python stable_audio_client.py --text "A dog barking" --audio_length 5.0
# With all parameters
python stable_audio_client.py --text "Thunder and rain" \
--audio_length 15.0 \
--negative_prompt "Low quality" \
--guidance_scale 7.0 \
--num_inference_steps 100 \
--seed 42 \
--output thunder.wav
"""
import argparse
import sys
import requests
def parse_args():
parser = argparse.ArgumentParser(description="Generate audio with Stable Audio via OpenAI-compatible API")
parser.add_argument(
"--api_url",
default="http://localhost:8000/v1/audio/generate",
help="API endpoint URL",
)
parser.add_argument(
"--text",
default="The sound of a cat purring",
help="Text prompt for audio generation",
)
parser.add_argument(
"--audio_length",
type=float,
default=10.0,
help="Audio length in seconds (max ~47s for stable-audio-open-1.0)",
)
parser.add_argument(
"--audio_start",
type=float,
default=0.0,
help="Audio start time in seconds",
)
parser.add_argument(
"--negative_prompt",
default="Low quality",
help="Negative prompt for classifier-free guidance",
)
parser.add_argument(
"--guidance_scale",
type=float,
default=7.0,
help="Guidance scale for diffusion (higher = more adherence to prompt)",
)
parser.add_argument(
"--num_inference_steps",
type=int,
default=100,
help="Number of inference steps (higher = better quality, slower)",
)
parser.add_argument(
"--seed",
type=int,
default=None,
help="Random seed for reproducibility",
)
parser.add_argument(
"--output",
default="stable_audio_output.wav",
help="Output file path",
)
parser.add_argument(
"--response_format",
default="wav",
choices=["wav", "mp3", "flac", "pcm"],
help="Audio output format",
)
return parser.parse_args()
def generate_audio(args):
"""Generate audio using the API."""
# Build request payload
payload = {
"input": args.text,
"audio_length": args.audio_length,
"audio_start": args.audio_start,
"response_format": args.response_format,
}
# Add optional parameters
if args.negative_prompt:
payload["negative_prompt"] = args.negative_prompt
if args.guidance_scale:
payload["guidance_scale"] = args.guidance_scale
if args.num_inference_steps:
payload["num_inference_steps"] = args.num_inference_steps
if args.seed is not None:
payload["seed"] = args.seed
print(f"\n{'=' * 60}")
print("Stable Audio - Text-to-Audio Generation")
print(f"{'=' * 60}")
print(f"API URL: {args.api_url}")
print(f"Prompt: {args.text}")
print(f"Audio length: {args.audio_length}s")
print(f"Negative prompt: {args.negative_prompt}")
print(f"Guidance scale: {args.guidance_scale}")
print(f"Inference steps: {args.num_inference_steps}")
if args.seed is not None:
print(f"Seed: {args.seed}")
print(f"Output: {args.output}")
print(f"{'=' * 60}\n")
try:
# Make the API request
print("Generating audio...")
response = requests.post(
args.api_url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=300, # 5 minute timeout for long generations
)
# Check for errors
if response.status_code != 200:
print(f"Error: API returned status code {response.status_code}")
print(f"Response: {response.text}")
return False
# Save the audio
with open(args.output, "wb") as f:
f.write(response.content)
print(f"✓ Audio saved to {args.output}")
print(f" File size: {len(response.content) / 1024:.1f} KB")
return True
except requests.exceptions.Timeout:
print("Error: Request timed out. Try reducing inference steps or audio length.")
return False
except requests.exceptions.ConnectionError:
print(f"Error: Could not connect to {args.api_url}")
print("Make sure the server is running.")
return False
except Exception as e:
print(f"Error: {e}")
return False
def main():
args = parse_args()
success = generate_audio(args)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
+1
View File
@@ -51,6 +51,7 @@ dev = [
"mooncake-transfer-engine==0.3.8.post1",
"av", # for ComfyUI tests
"openpyxl>=3.0.0", # for nightly CI
"pyttsx3>=2.99"
]
docs = [
+200 -94
View File
@@ -12,6 +12,7 @@ if "VLLM_TARGET_DEVICE" not in os.environ:
import concurrent.futures
import gc
import multiprocessing
import socket
import subprocess
import sys
@@ -217,102 +218,193 @@ def generate_synthetic_audio(
sample_rate: int = 48000, # Default use 48000Hz.
save_to_file: bool = False,
) -> dict[str, Any]:
""" "Generate synthetic audio with rain."""
"""
Generate TTS speech with pyttsx3 and return base64 string.
"""
import tempfile
import pyttsx3
import soundfile as sf
# Initialize audio data array
num_samples = int(sample_rate * duration)
def _pick_voice(engine: pyttsx3.Engine) -> str | None:
voices = engine.getProperty("voices")
if not voices:
return None
preferred_tokens = (
"natural",
"jenny",
"sonia",
"susan",
"zira",
"aria",
"hazel",
"samantha",
"ava",
"allison",
"female",
"woman",
"english-us",
"en-us",
"english",
)
discouraged_tokens = (
"espeak",
"robot",
"mbrola",
"microsoft david",
"male",
"man",
)
best_voice = voices[0]
best_score = float("-inf")
for voice in voices:
voice_text = f"{getattr(voice, 'id', '')} {getattr(voice, 'name', '')}".lower()
voice_languages = " ".join(
lang.decode(errors="ignore") if isinstance(lang, bytes) else str(lang)
for lang in getattr(voice, "languages", [])
).lower()
combined_text = f"{voice_text} {voice_languages}"
score = 0
for idx, token in enumerate(preferred_tokens):
if token in combined_text:
score += 20 - idx
for token in discouraged_tokens:
if token in combined_text:
score -= 10
if "english" in combined_text or "en_" in combined_text or "en-" in combined_text:
score += 4
if "en-us" in combined_text or "english-us" in combined_text:
score += 4
if score > best_score:
best_score = score
best_voice = voice
return best_voice.id
def _resample_audio(audio: np.ndarray, src_sr: int, dst_sr: int) -> np.ndarray:
if src_sr == dst_sr or len(audio) == 0:
return audio.astype(np.float32)
src_len = audio.shape[0]
dst_len = max(1, int(round(src_len * float(dst_sr) / float(src_sr))))
src_idx = np.arange(src_len, dtype=np.float32)
dst_idx = np.linspace(0, src_len - 1, dst_len, dtype=np.float32)
resampled_channels: list[np.ndarray] = []
for ch in range(audio.shape[1]):
resampled_channels.append(np.interp(dst_idx, src_idx, audio[:, ch]).astype(np.float32))
return np.stack(resampled_channels, axis=1)
def _match_channels(audio: np.ndarray, target_channels: int) -> np.ndarray:
current_channels = audio.shape[1]
if current_channels == target_channels:
return audio.astype(np.float32)
if target_channels == 1:
return np.mean(audio, axis=1, keepdims=True, dtype=np.float32)
if current_channels == 1:
return np.repeat(audio, target_channels, axis=1).astype(np.float32)
collapsed = np.mean(audio, axis=1, keepdims=True, dtype=np.float32)
return np.repeat(collapsed, target_channels, axis=1).astype(np.float32)
def _trim_silence(audio: np.ndarray, threshold: float = 0.01) -> np.ndarray:
if len(audio) == 0:
return audio
energy = np.max(np.abs(audio), axis=1)
voiced = np.where(energy > threshold)[0]
if len(voiced) == 0:
return audio
start = max(0, int(voiced[0]) - int(sample_rate * 0.02))
end = min(len(audio), int(voiced[-1]) + int(sample_rate * 0.04) + 1)
return audio[start:end]
def _enhance_speech(audio: np.ndarray) -> np.ndarray:
if len(audio) == 0:
return audio.astype(np.float32)
enhanced = audio.astype(np.float32).copy()
enhanced -= np.mean(enhanced, axis=0, keepdims=True, dtype=np.float32)
if len(enhanced) > 1:
preemphasis = enhanced.copy()
preemphasis[1:] = enhanced[1:] - 0.94 * enhanced[:-1]
enhanced = 0.7 * enhanced + 0.3 * preemphasis
# Mild dynamic-range compression for ASR/TTS robustness.
enhanced = np.sign(enhanced) * np.sqrt(np.abs(enhanced))
# Light fade to avoid clicks after trimming/repeating.
fade = min(len(enhanced) // 4, max(1, int(sample_rate * 0.01)))
if fade > 1:
ramp_in = np.linspace(0.0, 1.0, fade, dtype=np.float32)
ramp_out = np.linspace(1.0, 0.0, fade, dtype=np.float32)
enhanced[:fade] *= ramp_in[:, None]
enhanced[-fade:] *= ramp_out[:, None]
peak = float(np.max(np.abs(enhanced)))
if peak > 1e-8:
enhanced = enhanced / peak * 0.95
return enhanced.astype(np.float32)
phrase_text = "test"
num_samples = int(sample_rate * max(1, duration))
audio_data = np.zeros((num_samples, num_channels), dtype=np.float32)
# Configure parameters based on rain intensity
drop_density = 10 # Number of raindrops per second
drop_volume = 0.15 # Volume of individual raindrops
background_volume = 0.02 # Volume of background rain noise
engine = pyttsx3.init()
engine.setProperty("rate", 112)
engine.setProperty("volume", 1.0)
selected_voice = _pick_voice(engine)
if selected_voice is not None:
engine.setProperty("voice", selected_voice)
# Pink noise sounds more natural than white noise for rain
white_noise = np.random.randn(num_samples)
pink_noise = np.convolve(white_noise, np.ones(8) / 8, mode="same")
pink_noise = pink_noise / np.max(np.abs(pink_noise)) if np.max(np.abs(pink_noise)) > 0 else pink_noise
bg_noise = pink_noise * background_volume
temp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
temp_wav.close()
# Add background noise to all channels
for ch in range(num_channels):
audio_data[:, ch] += bg_noise
try:
engine.save_to_file(phrase_text, temp_wav.name)
engine.runAndWait()
engine.stop()
# Total number of raindrops = density × duration × channels for stereo effect
total_drops = int(drop_density * duration * num_channels)
ready = False
for _ in range(50):
if os.path.exists(temp_wav.name) and os.path.getsize(temp_wav.name) > 44:
ready = True
break
time.sleep(0.1)
for _ in range(total_drops):
# Random timing for raindrop
drop_time = random.uniform(0, duration)
if not ready:
raise RuntimeError("pyttsx3 did not produce a WAV file in time.")
# Random duration of raindrop sound (0.01-0.05 seconds)
drop_duration = random.uniform(0.01, 0.05)
tts_audio, tts_sr = sf.read(temp_wav.name, dtype="float32", always_2d=True)
finally:
if os.path.exists(temp_wav.name):
os.unlink(temp_wav.name)
# Random frequency gives variation in raindrop pitch
drop_freq = random.uniform(500, 5000) # Hz
if len(tts_audio) == 0:
raise RuntimeError("pyttsx3 produced an empty WAV file.")
# Random channel selection for stereo positioning
channel = random.randint(0, num_channels - 1)
tts_audio = _resample_audio(tts_audio, tts_sr, sample_rate)
tts_audio = _match_channels(tts_audio, num_channels)
tts_audio = _trim_silence(tts_audio, threshold=0.012)
tts_audio = _enhance_speech(tts_audio)
# Calculate sample positions for this raindrop
start_sample = int(drop_time * sample_rate)
drop_samples = int(drop_duration * sample_rate)
end_sample = min(start_sample + drop_samples, num_samples)
lead_silence = min(int(sample_rate * 0.02), num_samples // 8)
pause_samples = int(sample_rate * 0.18)
start = lead_silence
phrase_len = tts_audio.shape[0]
if start_sample < end_sample:
# Generate the raindrop sound
num_drop_samples = end_sample - start_sample
t = np.arange(num_drop_samples) / sample_rate
while start < num_samples:
take = min(phrase_len, num_samples - start)
audio_data[start : start + take] = tts_audio[:take]
start += phrase_len + pause_samples
# Basic sine wave for raindrop sound
drop_sound = drop_volume * np.sin(2 * math.pi * drop_freq * t)
# Apply envelope for natural attack and decay
envelope = np.ones(num_drop_samples)
attack_samples = int(num_drop_samples * 0.1) # 10% of samples for attack
decay_samples = num_drop_samples - attack_samples
if attack_samples > 0:
# Linear attack: volume increases from 0 to 1
envelope[:attack_samples] = np.linspace(0, 1, attack_samples)
if decay_samples > 0:
# Exponential decay for natural sound fade
decay = np.exp(-8 * t[attack_samples:] / drop_duration)
envelope[attack_samples:] = decay
# Apply envelope to raindrop sound
drop_sound *= envelope
# Add raindrop sound to selected channel
audio_data[start_sample:end_sample, channel] += drop_sound
# Step 3: Add simple reverb effect for realism
# Reverb simulates sound reflections in environment
if duration > 2:
# Single delay reverb (100ms delay)
delay_samples = int(0.1 * sample_rate)
if delay_samples < num_samples:
for ch in range(num_channels):
delayed = np.zeros(num_samples)
delayed[delay_samples:] = audio_data[:-delay_samples, ch] * 0.3
audio_data[:, ch] += delayed
# Step 4: Normalize audio to prevent clipping
# Find maximum amplitude and scale to 80% of maximum volume
max_amp = np.max(np.abs(audio_data))
max_amp = float(np.max(np.abs(audio_data)))
if max_amp > 0:
audio_data = audio_data / max_amp * 0.8
audio_data = audio_data / max_amp * 0.95
audio_array = audio_data.copy()
result = {
"np_array": audio_array,
audio_bytes: bytes | None = None
output_path: str | None = None
result: dict[str, Any] = {
"np_array": audio_data.copy(),
}
# Handle file saving
audio_bytes = None
if save_to_file:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = f"audio_{num_channels}ch_{timestamp}.wav"
@@ -617,24 +709,38 @@ def convert_audio_to_text(audio_data):
return text
def convert_audio_file_to_text(output_path):
def _whisper_transcribe_in_current_process(output_path: str) -> str:
import whisper
model = whisper.load_model("small")
text = model.transcribe(
output_path,
temperature=0.0,
word_timestamps=True,
condition_on_previous_text=False,
)["text"]
del model
if torch.cuda.is_available():
device = "cuda" if torch.cuda.is_available() else "cpu"
model = whisper.load_model("small", device=device)
try:
text = model.transcribe(
output_path,
temperature=0.0,
word_timestamps=True,
condition_on_previous_text=False,
)["text"]
finally:
# Sync GPU so in-flight ops finish before we free the model; otherwise
# freed memory may not show up until those ops complete.
if torch is not None and torch.cuda.is_available():
torch.cuda.synchronize()
del model
gc.collect()
torch.cuda.empty_cache()
if text:
return text
else:
return ""
if torch is not None and torch.cuda.is_available():
torch.cuda.empty_cache()
return text or ""
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.
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)
return future.result()
def merge_base64_and_convert_to_text(base64_list):
@@ -1105,7 +1211,7 @@ def assert_omni_response(response: OmniResponse, request_config: dict[str, Any],
assert response.success, "The request failed."
e2e_latency = response.e2e_latency
if e2e_latency is not None:
print(f"the avg e2e is: {e2e_latency}")
print(f"the avg e2e latency is: {e2e_latency}")
modalities = request_config.get("modalities", ["text", "audio"])
@@ -6,6 +6,7 @@ from vllm_omni.distributed.omni_connectors.kv_transfer_manager import (
OmniKVCacheConfig,
OmniKVTransferManager,
)
from vllm_omni.distributed.omni_connectors.utils.kv_utils import normalize_layer_kv
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.cache]
@@ -192,8 +193,7 @@ def test_normalize_layer_kv_rejects_invalid_inputs(kv_config, common_constants,
else:
layer_kv = (torch.randn(2, block_size, num_heads, head_dim), "not-a-tensor")
manager = OmniKVTransferManager(kv_config)
normalized = manager._normalize_layer_kv(layer_kv, req_id=req_id, layer_idx=0)
normalized = normalize_layer_kv(layer_kv, req_id=req_id, layer_idx=0)
assert normalized is None
@@ -13,7 +13,7 @@ stage_args:
model_arch: BagelForConditionalGeneration
worker_type: ar
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.35
gpu_memory_utilization: 0.45
enforce_eager: true
trust_remote_code: true
engine_output_type: text
@@ -46,7 +46,7 @@ stage_args:
max_batch_size: 1
engine_args:
model_stage: dit
gpu_memory_utilization: 0.55
gpu_memory_utilization: 0.45
enforce_eager: true
trust_remote_code: true
engine_output_type: image
@@ -13,7 +13,7 @@ stage_args:
model_arch: BagelForConditionalGeneration
worker_type: ar
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.35
gpu_memory_utilization: 0.45
enforce_eager: true
trust_remote_code: true
engine_output_type: text
@@ -45,7 +45,7 @@ stage_args:
max_batch_size: 1
engine_args:
model_stage: dit
gpu_memory_utilization: 0.55
gpu_memory_utilization: 0.45
enforce_eager: true
trust_remote_code: true
engine_output_type: image
@@ -27,7 +27,12 @@ def inference(model_name: str, offload: bool = True):
torch.cuda.reset_peak_memory_stats(device=device_index)
monitor = GPUMemoryMonitor(device_index=device_index, interval=0.02)
monitor.start()
m = Omni(model=model_name, enable_cpu_offload=offload)
m = Omni(
model=model_name,
# TODO: we might want to add overlapped feature e2e tests
# cache_backend="cache_dit",
enable_cpu_offload=offload,
)
torch.cuda.reset_peak_memory_stats(device=device_index)
height = 256
width = 256
@@ -38,6 +38,8 @@ def run_inference(
m = Omni(
model=model_name,
enable_layerwise_offload=layerwise_offload,
# TODO: we might want to add overlapped feature e2e tests
# cache_backend="cache_dit",
boundary_ratio=0.875,
flow_shift=5.0,
)
@@ -10,12 +10,29 @@ from tests.conftest import (
generate_synthetic_audio,
generate_synthetic_image,
generate_synthetic_video,
modify_stage_config,
)
from tests.utils import hardware_test
from vllm_omni.platforms import current_omni_platform
models = ["Qwen/Qwen2.5-Omni-7B"]
def get_cuda_graph_config():
path = modify_stage_config(
str(Path(__file__).parent.parent / "stage_configs" / "qwen2_5_omni_ci.yaml"),
updates={
"stage_args": {
0: {
"engine_args.enforce_eager": "true",
},
1: {"engine_args.enforce_eager": "true"},
},
},
)
return path
# CI stage config optimized for 24GB GPU (L4/RTX3090) or NPU
if current_omni_platform.is_npu():
stage_config = str(Path(__file__).parent / "stage_configs" / "npu" / "qwen2_5_omni_ci.yaml")
@@ -23,7 +40,7 @@ elif current_omni_platform.is_rocm():
# ROCm stage config optimized for MI325 GPU
stage_config = str(Path(__file__).parent.parent / "stage_configs" / "rocm" / "qwen2_5_omni_ci.yaml")
else:
stage_config = str(Path(__file__).parent.parent / "stage_configs" / "qwen2_5_omni_ci.yaml")
stage_config = get_cuda_graph_config()
# Create parameter combinations for model and stage config
test_params = [(model, stage_config) for model in models]
+18 -1
View File
@@ -13,18 +13,35 @@ import pytest
from tests.conftest import (
generate_synthetic_video,
modify_stage_config,
)
from tests.utils import hardware_test
from vllm_omni.platforms import current_omni_platform
models = ["Qwen/Qwen3-Omni-30B-A3B-Instruct"]
def get_cuda_graph_config():
path = modify_stage_config(
str(Path(__file__).parent.parent / "stage_configs" / "qwen3_omni_ci.yaml"),
updates={
"stage_args": {
0: {
"engine_args.enforce_eager": "true",
},
1: {"engine_args.enforce_eager": "true"},
},
},
)
return path
# CI stage config for 2xH100-80G GPUs or AMD GPU MI325
if current_omni_platform.is_rocm():
# ROCm stage config optimized for MI325 GPU
stage_configs = [str(Path(__file__).parent.parent / "stage_configs" / "rocm" / "qwen3_omni_ci.yaml")]
else:
stage_configs = [str(Path(__file__).parent.parent / "stage_configs" / "qwen3_omni_ci.yaml")]
stage_configs = [get_cuda_graph_config()]
# Create parameter combinations for model and stage config
test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
+3 -2
View File
@@ -114,7 +114,7 @@ def test_mix_to_text_audio_001(omni_server, openai_client) -> None:
"messages": messages,
"stream": True,
"key_words": {
"audio": ["water", "chirping", "crackling", "rain"],
"audio": ["test"],
"image": ["square", "quadrate"],
},
}
@@ -126,13 +126,14 @@ def test_mix_to_text_audio_001(omni_server, openai_client) -> None:
@pytest.mark.advanced_model
@pytest.mark.core_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_to_text_001(omni_server, openai_client) -> None:
"""
Test text input processing and text/audio output generation via OpenAI API.
Deploy Setting: default yaml
Input Modal: text
Output Modal: text + audio
Output Modal: text
Datasets: few requests
"""
messages = dummy_messages_from_mix_data(system_prompt=get_system_prompt(), content_text=get_prompt())
@@ -22,7 +22,7 @@ from tests.utils import hardware_test
models = ["Qwen/Qwen3-Omni-30B-A3B-Instruct"]
AUDIO_KEY = ["water", "chirping", "crackling", "rain"]
AUDIO_KEY = ["test"]
IMAGE_KEY = ["square", "quadrate"]
VIDEO_KEY = ["sphere", "globe", "circle", "round", "ball"]
@@ -90,12 +90,19 @@ def get_max_batch_size(size_type="few"):
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_to_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: text
Output Modal: audio
Input Setting: stream=True
Datasets: single request
"""
messages = dummy_messages_from_mix_data(system_prompt=get_system_prompt(), content_text=get_prompt())
request_config = {
"model": omni_server.model,
"messages": messages,
"modalities": ["audio"],
"stream": True,
"key_words": {"text": ["beijing"]},
}
@@ -107,6 +114,12 @@ def test_text_to_audio_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_to_text_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: text
Output Modal: text, audio
Input Setting: stream=False
Datasets: few requests
"""
messages = dummy_messages_from_mix_data(system_prompt=get_system_prompt(), content_text=get_prompt())
request_config = {
@@ -115,7 +128,7 @@ def test_text_to_text_audio_001(omni_server, openai_client) -> None:
"key_words": {"text": ["beijing"]},
}
openai_client.send_request(request_config)
openai_client.send_request(request_config, request_num=get_max_batch_size())
@pytest.mark.advanced_model
@@ -123,6 +136,12 @@ def test_text_to_text_audio_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_image_to_text_001(omni_server, openai_client) -> None:
"""
Input Modal: image
Output Modal: text
Input Setting: stream=True
Datasets: single request
"""
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
messages = dummy_messages_from_mix_data(image_data_url=image_data_url)
@@ -130,6 +149,7 @@ def test_image_to_text_001(omni_server, openai_client) -> None:
"model": omni_server.model,
"messages": messages,
"modalities": ["text"],
"stream": True,
"key_words": {"image": IMAGE_KEY},
}
@@ -141,6 +161,12 @@ def test_image_to_text_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_image_to_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: image
Output Modal: audio
Input Setting: stream=False
Datasets: single request
"""
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
messages = dummy_messages_from_mix_data(image_data_url=image_data_url)
@@ -159,6 +185,12 @@ def test_image_to_audio_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_image_to_text_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: image
Output Modal: text, audio
Input Setting: stream=False
Datasets: few requests
"""
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(1280, 720)['base64']}"
messages = dummy_messages_from_mix_data(image_data_url=image_data_url)
@@ -177,6 +209,12 @@ def test_image_to_text_audio_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_video_to_text_001(omni_server, openai_client) -> None:
"""
Input Modal: video
Output Modal: text
Input Setting: stream=False
Datasets: single request
"""
video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
messages = dummy_messages_from_mix_data(video_data_url=video_data_url)
@@ -195,6 +233,12 @@ def test_video_to_text_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_video_to_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: video
Output Modal: audio
Input Setting: stream=False
Datasets: single request
"""
video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
messages = dummy_messages_from_mix_data(video_data_url=video_data_url)
@@ -213,6 +257,12 @@ def test_video_to_audio_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_video_to_text_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: video
Output Modal: text, audio
Input Setting: stream=False
Datasets: few requests
"""
video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
messages = dummy_messages_from_mix_data(video_data_url=video_data_url)
@@ -231,6 +281,12 @@ def test_video_to_text_audio_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_audio_to_text_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: text, audio
Output Modal: text, audio
Input Setting: stream=False
Datasets: single request
"""
audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1)['base64']}"
messages = dummy_messages_from_mix_data(
audio_data_url=audio_data_url, system_prompt=get_system_prompt(), content_text=get_prompt("text_audio")
@@ -239,7 +295,6 @@ def test_text_audio_to_text_audio_001(omni_server, openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": messages,
"modalities": ["text"],
"key_words": {"audio": AUDIO_KEY},
}
@@ -251,6 +306,12 @@ def test_text_audio_to_text_audio_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_image_to_text_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: text, image
Output Modal: text, audio
Input Setting: stream=False
Datasets: single request
"""
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
messages = dummy_messages_from_mix_data(
@@ -260,19 +321,23 @@ def test_text_image_to_text_audio_001(omni_server, openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": messages,
"modalities": ["audio"],
"key_words": {"image": IMAGE_KEY},
}
openai_client.send_request(request_config)
@pytest.mark.skip(reason="There is a known issue with oom error.")
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_video_to_text_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: text, video
Output Modal: text, audio
Input Setting: stream=True
Datasets: single requests
"""
video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(1280, 720, 30)['base64']}"
messages = dummy_messages_from_mix_data(
@@ -282,10 +347,11 @@ def test_text_video_to_text_audio_001(omni_server, openai_client) -> None:
request_config = {
"model": omni_server.model,
"messages": messages,
"stream": True,
"key_words": {"video": VIDEO_KEY},
}
openai_client.send_request(request_config, request_num=get_max_batch_size())
openai_client.send_request(request_config)
@pytest.mark.skip(reason="There is a known issue with shape mismatch error.")
@@ -294,6 +360,12 @@ def test_text_video_to_text_audio_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_mix_to_text_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: text, audio, image, video
Output Modal: text, audio
Input Setting: stream=True
Datasets: few requests
"""
video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1)['base64']}"
@@ -0,0 +1,509 @@
# tests/entrypoints/openai_api/test_serving_audio_generate.py
import logging
from inspect import Signature, signature
from unittest.mock import MagicMock, patch
import pytest
import torch
from fastapi import FastAPI
from fastapi.testclient import TestClient
from vllm_omni.entrypoints.openai.protocol.audio import (
CreateAudio,
OpenAICreateAudioGenerateRequest,
)
from vllm_omni.entrypoints.openai.serving_audio_generate import (
OmniOpenAIServingAudioGenerate,
)
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
logger = logging.getLogger(__name__)
# Helper: create a mock audio output for endpoint tests
def create_mock_audio_output(
request_id: str = "audiogen-mock-123",
sample_rate: int = 44100,
num_samples: int = 44100,
audio_key: str = "audio",
) -> OmniRequestOutput:
"""Return an OmniRequestOutput mimicking diffusion audio model output."""
audio_tensor = torch.sin(torch.linspace(0, 440 * 2 * torch.pi, num_samples))
return OmniRequestOutput.from_diffusion(
request_id=request_id,
images=[],
prompt=None,
metrics={},
multimodal_output={audio_key: audio_tensor, "sr": sample_rate},
)
def _make_engine_client(*, audio_key: str = "audio", sample_rate: int = 44100):
"""Build a mock engine client producing audio output."""
mock_engine_client = MagicMock()
mock_engine_client.errored = False
mock_engine_client.model_type = "StableAudioPipeline"
mock_engine_client.default_sampling_params_list = [{}]
async def mock_generate_fn(*args, **kwargs):
yield create_mock_audio_output(
request_id=kwargs.get("request_id", "audiogen-mock"),
sample_rate=sample_rate,
audio_key=audio_key,
)
mock_engine_client.generate = MagicMock(side_effect=mock_generate_fn)
return mock_engine_client
def _make_server(engine_client=None):
"""Build an OmniOpenAIServingAudioGenerate with mocks."""
if engine_client is None:
engine_client = _make_engine_client()
mock_models = MagicMock()
mock_models.is_base_model.return_value = True
return OmniOpenAIServingAudioGenerate(
engine_client=engine_client,
models=mock_models,
request_logger=MagicMock(),
)
@pytest.fixture
def test_app():
server = _make_server()
original_fn = server.create_audio_generate
sig = signature(original_fn)
new_params = [p for name, p in sig.parameters.items() if name != "raw_request"]
new_sig = Signature(parameters=new_params, return_annotation=sig.return_annotation)
async def patched_create_audio_generate(*args, **kwargs):
return await original_fn(*args, **kwargs)
patched_create_audio_generate.__signature__ = new_sig
server.create_audio_generate = patched_create_audio_generate
app = FastAPI()
app.add_api_route(
"/v1/audio/generate",
server.create_audio_generate,
methods=["POST"],
response_model=None,
)
return app
@pytest.fixture
def client(test_app):
return TestClient(test_app)
# Request Validation (Pydantic model)
class TestRequestValidation:
"""Validate OpenAICreateAudioGenerateRequest pydantic constraints."""
def test_valid_minimal_request(self):
req = OpenAICreateAudioGenerateRequest(input="A calm piano melody")
assert req.input == "A calm piano melody"
assert req.response_format == "wav"
assert req.speed == 1.0
def test_fields_are_wired_correctly(self):
req = OpenAICreateAudioGenerateRequest(
input="rain sounds",
model="stable-audio",
response_format="flac",
speed=1.5,
audio_length=10.0,
audio_start=2.0,
negative_prompt="noise",
guidance_scale=7.5,
num_inference_steps=100,
seed=42,
)
assert req.input == "rain sounds"
assert req.model == "stable-audio"
assert req.response_format == "flac"
assert req.speed == 1.5
assert req.audio_length == 10.0
assert req.audio_start == 2.0
assert req.negative_prompt == "noise"
assert req.guidance_scale == 7.5
assert req.num_inference_steps == 100
assert req.seed == 42
def test_invalid_response_format(self):
with pytest.raises(Exception):
OpenAICreateAudioGenerateRequest(input="test", response_format="invalid_format")
def test_speed_lower_bound(self):
with pytest.raises(Exception):
OpenAICreateAudioGenerateRequest(input="test", speed=0.1)
def test_speed_upper_bound(self):
with pytest.raises(Exception):
OpenAICreateAudioGenerateRequest(input="test", speed=5.0)
def test_speed_at_boundaries(self):
req_low = OpenAICreateAudioGenerateRequest(input="test", speed=0.25)
assert req_low.speed == 0.25
req_high = OpenAICreateAudioGenerateRequest(input="test", speed=4.0)
assert req_high.speed == 4.0
def test_stream_format_sse_rejected(self):
with pytest.raises(Exception):
OpenAICreateAudioGenerateRequest(input="test", stream_format="sse")
def test_stream_format_audio_accepted(self):
req = OpenAICreateAudioGenerateRequest(input="test", stream_format="audio")
assert req.stream_format == "audio"
# Constructor & Class Methods
class TestConstructor:
def test_default_init(self):
server = _make_server()
assert server.diffusion_mode is False
def test_for_diffusion_factory(self):
engine_client = _make_engine_client()
mock_models = MagicMock()
mock_models.is_base_model.return_value = True
server = OmniOpenAIServingAudioGenerate.for_diffusion(
engine_client=engine_client,
models=mock_models,
request_logger=MagicMock(),
)
assert server.diffusion_mode is True
def test_is_stable_audio_model_true(self):
server = _make_server()
assert server._is_stable_audio_model() is True
def test_is_stable_audio_model_false(self):
engine = _make_engine_client()
engine.model_type = "SomeOtherModel"
server = _make_server(engine_client=engine)
assert server._is_stable_audio_model() is False
# Parameter Wiring — verify request params reach the engine
class TestParameterWiring:
"""Ensure request parameters are correctly forwarded to the engine."""
@pytest.fixture
def server_and_engine(self):
engine = _make_engine_client()
server = _make_server(engine_client=engine)
return server, engine
@pytest.mark.asyncio
async def test_prompt_wiring(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="birds chirping")
await server.create_audio_generate(req)
engine.generate.assert_called_once()
call_kwargs = engine.generate.call_args[1]
assert call_kwargs["prompt"]["prompt"] == "birds chirping"
assert call_kwargs["output_modalities"] == ["audio"]
@pytest.mark.asyncio
async def test_negative_prompt_wiring(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="a calm ocean", negative_prompt="noise distortion")
await server.create_audio_generate(req)
call_kwargs = engine.generate.call_args[1]
assert call_kwargs["prompt"]["negative_prompt"] == "noise distortion"
@pytest.mark.asyncio
async def test_negative_prompt_absent(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="a calm ocean")
await server.create_audio_generate(req)
call_kwargs = engine.generate.call_args[1]
assert "negative_prompt" not in call_kwargs["prompt"]
@pytest.mark.asyncio
async def test_guidance_scale_wiring(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="test", guidance_scale=12.0)
await server.create_audio_generate(req)
call_kwargs = engine.generate.call_args[1]
sp = call_kwargs["sampling_params_list"][0]
assert isinstance(sp, OmniDiffusionSamplingParams)
assert sp.guidance_scale == 12.0
@pytest.mark.asyncio
async def test_num_inference_steps_wiring(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="test", num_inference_steps=200)
await server.create_audio_generate(req)
sp = engine.generate.call_args[1]["sampling_params_list"][0]
assert sp.num_inference_steps == 200
@pytest.mark.asyncio
async def test_seed_creates_generator(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="test", seed=42)
with patch("vllm_omni.entrypoints.openai.serving_audio_generate.torch") as mock_torch:
mock_gen = MagicMock()
mock_gen.manual_seed.return_value = mock_gen
mock_torch.Generator.return_value = mock_gen
await server.create_audio_generate(req)
mock_torch.Generator.assert_called_once()
mock_gen.manual_seed.assert_called_once_with(42)
@pytest.mark.asyncio
async def test_seed_none_skips_generator(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="test")
await server.create_audio_generate(req)
sp = engine.generate.call_args[1]["sampling_params_list"][0]
assert sp.generator is None
@pytest.mark.asyncio
async def test_audio_length_wiring(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="test", audio_length=10.0, audio_start=2.0)
await server.create_audio_generate(req)
sp = engine.generate.call_args[1]["sampling_params_list"][0]
assert sp.extra_args["audio_start_in_s"] == 2.0
assert sp.extra_args["audio_end_in_s"] == 12.0 # start + length
@pytest.mark.asyncio
async def test_audio_length_default_start(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="test", audio_length=5.0)
await server.create_audio_generate(req)
sp = engine.generate.call_args[1]["sampling_params_list"][0]
assert sp.extra_args["audio_start_in_s"] == 0.0
assert sp.extra_args["audio_end_in_s"] == 5.0
@pytest.mark.asyncio
async def test_no_audio_length_skips_extra_args(self, server_and_engine):
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="test")
await server.create_audio_generate(req)
sp = engine.generate.call_args[1]["sampling_params_list"][0]
assert sp.extra_args == {}
@pytest.mark.asyncio
async def test_defaults_not_set_when_omitted(self, server_and_engine):
"""Guidance scale and num_inference_steps keep dataclass defaults when not in request."""
server, engine = server_and_engine
req = OpenAICreateAudioGenerateRequest(input="test")
await server.create_audio_generate(req)
sp = engine.generate.call_args[1]["sampling_params_list"][0]
defaults = OmniDiffusionSamplingParams()
assert sp.guidance_scale == defaults.guidance_scale
assert sp.num_inference_steps == defaults.num_inference_steps
# Audio Response Format
class TestAudioResponseFormat:
def test_wav_response(self, client):
payload = {"input": "a gentle rain", "response_format": "wav"}
response = client.post("/v1/audio/generate", json=payload)
assert response.status_code == 200
assert response.headers["content-type"] == "audio/wav"
assert len(response.content) > 0
def test_mp3_response(self, client):
payload = {"input": "a gentle rain", "response_format": "mp3"}
response = client.post("/v1/audio/generate", json=payload)
assert response.status_code == 200
assert response.headers["content-type"] == "audio/mpeg"
assert len(response.content) > 0
def test_flac_response(self, client):
payload = {"input": "a gentle rain", "response_format": "flac"}
response = client.post("/v1/audio/generate", json=payload)
assert response.status_code == 200
assert response.headers["content-type"] == "audio/flac"
assert len(response.content) > 0
def test_invalid_format_rejected(self, client):
payload = {"input": "test", "response_format": "banana"}
response = client.post("/v1/audio/generate", json=payload)
assert response.status_code == 422
@patch("vllm_omni.entrypoints.openai.serving_audio_generate.OmniOpenAIServingAudioGenerate.create_audio")
def test_speed_parameter_forwarded(self, mock_create_audio, test_app):
mock_audio_response = MagicMock()
mock_audio_response.audio_data = b"dummy_audio"
mock_audio_response.media_type = "audio/wav"
mock_create_audio.return_value = mock_audio_response
c = TestClient(test_app)
payload = {"input": "test", "response_format": "wav", "speed": 2.5}
c.post("/v1/audio/generate", json=payload)
mock_create_audio.assert_called_once()
audio_obj = mock_create_audio.call_args[0][0]
assert isinstance(audio_obj, CreateAudio)
assert audio_obj.speed == 2.5
@patch("vllm_omni.entrypoints.openai.serving_audio_generate.OmniOpenAIServingAudioGenerate.create_audio")
def test_sample_rate_from_output(self, mock_create_audio, test_app):
mock_audio_response = MagicMock()
mock_audio_response.audio_data = b"dummy"
mock_audio_response.media_type = "audio/wav"
mock_create_audio.return_value = mock_audio_response
c = TestClient(test_app)
payload = {"input": "test"}
c.post("/v1/audio/generate", json=payload)
audio_obj = mock_create_audio.call_args[0][0]
assert audio_obj.sample_rate == 44100 # Stable Audio default
# Error Handling
class TestErrorHandling:
@pytest.mark.asyncio
async def test_no_output_returns_error(self):
engine = _make_engine_client()
async def empty_gen(*args, **kwargs):
return
yield # noqa: unreachable – makes this an async generator
engine.generate = MagicMock(side_effect=empty_gen)
server = _make_server(engine_client=engine)
req = OpenAICreateAudioGenerateRequest(input="test")
resp = await server.create_audio_generate(req)
# create_error_response returns an ErrorResponse with .error.message
assert "No output generated" in resp.error.message
@pytest.mark.asyncio
async def test_no_audio_in_output_returns_error(self):
engine = _make_engine_client()
async def gen_without_audio(*args, **kwargs):
yield OmniRequestOutput.from_diffusion(
request_id="test",
images=[],
prompt=None,
metrics={},
multimodal_output={}, # no audio key
)
engine.generate = MagicMock(side_effect=gen_without_audio)
server = _make_server(engine_client=engine)
req = OpenAICreateAudioGenerateRequest(input="test")
resp = await server.create_audio_generate(req)
assert "did not produce audio" in resp.error.message
@pytest.mark.asyncio
async def test_engine_errored_raises(self):
engine = _make_engine_client()
engine.errored = True
engine.dead_error = RuntimeError("engine is dead")
server = _make_server(engine_client=engine)
req = OpenAICreateAudioGenerateRequest(input="test")
with pytest.raises(RuntimeError, match="engine is dead"):
await server.create_audio_generate(req)
@pytest.mark.asyncio
async def test_model_outputs_key_fallback(self):
"""Audio data under 'model_outputs' key should be accepted."""
engine = _make_engine_client(audio_key="model_outputs")
server = _make_server(engine_client=engine)
req = OpenAICreateAudioGenerateRequest(input="test")
resp = await server.create_audio_generate(req)
# Should succeed and return a Response with audio bytes
assert hasattr(resp, "body")
assert len(resp.body) > 0
@pytest.mark.asyncio
async def test_value_error_returns_error_response(self):
engine = _make_engine_client()
async def gen_value_error(*args, **kwargs):
raise ValueError("bad value")
yield # noqa: unreachable
engine.generate = MagicMock(side_effect=gen_value_error)
server = _make_server(engine_client=engine)
req = OpenAICreateAudioGenerateRequest(input="test")
resp = await server.create_audio_generate(req)
assert "bad value" in resp.error.message
@pytest.mark.asyncio
async def test_generic_exception_returns_error_response(self):
engine = _make_engine_client()
async def gen_runtime_error(*args, **kwargs):
raise RuntimeError("something went wrong")
yield # noqa: unreachable
engine.generate = MagicMock(side_effect=gen_runtime_error)
server = _make_server(engine_client=engine)
req = OpenAICreateAudioGenerateRequest(input="test")
resp = await server.create_audio_generate(req)
assert "Audio generation failed" in resp.error.message
# End-to-End via TestClient
class TestAudioGenerateAPI:
def test_basic_success(self, client):
payload = {"input": "ambient forest sounds"}
response = client.post("/v1/audio/generate", json=payload)
assert response.status_code == 200
assert len(response.content) > 0
def test_with_all_params(self, client):
payload = {
"input": "gentle piano",
"response_format": "wav",
"speed": 1.0,
"audio_length": 5.0,
"audio_start": 0.0,
"negative_prompt": "noise",
"guidance_scale": 7.0,
"num_inference_steps": 50,
"seed": 123,
}
response = client.post("/v1/audio/generate", json=payload)
assert response.status_code == 200
assert response.headers["content-type"] == "audio/wav"
def test_missing_input_rejected(self, client):
payload = {}
response = client.post("/v1/audio/generate", json=payload)
assert response.status_code == 422
def test_extra_unknown_fields_ignored(self, client):
payload = {"input": "test", "unknown_field": "value"}
response = client.post("/v1/audio/generate", json=payload)
# Pydantic v2 ignores extra fields by default
assert response.status_code == 200
+9 -9
View File
@@ -612,7 +612,7 @@ def test_initialize_stage_configs_called_when_none(
"""Test that stage configs are auto-loaded when stage_configs_path is None."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -687,7 +687,7 @@ def test_generate_raises_on_length_mismatch(monkeypatch: pytest.MonkeyPatch, moc
"""Test that generate raises ValueError when sampling_params_list length doesn't match."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -742,7 +742,7 @@ def test_generate_pipeline_and_final_outputs(monkeypatch: pytest.MonkeyPatch, mo
stage_cfg1["processed_input"] = ["processed-for-stage-1"]
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -848,7 +848,7 @@ def test_generate_pipeline_with_batch_input(monkeypatch: pytest.MonkeyPatch, moc
stage_cfg1["stage_id"] = 1
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -969,7 +969,7 @@ def test_generate_no_final_output_returns_empty(
stage_cfg1["final_output"] = False
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -1062,7 +1062,7 @@ def test_generate_sampling_params_none_use_default(
stage_cfg1["final_output"] = False
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -1140,7 +1140,7 @@ def test_wait_for_stages_ready_timeout(monkeypatch: pytest.MonkeyPatch, mocker:
"""Test that _wait_for_stages_ready handles timeout correctly."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -1202,7 +1202,7 @@ def test_generate_handles_error_messages(monkeypatch: pytest.MonkeyPatch, mocker
"""Test that generate handles error messages from stages correctly."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -1283,7 +1283,7 @@ def test_close_sends_shutdown_signal(monkeypatch: pytest.MonkeyPatch, mocker: Mo
"""Test that close() sends shutdown signal to all input queues."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
+8 -8
View File
@@ -577,7 +577,7 @@ def test_initialize_stage_configs_called_when_none(
"""Test that stage configs are auto-loaded when stage_configs_path is None."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -654,7 +654,7 @@ def test_generate_raises_on_length_mismatch(monkeypatch: pytest.MonkeyPatch, moc
"""Test that generate raises ValueError when sampling_params_list length doesn't match."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -711,7 +711,7 @@ def test_generate_pipeline_and_final_outputs(monkeypatch: pytest.MonkeyPatch, mo
stage_cfg1["processed_input"] = ["processed-for-stage-1"]
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -819,7 +819,7 @@ def test_generate_no_final_output_returns_empty(
stage_cfg1["final_output"] = False
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -911,7 +911,7 @@ def test_generate_sampling_params_none_use_default(
stage_cfg1["final_output"] = False
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -988,7 +988,7 @@ def test_wait_for_stages_ready_timeout(monkeypatch: pytest.MonkeyPatch, mocker:
"""Test that _wait_for_stages_ready handles timeout correctly."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -1052,7 +1052,7 @@ def test_generate_handles_error_messages(monkeypatch: pytest.MonkeyPatch, mocker
"""Test that generate handles error messages from stages correctly."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -1135,7 +1135,7 @@ def test_close_sends_shutdown_signal(monkeypatch: pytest.MonkeyPatch, mocker: Mo
"""Test that close() sends shutdown signal to all input queues."""
def _fake_loader(
model: str,
config_path: str,
stage_configs_path: str | None = None,
base_engine_args: dict | None = None,
default_stage_cfg_factory=None,
@@ -7,7 +7,10 @@ from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.stage_input_processors.qwen3_tts import talker2code2wav_async_chunk
from vllm_omni.model_executor.stage_input_processors.qwen3_tts import (
talker2code2wav,
talker2code2wav_async_chunk,
)
_FRAME = [1, 2, 3, 4] # 4-codebook frame
_Q = len(_FRAME) # num quantizers
@@ -29,6 +32,7 @@ def _tm(*, chunk_frames=25, left_context=25, initial_chunk=0):
return SimpleNamespace(
code_prompt_token_ids=defaultdict(list),
put_req_chunk=defaultdict(int),
request_payload={},
connector=SimpleNamespace(
config={
"extra": {
@@ -152,3 +156,113 @@ def test_per_request_override_wins_over_stage_config():
tm = _tm(initial_chunk=5)
payload = _call(tm, "r-override2", n_frames=10, put_req=0, req_ic=15)
assert payload is None
def test_first_streaming_chunk_prepends_ref_code_context():
tm = _tm(initial_chunk=10)
rid = "r-ref"
tm.code_prompt_token_ids[rid] = [_FRAME[:] for _ in range(10)]
ref_code = torch.tensor([[9, 9, 9, 9], [8, 8, 8, 8]], dtype=torch.long)
payload = talker2code2wav_async_chunk(
transfer_manager=tm,
pooling_output={"audio_codes": torch.zeros((0,)), "ref_code": ref_code},
request=_req(rid, finished=False, initial_codec_chunk_frames=10),
is_finished=False,
)
assert payload is not None
assert payload["left_context_size"] == 2
assert len(payload["code_predictor_codes"]) == _Q * 12
def test_ref_code_context_only_applies_to_first_streaming_chunk():
tm = _tm(initial_chunk=10)
rid = "r-ref2"
tm.code_prompt_token_ids[rid] = [_FRAME[:] for _ in range(20)]
tm.put_req_chunk[rid] = 1
ref_code = torch.tensor([[9, 9, 9, 9], [8, 8, 8, 8]], dtype=torch.long)
payload = talker2code2wav_async_chunk(
transfer_manager=tm,
pooling_output={"audio_codes": torch.zeros((0,)), "ref_code": ref_code},
request=_req(rid, finished=False, initial_codec_chunk_frames=10),
is_finished=False,
)
assert payload is not None
assert payload["left_context_size"] == 10
assert len(payload["code_predictor_codes"]) == _Q * 20
def test_ref_code_context_can_be_buffered_before_first_emit():
tm = _tm(initial_chunk=10)
rid = "r-ref-buffered"
ref_code = torch.tensor([[9, 9, 9, 9], [8, 8, 8, 8]], dtype=torch.long)
first_payload = talker2code2wav_async_chunk(
transfer_manager=tm,
pooling_output={"audio_codes": torch.tensor([[1, 2, 3, 4]]), "ref_code": ref_code},
request=_req(rid, finished=False, initial_codec_chunk_frames=10),
is_finished=False,
)
assert first_payload is None
assert rid in tm.request_payload
for _ in range(8):
talker2code2wav_async_chunk(
transfer_manager=tm,
pooling_output={"audio_codes": torch.tensor([[1, 2, 3, 4]])},
request=_req(rid, finished=False, initial_codec_chunk_frames=10),
is_finished=False,
)
payload = talker2code2wav_async_chunk(
transfer_manager=tm,
pooling_output={"audio_codes": torch.tensor([[1, 2, 3, 4]])},
request=_req(rid, finished=False, initial_codec_chunk_frames=10),
is_finished=False,
)
assert payload is not None
assert payload["left_context_size"] == 2
assert len(payload["code_predictor_codes"]) == _Q * 12
assert rid not in tm.request_payload
def test_non_async_processor_prepends_ref_code_and_sets_trim_context():
ref_code = torch.tensor([[9, 9, 9, 9], [8, 8, 8, 8]], dtype=torch.long)
audio_codes = torch.tensor(
[
[0, 0, 0, 0],
[1, 2, 3, 4],
[5, 6, 7, 8],
],
dtype=torch.long,
)
output = SimpleNamespace(multimodal_output={"audio_codes": audio_codes, "ref_code": ref_code})
stage = SimpleNamespace(engine_outputs=[SimpleNamespace(outputs=[output])])
prompts = talker2code2wav(stage_list=[stage], engine_input_source=[0])
assert len(prompts) == 1
prompt = prompts[0]
assert prompt["additional_information"] == {"left_context_size": [2]}
assert prompt["prompt_token_ids"] == [
9,
8,
1,
5,
9,
8,
2,
6,
9,
8,
3,
7,
9,
8,
4,
8,
]
+8 -6
View File
@@ -107,7 +107,7 @@ class ModelHook:
The output of the forward pass.
"""
args, kwargs = self.pre_forward(module, *args, **kwargs)
output = module._original_forward(*args, **kwargs) # type: ignore[attr-defined]
output = module._omni_original_forward(*args, **kwargs) # type: ignore[attr-defined]
return self.post_forward(module, output)
def reset_state(self, module: nn.Module) -> nn.Module:
@@ -131,7 +131,7 @@ class _WrappedForward:
def __call__(self, *args: Any, **kwargs: Any):
registry: HookRegistry | None = getattr(self.module, "_hook_registry", None)
if registry is None or not registry._hooks:
return self.module._original_forward(*args, **kwargs)
return self.module._omni_original_forward(*args, **kwargs)
return registry.dispatch(*args, **kwargs)
@@ -162,8 +162,10 @@ class HookRegistry:
setattr(module, "_hook_registry", registry)
# Wrap module.forward once so hooks can intercept calls.
if not hasattr(module, "_original_forward"):
module._original_forward = module.forward # type: ignore[attr-defined]
# NOTE: Use `_omni_original_forward` to avoid collision with cache-dit's
# `_original_forward` attribute on the same module.
if not hasattr(module, "_omni_original_forward"):
module._omni_original_forward = module.forward # type: ignore[attr-defined]
module.forward = _WrappedForward(module) # type: ignore[assignment]
return registry
@@ -212,7 +214,7 @@ class HookRegistry:
The output of the forward pass.
"""
if not self._hooks:
return self.module._original_forward(*args, **kwargs) # type: ignore[attr-defined]
return self.module._omni_original_forward(*args, **kwargs) # type: ignore[attr-defined]
# For single hook case, call directly
if len(self._hooks) == 1:
@@ -228,7 +230,7 @@ class HookRegistry:
args, kwargs = hook.pre_forward(self.module, *args, **kwargs)
# Call original forward
output = self.module._original_forward(*args, **kwargs) # type: ignore[attr-defined]
output = self.module._omni_original_forward(*args, **kwargs) # type: ignore[attr-defined]
# Apply all post_forward hooks in reverse order
for _, hook in reversed(sorted_hooks):
@@ -332,6 +332,9 @@ class BagelPipeline(nn.Module):
else:
gen_context["ropes"] = [seq_len]
if req.sampling_params.kv_metadata and "image_shape" in req.sampling_params.kv_metadata:
image_shape = tuple(req.sampling_params.kv_metadata["image_shape"])
cfg_text_kv = getattr(req.sampling_params, "cfg_text_past_key_values", None)
if cfg_text_kv is not None:
logger.info("CFG enabled with multi-KV: using injected cfg_text KV Cache")
@@ -356,7 +356,6 @@ class StableAudioPipeline(nn.Module, SupportAudioOutput):
negative_prompt: str | list[str] | None = None,
audio_end_in_s: float | None = None,
audio_start_in_s: float = 0.0,
num_inference_steps: int = 100,
guidance_scale: float = 7.0,
num_waveforms_per_prompt: int = 1,
generator: torch.Generator | list[torch.Generator] | None = None,
@@ -374,7 +373,6 @@ class StableAudioPipeline(nn.Module, SupportAudioOutput):
negative_prompt: Negative prompt for CFG
audio_end_in_s: Audio end time in seconds (max ~47s for stable-audio-open-1.0)
audio_start_in_s: Audio start time in seconds
num_inference_steps: Number of denoising steps
guidance_scale: CFG scale
num_waveforms_per_prompt: Number of audio outputs per prompt
generator: Random generator for reproducibility
@@ -395,7 +393,7 @@ class StableAudioPipeline(nn.Module, SupportAudioOutput):
elif req.prompts:
negative_prompt = ["" if isinstance(p, str) else (p.get("negative_prompt") or "") for p in req.prompts]
num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps
num_inference_steps = req.sampling_params.num_inference_steps
if req.sampling_params.guidance_scale_provided:
guidance_scale = req.sampling_params.guidance_scale
@@ -50,6 +50,9 @@ class LayerwiseOffloadHook(ModelHook):
# Per-block synchronization primitive: set after H2D copy completes.
self._prefetch_done: current_omni_platform.Event | None = None
# Backward link to the hook that is responsible for prefetching *this* block's weights
self._prev_hook: LayerwiseOffloadHook | None = None
self.next_block_parameters: dict[str, nn.Parameter] = {}
self.next_block_buffers: dict[str, torch.Tensor] = {}
self.dtype_cpu_flattened_weights: dict[torch.dtype, torch.Tensor] = {}
@@ -128,6 +131,14 @@ class LayerwiseOffloadHook(ModelHook):
return dtype_cpu_flattened_weights, dtype_metadata
@property
def is_materialized(self) -> bool:
"""Check whether this block's parameters hold real data on device."""
for param in self.block_parameters.values():
return param.data.dim() > 0
return True
@torch.compiler.disable
def prefetch_layer(self, non_blocking: bool = True) -> None:
"""Copy layer weights from CPU -> GPU.
@@ -185,6 +196,12 @@ class LayerwiseOffloadHook(ModelHook):
buf.data = torch.empty((), device=self.device, dtype=buf.dtype)
def pre_forward(self, module: nn.Module, *args: Any, **kwargs: Any) -> tuple[tuple, dict]:
# if the previous hook was skipped and the weights are not on device,
# (e.g. by cache-dit block caching), ask the previous hook to
# synchronously prefetch *this* block's weights before computation
if not self.is_materialized and self._prev_hook is not None:
self._prev_hook.prefetch_layer(non_blocking=False)
self.prefetch_layer(non_blocking=True)
return args, kwargs
@@ -293,13 +310,23 @@ class LayerWiseOffloadBackend(OffloadBackend):
# For subsequent requests, the first layer/block will be pre-fetched
# during the last layer compute of the previous request.
last_block, first_block = blocks[-1], blocks[0]
hook = apply_block_hook(last_block, first_block, self.device, self.copy_stream, self.config.pin_cpu_memory)
hook.prefetch_layer(non_blocking=False)
last_hook = apply_block_hook(
last_block, first_block, self.device, self.copy_stream, self.config.pin_cpu_memory
)
last_hook.prefetch_layer(non_blocking=False)
block_hooks: list[LayerwiseOffloadHook] = [last_hook]
# Register hook for each of blocks
for i, block in enumerate(blocks[:-1]):
next_block = blocks[(i + 1) % num_blocks]
apply_block_hook(block, next_block, self.device, self.copy_stream, self.config.pin_cpu_memory)
hook = apply_block_hook(block, next_block, self.device, self.copy_stream, self.config.pin_cpu_memory)
block_hooks.append(hook)
# NOTE(yuanheng-zhao): We make each hook gets a backward reference to the hook
# that is responsible for prefetching its block's weights. This is specifically a
# workaround for that arbitrary blocks are skipped by caching systems (e.g., cache-dit)
for i in range(len(block_hooks)):
block_hooks[i]._prev_hook = block_hooks[i - 1]
logger.info(f"Layer-wise offloading enabled on {num_blocks} layers (blocks)")
@@ -35,6 +35,23 @@ class SequentialOffloadHook(ModelHook):
self.device = device
self.pin_memory = pin_memory
@staticmethod
def _move_params(module: nn.Module, device: torch.device) -> None:
"""Move module parameters and buffers to device.
This cls method specifically prevents recursion device movement,
E.g., Cache-DiT CachedBlocks has attr `transformer` as a ref to original
transformer blocks, thus `module.to(device)` will fail for recursion calling,
refer to
https://github.com/vipshop/cache-dit/blob/v1.2.3/src/cache_dit/caching/cache_blocks/__init__.py#L83
"""
for p in module.parameters():
if p.data.device != device:
p.data = p.data.to(device, non_blocking=True)
for b in module.buffers():
if b.device != device:
b.data = b.data.to(device, non_blocking=True)
def _to_cpu(self, module: nn.Module) -> None:
"""Move module to CPU."""
try:
@@ -42,13 +59,12 @@ class SequentialOffloadHook(ModelHook):
except StopIteration:
return
previous_device = param.device
# Skip if already on CPU
if previous_device.type == "cpu":
if param.device.type == "cpu":
return
module.to("cpu")
torch.cuda.empty_cache()
self._move_params(module, torch.device("cpu"))
current_omni_platform.empty_cache()
if self.pin_memory:
for p in module.parameters():
@@ -64,7 +80,7 @@ class SequentialOffloadHook(ModelHook):
except StopIteration:
return
module.to(self.device)
self._move_params(module, self.device)
def pre_forward(self, module: nn.Module, *args, **kwargs) -> tuple[tuple, dict]:
# Offload target modules to CPU
@@ -186,19 +202,6 @@ class ModelLevelOffloadBackend(OffloadBackend):
except Exception as exc:
logger.debug("Failed to move VAE to GPU: %s", exc)
# Initial state: keep DiT modules on CPU (encoders typically run first)
# TODO: This part seems to be unnecessary, remove it after testing
# for dit_mod in modules.dits:
# dit_mod.to("cpu")
# torch.cuda.empty_cache()
# if self.config.pin_cpu_memory:
# for dit_mod in modules.dits:
# for p in dit_mod.parameters():
# if p.data.device.type == "cpu" and not p.data.is_pinned():
# p.data = p.data.pin_memory()
# Apply sequential offloading hooks
apply_sequential_offload(
dit_modules=modules.dits,
@@ -12,6 +12,7 @@ from vllm.logger import init_logger
from .factory import OmniConnectorFactory
from .utils.config import ConnectorSpec
from .utils.kv_utils import normalize_layer_kv
logger = init_logger(__name__)
@@ -200,8 +201,12 @@ class OmniKVTransferManager:
logger.warning(f"Request {req_id} has no block IDs, skipping")
continue
custom_metadata = data.get("custom_metadata")
# Extract KV cache from GPU blocks -> CPU tensors
kv_data = self._extract_kv_cache(req_id, block_ids, seq_len, kv_caches, block_size, cache_dtype)
kv_data = self._extract_kv_cache(
req_id, block_ids, seq_len, kv_caches, block_size, cache_dtype, custom_metadata
)
if kv_data:
# Resolve global request ID if available
transfer_req_id = request_id_resolver(req_id) if request_id_resolver else req_id
@@ -224,6 +229,7 @@ class OmniKVTransferManager:
kv_caches: list[LayerKV],
block_size: int,
cache_dtype: str,
custom_metadata: dict[str, Any] | None = None,
) -> KVCacheTransferData | None:
"""Extract KV cache from GPU blocks for a single request.
@@ -234,6 +240,7 @@ class OmniKVTransferManager:
kv_caches: List of KV cache (tensor or tuple) per layer
block_size: Size of each cache block
cache_dtype: Data type of the cache
custom_metadata: Optional custom metadata to include
Note: If key/value block counts differ, extraction uses only the overlapping
block range. Extra key/value blocks are ignored, so returned KV may be partial.
@@ -246,7 +253,7 @@ class OmniKVTransferManager:
value_cache: list[torch.Tensor | None] = [None] * num_layers
for layer_idx, layer_kv in enumerate(kv_caches):
kv_pair = self._normalize_layer_kv(layer_kv, req_id=req_id, layer_idx=layer_idx)
kv_pair = normalize_layer_kv(layer_kv, req_id=req_id, layer_idx=layer_idx)
if kv_pair is None:
continue
key_blocks, value_blocks = kv_pair
@@ -289,57 +296,10 @@ class OmniKVTransferManager:
"num_layers": num_layers,
"dtype": str(cache_dtype),
"seq_len": seq_len,
**(custom_metadata or {}),
},
)
def _normalize_layer_kv(
self,
layer_kv: LayerKV,
req_id: str,
layer_idx: int,
) -> tuple[torch.Tensor, torch.Tensor] | None:
"""Normalize one layer KV cache to a `(key_blocks, value_blocks)` tuple.
Args:
layer_kv: The raw KV cache (tensor or tuple) for the layer
req_id: Request ID for logging
layer_idx: Layer index for logging
Returns:
Tuple of (key_blocks, value_blocks) if valid, None otherwise
"""
if isinstance(layer_kv, torch.Tensor):
if layer_kv.ndim < 3 or layer_kv.shape[0] != 2:
logger.warning(
f"Layer {layer_idx} for request {req_id} has invalid stacked KV shape: "
f"expected [2, blocks, block_size, ...], got {tuple(layer_kv.shape)}"
)
return None
key_blocks = layer_kv[0]
value_blocks = layer_kv[1]
elif isinstance(layer_kv, tuple):
if len(layer_kv) != 2:
logger.warning(
f"Layer {layer_idx} for request {req_id} has KV pair length {len(layer_kv)} (expected 2)"
)
return None
key_blocks, value_blocks = layer_kv
if not isinstance(key_blocks, torch.Tensor) or not isinstance(value_blocks, torch.Tensor):
logger.warning(f"Layer {layer_idx} for request {req_id} has non-tensor KV pair entries")
return None
else:
logger.warning(f"Layer {layer_idx} for request {req_id} has unsupported KV type {type(layer_kv).__name__}")
return None
# ensure key/value blocks are at least 2D for block indexing
if key_blocks.ndim < 2 or value_blocks.ndim < 2:
logger.warning(
f"Layer {layer_idx} for request {req_id} has invalid KV block shape: "
f"got key={tuple(key_blocks.shape)} value={tuple(value_blocks.shape)}"
)
return None
return key_blocks, value_blocks
def _transfer_kv_cache(self, kv_data: KVCacheTransferData, transfer_req_id: str) -> None:
"""Transfer KV cache data to downstream stage via OmniConnector.
@@ -496,6 +456,8 @@ class OmniKVTransferManager:
if "metadata" in data:
req.kv_metadata = data["metadata"]
if hasattr(req, "sampling_params") and req.sampling_params is not None:
req.sampling_params.kv_metadata = data["metadata"]
# Legacy compatibility method
def receive_kv_cache(self, req: Any, target_device: torch.device | None = None) -> bool:
@@ -0,0 +1,85 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Utility helpers for KV cache manipulation."""
import torch
from vllm.logger import init_logger
logger = init_logger(__name__)
LayerKV = torch.Tensor | tuple[torch.Tensor, torch.Tensor]
def normalize_layer_kv(
layer_kv: LayerKV,
*,
req_id: str = "",
layer_idx: int = -1,
) -> tuple[torch.Tensor, torch.Tensor] | None:
"""Normalize one layer KV cache to a ``(key_blocks, value_blocks)`` tuple.
In vLLM, different attention backends return paged-attention KV blocks
with different layouts. For example:
* **FlashAttention** (``vllm/v1/attention/backends/flash_attn.py``)
returns shape ``(2, num_blocks, block_size, num_kv_heads, head_size)``
– the key/value dimension is at **dim 0**.
* **FlashInfer** (``vllm/v1/attention/backends/flashinfer.py``)
returns shape ``(num_blocks, 2, block_size, num_kv_heads, head_size)``
– the key/value dimension is at **dim 1**.
This utility handles both layouts (and the tuple case) so that
downstream code can work with any backend.
Supported layouts:
* **Stacked tensor** ``[2, num_blocks, block_size, n_heads, head_dim]`` –
dim-0 selects key / value.
* **Stacked tensor** ``[num_blocks, 2, block_size, n_heads, head_dim]`` –
dim-1 selects key / value.
* **Tuple** ``(key_tensor, value_tensor)`` – returned as-is after
validation.
Args:
layer_kv: The raw KV cache (tensor or tuple) for the layer.
req_id: Request ID used only for diagnostic log messages.
layer_idx: Layer index used only for diagnostic log messages.
Returns:
``(key_blocks, value_blocks)`` if *layer_kv* is valid, ``None``
otherwise.
"""
if isinstance(layer_kv, torch.Tensor):
if layer_kv.ndim >= 3 and layer_kv.shape[0] == 2:
key_blocks = layer_kv[0]
value_blocks = layer_kv[1]
elif layer_kv.ndim >= 3 and layer_kv.shape[1] == 2:
key_blocks = layer_kv[:, 0]
value_blocks = layer_kv[:, 1]
else:
logger.warning(
f"Layer {layer_idx} for request {req_id} has invalid stacked KV shape: "
f"expected [2, ...] or [..., 2, ...] at dim 0/1, got {tuple(layer_kv.shape)}"
)
return None
elif isinstance(layer_kv, tuple):
if len(layer_kv) != 2:
logger.warning(f"Layer {layer_idx} for request {req_id} has KV pair length {len(layer_kv)} (expected 2)")
return None
key_blocks, value_blocks = layer_kv
if not isinstance(key_blocks, torch.Tensor) or not isinstance(value_blocks, torch.Tensor):
logger.warning(f"Layer {layer_idx} for request {req_id} has non-tensor KV pair entries")
return None
else:
logger.warning(f"Layer {layer_idx} for request {req_id} has unsupported KV type {type(layer_kv).__name__}")
return None
# ensure key/value blocks are at least 2D for block indexing
if key_blocks.ndim < 2 or value_blocks.ndim < 2:
logger.warning(
f"Layer {layer_idx} for request {req_id} has invalid KV block shape: "
f"got key={tuple(key_blocks.shape)} value={tuple(value_blocks.shape)}"
)
return None
return key_blocks, value_blocks
+4 -2
View File
@@ -28,6 +28,7 @@ from vllm_omni.entrypoints.utils import (
load_stage_configs_from_model,
load_stage_configs_from_yaml,
resolve_model_config_path,
resolve_model_type,
)
logger = init_logger(__name__)
@@ -84,12 +85,13 @@ class OmniLLM(LLM):
self.worker_backend = kwargs.get("worker_backend", "multi_process")
self.ray_address = kwargs.get("ray_address", None)
self.batch_timeout = batch_timeout
self.model_type = resolve_model_type(model)
self.log_stats: bool = bool(log_stats)
# Load stage configurations
if stage_configs_path is None:
self.config_path = resolve_model_config_path(model)
self.stage_configs = load_stage_configs_from_model(model)
self.config_path = resolve_model_config_path(self.model_type)
self.stage_configs = load_stage_configs_from_model(config_path=self.config_path)
else:
self.config_path = stage_configs_path
self.stage_configs = load_stage_configs_from_yaml(stage_configs_path)
+74 -4
View File
@@ -87,7 +87,10 @@ from vllm_omni.entrypoints.openai.image_api_utils import (
encode_image_base64,
parse_size,
)
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
from vllm_omni.entrypoints.openai.protocol.audio import (
OpenAICreateAudioGenerateRequest,
OpenAICreateSpeechRequest,
)
from vllm_omni.entrypoints.openai.protocol.images import (
ImageData,
ImageGenerationRequest,
@@ -97,6 +100,7 @@ from vllm_omni.entrypoints.openai.protocol.videos import (
VideoGenerationRequest,
VideoGenerationResponse,
)
from vllm_omni.entrypoints.openai.serving_audio_generate import OmniOpenAIServingAudioGenerate
from vllm_omni.entrypoints.openai.serving_chat import OmniOpenAIServingChat
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
from vllm_omni.entrypoints.openai.serving_video import OmniOpenAIServingVideo
@@ -175,8 +179,29 @@ class _DiffusionServingModels:
provide a lightweight fallback.
"""
class _NullModelConfig:
def __getattr__(self, name):
return None
class _Unsupported:
def __init__(self, name: str):
self.name = name
def __call__(self, *args, **kwargs):
raise NotImplementedError(f"{self.name} is not supported in diffusion mode")
def __getattr__(self, attr):
raise NotImplementedError(f"{self.name}.{attr} is not supported in diffusion mode")
def __init__(self, base_model_paths: list[BaseModelPath]) -> None:
self._base_model_paths = base_model_paths
self.model_config = self._NullModelConfig()
def __getattr__(self, name):
"""Return a sentinel that raises NotImplementedError if called or
accessed, so any use of unsupported OpenAIServingModels features in
diffusion mode fails loudly with a descriptive message."""
return self._Unsupported(name)
async def show_available_models(self) -> ModelList:
return ModelList(
@@ -434,10 +459,10 @@ async def omni_init_app_state(
# For omni models
state.stage_configs = engine_client.stage_configs if hasattr(engine_client, "stage_configs") else None
model_name = served_model_names[0] if served_model_names else args.model
# Pure Diffusion mode: use simplified initialization logic
if is_pure_diffusion:
model_name = served_model_names[0] if served_model_names else args.model
state.vllm_config = None
state.diffusion_engine = engine_client
state.openai_serving_models = _DiffusionServingModels(base_model_paths)
@@ -450,6 +475,16 @@ async def omni_init_app_state(
model_name=model_name,
)
# audio related
state.openai_serving_speech = None
state.openai_serving_audio_generate = OmniOpenAIServingAudioGenerate.for_diffusion(
engine_client,
state.openai_serving_models,
request_logger=request_logger,
model_name=model_name,
)
# video related
diffusion_stage_configs = engine_client.stage_configs if hasattr(engine_client, "stage_configs") else None
state.openai_serving_video = OmniOpenAIServingVideo.for_diffusion(
diffusion_engine=engine_client, # type: ignore
@@ -457,7 +492,6 @@ async def omni_init_app_state(
stage_configs=diffusion_stage_configs,
)
state.enable_server_load_tracking = getattr(args, "enable_server_load_tracking", False)
state.server_load_metrics = 0
logger.info("Pure diffusion API server initialized for model: %s", model_name)
return
@@ -732,7 +766,11 @@ async def omni_init_app_state(
)
state.openai_serving_speech = OmniOpenAIServingSpeech(
engine_client, state.openai_serving_models, request_logger=request_logger
engine_client, state.openai_serving_models, request_logger=request_logger, model_name=model_name
)
state.openai_serving_audio_generate = OmniOpenAIServingAudioGenerate(
engine_client, state.openai_serving_models, request_logger=request_logger, model_name=model_name
)
state.openai_serving_video = OmniOpenAIServingVideo(
@@ -757,6 +795,10 @@ def Omnispeech(request: Request) -> OmniOpenAIServingSpeech | None:
return request.app.state.openai_serving_speech
def OmniAudioGenerate(request: Request) -> OmniOpenAIServingAudioGenerate | None:
return getattr(request.app.state, "openai_serving_audio_generate", None)
@router.post(
"/v1/chat/completions",
dependencies=[Depends(validate_json_request)],
@@ -867,6 +909,34 @@ async def create_speech(request: OpenAICreateSpeechRequest, raw_request: Request
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value, detail=str(e)) from e
@router.post(
"/v1/audio/generate",
dependencies=[Depends(validate_json_request)],
responses={
HTTPStatus.OK.value: {"content": {"audio/*": {}}},
HTTPStatus.BAD_REQUEST.value: {"model": ErrorResponse},
HTTPStatus.NOT_FOUND.value: {"model": ErrorResponse},
HTTPStatus.INTERNAL_SERVER_ERROR.value: {"model": ErrorResponse},
},
)
@with_cancellation
@load_aware_call
async def create_audio_generate(request: OpenAICreateAudioGenerateRequest, raw_request: Request):
handler = OmniAudioGenerate(raw_request)
if handler is None:
base_server = getattr(raw_request.app.state, "openai_serving_tokenization", None)
if base_server is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND.value,
detail="The model does not support Audio Generate API",
)
return base_server.create_error_response(message="The model does not support Audio Generate API")
try:
return await handler.create_audio_generate(request, raw_request)
except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value, detail=str(e)) from e
@router.get(
"/v1/audio/voices",
responses={
@@ -45,6 +45,10 @@ class AudioMixin:
"Only mono (1D) and stereo (2D) are supported."
)
if audio_tensor.ndim == 2 and audio_tensor.shape[0] == 2:
# Convert from [channels, samples] to [samples, channels]
audio_tensor = audio_tensor.T
audio_tensor, sample_rate = self._apply_speed_adjustment(audio_tensor, speed, sample_rate)
supported_formats = {
@@ -85,6 +85,53 @@ class OpenAICreateSpeechRequest(BaseModel):
return self
class OpenAICreateAudioGenerateRequest(BaseModel):
"""Request model for audio generation via diffusion models (e.g. Stable Audio)."""
input: str = Field(
description="Text prompt describing the audio to generate",
)
model: str | None = None
response_format: Literal["wav", "pcm", "flac", "mp3", "aac", "opus"] = "wav"
speed: float | None = Field(
default=1.0,
ge=0.25,
le=4.0,
)
stream_format: Literal["sse", "audio"] | None = "audio"
audio_length: float | None = Field(
default=None,
description="Audio length in seconds",
)
audio_start: float | None = Field(
default=0.0,
description="Audio start time in seconds",
)
negative_prompt: str | None = Field(
default=None,
description="Negative prompt for classifier-free guidance",
)
guidance_scale: float | None = Field(
default=None,
description="Guidance scale for diffusion models",
)
num_inference_steps: int | None = Field(
default=None,
description="Number of inference steps",
)
seed: int | None = Field(
default=None,
description="Random seed for reproducibility",
)
@field_validator("stream_format")
@classmethod
def validate_stream_format(cls, v: str) -> str:
if v == "sse":
raise ValueError("'sse' is not a supported stream_format yet. Please use 'audio'.")
return v
class CreateAudio(BaseModel):
audio_tensor: np.ndarray
sample_rate: int = 24000
@@ -0,0 +1,168 @@
import asyncio
import torch
from fastapi import Request
from fastapi.responses import Response
from vllm.entrypoints.openai.engine.serving import OpenAIServing
from vllm.logger import init_logger
from vllm.utils import random_uuid
from vllm_omni.entrypoints.openai.audio_utils_mixin import AudioMixin
from vllm_omni.entrypoints.openai.protocol.audio import (
AudioResponse,
CreateAudio,
OpenAICreateAudioGenerateRequest,
)
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
logger = init_logger(__name__)
class OmniOpenAIServingAudioGenerate(OpenAIServing, AudioMixin):
"""Serving class for audio generation via diffusion models (e.g. Stable Audio)."""
def __init__(self, *args, **kwargs):
self.model_name = kwargs.pop("model_name", None)
super().__init__(*args, **kwargs)
self.diffusion_mode = False
@classmethod
def for_diffusion(cls, *args, **kwargs) -> "OmniOpenAIServingAudioGenerate":
"""Create an instance configured to run in diffusion mode."""
instance = cls(*args, **kwargs)
instance.diffusion_mode = True
return instance
def _is_stable_audio_model(self) -> bool:
return self.engine_client.model_type == "StableAudioPipeline"
async def create_audio_generate(
self,
request: OpenAICreateAudioGenerateRequest,
raw_request: Request | None = None,
):
"""
Generate audio using diffusion-based models (e.g. Stable Audio).
This endpoint is designed for audio generation models as
opposed to TTS models that specifically generate speech.
"""
error_check_ret = await self._check_model(request)
if error_check_ret is not None:
logger.error("Error with model %s", error_check_ret)
return error_check_ret
if self.engine_client.errored:
raise self.engine_client.dead_error
request_id = f"audiogen-{random_uuid()}"
try:
default_sr = 44100 # Default sample rate for Stable Audio
# Build prompt for diffusion audio generation
prompt = {
"prompt": request.input,
}
if request.negative_prompt:
prompt["negative_prompt"] = request.negative_prompt
# Build sampling params for diffusion
sampling_params_list = [OmniDiffusionSamplingParams(num_outputs_per_prompt=1)]
# Create generator if seed provided
if request.seed is not None:
from vllm_omni.platforms import current_omni_platform
generator = torch.Generator(device=current_omni_platform.device_type).manual_seed(request.seed)
sampling_params_list[0].generator = generator
if request.guidance_scale is not None:
sampling_params_list[0].guidance_scale = request.guidance_scale
if request.num_inference_steps is not None:
sampling_params_list[0].num_inference_steps = request.num_inference_steps
# Set up audio duration parameters
if request.audio_length is not None:
audio_length = request.audio_length
audio_start = request.audio_start if request.audio_start is not None else 0.0
audio_end_in_s = audio_start + audio_length
sampling_params_list[0].extra_args = {
"audio_start_in_s": audio_start,
"audio_end_in_s": audio_end_in_s,
}
logger.info(
"Audio generation request %s: prompt=%r",
request_id,
request.input[:50] + "..." if len(request.input) > 50 else request.input,
)
generator = self.engine_client.generate(
prompt=prompt,
request_id=request_id,
sampling_params_list=sampling_params_list,
output_modalities=["audio"],
)
final_output: OmniRequestOutput | None = None
async for res in generator:
final_output = res
if final_output is None:
return self.create_error_response("No output generated from the model.")
# Extract audio from output
audio_output = None
if hasattr(final_output, "multimodal_output") and final_output.multimodal_output:
audio_output = final_output.multimodal_output
if not audio_output and hasattr(final_output, "request_output"):
if final_output.request_output and hasattr(final_output.request_output, "multimodal_output"):
audio_output = final_output.request_output.multimodal_output
# Check for audio data using either "audio" or "model_outputs" key
audio_key = None
if audio_output:
if "audio" in audio_output:
audio_key = "audio"
elif "model_outputs" in audio_output:
audio_key = "model_outputs"
if not audio_output or audio_key is None:
return self.create_error_response("Audio generation model did not produce audio output.")
audio_tensor = audio_output[audio_key]
sample_rate = audio_output.get("sr", default_sr)
if hasattr(sample_rate, "item"):
sample_rate = sample_rate.item()
# Convert tensor to numpy
if hasattr(audio_tensor, "float"):
audio_tensor = audio_tensor.float().detach().cpu().numpy()
# Squeeze batch dimension if present, but preserve channel dimension for stereo
if audio_tensor.ndim > 1:
audio_tensor = audio_tensor.squeeze()
audio_obj = CreateAudio(
audio_tensor=audio_tensor,
sample_rate=int(sample_rate),
response_format=request.response_format or "wav",
speed=request.speed or 1.0,
stream_format=request.stream_format,
base64_encode=False,
)
audio_response: AudioResponse = self.create_audio(audio_obj)
return Response(content=audio_response.audio_data, media_type=audio_response.media_type)
except asyncio.CancelledError:
return self.create_error_response("Client disconnected")
except ValueError as e:
return self.create_error_response(e)
except Exception as e:
logger.exception("Audio generation failed: %s", e)
return self.create_error_response(f"Audio generation failed: {e}")
@@ -11,6 +11,7 @@ from typing import Any
import numpy as np
from fastapi import Request, UploadFile
from fastapi.responses import Response, StreamingResponse
from transformers.utils.hub import cached_file
from vllm.entrypoints.openai.engine.serving import OpenAIServing
from vllm.logger import init_logger
from vllm.multimodal.media import MediaConnector
@@ -83,6 +84,7 @@ def _validate_path_within_directory(file_path: Path, directory: Path) -> bool:
class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
def __init__(self, *args, **kwargs):
self.model_name = kwargs.pop("model_name", None)
super().__init__(*args, **kwargs)
# Initialize uploaded speakers storage
speech_voice_samples_dir = os.environ.get("SPEECH_VOICE_SAMPLES", "/tmp/voice_samples")
@@ -120,7 +122,9 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
try:
model_path = self.engine_client.model_config.model
st_config_path = os.path.join(model_path, "speech_tokenizer", "config.json")
if os.path.exists(st_config_path):
if not os.path.exists(st_config_path):
st_config_path = cached_file(model_path, "speech_tokenizer/config.json")
if st_config_path is not None and os.path.exists(st_config_path):
with open(st_config_path) as f:
st_config = json.load(f)
output_sr = st_config.get("output_sample_rate")
@@ -753,6 +757,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
request_id = f"speech-{random_uuid()}"
try:
sampling_params_list = self.engine_client.default_sampling_params_list
default_sr = 24000 # Default sample rate for TTS models
if self._is_tts:
# Validate TTS parameters
validation_error = self._validate_tts_request(request)
@@ -778,8 +784,6 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
tts_params.get("task_type", ["unknown"])[0],
)
sampling_params_list = self.engine_client.default_sampling_params_list
generator = self.engine_client.generate(
prompt=prompt,
request_id=request_id,
@@ -806,7 +810,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
return self.create_error_response("TTS model did not produce audio output.")
audio_tensor = audio_output[audio_key]
sr_raw = audio_output.get("sr", 24000)
sr_raw = audio_output.get("sr", default_sr)
sr_val = sr_raw[-1] if isinstance(sr_raw, list) and sr_raw else sr_raw
sample_rate = sr_val.item() if hasattr(sr_val, "item") else int(sr_val)
+43 -34
View File
@@ -169,22 +169,46 @@ def _convert_dataclasses_to_dict(obj: Any) -> Any:
return obj
def resolve_model_config_path(model: str) -> str:
"""Resolve the stage config file path from the model name.
def resolve_model_config_path(model_type: str) -> str | None:
"""Resolve the stage config file path from the model type.
Resolves stage configuration path based on the model type and device type.
First tries to find a device-specific YAML file from stage_configs/{device_type}/
directory. If not found, falls back to the default config file.
Args:
model: Model name or path (used to determine model_type)
model_type: Model type string
Returns:
String path to the stage configuration file
String path to the stage configuration file if found, None otherwise
"""
default_config_path = current_omni_platform.get_default_stage_config_path()
config_file_name = f"{model_type}.yaml"
complete_config_path = PROJECT_ROOT / default_config_path / config_file_name
if os.path.exists(complete_config_path):
return str(complete_config_path)
# Fall back to default config
stage_config_file = f"vllm_omni/model_executor/stage_configs/{model_type}.yaml"
stage_config_path = PROJECT_ROOT / stage_config_file
if not os.path.exists(stage_config_path):
return None
return str(stage_config_path)
def resolve_model_type(model: str) -> str:
"""Resolve the model type from the model name.
Args:
model: Model name or path
Returns:
Model type string (e.g. ``"Qwen3TTSForConditionalGeneration"``,
``"StableAudioPipeline"``).
Raises:
ValueError: If model_type cannot be determined
FileNotFoundError: If no stage config file exists for the model type
ValueError: If the model type cannot be determined from any
available configuration file.
"""
# Try to get config from standard transformers format first
try:
@@ -217,42 +241,26 @@ def resolve_model_config_path(model: str) -> str:
f"Please ensure the model has proper configuration files with 'model_type' field"
)
default_config_path = current_omni_platform.get_default_stage_config_path()
model_type_str = f"{model_type}.yaml"
complete_config_path = PROJECT_ROOT / default_config_path / model_type_str
if os.path.exists(complete_config_path):
return str(complete_config_path)
# Fall back to default config
stage_config_file = f"vllm_omni/model_executor/stage_configs/{model_type}.yaml"
stage_config_path = PROJECT_ROOT / stage_config_file
if not os.path.exists(stage_config_path):
return None
return str(stage_config_path)
return model_type
def load_stage_configs_from_model(model: str, base_engine_args: dict | None = None) -> list:
"""Load stage configurations from model's default config file.
Loads stage configurations based on the model type and device type.
First tries to load a device-specific YAML file from stage_configs/{device_type}/
directory. If not found, falls back to the default config file.
def load_stage_configs_from_model(config_path: str | None, base_engine_args: dict | None = None) -> list:
"""Load stage configurations from a resolved config file path.
Args:
model: Model name or path (used to determine model_type)
config_path: Path to the YAML configuration file, or None.
When None, returns an empty list.
base_engine_args: Optional engine arguments to merge with stage configs.
Returns:
List of stage configuration dictionaries
Raises:
FileNotFoundError: If no stage config file exists for the model type
List of stage configuration dictionaries, or empty list if
config_path is None.
"""
if base_engine_args is None:
base_engine_args = {}
stage_config_path = resolve_model_config_path(model)
if stage_config_path is None:
if config_path is None:
return []
stage_configs = load_stage_configs_from_yaml(config_path=stage_config_path, base_engine_args=base_engine_args)
stage_configs = load_stage_configs_from_yaml(config_path=config_path, base_engine_args=base_engine_args)
return stage_configs
@@ -306,9 +314,10 @@ def load_and_resolve_stage_configs(
Returns:
Tuple of (config_path, stage_configs)
"""
model_type = resolve_model_type(model)
if stage_configs_path is None:
config_path = resolve_model_config_path(model)
stage_configs = load_stage_configs_from_model(model, base_engine_args=kwargs)
config_path = resolve_model_config_path(model_type)
stage_configs = load_stage_configs_from_model(config_path, base_engine_args=kwargs)
if not stage_configs:
if default_stage_cfg_factory is not None:
default_stage_cfg = default_stage_cfg_factory()
+5 -1
View File
@@ -1,4 +1,8 @@
from .bagel.bagel import OmniBagelForConditionalGeneration
from .qwen3_omni import Qwen3OmniMoeForConditionalGeneration
from .registry import OmniModelRegistry # noqa: F401
__all__ = ["Qwen3OmniMoeForConditionalGeneration"]
__all__ = [
"Qwen3OmniMoeForConditionalGeneration",
"OmniBagelForConditionalGeneration",
]
@@ -0,0 +1,3 @@
from .bagel import OmniBagelForConditionalGeneration
__all__ = ["OmniBagelForConditionalGeneration"]
File diff suppressed because it is too large. Load diff
@@ -447,6 +447,7 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
logger.warning_once("runtime_additional_information is deprecated, use model_intermediate_buffer")
audio_codes_list: list[torch.Tensor] = []
ref_code_len_list: list[torch.Tensor] = []
ref_code_tensor: torch.Tensor | None = None
codec_streaming_list: list[torch.Tensor] = []
for info in info_dicts:
if not isinstance(info, dict):
@@ -459,6 +460,9 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
codec_streaming_list.append(
torch.full((int(ac.shape[0]),), int(cs), dtype=torch.int8, device=ac.device)
)
ref_code = info.get("ref_code")
if isinstance(ref_code, torch.Tensor) and ref_code.numel() > 0:
ref_code_tensor = ref_code
ref_len = info.get("ref_code_len")
if ref_len is None:
continue
@@ -487,6 +491,8 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
mm: dict[str, torch.Tensor] = {"audio_codes": audio_codes}
if ref_code_len_list:
mm["ref_code_len"] = torch.cat(ref_code_len_list, dim=0)[:span_len]
if ref_code_tensor is not None:
mm["ref_code"] = [ref_code_tensor]
if codec_streaming_list:
mm["codec_streaming"] = torch.cat(codec_streaming_list, dim=0)[:span_len]
return OmniOutput(text_hidden_states=hidden, multimodal_outputs=mm)
@@ -538,8 +544,8 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
# Subsequent prefill rounds (multi-chunk): prompt_embeds_cpu is a Tensor stored by the first round.
is_first_prefill = not isinstance(prompt_embeds_cpu, torch.Tensor) or prompt_embeds_cpu.ndim != 2
if is_first_prefill:
full_prompt_embeds, tailing_text_hidden, tts_pad_embed, ref_code_len = self._build_prompt_embeds(
task_type=task_type, info_dict=info_dict
full_prompt_embeds, tailing_text_hidden, tts_pad_embed, ref_code_len, ref_code = (
self._build_prompt_embeds(task_type=task_type, info_dict=info_dict)
)
# Store full prompt embeddings + trailing queue on CPU for later chunks/steps.
prompt_embeds_cpu = full_prompt_embeds.detach().to("cpu").contiguous()
@@ -550,6 +556,8 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
"talker_prefill_offset": 0,
"codec_streaming": codec_streaming,
}
if isinstance(ref_code, torch.Tensor) and ref_code.numel() > 0:
info_update["ref_code"] = ref_code.detach().to("cpu").contiguous()
if ref_code_len is not None:
info_update["ref_code_len"] = int(ref_code_len)
# Always return a span_len slice; if the scheduled placeholder is longer, pad with tts_pad_embed.
@@ -773,7 +781,6 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
if ref_code_len is None and estimate_ref_code_len is not None:
ref_code_len = estimate_ref_code_len(info.get("ref_audio"))
if ref_code_len is None:
raise ValueError(
"Base in-context voice cloning requires either `voice_clone_prompt.ref_code` "
@@ -1183,7 +1190,7 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
*,
task_type: str,
info_dict: dict[str, Any],
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int | None]:
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int | None, torch.Tensor | None]:
text = (info_dict.get("text") or [""])[0]
language = (info_dict.get("language") or ["Auto"])[0]
non_streaming_mode_val = info_dict.get("non_streaming_mode")
@@ -1268,6 +1275,7 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
# Speaker embedding/token (task-dependent)
speaker_embed = None
ref_code_len: int | None = None
ref_code_prompt: torch.Tensor | None = None
def _as_singleton(x: object) -> object:
if isinstance(x, list):
@@ -1333,6 +1341,8 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
wav_np, sr = self._normalize_ref_audio(ref_audio_list[0])
ref_code_t = self._encode_ref_audio_to_code(wav_np, sr).to(device=input_ids.device)
ref_code_len = int(ref_code_t.shape[0])
if isinstance(ref_code_t, torch.Tensor):
ref_code_prompt = ref_code_t
# Speaker embedding: use prompt embed if provided; otherwise extract from audio.
spk = None
@@ -1521,6 +1531,7 @@ class Qwen3TTSTalkerForConditionalGeneration(nn.Module):
trailing_text_hidden.squeeze(0), # [T, H]
tts_pad_embed.squeeze(0), # [1, H]
ref_code_len,
ref_code_prompt.contiguous() if isinstance(ref_code_prompt, torch.Tensor) else None,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
@@ -119,6 +119,11 @@ _OMNI_MODELS = {
"glm_image_ar",
"GlmImageForConditionalGeneration",
),
"OmniBagelForConditionalGeneration": (
"bagel",
"bagel",
"OmniBagelForConditionalGeneration",
),
"HunyuanImage3ForCausalMM": (
"hunyuan_image3",
"hunyuan_image3",
@@ -6,15 +6,14 @@ stage_args:
prompt_expand_func: vllm_omni.model_executor.stage_input_processors.bagel.expand_cfg_prompts
runtime:
devices: "0"
# 2 = 1 user prompt + 1 CFG companion (text-unconditional).
# For multi-prompt batches this should scale as batch_size × 2.
max_batch_size: 2
# 3 = 1 user prompt + 2 CFG companions (text-unconditional + image-unconditional).
max_batch_size: 3
engine_args:
model_stage: thinker
model_arch: BagelForConditionalGeneration
model_arch: OmniBagelForConditionalGeneration
worker_type: ar
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.35
gpu_memory_utilization: 0.45
enforce_eager: true
trust_remote_code: true
engine_output_type: text
@@ -46,7 +45,7 @@ stage_args:
max_batch_size: 1
engine_args:
model_stage: dit
gpu_memory_utilization: 0.55
gpu_memory_utilization: 0.45
enforce_eager: true
trust_remote_code: true
engine_output_type: image
@@ -11,7 +11,7 @@ stage_args:
model_arch: BagelForConditionalGeneration
worker_type: ar
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.35
gpu_memory_utilization: 0.45
enforce_eager: true
trust_remote_code: true
engine_output_type: text
@@ -45,7 +45,7 @@ stage_args:
max_batch_size: 1
engine_args:
model_stage: dit
gpu_memory_utilization: 0.55
gpu_memory_utilization: 0.45
enforce_eager: true
trust_remote_code: true
engine_output_type: image
@@ -20,6 +20,7 @@ from typing import Any
logger = logging.getLogger(__name__)
CFG_TEXT_SUFFIX = "__cfg_text"
CFG_IMG_SUFFIX = "__cfg_img"
@dataclass
@@ -73,7 +74,37 @@ def expand_cfg_prompts(
),
]
# img2img: more complex (3 distinct caches). Reserve for future.
if "img2img" in modalities:
IMG2IMG_PLACEHOLDER = "<|fim_middle|>"
cfg_text_dict: dict[str, Any] = {
"prompt": f"{IMG2IMG_PLACEHOLDER}{neg_prompt}",
"modalities": ["img2img"],
}
mm_data = prompt.get("multi_modal_data")
if mm_data:
cfg_text_dict["multi_modal_data"] = mm_data
original_text = prompt.get("prompt", "")
cfg_img_text = original_text.replace(IMG2IMG_PLACEHOLDER, "")
cfg_img_dict: dict[str, Any] = {
"prompt": cfg_img_text,
"modalities": ["img2img"],
}
return [
ExpandedPrompt(
prompt=cfg_text_dict,
role="cfg_text",
request_id_suffix=CFG_TEXT_SUFFIX,
),
ExpandedPrompt(
prompt=cfg_img_dict,
role="cfg_img",
request_id_suffix=CFG_IMG_SUFFIX,
),
]
return []
@@ -27,13 +27,24 @@ def talker2code2wav(
# Filter zero-padded frames (EOS/invalid steps), matching _extract_last_frame behavior
valid_mask = audio_codes.any(dim=1)
audio_codes = audio_codes[valid_mask]
ref_code = output.multimodal_output.get("ref_code")
if isinstance(ref_code, list):
ref_code = ref_code[0] if ref_code else None
if isinstance(ref_code, torch.Tensor) and ref_code.numel() > 0:
ref_code = ref_code.to(torch.long).cpu().contiguous()
ref_code_len = int(ref_code.shape[0])
audio_codes = torch.cat([ref_code.to(audio_codes.device), audio_codes], dim=0)
else:
ref_code_len = 0
# Code2Wav expects codebook-major flat: [Q*num_frames]
codec_codes = audio_codes.transpose(0, 1).cpu().reshape(-1).tolist()
additional_information = {"left_context_size": [ref_code_len]} if ref_code_len > 0 else None
code2wav_inputs.append(
OmniTokensPrompt(
prompt_token_ids=codec_codes,
multi_modal_data=None,
mm_processor_kwargs=None,
additional_information=additional_information,
)
)
return code2wav_inputs
@@ -61,12 +72,19 @@ def talker2code2wav_async_chunk(
) -> dict[str, Any] | None:
request_id = request.external_req_id
finished = bool(is_finished or request.is_finished())
request_payload = getattr(transfer_manager, "request_payload", None)
if request_payload is None:
request_payload = {}
transfer_manager.request_payload = request_payload
if isinstance(pooling_output, dict):
frame = _extract_last_frame(pooling_output)
if frame is not None:
codec_codes = frame.cpu().tolist()
transfer_manager.code_prompt_token_ids[request_id].append(codec_codes)
ref_code = pooling_output.get("ref_code")
if isinstance(ref_code, torch.Tensor) and ref_code.numel() > 0 and request_payload.get(request_id) is None:
request_payload[request_id] = ref_code.to(torch.long).cpu().contiguous()
elif not finished:
# Some steps may not produce pooling_output. Only flush on finish.
return None
@@ -139,6 +157,14 @@ def talker2code2wav_async_chunk(
left_context_size = max(0, int(end_index - context_length))
window_frames = transfer_manager.code_prompt_token_ids[request_id][-end_index:]
# Match offline Base ICL decoding by prepending ref_code only once to the first decoder window.
if transfer_manager.put_req_chunk[request_id] == 0:
ref_code = request_payload.pop(request_id, None)
if isinstance(ref_code, torch.Tensor) and ref_code.numel() > 0:
ref_frames = ref_code.tolist()
window_frames = ref_frames + window_frames
left_context_size += len(ref_frames)
# Pack context + chunk into codebook-major flat codes for adapter.
code_predictor_codes = torch.tensor(window_frames).transpose(0, 1).reshape(-1).tolist()
+16
View File
@@ -1,6 +1,8 @@
import sys
from functools import cached_property as _cached_property
from aenum import extend_enum
from vllm.config import ModelConfig as _ModelConfig
from vllm.inputs.data import TokensPrompt as _OriginalTokensPrompt
from vllm.model_executor.layers.rotary_embedding import (
MRotaryEmbedding as _OriginalMRotaryEmbedding,
@@ -43,6 +45,20 @@ except ImportError:
# GlmImageTextConfig not available, skip patching
pass
# Patch ModelConfig.is_mm_prefix_lm to include Bagel (bidirectional attention
# for multimodal prefix positions, same as Gemma3/Molmo2/PaliGemma).
_orig_is_mm_prefix_lm = _ModelConfig.__dict__["is_mm_prefix_lm"].func
@_cached_property
def _patched_is_mm_prefix_lm(self) -> bool:
return _orig_is_mm_prefix_lm(self) or getattr(self.hf_config, "model_type", None) == "bagel"
_patched_is_mm_prefix_lm.__set_name__(_ModelConfig, "is_mm_prefix_lm")
_ModelConfig.is_mm_prefix_lm = _patched_is_mm_prefix_lm
# Extend RequestStatus enum with omni-specific statuses
if not hasattr(RequestStatus, "WAITING_FOR_CHUNK"):
# The value - 1 is intentionally chosen to ensure it is treated
+21 -1
View File
@@ -98,9 +98,25 @@ class GPUARModelRunner(OmniGPUModelRunner):
if self.execute_model_state is not None:
raise RuntimeError("State error: sample_tokens() must be called after execute_model() returns None.")
if not getattr(self, "_warmup_state_cleared", False):
self._warmup_state_cleared = True
if hasattr(self.model, "_clear_warmup_state"):
self.model._clear_warmup_state()
# [Omni] Handle KV transfer BEFORE updating states (which removes finished requests)
finished_reqs = getattr(scheduler_output, "finished_requests_needing_kv_transfer", {})
if finished_reqs and hasattr(self.model, "get_kv_transfer_metadata"):
for req_id, data in finished_reqs.items():
try:
model_meta = self.model.get_kv_transfer_metadata(req_id)
if model_meta:
existing = data.get("custom_metadata") or {}
existing.update(model_meta)
data["custom_metadata"] = existing
except Exception as e:
logger.warning(f"Failed to get custom metadata from model for {req_id}: {e}")
self.kv_extracted_req_ids = self.kv_transfer_manager.handle_finished_requests_kv_transfer(
finished_reqs=getattr(scheduler_output, "finished_requests_needing_kv_transfer", {}),
finished_reqs=finished_reqs,
kv_caches=self.kv_caches,
block_size=self.cache_config.block_size,
cache_dtype=str(self.cache_config.cache_dtype),
@@ -290,6 +306,10 @@ class GPUARModelRunner(OmniGPUModelRunner):
sampler=self.sampler,
)
# [Omni] Map pending ropes metadata to req_ids.
if hasattr(self.model, "flush_pending_metadata"):
self.model.flush_pending_metadata(list(req_ids))
with record_function_or_nullcontext("gpu_model_runner: postprocess"):
if self.use_aux_hidden_state_outputs:
# True when EAGLE 3 is used.