[CI] Reducing mithril-h100-pool in ready&merge CI, optimize ready CI duration (#4354)

Signed-off-by: [Your Name] <your.email@example.com>
Signed-off-by: wangyu <410167048@qq.com>
Signed-off-by: wangyu <53896905+yenuo26@users.noreply.github.com>
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Signed-off-by: [Your Name] <[Your Email]>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
wangyuandHongsheng Liu authored and GitHub committed 2026-06-29 05:17:57 +00:00
1 parent e4026a50ad
commit 7619ed6b82
84 files changed
+1718 -2428

No files matched your search

-26
View File
@@ -179,32 +179,6 @@ steps:
pytest -s -v tests/e2e/online_serving/test_qwen3_tts_base.py tests/e2e/offline_inference/test_qwen3_tts_base.py -m "advanced_model" --run-level "advanced_model"
'
# It is the whisper ASR validation step that is slow, not the Voxtral TTS generation.
# whisper ASR runs on CPU.
- label: "Voxtral-TTS E2E Test (Online Serving)"
agent_pool: mi325_1
depends_on: amd-build
mirror_hardwares: [amdproduction]
grade: Blocking
commands:
- |
timeout 40m bash -c '
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -s -v tests/e2e/online_serving/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
'
- label: "Voxtral-TTS E2E Test (Offline Inference)"
agent_pool: mi325_1
depends_on: amd-build
mirror_hardwares: [amdproduction]
grade: Blocking
commands:
- |
timeout 30m bash -c '
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -s -v tests/e2e/offline_inference/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
'
# TODO: Bagel test on ROCm is very unstable. @tjtanaa
# Need to debug before reneable numerical changes across large PRs
# # split Bagel Model Test with H100 (Real Weights) into three tests
+1 -14
View File
@@ -161,19 +161,6 @@ steps:
- group: ":card_index_dividers: E2E Test"
steps:
- label: "MiMo-Audio E2E Test with H100"
agent_pool: mi325_1
depends_on: amd-build
mirror_hardwares: [amdproduction]
grade: Blocking
commands:
- |
timeout 30m bash -c '
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -s -v tests/e2e/online_serving/test_mimo_audio.py -m "core_model" --run-level "core_model"
'
- label: "Qwen3-TTS E2E Test"
agent_pool: mi325_1
depends_on: amd-build
@@ -193,7 +180,7 @@ steps:
commands:
- |
timeout 20m bash -c '
pytest -s -v tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py -m "full_model" --run-level "core_model"
pytest -s -v tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py -m "slow" --run-level "core_model"
'
# TODO: Bagel test on ROCm is very unstable. @tjtanaa
+12 -373
View File
@@ -61,13 +61,14 @@ steps:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Distributed Test"
depends_on: upload-merge-pipeline
timeout_in_minutes: 60
commands:
- |
timeout 55m bash -c "
set -e
export VLLM_IMAGE_FETCH_TIMEOUT=60
pytest -s -v tests/distributed -m 'advanced_model and cuda' --run-level 'advanced_model'
pytest -s -v tests/distributed -m 'advanced_model and cuda and H100' --run-level 'advanced_model'
"
agents:
queue: "mithril-h100-pool"
@@ -164,28 +165,6 @@ steps:
- group: ":card_index_dividers: E2E Test"
depends_on: upload-merge-pipeline
steps:
- label: "TTS · Step-Audio2 E2E Test"
timeout_in_minutes: 20
commands:
- |
timeout 20m bash -c "
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -s -v tests/e2e/offline_inference/test_step_audio2.py -m 'advanced_model' --run-level 'advanced_model'
"
agents:
queue: "gpu_4_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "TTS · Qwen3-TTS CustomVoice Test"
source_file_dependencies:
- tests/e2e/online_serving/test_qwen3_tts_customvoice.py
@@ -240,19 +219,19 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "TTS · Ming-Omni-TTS Test"
- label: "TTS · Higgs-Audio-V3 Test"
source_file_dependencies:
- tests/e2e/online_serving/test_ming_tts.py
- tests/e2e/offline_inference/test_ming_tts.py
- vllm_omni/model_executor/models/ming_tts/
- vllm_omni/model_executor/stage_input_processors/ming_tts.py
- vllm_omni/deploy/ming_tts.yaml
timeout_in_minutes: 20
- tests/e2e/online_serving/test_higgs_audio_v3.py
- vllm_omni/model_executor/models/higgs_audio_v3/
- vllm_omni/model_executor/stage_input_processors/higgs_audio_v3.py
- vllm_omni/deploy/higgs_multimodal_qwen3.yaml
commands:
- |
timeout 20m bash -c "
export VLLM_LOGGING_LEVEL=DEBUG
pytest -s -v tests/e2e/online_serving/test_ming_tts.py tests/e2e/offline_inference/test_ming_tts.py -m 'advanced_model and cuda' --run-level 'advanced_model'
export VLLM_WORKER_MULTIPROC_METHOD=spawn
export VLLM_USE_DEEP_GEMM=0
export VLLM_MOE_USE_DEEP_GEMM=0
pytest -s -v tests/e2e/online_serving/test_higgs_audio_v3.py -m 'advanced_model' --run-level 'advanced_model'
"
agents:
queue: "gpu_1_queue"
@@ -264,105 +243,10 @@ steps:
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "TTS · Voxtral-TTS Test"
source_file_dependencies:
- tests/e2e/offline_inference/test_voxtral_tts.py
- tests/e2e/online_serving/test_voxtral_tts.py
- vllm_omni/model_executor/models/voxtral_tts/
- vllm_omni/model_executor/stage_input_processors/voxtral_tts.py
- vllm_omni/deploy/voxtral_tts.yaml
timeout_in_minutes: 20
commands:
- |
timeout 20m bash -c "
export VLLM_LOGGING_LEVEL=DEBUG
pytest -s -v tests/e2e/offline_inference/test_voxtral_tts.py tests/e2e/online_serving/test_voxtral_tts.py -m 'advanced_model and cuda' --run-level 'advanced_model'
"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "TTS · GLM-TTS Test"
source_file_dependencies:
- tests/e2e/online_serving/test_glm_tts.py
- tests/e2e/offline_inference/test_glm_tts.py
- vllm_omni/model_executor/models/glm_tts/
- vllm_omni/model_executor/stage_input_processors/glm_tts.py
- vllm_omni/deploy/glm_tts.yaml
timeout_in_minutes: 20
commands:
- |
timeout 20m bash -c "
export VLLM_LOGGING_LEVEL=DEBUG
pytest -s -v tests/e2e/online_serving/test_glm_tts.py tests/e2e/offline_inference/test_glm_tts.py -m 'advanced_model and cuda' --run-level 'advanced_model'
"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "TTS · IndexTTS2 Test"
source_file_dependencies:
- tests/e2e/online_serving/test_indextts2.py
@@ -443,49 +327,6 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Omni · Mimo Audio Test"
source_file_dependencies:
- tests/e2e/online_serving/test_mimo_audio.py
- vllm_omni/model_executor/models/mimo_audio/
- vllm_omni/model_executor/stage_input_processors/mimo_audio.py
- vllm_omni/deploy/mimo_audio.yaml
timeout_in_minutes: 30
commands:
- pytest -s -v tests/e2e/online_serving/test_mimo_audio.py -m 'advanced_model and cuda' --run-level 'advanced_model'
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "TTS · VoxCPM2 Test"
source_file_dependencies:
- tests/e2e/offline_inference/test_voxcpm2_tts.py
@@ -514,37 +355,6 @@ steps:
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Omni · Higgs-Audio V2 Test"
source_file_dependencies:
- tests/e2e/online_serving/test_higgs_audio_v2.py
- tests/e2e/offline_inference/test_higgs_audio_v2.py
- vllm_omni/model_executor/models/higgs_audio_v2/
- vllm_omni/model_executor/stage_input_processors/higgs_audio_v2.py
- vllm_omni/deploy/higgs_audio_v2.yaml
timeout_in_minutes: 30
commands:
- |
timeout 30m bash -c "
export VLLM_LOGGING_LEVEL=DEBUG
export VLLM_WORKER_MULTIPROC_METHOD=spawn
export VLLM_USE_DEEP_GEMM=0
export VLLM_MOE_USE_DEEP_GEMM=0
pytest -s -v tests/e2e/online_serving/test_higgs_audio_v2.py tests/e2e/offline_inference/test_higgs_audio_v2.py -m 'advanced_model and cuda' --run-level 'advanced_model'
"
agents:
queue: "gpu_1_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "TTS · SoulX-Singer Test"
source_file_dependencies:
- tests/e2e/offline_inference/test_soulxsinger.py
@@ -614,88 +424,6 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Diffusion · Longcat Image Edit Test"
source_file_dependencies:
- tests/e2e/online_serving/test_longcat_image_edit.py
- vllm_omni/diffusion/models/longcat_image/
timeout_in_minutes: 20
commands:
- pytest -s -v tests/e2e/online_serving/test_longcat_image_edit.py -m 'advanced_model and cuda' --run-level 'advanced_model'
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Diffusion · Longcat Image Test"
source_file_dependencies:
- tests/e2e/online_serving/test_longcat_image.py
- vllm_omni/diffusion/models/longcat_image/
timeout_in_minutes: 20
commands:
- pytest -s -v tests/e2e/online_serving/test_longcat_image.py -m 'advanced_model and cuda' --run-level 'advanced_model'
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Diffusion · Bagel Test"
source_file_dependencies:
- tests/e2e/offline_inference/test_bagel.py
@@ -875,50 +603,6 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Diffusion · HunyuanVideo 1.5 Test"
source_file_dependencies:
- tests/e2e/online_serving/test_hunyuan_video_15.py
- vllm_omni/diffusion/models/hunyuan_video/
timeout_in_minutes: 40
commands:
- |
timeout 40m bash -c "
pytest -s -v tests/e2e/online_serving/test_hunyuan_video_15.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Diffusion · Z Image Test"
source_file_dependencies:
- tests/e2e/offline_inference/test_z_image.py
@@ -960,48 +644,3 @@ steps:
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Diffusion · SD3 Test"
source_file_dependencies:
- tests/e2e/online_serving/test_sd3.py
- vllm_omni/diffusion/models/sd3/
timeout_in_minutes: 45
commands:
- |
timeout 45m bash -c "
export VLLM_IMAGE_FETCH_TIMEOUT=60
pytest -s -v tests/e2e/online_serving/test_sd3.py -m 'advanced_model and cuda and diffusion' --run-level 'advanced_model'
"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 4
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
+52 -117
View File
@@ -49,25 +49,6 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Omni · Function Test with L4"
timeout_in_minutes: 90
commands:
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/e2e/*/test_qwen2_5_omni_expansion.py --run-level "core_model"
agents:
queue: "gpu_4_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: ":full_moon: Omni · Doc Test with L4"
timeout_in_minutes: 90
commands:
@@ -398,6 +379,8 @@ steps:
timeout_in_minutes: 90
commands:
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- export VLLM_USE_DEEP_GEMM="0"
- export VLLM_MOE_USE_DEEP_GEMM="0"
- pytest -s -v tests/e2e/ -m "full_model and L4 and tts" --run-level "full_model" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_voxcpm2_tts_expansion.py
agents:
queue: "gpu_1_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
@@ -568,54 +551,52 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: TTS · Higgs-Audio-V3 · Perf Test"
key: nightly-tts-performance-higgs-audio-v3
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test" || build.pull_request.labels includes "tts-test"
timeout_in_minutes: 180
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- export VLLM_USE_DEEP_GEMM="0"
- export VLLM_MOE_USE_DEEP_GEMM="0"
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_higgs_audio_v3.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- label: ":full_moon: TTS · Higgs-Audio-V3 · Perf Test"
key: nightly-tts-performance-higgs-audio-v3
timeout_in_minutes: 180
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- export VLLM_USE_DEEP_GEMM="0"
- export VLLM_MOE_USE_DEEP_GEMM="0"
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_higgs_audio_v3.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
mountPath: /dev/shm
emptyDir:
medium: Memory
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
# Diffusion X2I suite: x2i / x2a / x2t and related non-video paths; x2v is only in "Diffusion X2V Model Test" below.
- group: ":card_index_dividers: Diffusion X2I(&A&T) Model Test"
@@ -636,11 +617,7 @@ steps:
tests/e2e/online_serving/test_qwen_image_expansion.py
tests/e2e/online_serving/test_qwen_image_edit_expansion.py
tests/e2e/online_serving/test_qwen_image_layered_expansion.py
tests/e2e/online_serving/test_longcat_image_expansion.py
tests/e2e/online_serving/test_longcat_image_edit_expansion.py
tests/e2e/online_serving/test_flux_2_dev_expansion.py
tests/e2e/online_serving/test_bagel_expansion.py
tests/e2e/offline_inference/test_sensenova_u1_expansion.py
-m "full_model and diffusion and H100 and not distributed_cuda"
--run-level "full_model"
agents:
@@ -726,10 +703,6 @@ steps:
- >-
pytest -sv
tests/e2e/online_serving/test_qwen_image_layered_expansion.py
tests/e2e/online_serving/test_longcat_image_expansion.py
tests/e2e/online_serving/test_longcat_image_edit_expansion.py
tests/e2e/online_serving/test_flux_2_dev_expansion.py
tests/e2e/offline_inference/test_glm_image_autoround_w4a16_expansion.py
-m "full_model and diffusion and H100 and distributed_cuda"
--run-level "full_model"
agents:
@@ -808,13 +781,14 @@ steps:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with H100 · Multi-GPU DreamZero"
- label: ":full_moon: Diffusion X2I(&A&T) · GLM-Image Function Test with H100"
timeout_in_minutes: 120
commands:
- >-
pytest -sv
tests/e2e/online_serving/test_dreamzero_expansion.py
-m "full_model and diffusion and H100 and distributed_cuda"
tests/e2e/online_serving/test_glm_image_expansion.py
tests/e2e/offline_inference/test_glm_image_autoround_w4a16_expansion.py
-m "full_model and diffusion and H100"
--run-level "full_model"
agents:
queue: "mithril-h100-pool"
@@ -851,45 +825,6 @@ steps:
type: DirectoryOrCreate
- label: ":full_moon: Diffusion X2I(&A&T) · GLM-Image Function Test with H100"
timeout_in_minutes: 120
commands:
- pytest -sv tests/e2e/online_serving/test_glm_image_expansion.py -m "full_model and diffusion and H100" --run-level "full_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with L4"
timeout_in_minutes: 60
commands:
@@ -1356,7 +1291,7 @@ steps:
timeout_in_minutes: 90
commands:
- pytest -s -v tests/e2e/offline_inference/test_wan22_autoround_w4a16_expansion.py -m "full_model and cuda" --run-level "full_model"
- pytest -s -v tests/e2e/online_serving/test_wan_2_1_vace_expansion.py tests/e2e/online_serving/test_hunyuan_video_15_expansion.py tests/e2e/online_serving/test_ltx2_expansion.py -m "full_model and cuda" --run-level "full_model"
- pytest -s -v tests/e2e/online_serving/test_wan_2_1_vace_expansion.py tests/e2e/online_serving/test_ltx2_expansion.py -m "full_model and cuda" --run-level "full_model"
agents:
queue: "mithril-h100-pool"
plugins:
@@ -1580,7 +1515,7 @@ steps:
key: nightly-testcase-statistics
timeout_in_minutes: 120
depends_on: upload-nightly-pipeline
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test"
if: build.env("NIGHTLY") == "1"
commands:
- python tools/nightly/buildkite_testcase_statistics.py -o tests/dfx/perf/results/buildkite_testcase_statistics.html
- buildkite-agent artifact upload "tests/dfx/perf/results/*.html"
+32 -85
View File
@@ -184,96 +184,43 @@ steps:
commands:
- |
timeout 20m bash -c "
pytest -sv tests/distributed/ -m 'core_model and cuda' --run-level 'core_model'
pytest -sv tests/distributed/ -m 'core_model and cuda and L4' --run-level 'core_model'
"
agents:
queue: "mithril-h100-pool"
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- group: ":card_index_dividers: Entrypoints Test"
- label: "Entrypoints Test"
depends_on: upload-ready-pipeline
steps:
- label: "Entrypoints Test with H100"
commands:
- |
timeout 30m bash -c "
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -sv tests/entrypoints/ -m 'core_model and cuda and H100' --run-level 'core_model'
"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Entrypoints Test with L4"
commands:
- |
timeout 30m bash -c "
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -sv tests/entrypoints/ -m 'core_model and cuda and L4' --run-level 'core_model'
"
agents:
queue: "gpu_1_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
commands:
- |
timeout 30m bash -c "
export VLLM_WORKER_MULTIPROC_METHOD=spawn
pytest -sv tests/entrypoints/ -m 'core_model and cuda' --run-level 'core_model'
"
agents:
queue: "gpu_1_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- group: ":card_index_dividers: E2E Test"
depends_on: upload-ready-pipeline
+243
View File
@@ -206,3 +206,246 @@ steps:
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- group: ":card_index_dividers: E2E Tests"
depends_on: upload-weekly-pipeline
if: build.env("WEEKLY") == "1" || build.pull_request.labels includes "weekly-test"
steps:
- label: "Omni · L4"
timeout_in_minutes: 90
commands:
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/e2e/*/test_qwen2_5_omni_expansion.py --run-level "core_model"
- >-
pytest -s -v tests/e2e/
--ignore=tests/e2e/online_serving/test_qwen2_5_omni_expansion.py
--ignore=tests/e2e/offline_inference/test_qwen2_5_omni_expansion.py
-m "slow and L4 and omni"
--run-level "full_model"
agents:
queue: "gpu_4_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Omni · H100"
timeout_in_minutes: 90
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and H100 and omni"
--run-level "full_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "TTS · L4"
timeout_in_minutes: 90
commands:
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- export VLLM_USE_DEEP_GEMM="0"
- export VLLM_MOE_USE_DEEP_GEMM="0"
- |
failed=0
for f in $(pytest --collect-only -q tests/e2e/ -m "slow and L4 and tts" --run-level "full_model" | grep '\.py::' | sed -E 's|::.*||' | sort -u); do
pytest -s -v "$$f" -m "slow and L4 and tts" --run-level "full_model" || failed=1
done
exit $$failed
agents:
queue: "gpu_1_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "TTS · H100"
timeout_in_minutes: 90
commands:
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- export VLLM_USE_DEEP_GEMM="0"
- export VLLM_MOE_USE_DEEP_GEMM="0"
- >-
pytest -sv
tests/e2e/ -m "slow and H100 and tts"
--run-level "full_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Diffusion · L4"
timeout_in_minutes: 60
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and diffusion and L4"
--run-level "full_model"
agents:
queue: "gpu_4_queue"
plugins:
- docker#v5.2.0:
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
always-pull: true
propagate-environment: true
shm-size: "8gb"
environment:
- "HF_HOME=/fsx/hf_cache"
- "HF_TOKEN"
volumes:
- "/fsx/hf_cache:/fsx/hf_cache"
- label: "Diffusion · H100 · Single-GPU"
timeout_in_minutes: 120
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and diffusion and H100 and not distributed_cuda"
--run-level "full_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 1
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
- label: "Diffusion · H100 · Multi-GPU"
timeout_in_minutes: 90
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and diffusion and H100 and distributed_cuda"
--run-level "full_model"
agents:
queue: "mithril-h100-pool"
plugins:
- kubernetes:
podSpec:
containers:
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
resources:
limits:
nvidia.com/gpu: 2
volumeMounts:
- name: devshm
mountPath: /dev/shm
- name: hf-cache
mountPath: /root/.cache/huggingface
env:
- name: HF_HOME
value: /root/.cache/huggingface
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token-secret
key: token
nodeSelector:
node.kubernetes.io/instance-type: gpu-h100-sxm
volumes:
- name: devshm
emptyDir:
medium: Memory
- name: hf-cache
hostPath:
path: /mnt/hf-cache
type: DirectoryOrCreate
+19 -10
View File
@@ -67,7 +67,7 @@ Through five levels (L1-L5) and common (Common) specifications, the system clari
<tr>
<td><strong>L2</strong><br>(E2E across models & GPU-required UT)</td>
<td>Online (basic deployment scenarios):<br>dummy, normal inference function (output format, stream), some instance startup UT</td>
<td>High-priority models + online basic scenarios + request success validation</td>
<td>High-priority models + online basic scenarios; request success, non-empty output, format match (no Whisper/accuracy)</td>
<td>High-priority features (using random lightweight models)</td>
<td>High-priority interfaces (using random lightweight models)</td>
<td><code>core_model and hardware_test(H100, L4, etc.) and omni/tts/diffusion</code></td>
@@ -89,7 +89,7 @@ Through five levels (L1-L5) and common (Common) specifications, the system clari
<tr>
<td><strong>L3</strong><br>(Important Perf & Integration & Accuracy)</td>
<td>Online & Offline (multiple deployment scenarios):<br>real model, normal inference function, normal accuracy</td>
<td>High/medium-priority models with real weights + online/offline key scenarios + basic accuracy validation</td>
<td>High/medium-priority models + key online/offline scenarios; real weights, Whisper/similarity, preset voice gender, basic accuracy</td>
<td>Medium-priority features (using random lightweight models)</td>
<td>Medium-priority interfaces (using random lightweight models)</td>
<td><code>advanced_model and hardware_test(H100, L4, etc.) and omni/tts/diffusion</code></td>
@@ -113,9 +113,9 @@ Through five levels (L1-L5) and common (Common) specifications, the system clari
<tr>
<td><strong>L4</strong><br>(Perf & Integration & Accuracy)</td>
<td>Online: full functional scenarios + performance test + doc test + accuracy test</td>
<td>High-priority models: function, performance, accuracy, and doc testing<br>Medium/low-priority models: function and doc testing</td>
<td>Low-priority features (using random lightweight models)</td>
<td>Low-priority interfaces (using random lightweight models)</td>
<td>High-priority models: function, performance, accuracy, and doc testing<br>Medium-priority models: function and doc testing</td>
<td>Low-priority features (using real weights)</td>
<td>Low-priority interfaces (using real weights)</td>
<td><code>full_model and hardware_test(H100, L4, etc.) and omni/tts/diffusion</code></td>
<td>&lt;3 hour</td>
<td>
@@ -144,7 +144,7 @@ Through five levels (L1-L5) and common (Common) specifications, the system clari
<tr>
<td><strong>L5</strong><br>(Stability & Reliability)</td>
<td>Online: long-term stability test + reliability test</td>
<td>Long-term stability and reliability testing for high-priority models</td>
<td>Long-term stability and reliability testing for high-priority models<br>Low-priority models: function and doc testing</td>
<td>/</td>
<td>Invalid-parameter validation for high-priority interfaces</td>
<td><code>slow and hardware_test(H100, L4, etc.) and omni/tts/diffusion</code></td>
@@ -446,7 +446,11 @@ L2 level testing builds upon L1 by introducing GPU resources and verifying that
- ***Time Cost***: Execution time is controlled within ***15 minutes*** to ensure fast feedback.
- ***L2 (Basic End-to-End Testing)***:
- - ***Scope***: Covers two basic deployment scenarios: `online` (serving) and `offline` (inference).
- ***Focus***: Uses `dummy` models or lightweight real models to verify that the entire chain from request input to result output works normally, including output data structure, streaming (stream) support, etc. Also includes some unit tests that require launching independent service instances.
- ***Focus***: Uses `dummy` weights (via deploy YAML patching at `core_model`) or lightweight real models to verify that the entire chain from request input to result output works normally, including output data structure, streaming (stream) support, and **cheap payload checks** at `--run-level core_model` (see below). Also includes some unit tests that require launching independent service instances.
- ***L2 response validation (`core_model`)***: Implemented in `tests/helpers/assertions.py` and invoked by `OpenAIClientHandler` based on `--run-level`. At L2 we require **request success** plus minimal output sanity—not full accuracy:
- **Speech / TTS** (`assert_audio_speech_response`): decoded audio must be present and non-empty (or exceed `min_audio_bytes` when set in `request_config`); `response_format` must match the returned content-type (e.g. `wav`, `pcm`). Whisper transcript similarity, PCM HNR, and preset-voice gender checks run only at L3+.
- **Diffusion** (`assert_diffusion_response`): at least one non-empty image, video, or audio artifact. Resolution/frame-count parity and other deep checks run only at L3+.
- **Omni multimodal** (`assert_omni_response`): L2 asserts successful completion; keyword, transcript, and cross-modal similarity checks run only at L3+.
- ***Characteristic***: Requires ***GPU*** resources to perform model computations.
### 1.3 Test Directory and Execution Files
@@ -555,6 +559,11 @@ L3 level testing executes after code is merged into the main branch. Its core pu
- 1. ***Inference Functionality***: Ensures real models can perform forward computation normally and return results.
2. ***Accuracy Compliance***: Verifies that the model's evaluation metrics (e.g., accuracy) meet the expected baseline, preventing code changes from introducing accuracy issues.
3. ***Important Performance***: Verifies whether performance (e.g., P99 latency, throughput) in core scenarios meets preset thresholds.
- ***L3 response validation (`advanced_model`)***: At `--run-level advanced_model`, `tests/helpers/assertions.py` adds semantic checks on top of L2 payload gates (see Chapter 1):
- **Speech / TTS** (`assert_audio_speech_response`): Whisper transcript of returned audio vs `request_config["input"]` (cosine similarity &gt; 0.9 when input text is set); optional `min_audio_bytes` floor; PCM harmonic-to-noise ratio when `response_format` is `pcm`; **preset voice gender** via `_assert_preset_voice_gender_from_audio` when `voice` matches a known preset in `_PRESET_VOICE_GENDER_MAP` (pitch/F0-based classifier; skipped for unknown voices or `pcm` output).
- **Omni multimodal** (`assert_omni_response`): non-empty text/audio outputs; `key_words` in transcript or text; text–audio similarity / containment; preset **`speaker`** gender check (same helper as TTS).
- **Diffusion** (`assert_*_diffusion_response`): image/video dimension and frame-count parity with request parameters where configured.
- **Accuracy suites**: pixel/video similarity and metric baselines under `/tests/e2e/accuracy/` (separate from inline helper assertions).
### 2.3 Test Directory and Execution Files
@@ -582,9 +591,9 @@ L3 level testing executes after code is merged into the main branch. Its core pu
**Explanation**:
@pytest.mark.advanced_model: Marks the test as L3 merge level, indicating deep validation with real models. @pytest.mark.full_model: Marks L4 nightly-only suites (e.g. `test_*_expansion.py`, doc examples).
@pytest.mark.advanced_model: Marks the test as L3 merge level (`--run-level advanced_model`): real weights, Whisper/similarity/keyword checks, diffusion deep checks, and **preset voice/speaker gender** validation where applicable. @pytest.mark.full_model: Marks L4 nightly-only suites (e.g. `test_*_expansion.py`, doc examples).
@pytest.mark.core_model: Marks the test as L1 or L2 level, indicating that this test case validates the basic functionality of the core model. It uses mock weights and only checks if the relevant interface functions correctly.
@pytest.mark.core_model: Marks the test as L1 or L2 level. At `--run-level core_model`, validation is limited to request success plus cheap payload checks (e.g. non-empty audio bytes, response format/content-type, non-empty diffusion outputs)—not Whisper, keyword, or accuracy gates. Deploy YAML may use `load_format: dummy` for fast PR feedback.
@pytest.mark.parametrize: A parameterization decorator that allows abstracting test data into parameters, enabling reuse of the same test logic across different data configurations. indirect=True indicates that parameters will be passed to the fixture for processing.
@@ -680,7 +689,7 @@ L3 level testing executes after code is merged into the main branch. Its core pu
**Single Request**: The comment clearly states this is a single-request completion test. For concurrent testing, it can be extended to multiple requests using request_num = n.
**Implicit Validation**: The `send_omni_request` and `send_diffusion_request` methods internally includes validation logic dynamically selected based on the --run-level parameter: core_model performs basic validation, while advanced_model and full_model perform deep validation.
**Implicit Validation**: `send_omni_request`, `send_audio_speech_request`, and `send_diffusion_request` call `assert_*_response` helpers with the session `--run-level`. At `core_model`, checks are limited to success plus cheap payload sanity (non-empty audio/media, format/content-type, optional `min_audio_bytes`). At `advanced_model` and `full_model`, deep validation adds Whisper transcripts, keyword/similarity, diffusion dimension checks, PCM HNR, etc.
**Audio output debugging**: Deep validation may transcribe returned audio via `convert_audio_bytes_to_text` (Whisper). If an audio keyword or text–audio similarity assertion fails, set `VLLM_OMNI_KEEP_REQUEST_MEDIA=1` before running pytest to keep the intermediate WAV files for inspection (see [Test helper environment variables](#test-helper-environment-variables)).
@@ -101,7 +101,7 @@ def _extract_sample_rate(audio_mm: dict) -> int:
def _modify_deploy_config(base_path: str, async_chunk: bool) -> str:
"""Build deploy config with explicit sync/async mode and eager execution.
Mirrors the logic in ``tests/e2e/offline_inference/test_glm_tts.py``
Mirrors the logic in ``tests/e2e/offline_inference/test_glm_tts_expansion.py``
(``_get_deploy_config``) so that example runs match CI behavior.
"""
with open(base_path) as f:
+2 -1
View File
@@ -68,7 +68,8 @@ dev = [
"torchdiffeq>=0.2.5", # for covo_audio Test
"FlagEmbedding", # for hunyuan_image3 tests
"kernels<0.15.0", # for accuracy tests comparison with diffusers output (0.15.0 incompatible with diffusers 0.38.0)
"s3tokenizer>=0.3.0" # for tts test
"s3tokenizer>=0.3.0", # for tts test
"step-audio2>=1.0.0", # Step-Audio2 Token2Wav (flashcosyvoice / hyperpyyaml)
]
demo = [
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""HTTP + WebSocket validation for Qwen3-TTS Base: ``/v1/audio/speech``, ``/v1/audio/speech/stream``, batch, voices."""
"""HTTP + WebSocket validation for Qwen3-TTS Base and Higgs Audio V3 ``/v1/audio/speech`` (and related routes)."""
from __future__ import annotations
@@ -57,6 +57,19 @@ _QWEN3_TTS_SPEECH = [
),
]
_HIGGS_AUDIO_V3_SPEECH = [
pytest.param(
OmniServerParams(
model="bosonai/higgs-audio-v3-tts-4b",
stage_config_path=get_deploy_config_path("higgs_multimodal_qwen3.yaml"),
server_args=_SPEECH_SERVER_ARGS,
env_dict={"VLLM_USE_DEEP_GEMM": "0", "VLLM_MOE_USE_DEEP_GEMM": "0"},
),
id="higgs_audio_v3",
marks=_L4_SPEECH_HW,
),
]
def _apply_batch_overrides(body: dict[str, object], *, loc: str, overrides: dict[str, object]) -> None:
if loc == "batch":
@@ -240,6 +253,78 @@ def test_speech_invalid_field_values(
)
# Higgs Audio V3: ``references[]`` cookbook alias and serving-layer 400 rejections.
# Raw HTTP keeps JSON bodies intact (OpenAI SDK strips unknown fields such as ``references``).
@pytest.mark.parametrize(
"extra_json, err_message",
[
pytest.param({"input": "", "response_format": "wav"}, ("input", "empty"), id="input_empty"),
pytest.param({"input": " ", "response_format": "wav"}, ("input", "empty"), id="input_whitespace_only"),
pytest.param(
{"input": "Hello world.", "max_new_tokens": 0}, ("max_new_tokens", "least 1"), id="max_new_tokens_below_min"
),
pytest.param(
{
"input": "Hello world.",
"references": [
{"audio_path": REF_AUDIO_URL, "text": REF_TEXT},
{"audio_path": REF_AUDIO_URL, "text": REF_TEXT},
],
"response_format": "wav",
},
("references", "only supports a single reference"),
id="multi_reference_payload",
),
pytest.param(
{"input": "Hello world.", "references": [], "response_format": "wav"},
("references", "non-empty"),
id="references_empty_array",
),
pytest.param(
{"input": "Hello world.", "references": [{"text": REF_TEXT}], "response_format": "wav"},
("audio_path", "required"),
id="references_missing_audio_path",
),
pytest.param(
{"input": "Hello world.", "references": [1], "response_format": "wav"},
("references[0]", "must be an object"),
id="references_bad_element_type",
),
pytest.param(
{
"input": "Hello world.",
"ref_audio": REF_AUDIO_URL,
"references": [{"audio_path": "https://example.com/other.wav"}],
"response_format": "wav",
},
("mutually exclusive", "ref_audio", "references"),
id="ref_audio_and_references_exclusive",
),
pytest.param(
{
"input": "Hello world.",
"ref_text": "different transcript",
"references": [{"audio_path": REF_AUDIO_URL, "text": REF_TEXT}],
"response_format": "wav",
},
("references[0].text", "ref_text", "conflict"),
id="references_text_conflicts_ref_text",
),
],
)
@pytest.mark.parametrize("omni_server", _HIGGS_AUDIO_V3_SPEECH, indirect=True)
def test_speech_higgs_invalid_field_values(
omni_server: OmniServer,
openai_client: OpenAIClientHandler,
extra_json: dict[str, object],
err_message: str | tuple[str, ...],
) -> None:
body = {"model": omni_server.model, **extra_json}
openai_client.send_audio_speech_http_request(
{"json": body, "timeout": 120, "err_code": 400, "err_message": err_message}
)
# ─────────────────────────────────────────────────────────────────────────────
# POST /v1/audio/speech/batch (JSON)
# ─────────────────────────────────────────────────────────────────────────────
@@ -266,7 +266,6 @@ def _load_mooncake_config(host: str, rpc_port: int, http_port: int) -> str:
return temp_file.name
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.diffusion
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@@ -227,7 +227,6 @@ def _resolve_deploy_config(config_path: str, run_level: str) -> str:
return config_path
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.diffusion
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
@@ -244,7 +243,6 @@ def test_bagel_img2img_shared_memory_connector(run_level):
_validate_pixels(generated_image)
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.diffusion
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
@@ -21,7 +21,7 @@ models = [os.environ.get("AUDIOX_TEST_MODEL", "zhangj1an/audiox_random")]
_OMNI_RUNNER_PARAMS = [(m, None, {"model_class_name": "AudioXPipeline"}) for m in models]
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.diffusion,
pytest.mark.parametrize("omni_runner", _OMNI_RUNNER_PARAMS, indirect=True, ids=models),
]
@@ -141,7 +141,7 @@ _OMNI_RUNNER_PARAMS = [
),
]
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.tts,
pytest.mark.parametrize("omni_runner", _OMNI_RUNNER_PARAMS, indirect=True),
]
@@ -21,7 +21,7 @@ def get_question():
return "请回答这段音频里的问题。"
@pytest.mark.full_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100"}, num_cards={"cuda": 1})
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
@@ -30,7 +30,7 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion, pytest.mark.cache, pytest.mark.slow]
pytestmark = [pytest.mark.diffusion, pytest.mark.cache, pytest.mark.slow]
REPO_ROOT = Path(__file__).resolve().parents[3]
DEFAULT_MODEL_PATH = REPO_ROOT / "dreamid_omni"
@@ -34,7 +34,7 @@ test_params = [(model, stage_config) for model in models for stage_config in sta
DYNIN_CONFIG_PATH = str(_DEFAULT_DYNIN_CONFIG_PATH) if _DEFAULT_DYNIN_CONFIG_PATH is not None else None
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.omni,
pytest.mark.parametrize("omni_runner", test_params, indirect=True),
]
@@ -290,7 +290,7 @@ def _numel(value: Any) -> int:
return 0
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
def test_dynin_t2i_decode_to_image(omni_runner) -> None:
_configure_dynin_config_env()
prompt = _build_t2i_decode_prompt(dynin_config_path=DYNIN_CONFIG_PATH)
@@ -304,7 +304,7 @@ def test_dynin_t2i_decode_to_image(omni_runner) -> None:
assert _numel(image_value) > 0
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
def test_dynin_mmu_to_text(omni_runner) -> None:
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(omni_runner.model_name, trust_remote_code=True)
@@ -322,7 +322,7 @@ def test_dynin_mmu_to_text(omni_runner) -> None:
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
def test_dynin_image_to_text(omni_runner) -> None:
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(omni_runner.model_name, trust_remote_code=True)
@@ -341,7 +341,7 @@ def test_dynin_image_to_text(omni_runner) -> None:
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
def test_dynin_speech_to_text(omni_runner) -> None:
_configure_dynin_config_env()
tokenizer = AutoTokenizer.from_pretrained(omni_runner.model_name, trust_remote_code=True)
@@ -360,7 +360,7 @@ def test_dynin_speech_to_text(omni_runner) -> None:
assert text_content
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
def test_dynin_t2s_decode_to_audio(omni_runner) -> None:
_configure_dynin_config_env()
prompt = _build_t2s_decode_prompt(dynin_config_path=DYNIN_CONFIG_PATH)
@@ -30,7 +30,7 @@ _OMNI_RUNNER_PARAM = (
)
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.diffusion,
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
@@ -105,7 +105,7 @@ def _generate_image(model_name: str, **extra_kwargs) -> tuple[list, float]:
@pytest.mark.skip(reason="BF16 baseline model cannot be loaded correctly yet.")
@pytest.mark.full_model
@pytest.mark.slow
@pytest.mark.diffusion
@hardware_test(res={"cuda": "L4"})
@pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)
@@ -124,7 +124,7 @@ def test_flux_autoround_w4a16_generates_image(omni_runner: OmniRunner):
@pytest.mark.skip(reason="BF16 baseline model cannot be loaded correctly yet.")
@pytest.mark.full_model
@pytest.mark.slow
@pytest.mark.diffusion
@hardware_test(res={"cuda": "L4"})
def test_flux_autoround_w4a16_memory_savings():
@@ -14,6 +14,7 @@ from __future__ import annotations
import pytest
from vllm.assets.image import ImageAsset
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunnerHandler
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
@@ -30,7 +31,7 @@ _OMNI_RUNNER_PARAM = (
)
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.diffusion,
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
@@ -45,6 +46,7 @@ def _sampling_512() -> OmniDiffusionSamplingParams:
)
@hardware_test(res={"cuda": "H100"}, num_cards=2)
def test_flux_kontext_text_to_image(omni_runner_handler: OmniRunnerHandler):
"""Test FluxKontext text-to-image generation with real model."""
request_config = {
@@ -55,6 +57,7 @@ def test_flux_kontext_text_to_image(omni_runner_handler: OmniRunnerHandler):
omni_runner_handler.send_diffusion_request(request_config)
@hardware_test(res={"cuda": "H100"}, num_cards=2)
def test_flux_kontext_image_edit(omni_runner_handler: OmniRunnerHandler):
"""Test FluxKontext image-to-image editing with real model."""
input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB")
@@ -5,6 +5,7 @@
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunnerHandler
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
@@ -13,12 +14,13 @@ MODEL = "black-forest-labs/FLUX.1-schnell"
_OMNI_RUNNER_PARAM = (MODEL, None)
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.diffusion,
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_flux_schnell_text_to_image(omni_runner_handler: OmniRunnerHandler):
"""Test FLUX.1-schnell text-to-image generation."""
request_config = {
@@ -1,10 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E Offline inference tests for GLM-TTS voice cloning.
GLM-TTS is a zero-shot voice cloning model that always requires ref_audio.
There is no text-only / non-clone inference path in the official implementation.
"""
"""E2E expansion tests for GLM-TTS voice cloning offline inference (nightly CI)."""
import os
import tempfile
@@ -23,11 +19,12 @@ from tests.helpers.runtime import OmniResponse
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
from vllm_omni.model_executor.models.glm_tts.glm_tts import build_glm_tts_prefill_metadata
pytestmark = [pytest.mark.slow, pytest.mark.tts]
MODEL = os.environ.get("GLM_TTS_MODEL_PATH", "zai-org/GLM-TTS")
REF_TEXT = "他当时还跟线下其他的站姐吵架,然后,打架进局子了。"
DEPLOY_CONFIG = get_deploy_config_path("glm_tts.yaml")
REFERENCE_PROMPT_WAV_PATH = get_asset_path("glm_tts/jiayan_zh.wav")
ASYNC_CHUNK_MODES = [
@@ -84,9 +81,7 @@ def _get_deploy_config(*, async_chunk: bool) -> str:
)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("async_chunk", ASYNC_CHUNK_MODES)
def test_offline_voice_clone_zh(async_chunk: bool) -> None:
"""
@@ -94,9 +89,6 @@ def test_offline_voice_clone_zh(async_chunk: bool) -> None:
Deploy Setting: glm_tts.yaml with explicit sync/async mode, enforce_eager
Input Modal: text + ref_audio + ref_text
Output Modal: audio
Uses the official jiayan_zh.wav reference audio from the upstream
GLM-TTS repository (real human speech matching REF_TEXT).
"""
synth_text = "今天天气真不错,适合出去散散步。"
prompt_audio = _load_ref_audio()
@@ -144,5 +136,5 @@ def test_offline_voice_clone_zh(async_chunk: bool) -> None:
audio_format="audio/wav",
),
{"input": synth_text, "response_format": "wav"},
run_level="advanced_model",
run_level="full_model",
)
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
End-to-end offline inference tests for higgs-audio v2.
E2E Offline expansion tests for higgs-audio v2.
Mirrors examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py:
build Stage-0 prompt_token_ids with the model's own processor + plain-text
@@ -34,6 +34,8 @@ from tests.helpers.media import get_asset_path
from tests.helpers.runtime import OmniRunner
from tests.helpers.stage_config import get_deploy_config_path
pytestmark = [pytest.mark.slow, pytest.mark.tts]
MODEL = "bosonai/higgs-audio-v2-generation-3B-base"
STAGE_CONFIG = get_deploy_config_path("higgs_audio_v2.yaml")
SAMPLE_RATE = 24_000
@@ -89,9 +91,7 @@ def _extract_pcm(outputs) -> torch.Tensor:
raise AssertionError(f"no audio payload in any stage output (got {len(outputs)} stages)")
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True)
def test_higgs_audio_v2_offline_plain_text(omni_runner_function: OmniRunner) -> None:
"""
@@ -131,9 +131,7 @@ def test_higgs_audio_v2_offline_plain_text(omni_runner_function: OmniRunner) ->
assert np.isfinite(arr).all(), "audio contains non-finite samples (nan/inf)"
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True)
def test_higgs_audio_v2_offline_batch_two_prompts(omni_runner_function: OmniRunner) -> None:
"""
@@ -199,9 +197,7 @@ def _compose_voice_clone_request(model_name: str, text: str, ref_text: str, ref_
}
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True)
def test_higgs_audio_v2_offline_voice_clone(omni_runner_function: OmniRunner) -> None:
"""
@@ -36,7 +36,7 @@ _OMNI_RUNNER_PARAM_OFFLOAD = (
)
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.diffusion,
pytest.mark.parametrize(
"omni_runner",
@@ -99,7 +99,7 @@ def _sample_pixels(img_tensor: torch.Tensor) -> list[float]:
# End-to-end test
# ---------------------------------------------------------------------------
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.diffusion,
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
@@ -49,7 +49,7 @@ test_params_thinker = [(m, c) for m in models for c in [get_eager_config_thinker
test_params = [(m, c) for m in models for c in [get_eager_config()]]
@pytest.mark.core_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
@@ -65,7 +65,7 @@ def test_text_to_text(omni_runner, omni_runner_handler) -> None:
omni_runner_handler.send_omni_request(request_config)
@pytest.mark.core_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
@@ -82,7 +82,7 @@ def test_image_to_text(omni_runner, omni_runner_handler) -> None:
omni_runner_handler.send_omni_request(request_config)
@pytest.mark.core_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
@@ -101,7 +101,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler) -> None:
omni_runner_handler.send_omni_request(request_config)
@pytest.mark.core_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
@@ -118,7 +118,7 @@ def test_video_to_text(omni_runner, omni_runner_handler) -> None:
omni_runner_handler.send_omni_request(request_config)
@pytest.mark.core_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
@@ -138,7 +138,7 @@ def test_mixed_to_text(omni_runner, omni_runner_handler) -> None:
omni_runner_handler.send_omni_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
@@ -154,7 +154,7 @@ def test_text_to_audio(omni_runner, omni_runner_handler) -> None:
omni_runner_handler.send_omni_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""End-to-end offline inference tests for Ming-omni-tts."""
"""E2E expansion tests for Ming-omni-tts offline inference (nightly CI)."""
import asyncio
import uuid
@@ -32,6 +32,12 @@ TEST_TEXT = "我会一直在这里陪着你,直到你慢慢地沉入那个最
TEST_INSTRUCTION = "轻柔的ASMR耳语,慢速,贴近麦克风"
MIN_AUDIO_SAMPLES = 1000
pytestmark = [
pytest.mark.slow,
pytest.mark.tts,
pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4704"),
]
@pytest.fixture(scope="module")
def ming_tokenizer():
@@ -163,8 +169,6 @@ def _extract_multimodal_output(output) -> Mapping[str, Any]:
raise AssertionError("No multimodal audio output found in Ming generate results")
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_ming_tts_offline_basic(ming_engine, ming_tokenizer) -> None:
"""Test blocking Ming generation through Omni."""
@@ -185,8 +189,6 @@ def test_ming_tts_offline_basic(ming_engine, ming_tokenizer) -> None:
assert sample_rate == SAMPLE_RATE, f"Expected Ming output sample rate {SAMPLE_RATE}, got {sample_rate}"
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_ming_tts_speaker_conditioning_differs(ming_engine, ming_tokenizer) -> None:
"""Test that different Ming speaker controls produce different waveform outputs."""
@@ -220,8 +222,6 @@ def test_ming_tts_speaker_conditioning_differs(ming_engine, ming_tokenizer) -> N
)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_ming_tts_multiple_prompts_queued(ming_engine, ming_tokenizer) -> None:
"""Regression: supported max_num_seqs=1 config must still drain queued prompts."""
@@ -251,8 +251,6 @@ def test_ming_tts_multiple_prompts_queued(ming_engine, ming_tokenizer) -> None:
assert np.max(np.abs(waveform.numpy())) > 0.01, f"Request {i} audio appears silent"
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_ming_tts_offline_streaming(async_omni_engine, ming_tokenizer) -> None:
"""Test async_chunk streaming Ming generation through AsyncOmni."""
@@ -2,9 +2,9 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E offline inference tests for MOSS-VoiceGenerator (MossTTSDelayModel, 1.7B).
Uses the standard omni_runner + pytestmark pattern (one module-scoped engine
per file, skill invariant I4). The realtime variant lives in the sibling file
test_moss_tts_realtime.py.
Weekly expansion coverage (``pytest.mark.slow``) for the delay-model path.
Upstream keeps the same tests in ``test_moss_tts.py``; this branch uses the
``*_expansion.py`` naming convention and deletes the base file on merge.
MOSS-VoiceGenerator synthesizes speech from a text *instruction* that describes
the desired voice (e.g. "a warm female voice with an American accent"). It does
@@ -14,6 +14,8 @@ pair into the (prompt_token_ids, codes.ref) grid that the MossTTSDelayModel
talker expects — same as examples/offline_inference/text_to_speech/moss_tts/
end2end.py:_build_unified_codes.
Realtime coverage: ``test_moss_tts_realtime_expansion.py``.
No determinism test: VoiceGenerator produces variable-length output even with
a fixed seed; waveform length reproducibility is not guaranteed.
"""
@@ -31,10 +33,6 @@ from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunner
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
MODEL = "OpenMOSS-Team/MOSS-VoiceGenerator"
SAMPLE_RATE = 24_000
@@ -58,18 +56,9 @@ _STAGE1_PARAMS = SamplingParams(
)
_SAMPLING = [_STAGE0_PARAMS, _STAGE1_PARAMS]
# ---------------------------------------------------------------------------
# Deploy config
# ---------------------------------------------------------------------------
def _get_test_config() -> str:
"""Derive a CI-friendly config from moss_voice_generator.yaml.
Reduces Stage 0 gpu_memory_utilization from 0.60 → 0.45 to leave headroom
for Stage 1 (0.12) on a shared L4/A10G. max_num_seqs=1 keeps per-test
peak memory predictable.
"""
"""Derive a CI-friendly config from moss_voice_generator.yaml."""
return modify_stage_config(
get_deploy_config_path("moss_voice_generator.yaml"),
updates={
@@ -83,35 +72,16 @@ def _get_test_config() -> str:
)
# ---------------------------------------------------------------------------
# pytestmark — one engine for the whole module
# ---------------------------------------------------------------------------
pytestmark = [
pytest.mark.slow,
pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4700"),
pytest.mark.full_model,
pytest.mark.tts,
pytest.mark.parametrize("omni_runner", [(MODEL, _get_test_config())], indirect=True),
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _build_request(text: str, instruction: str) -> dict:
"""Build a VoiceGenerator request using AutoProcessor.
Calls proc.build_user_message + proc() to produce the unified-codes grid
(L, 1+n_vq) and splits it into prompt_token_ids + codes.ref, exactly as
end2end.py:_build_unified_codes does for the voice_generator variant.
The processor (including its CPU-resident audio_tokenizer) is freed before
returning so it does not compete with the running engine.
Set MOSS_TTS_SKIP_ON_NET_FAIL=1 to skip in air-gapped environments where
the HF snapshot is not cached.
"""
"""Build a VoiceGenerator request using AutoProcessor."""
from transformers import AutoProcessor
try:
@@ -124,9 +94,9 @@ def _build_request(text: str, instruction: str) -> dict:
user_msg = proc.build_user_message(text=text, instruction=instruction)
batch = proc(conversations=[[user_msg]], mode="generation")
unified = batch["input_ids"][0] # (L, 1+n_vq)
unified = batch["input_ids"][0]
text_ids = unified[:, 0].tolist()
audio_codes = unified[:, 1:].contiguous().to(torch.int64) # (L, n_vq)
audio_codes = unified[:, 1:].contiguous().to(torch.int64)
del proc
gc.collect()
@@ -137,15 +107,6 @@ def _build_request(text: str, instruction: str) -> dict:
def _collect_audio(omni_runner: OmniRunner, request: dict) -> tuple[torch.Tensor, int]:
"""Run one request and return (waveform_cpu, sample_rate).
Iterates all OmniRequestOutputs from generate() via the top-level
multimodal_output property (which aggregates from completion outputs).
AR-stage outputs have empty multimodal_output and are silently skipped.
With async_chunk=True and FINAL_ONLY, the codec stage consolidates its
chunks into a single tensor before yielding — but we handle list fallback
for robustness.
"""
chunks: list[torch.Tensor] = []
sr_final = SAMPLE_RATE
for out in omni_runner.omni.generate(request, _SAMPLING):
@@ -172,14 +133,8 @@ def _collect_audio(omni_runner: OmniRunner, request: dict) -> tuple[torch.Tensor
return torch.cat(chunks, dim=0), sr_final
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
@pytest.mark.advanced_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_moss_tts_english(omni_runner: OmniRunner) -> None:
def test_moss_tts_delay_english(omni_runner: OmniRunner) -> None:
"""VoiceGenerator: English instruction produces non-empty 24 kHz audio."""
req = _build_request(
"Hello, this is a MOSS voice design test.",
@@ -192,9 +147,8 @@ def test_moss_tts_english(omni_runner: OmniRunner) -> None:
assert not torch.all(audio == 0), "Audio is silence"
@pytest.mark.advanced_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_moss_tts_chinese(omni_runner: OmniRunner) -> None:
def test_moss_tts_delay_chinese(omni_runner: OmniRunner) -> None:
"""VoiceGenerator: Chinese input produces non-empty audio."""
req = _build_request(
"你好,这是语音合成测试。",
@@ -207,9 +161,8 @@ def test_moss_tts_chinese(omni_runner: OmniRunner) -> None:
assert not torch.all(audio == 0)
@pytest.mark.advanced_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_moss_tts_batch(omni_runner: OmniRunner) -> None:
def test_moss_tts_delay_batch(omni_runner: OmniRunner) -> None:
"""VoiceGenerator: batch of two requests each returns non-empty audio."""
requests = [
_build_request("First sentence.", "a warm female voice"),
@@ -233,9 +186,6 @@ def test_moss_tts_batch(omni_runner: OmniRunner) -> None:
if isinstance(audio, torch.Tensor) and audio.numel() > 0:
results.append(audio.reshape(-1).cpu())
# With async_chunk streaming + FINAL_ONLY, each request produces one
# consolidated output. The >= 2 guard handles backward-compatibility if
# consolidation behavior changes.
assert len(results) >= 2, f"Expected at least 2 audio outputs, got {len(results)}"
for i, audio in enumerate(results):
assert audio.numel() > 0, f"Audio chunk[{i}] is empty"
@@ -32,7 +32,7 @@ _OMNI_RUNNER_PARAM = (
)
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.tts,
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
@@ -0,0 +1,125 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E offline inference tests for MOSS-TTS-Realtime (MossTTSRealtime, 1.7B).
Exercises the local-transformer generation path on L4. Delay-model coverage is in
``test_moss_tts_expansion.py``.
No determinism test: MossTTSRealtime is sensitive to async scheduling timing;
waveform reproducibility is not guaranteed across sequential ``generate`` calls.
"""
from __future__ import annotations
import os
import urllib.request
import pytest
import torch
from vllm import SamplingParams
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunner
from tests.helpers.stage_config import get_deploy_config_path
MODEL = "OpenMOSS-Team/MOSS-TTS-Realtime"
DEPLOY_CONFIG = get_deploy_config_path("moss_tts_realtime.yaml")
_OMNI_RUNNER_PARAM = (MODEL, DEPLOY_CONFIG, {"stage_init_timeout": 300})
pytestmark = [
pytest.mark.slow,
pytest.mark.tts,
pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4700"),
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
SAMPLE_RATE = 24_000
REF_AUDIO_URL = "https://raw.githubusercontent.com/OpenMOSS/MOSS-TTS/main/assets/audio/reference_zh_1.wav"
_DEFAULT_SAMPLING = SamplingParams(
temperature=1.7,
top_p=0.8,
top_k=25,
max_tokens=512,
seed=42,
detokenize=False,
)
@pytest.fixture(scope="session")
def ref_audio_path(tmp_path_factory) -> str:
cache_dir = tmp_path_factory.mktemp("moss_tts_realtime_ref")
target = cache_dir / "zh_1.wav"
try:
with urllib.request.urlopen(REF_AUDIO_URL, timeout=30) as resp:
target.write_bytes(resp.read())
except Exception as exc:
msg = f"Cannot fetch reference clip {REF_AUDIO_URL}: {exc}"
if os.environ.get("MOSS_TTS_SKIP_ON_NET_FAIL"):
pytest.skip(msg)
pytest.fail(msg)
if not target.exists() or target.stat().st_size == 0:
pytest.fail(f"Reference clip empty after download: {target}")
return str(target)
def _build_request(text: str, ref_audio_path: str, seed: int = 42) -> dict:
return {
"prompt": "<|im_start|>assistant\n",
"additional_information": {
"task_type": ["voice_clone"],
"text": [text],
"mode": ["voice_clone"],
"prompt_audio_path": [ref_audio_path],
"seed": [seed],
},
}
def _sampling_for(omni_runner: OmniRunner) -> SamplingParams | list[SamplingParams]:
omni = omni_runner.omni
if omni.num_stages == 1:
return _DEFAULT_SAMPLING
params = omni_runner.get_default_sampling_params_list()
params[0] = _DEFAULT_SAMPLING
return params
def _audio_from_stage(stage_outputs) -> tuple[torch.Tensor, int] | None:
mm = stage_outputs.multimodal_output
if not mm:
return None
audio = mm.get("audio")
if audio is None:
audio = mm.get("model_outputs")
if audio is None:
return None
if isinstance(audio, list):
audio = torch.cat(
[t.reshape(-1) for t in audio if isinstance(t, torch.Tensor) and t.numel() > 0],
dim=0,
)
if not isinstance(audio, torch.Tensor) or audio.numel() == 0:
return None
sr = mm.get("sr")
sample_rate = int(sr.item()) if sr is not None else SAMPLE_RATE
return audio.reshape(-1).cpu(), sample_rate
def _collect_audio(omni_runner: OmniRunner, request: dict) -> tuple[torch.Tensor, int]:
for stage_outputs in omni_runner.omni.generate(request, _sampling_for(omni_runner)):
parsed = _audio_from_stage(stage_outputs)
if parsed is not None:
return parsed
raise AssertionError("No stage outputs received")
@hardware_test(res={"cuda": "L4"})
def test_moss_tts_realtime_english(omni_runner: OmniRunner, ref_audio_path) -> None:
"""MossTTSRealtime: English voice_clone produces non-empty 24 kHz audio."""
req = _build_request("This is a real-time TTS streaming test.", ref_audio_path)
audio, sr = _collect_audio(omni_runner, req)
assert sr == SAMPLE_RATE, f"Expected {SAMPLE_RATE} Hz, got {sr}"
assert audio.numel() > 0, "Audio tensor is empty"
assert not torch.all(audio == 0), "Audio is silence"
@@ -1,113 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E offline inference test for MOSS-TTS-v1.5 (MossTTSDelay-8B).
MOSS-TTS-v1.5 is a continued-training upgrade of MOSS-TTS 1.0 with the SAME
``MossTTSDelay`` architecture and API, so it reuses the existing talker/codec
path unchanged — this test just pins that the 8B v1.5 checkpoint loads and
produces audio.
The model is 8B, so this lives in its own file (one module-scoped engine per
file, per skill invariant I4) and is gated behind an 80 GB GPU; the small-GPU
MossTTSDelay coverage stays in ``test_moss_tts.py`` (1.7B VoiceGenerator).
"""
from __future__ import annotations
import os
import urllib.request
import pytest
import torch
from vllm import SamplingParams
from tests.helpers.mark import hardware_test
from vllm_omni import Omni
# H100-gated (8B). Picked up by the nightly "TTS · Function Test with H100"
# job (-m "full_model and H100 and tts"); the L4 MOSS-TTS tests land in the
# sibling L4 tts lane instead.
# TODO: Fix this test
# pytestmark = [pytest.mark.full_model, pytest.mark.tts]
SAMPLE_RATE = 24_000
REF_AUDIO_URL = "https://raw.githubusercontent.com/OpenMOSS/MOSS-TTS/main/assets/audio/reference_zh_1.wav"
_MODEL = "OpenMOSS-Team/MOSS-TTS-v1.5"
_DEFAULT_SAMPLING = SamplingParams(
temperature=1.7,
top_p=0.8,
top_k=25,
max_tokens=512, # short for tests (~20 s at 24 kHz, ~12.5 tokens/s)
seed=42,
detokenize=False,
)
@pytest.fixture(scope="session")
def ref_audio_path(tmp_path_factory) -> str:
"""Download the upstream reference clip once per session.
Set ``MOSS_TTS_SKIP_ON_NET_FAIL=1`` to skip in air-gapped environments.
"""
target = tmp_path_factory.mktemp("moss_tts_v15_ref") / "zh_1.wav"
try:
with urllib.request.urlopen(REF_AUDIO_URL, timeout=30) as resp:
target.write_bytes(resp.read())
except Exception as exc: # noqa: BLE001
msg = f"Cannot fetch reference clip {REF_AUDIO_URL}: {exc}"
if os.environ.get("MOSS_TTS_SKIP_ON_NET_FAIL"):
pytest.skip(msg)
pytest.fail(msg)
if not target.exists() or target.stat().st_size == 0:
pytest.fail(f"Reference clip empty after download: {target}")
return str(target)
@pytest.fixture(scope="module")
def v15_engine():
return Omni(model=_MODEL, stage_init_timeout=600)
def _build_request(text: str, ref_audio_path: str) -> dict:
return {
"prompt": "<|im_start|>assistant\n",
"additional_information": {
"task_type": ["voice_clone"],
"text": [text],
"mode": ["voice_clone"],
"prompt_audio_path": [ref_audio_path],
"seed": [42],
},
}
def _collect_audio(omni: Omni, request: dict) -> tuple[torch.Tensor, int]:
for stage_outputs in omni.generate(request, _DEFAULT_SAMPLING):
for req_output in stage_outputs.request_output:
mm = req_output.outputs[0].multimodal_output
assert mm is not None, "Expected non-None multimodal_output"
audio = mm.get("audio")
if audio is None:
audio = mm.get("model_outputs")
sr = mm.get("sr")
assert audio is not None, "Expected 'audio' or 'model_outputs' in multimodal_output"
if isinstance(audio, list):
audio = torch.cat(
[t.reshape(-1) for t in audio if isinstance(t, torch.Tensor) and t.numel() > 0],
dim=0,
)
assert isinstance(audio, torch.Tensor), f"Expected Tensor, got {type(audio)}"
return audio.reshape(-1).cpu(), int(sr.item()) if sr is not None else SAMPLE_RATE
raise AssertionError("No stage outputs received")
@hardware_test(res={"cuda": "H100"})
def test_moss_tts_v15_voice_clone(v15_engine, ref_audio_path):
"""MOSS-TTS-v1.5 (8B): voice_clone produces non-empty 24 kHz audio."""
req = _build_request("Hello, this is a MOSS-TTS v1.5 voice cloning test.", ref_audio_path)
audio, sr = _collect_audio(v15_engine, req)
assert sr == SAMPLE_RATE, f"Expected {SAMPLE_RATE} Hz, got {sr}"
assert audio.numel() > 0, "Audio tensor is empty"
assert not torch.all(audio == 0), "Audio is silence"
@@ -0,0 +1,115 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E offline inference test for MOSS-TTS-v1.5 (MossTTSDelay-8B).
MOSS-TTS-v1.5 is a continued-training upgrade of MOSS-TTS 1.0 with the SAME
``MossTTSDelay`` architecture and API. The 8B checkpoint is H100-gated; small-GPU
MossTTSDelay coverage stays in ``test_moss_tts_expansion.py``.
"""
from __future__ import annotations
import os
import urllib.request
import pytest
import torch
from vllm import SamplingParams
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunner
from tests.helpers.stage_config import get_deploy_config_path
MODEL = "OpenMOSS-Team/MOSS-TTS-v1.5"
DEPLOY_CONFIG = get_deploy_config_path("moss_tts.yaml")
_OMNI_RUNNER_PARAM = (MODEL, DEPLOY_CONFIG, {"stage_init_timeout": 600})
pytestmark = [
pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4643"),
pytest.mark.slow,
pytest.mark.tts,
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
SAMPLE_RATE = 24_000
REF_AUDIO_URL = "https://raw.githubusercontent.com/OpenMOSS/MOSS-TTS/main/assets/audio/reference_zh_1.wav"
_DEFAULT_SAMPLING = SamplingParams(
temperature=1.7,
top_p=0.8,
top_k=25,
max_tokens=512,
seed=42,
detokenize=False,
)
@pytest.fixture(scope="session")
def ref_audio_path(tmp_path_factory) -> str:
target = tmp_path_factory.mktemp("moss_tts_v15_ref") / "zh_1.wav"
try:
with urllib.request.urlopen(REF_AUDIO_URL, timeout=30) as resp:
target.write_bytes(resp.read())
except Exception as exc: # noqa: BLE001
msg = f"Cannot fetch reference clip {REF_AUDIO_URL}: {exc}"
if os.environ.get("MOSS_TTS_SKIP_ON_NET_FAIL"):
pytest.skip(msg)
pytest.fail(msg)
if not target.exists() or target.stat().st_size == 0:
pytest.fail(f"Reference clip empty after download: {target}")
return str(target)
def _build_request(text: str, ref_audio_path: str) -> dict:
return {
"prompt": "<|im_start|>assistant\n",
"additional_information": {
"task_type": ["voice_clone"],
"text": [text],
"mode": ["voice_clone"],
"prompt_audio_path": [ref_audio_path],
"seed": [42],
},
}
def _sampling_for(omni_runner: OmniRunner) -> SamplingParams | list[SamplingParams]:
omni = omni_runner.omni
if omni.num_stages == 1:
return _DEFAULT_SAMPLING
params = omni_runner.get_default_sampling_params_list()
params[0] = _DEFAULT_SAMPLING
return params
def _collect_audio(omni_runner: OmniRunner, request: dict) -> tuple[torch.Tensor, int]:
for stage_outputs in omni_runner.omni.generate(request, _sampling_for(omni_runner)):
mm = stage_outputs.multimodal_output
if not mm:
continue
audio = mm.get("audio")
if audio is None:
audio = mm.get("model_outputs")
if audio is None:
continue
if isinstance(audio, list):
audio = torch.cat(
[t.reshape(-1) for t in audio if isinstance(t, torch.Tensor) and t.numel() > 0],
dim=0,
)
if not isinstance(audio, torch.Tensor) or audio.numel() == 0:
continue
sr = mm.get("sr")
return audio.reshape(-1).cpu(), int(sr.item()) if sr is not None else SAMPLE_RATE
raise AssertionError("No stage outputs received")
@hardware_test(res={"cuda": "H100"})
def test_moss_tts_v15_voice_clone(omni_runner: OmniRunner, ref_audio_path) -> None:
"""MOSS-TTS-v1.5 (8B): voice_clone produces non-empty 24 kHz audio."""
req = _build_request("Hello, this is a MOSS-TTS v1.5 voice cloning test.", ref_audio_path)
audio, sr = _collect_audio(omni_runner, req)
assert sr == SAMPLE_RATE, f"Expected {SAMPLE_RATE} Hz, got {sr}"
assert audio.numel() > 0, "Audio tensor is empty"
assert not torch.all(audio == 0), "Audio is silence"
@@ -31,7 +31,7 @@ _OMNI_RUNNER_PARAM = (
)
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.tts,
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
@@ -23,7 +23,7 @@ from tests.helpers.media import (
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.omni,
]
@@ -36,6 +36,10 @@ BASELINE_MODEL = os.environ.get("QWEN2_5_OMNI_BASELINE_MODEL", BASELINE_MODEL)
_CI_DEPLOY = get_deploy_config_path("ci/qwen2_5_omni.yaml")
_SKIP_ISSUE_4663 = pytest.mark.skip(
reason="https://github.com/vllm-project/vllm-omni/issues/4663",
)
def _get_stage_config():
"""Build a CI-friendly stage config with eager mode."""
@@ -61,6 +65,7 @@ quant_params = [(QUANTIZED_MODEL, stage_config)]
# ------------------------------------------------------------------
@_SKIP_ISSUE_4663
@hardware_test(res={"cuda": "L4"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
def test_text_to_text(omni_runner, omni_runner_handler):
@@ -79,6 +84,7 @@ def test_text_to_text(omni_runner, omni_runner_handler):
# ------------------------------------------------------------------
@_SKIP_ISSUE_4663
@hardware_test(res={"cuda": "L4"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
def test_audio_to_text(omni_runner, omni_runner_handler):
@@ -102,6 +108,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler):
# ------------------------------------------------------------------
@_SKIP_ISSUE_4663
@hardware_test(res={"cuda": "L4"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
def test_image_to_text(omni_runner, omni_runner_handler):
@@ -123,6 +130,7 @@ def test_image_to_text(omni_runner, omni_runner_handler):
# ------------------------------------------------------------------
@_SKIP_ISSUE_4663
@hardware_test(res={"cuda": "L4"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
def test_video_to_text(omni_runner, omni_runner_handler):
@@ -144,6 +152,7 @@ def test_video_to_text(omni_runner, omni_runner_handler):
# ------------------------------------------------------------------
@_SKIP_ISSUE_4663
@hardware_test(res={"cuda": "L4"}, num_cards=4)
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
def test_mix_to_audio(omni_runner, omni_runner_handler):
@@ -51,7 +51,7 @@ def get_question(prompt_type="mix"):
return prompts.get(prompt_type, prompts["mix"])
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325", "xpu": "B60"}, num_cards={"cuda": 4, "rocm": 2, "xpu": 3})
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
@@ -82,7 +82,7 @@ def test_mix_to_audio(omni_runner, omni_runner_handler) -> None:
omni_runner_handler.send_omni_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325", "xpu": "B60"}, num_cards={"cuda": 4, "rocm": 2, "xpu": 3})
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
@@ -124,7 +124,7 @@ def _make_file_lora_request(adapter_dir: Path) -> LoRARequest:
# ---------------------------------------------------------------------------
@pytest.mark.full_model
@pytest.mark.slow
@pytest.mark.diffusion
@hardware_test(res={"cuda": "H100"})
def test_sensenova_u1_lora_scale_and_deactivation(tmp_path) -> None:
@@ -153,9 +153,9 @@ def _generate_sensenova_u1_img2img(
return generated_image
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.diffusion
@pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4636")
@hardware_test(res={"cuda": "H100"})
def test_sensenova_u1_img2img(run_level):
"""Test SenseNova-U1 img2img (single-stage diffusion, no deploy YAML)."""
@@ -165,11 +165,11 @@ def test_sensenova_u1_img2img(run_level):
stage_configs_path=None,
) as runner:
generated_image = _generate_sensenova_u1_img2img(runner.omni, input_image)
if run_level == "advanced_model":
if run_level == "full_model" or run_level == "advanced_model":
_validate_pixels(generated_image)
@pytest.mark.core_model
@pytest.mark.slow
@pytest.mark.diffusion
@pytest.mark.cache
@hardware_test(res={"cuda": "H100"})
@@ -143,9 +143,9 @@ def _generate_sensenova_u1_image(
return generated_image
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.diffusion
@pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4636")
@hardware_test(res={"cuda": "H100"})
def test_sensenova_u1_text2img(run_level):
"""Test SenseNova-U1 text2img (single-stage diffusion, no deploy YAML)."""
@@ -154,11 +154,11 @@ def test_sensenova_u1_text2img(run_level):
stage_configs_path=None,
) as runner:
generated_image = _generate_sensenova_u1_image(runner.omni)
if run_level == "advanced_model":
if run_level == "full_model" or run_level == "advanced_model":
_validate_pixels(generated_image)
@pytest.mark.core_model
@pytest.mark.slow
@pytest.mark.diffusion
@pytest.mark.cache
@hardware_test(res={"cuda": "H100"})
@@ -23,7 +23,7 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
pytestmark = [pytest.mark.slow, pytest.mark.diffusion]
_MODEL_REPO = "stabilityai/stable-audio-open-1.0"
@@ -1,303 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E tests for Step-Audio2 model with audio input and audio output.
These tests verify:
1. Processor loads correctly and handles audio input
2. Field configuration for vLLM batching (flat_from_sizes)
3. Tensor shapes are correct (3D not 4D for profiling)
4. Basic E2E inference works with audio input
"""
from pathlib import Path
import numpy as np
import pytest
import torch
from tests.helpers.runtime import OmniRunner
models = ["stepfun-ai/Step-Audio-2-mini"]
stage_configs = [str(Path(__file__).parent / "stage_configs" / "step_audio2_ci.yaml")]
test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
def create_dummy_audio(sample_rate: int = 16000, duration_sec: float = 1.0) -> tuple[np.ndarray, int]:
"""Create a dummy audio signal (sine wave) for testing."""
t = np.linspace(0, duration_sec, int(sample_rate * duration_sec), dtype=np.float32)
audio = 0.5 * np.sin(2 * np.pi * 440 * t)
return audio, sample_rate
class StepAudio2OmniRunner(OmniRunner):
"""Test runner specialized for Step-Audio2 model."""
def get_omni_inputs(
self,
prompts: list[str] | str,
system_prompt: str | None = None,
audios=None,
**kwargs,
) -> list[dict]:
"""Construct Omni input format for Step-Audio2."""
if system_prompt is None:
system_prompt = "你是一个语音对话助手,能够理解音频输入并生成语音回复。"
audio_placeholder = "<audio_patch>"
if isinstance(prompts, str):
prompts = [prompts]
num_prompts = len(prompts)
if audios is None:
audios_list = [None] * num_prompts
elif isinstance(audios, list):
audios_list = audios if len(audios) == num_prompts else [audios[0]] * num_prompts
else:
audios_list = [audios] * num_prompts
omni_inputs = []
for i, prompt_text in enumerate(prompts):
user_content = ""
multi_modal_data = {}
audio = audios_list[i]
if audio is not None:
user_content += audio_placeholder
multi_modal_data["audio"] = audio
user_content += prompt_text
full_prompt = (
f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
f"<|im_start|>user\n{user_content}<|im_end|>\n"
f"<|im_start|>assistant\n"
)
input_dict = {"prompt": full_prompt}
if multi_modal_data:
input_dict["multi_modal_data"] = multi_modal_data
omni_inputs.append(input_dict)
return omni_inputs
@pytest.fixture(scope="session")
def step_audio2_runner():
return StepAudio2OmniRunner
@pytest.mark.core_model
def test_processor_loads_correctly():
"""Test that Step-Audio2 processor can be imported and initialized."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
StepAudio2MultiModalProcessor,
StepAudio2ProcessingInfo,
StepAudio2Processor,
)
assert StepAudio2MultiModalProcessor is not None
assert StepAudio2ProcessingInfo is not None
assert StepAudio2Processor is not None
@pytest.mark.core_model
def test_audio_preprocessing():
"""Test that audio preprocessing produces correct tensor shapes."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
log_mel_spectrogram,
padding_mels,
)
audio, sample_rate = create_dummy_audio(sample_rate=16000, duration_sec=1.0)
mel = log_mel_spectrogram(audio)
assert mel.ndim == 2, f"Expected 2D tensor, got {mel.ndim}D"
assert mel.shape[0] == 128, f"Expected 128 mel bins, got {mel.shape[0]}"
mels = [mel, mel]
padded_mels, mel_lens = padding_mels(mels)
assert padded_mels.ndim == 3, f"Expected 3D tensor, got {padded_mels.ndim}D"
assert padded_mels.shape[0] == 2, f"Expected batch size 2, got {padded_mels.shape[0]}"
assert padded_mels.shape[1] == 128, f"Expected 128 mel bins, got {padded_mels.shape[1]}"
@pytest.mark.core_model
def test_feature_length_calculation():
"""Test that audio feature length calculation is correct."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
calculate_audio_feature_length,
)
assert calculate_audio_feature_length(1000) == 125
assert calculate_audio_feature_length(100) > 0
assert calculate_audio_feature_length(1) >= 1
@pytest.mark.core_model
def test_token_config_constants():
"""Test that token configuration constants are set correctly."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_constants import (
DEFAULT_TOKEN_CONFIG,
STEP_AUDIO2_AUDIO_END,
STEP_AUDIO2_AUDIO_PATCH_TOKEN_ID,
STEP_AUDIO2_AUDIO_START,
STEP_AUDIO2_AUDIO_VOCAB_SIZE,
STEP_AUDIO2_TEXT_MAX,
)
assert STEP_AUDIO2_TEXT_MAX < STEP_AUDIO2_AUDIO_START, "Text tokens should come before audio tokens"
assert STEP_AUDIO2_AUDIO_END == STEP_AUDIO2_AUDIO_START + STEP_AUDIO2_AUDIO_VOCAB_SIZE - 1
assert STEP_AUDIO2_AUDIO_PATCH_TOKEN_ID > STEP_AUDIO2_TEXT_MAX
assert STEP_AUDIO2_AUDIO_PATCH_TOKEN_ID < STEP_AUDIO2_AUDIO_START
assert DEFAULT_TOKEN_CONFIG.text_max == STEP_AUDIO2_TEXT_MAX
assert DEFAULT_TOKEN_CONFIG.audio_start == STEP_AUDIO2_AUDIO_START
@pytest.mark.core_model
def test_mm_field_config_structure():
"""Test that multimodal field config is properly structured for vLLM batching."""
from vllm.multimodal.inputs import MultiModalFieldConfig
audio_lens = torch.tensor([100, 200, 150], dtype=torch.int32)
field_config = MultiModalFieldConfig.flat_from_sizes("audio", audio_lens)
assert field_config is not None
@pytest.mark.core_model
def test_audio_encoder_output_shape():
"""Test that audio encoder produces correct output shapes."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
Adaptor,
AudioEncoder,
)
encoder = AudioEncoder(n_mels=128, n_ctx=1500, n_state=512, n_head=8, n_layer=6)
batch_size = 2
n_mels = 128
time_steps = 400
x = torch.randn(batch_size, n_mels, time_steps)
x_len = torch.tensor([time_steps, time_steps // 2], dtype=torch.int32)
encoded, encoded_lens = encoder(x, x_len)
assert encoded.ndim == 3, f"Expected 3D tensor, got {encoded.ndim}D"
assert encoded.shape[0] == batch_size
assert encoded.shape[2] == 512
adapter = Adaptor(n_state=512, n_hidden=4096, kernel_size=3, stride=2)
adapted = adapter(encoded)
assert adapted.ndim == 3
assert adapted.shape[0] == batch_size
assert adapted.shape[2] == 4096
@pytest.mark.core_model
def test_token_separation():
"""Test that token separation works correctly."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
StepAudio2ThinkerForConditionalGeneration,
)
token_ids = [100, 200, 151700, 300, 151800, 400]
text_tokens, audio_tokens = StepAudio2ThinkerForConditionalGeneration.separate_tokens(token_ids)
assert text_tokens == [100, 200, 300, 400]
assert audio_tokens == [4, 104]
@pytest.mark.core_model
def test_has_audio_output():
"""Test detection of audio tokens in output."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
StepAudio2ThinkerForConditionalGeneration,
)
text_only = [100, 200, 300]
assert not StepAudio2ThinkerForConditionalGeneration.has_audio_output(text_only)
with_audio = [100, 200, 151700, 300]
assert StepAudio2ThinkerForConditionalGeneration.has_audio_output(with_audio)
@pytest.mark.advanced_model
@pytest.mark.parametrize("test_config", test_params)
def test_audio_to_text_and_audio(step_audio2_runner: type[StepAudio2OmniRunner], test_config: tuple[str, str]) -> None:
"""Test processing audio input and generating text + audio output."""
model, stage_config_path = test_config
with step_audio2_runner(model, seed=42, stage_configs_path=stage_config_path) as runner:
audio = create_dummy_audio(sample_rate=16000, duration_sec=2.0)
outputs = runner.generate_multimodal(
prompts="请复述这段音频的内容。",
audios=audio,
)
assert len(outputs) > 0
text_output = None
for stage_output in outputs:
if stage_output.final_output_type == "text":
text_output = stage_output
break
assert text_output is not None
assert text_output.request_output is not None
text_content = text_output.request_output.outputs[0].text
assert text_content is not None
assert len(text_content.strip()) > 0
audio_output = None
for stage_output in outputs:
if stage_output.final_output_type == "audio":
audio_output = stage_output
break
if audio_output is not None:
assert audio_output.request_output is not None
audio_tensor = audio_output.request_output.outputs[0].multimodal_output.get("audio")
if audio_tensor is not None:
assert audio_tensor.numel() > 0
@pytest.mark.advanced_model
@pytest.mark.parametrize("test_config", test_params)
def test_text_only_input(step_audio2_runner: type[StepAudio2OmniRunner], test_config: tuple[str, str]) -> None:
"""Test processing text-only input (no audio)."""
model, stage_config_path = test_config
with step_audio2_runner(model, seed=42, stage_configs_path=stage_config_path) as runner:
outputs = runner.generate_multimodal(
prompts="你好,请用中文回答。",
audios=None,
)
assert len(outputs) > 0
text_output = None
for stage_output in outputs:
if stage_output.final_output_type == "text":
text_output = stage_output
break
assert text_output is not None
assert text_output.request_output is not None
text_content = text_output.request_output.outputs[0].text
assert text_content is not None
assert len(text_content.strip()) > 0
@@ -0,0 +1,96 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E expansion tests for Step-Audio2 offline inference (nightly CI).
Full-model inference with audio input and audio output.
"""
from pathlib import Path
from typing import Any
import numpy as np
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.media import generate_synthetic_audio
from tests.helpers.runtime import OmniRunner
from vllm_omni.outputs import OmniRequestOutput
MODEL = "stepfun-ai/Step-Audio-2-mini"
STAGE_CONFIG = str(Path(__file__).parent / "stage_configs" / "step_audio2_ci.yaml")
TEST_PARAMS = [(MODEL, STAGE_CONFIG)]
pytestmark = [pytest.mark.slow, pytest.mark.tts]
DEFAULT_SYSTEM_PROMPT = "你是一个语音对话助手,能够理解音频输入并生成语音回复。"
SAMPLE_RATE = 16000
def _synthetic_audio(duration_sec: int = 2) -> tuple[np.ndarray, int]:
audio = generate_synthetic_audio(duration_sec, 1, SAMPLE_RATE)["np_array"]
if len(audio.shape) == 2:
audio = audio.squeeze()
return np.asarray(audio, dtype=np.float32), SAMPLE_RATE
def _build_step_audio2_input(
user_text: str,
*,
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
audio: tuple[np.ndarray, int] | None = None,
) -> dict[str, Any]:
user_content = ""
multi_modal_data: dict[str, Any] = {}
if audio is not None:
user_content += "<audio_patch>"
multi_modal_data["audio"] = audio
user_content += user_text
full_prompt = (
f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
f"<|im_start|>user\n{user_content}<|im_end|>\n"
f"<|im_start|>assistant\n"
)
input_dict: dict[str, Any] = {"prompt": full_prompt}
if multi_modal_data:
input_dict["multi_modal_data"] = multi_modal_data
return input_dict
def _assert_non_empty_text_output(outputs: list[OmniRequestOutput]) -> None:
assert len(outputs) > 0
text_output = next((o for o in outputs if o.final_output_type == "text"), None)
assert text_output is not None
assert text_output.request_output is not None
text_content = text_output.request_output.outputs[0].text
assert text_content is not None
assert len(text_content.strip()) > 0
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_runner", TEST_PARAMS, indirect=True)
def test_audio_to_text_and_audio(omni_runner: OmniRunner) -> None:
"""Test processing audio input and generating text + audio output."""
audio = _synthetic_audio(duration_sec=2)
omni_input = _build_step_audio2_input("请复述这段音频的内容。", audio=audio)
outputs = omni_runner.generate([omni_input])
_assert_non_empty_text_output(outputs)
audio_output = next((o for o in outputs if o.final_output_type == "audio"), None)
if audio_output is not None:
assert audio_output.request_output is not None
audio_tensor = audio_output.request_output.outputs[0].multimodal_output.get("audio")
if audio_tensor is not None:
assert audio_tensor.numel() > 0
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_runner", TEST_PARAMS, indirect=True)
def test_text_only_input(omni_runner: OmniRunner) -> None:
"""Test processing text-only input (no audio)."""
omni_input = _build_step_audio2_input("你好,请用中文回答。")
outputs = omni_runner.generate([omni_input])
_assert_non_empty_text_output(outputs)
@@ -14,12 +14,10 @@ VOXCPM2_MODEL = "openbmb/VoxCPM2"
DEPLOY_CONFIG = get_deploy_config_path("voxcpm2.yaml")
SAMPLE_RATE = 48000
# (model, stage_config_path) — see ``omni_runner`` in tests.helpers.fixtures.runtime
_OMNI_RUNNER_PARAM = (VOXCPM2_MODEL, DEPLOY_CONFIG)
@pytest.fixture(scope="module")
def voxcpm2_engine():
"""Create VoxCPM2 engine for testing."""
with OmniRunner(VOXCPM2_MODEL, stage_configs_path=DEPLOY_CONFIG) as runner:
yield runner.omni
pytestmark = pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)
def _extract_audio(multimodal_output: dict) -> torch.Tensor:
@@ -45,9 +43,9 @@ def _extract_audio(multimodal_output: dict) -> torch.Tensor:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voxcpm2_zero_shot_001(voxcpm2_engine):
def test_voxcpm2_zero_shot_001(omni_runner: OmniRunner) -> None:
"""Test zero-shot TTS produces valid audio output."""
outputs = voxcpm2_engine.generate([{"prompt": "Hello, this is a test."}])
outputs = omni_runner.omni.generate([{"prompt": "Hello, this is a test."}])
assert len(outputs) == 1
audio = _extract_audio(outputs[0].outputs[0].multimodal_output)
@@ -59,7 +57,7 @@ def test_voxcpm2_zero_shot_001(voxcpm2_engine):
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voxcpm2_voice_clone_002(voxcpm2_engine):
def test_voxcpm2_voice_clone_002(omni_runner: OmniRunner) -> None:
"""Test voice cloning with a reference audio file.
Uses the example ``reference_speaker.wav`` bundled with the voxcpm
@@ -82,7 +80,7 @@ def test_voxcpm2_voice_clone_002(voxcpm2_engine):
if ref_path is None:
pytest.skip("No reference audio available for voice clone test")
outputs = voxcpm2_engine.generate(
outputs = omni_runner.omni.generate(
[
{
"prompt": "Hello, this is a voice clone demo.",
@@ -101,7 +99,7 @@ def test_voxcpm2_voice_clone_002(voxcpm2_engine):
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voxcpm2_prefill_decode_mixed_batch_003(voxcpm2_engine):
def test_voxcpm2_prefill_decode_mixed_batch_003(omni_runner: OmniRunner) -> None:
"""Regression: prefill+decode mixed batch must not crash (PR #2903)."""
long_prompt = (
"This is a deliberately long prompt that will stay in the decode "
@@ -117,7 +115,7 @@ def test_voxcpm2_prefill_decode_mixed_batch_003(voxcpm2_engine):
]
requests = [{"prompt": long_prompt}] + [{"prompt": p} for p in short_prompts]
outputs = voxcpm2_engine.generate(requests)
outputs = omni_runner.omni.generate(requests)
assert len(outputs) == len(requests)
for i, out in enumerate(outputs):
@@ -2,16 +2,9 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
End-to-end offline inference tests for Voxtral TTS model.
E2E expansion tests for Voxtral TTS offline inference (nightly CI).
Tests both synchronous (Omni) and async streaming (AsyncOmni) paths.
Equivalent to running:
python3 examples/offline_inference/text_to_speech/voxtral_tts/end2end.py \
--model mistralai/Voxtral-4B-TTS-2603 \
--voice casual_female \
--text "Hello, how are you?" \
--write-audio
"""
import asyncio
@@ -36,14 +29,12 @@ from vllm_omni.entrypoints.async_omni import AsyncOmni
MODEL = "mistralai/Voxtral-4B-TTS-2603"
STAGE_CONFIG = get_deploy_config_path("voxtral_tts.yaml")
SAMPLE_RATE = 24000
# Minimum expected audio samples for a short sentence (~0.04s of 24kHz audio)
MIN_AUDIO_SAMPLES = 1000
VOICE = "casual_female"
TEST_TEXT = "Hello, how are you?"
# (model, stage_config_path, extra_omni_kwargs) for indirect parametrize on
# ``omni_runner_function`` (function-scoped: must exit before ``AsyncOmni`` test).
pytestmark = [pytest.mark.slow, pytest.mark.tts]
_OMNI_RUNNER_PARAM = (
MODEL,
STAGE_CONFIG,
@@ -66,10 +57,8 @@ def _compose_request(model_name: str, text: str, voice: str) -> dict:
}
@pytest.mark.advanced_model
@pytest.mark.tts
@pytest.mark.parametrize("omni_runner_function", [_OMNI_RUNNER_PARAM], indirect=True)
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voxtral_tts_offline_basic(omni_runner_function: OmniRunner) -> None:
"""Offline sync path; function-scoped runner so AsyncOmni test does not overlap a live Omni."""
omni = omni_runner_function.omni
@@ -82,7 +71,6 @@ def test_voxtral_tts_offline_basic(omni_runner_function: OmniRunner) -> None:
assert len(outputs) > 0, "No outputs generated"
# Find audio output from the final stage
audio_data = None
for o in outputs:
mm = getattr(o, "multimodal_output", None)
@@ -92,7 +80,6 @@ def test_voxtral_tts_offline_basic(omni_runner_function: OmniRunner) -> None:
assert audio_data is not None, "No audio output found in any stage output"
# Concatenate audio chunks if returned as a list of tensors
if isinstance(audio_data, list):
audio_tensor = torch.cat(audio_data)
elif isinstance(audio_data, torch.Tensor):
@@ -105,14 +92,10 @@ def test_voxtral_tts_offline_basic(omni_runner_function: OmniRunner) -> None:
assert len(audio_array) > MIN_AUDIO_SAMPLES, (
f"Audio too short: {len(audio_array)} samples, expected > {MIN_AUDIO_SAMPLES}"
)
# Verify audio isn't all zeros / silence
assert np.max(np.abs(audio_array)) > 0.01, "Audio appears to be silence"
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voxtral_tts_offline_streaming():
"""Test AsyncOmni streaming inference for Voxtral TTS."""
@@ -126,7 +109,6 @@ def test_voxtral_tts_offline_streaming():
try:
inputs = _compose_request(MODEL, TEST_TEXT, VOICE)
sampling_params = SamplingParams(max_tokens=2500)
all_audio_chunks = []
@@ -146,8 +128,6 @@ def test_voxtral_tts_offline_streaming():
if isinstance(audio_chunk, torch.Tensor):
if finished:
# Last chunk may return whole audio instead of
# a delta — cut already-accumulated samples.
audio_np = audio_chunk[accumulated_samples:].float().detach().cpu().numpy()
else:
audio_np = audio_chunk.float().detach().cpu().numpy()
@@ -167,8 +147,6 @@ def test_voxtral_tts_offline_streaming():
assert len(audio_array) > MIN_AUDIO_SAMPLES, (
f"Audio too short: {len(audio_array)} samples, expected > {MIN_AUDIO_SAMPLES}"
)
# Verify audio isn't all zeros / silence
assert np.max(np.abs(audio_array)) > 0.01, "Audio appears to be silence"
finally:
@@ -35,7 +35,7 @@ def _audiox_server_cases(model: str):
]
@pytest.mark.full_model
@pytest.mark.slow
@pytest.mark.diffusion
@pytest.mark.parametrize("omni_server", _audiox_server_cases(AUDIOX_TEST_MODEL), indirect=True)
def test_audiox_t2a_online(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None:
@@ -18,7 +18,7 @@ from tests.helpers.media import load_test_audio_data_url
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
pytestmark = [pytest.mark.full_model, pytest.mark.tts]
pytestmark = [pytest.mark.slow, pytest.mark.tts]
MODEL = "FunAudioLLM/Fun-CosyVoice3-0.5B-2512"
@@ -66,6 +66,7 @@ tts_async_chunk_server_params = [
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4644")
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
def test_voice_clone_zh_001(omni_server, openai_client) -> None:
"""
@@ -88,6 +89,7 @@ def test_voice_clone_zh_001(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4644")
@pytest.mark.parametrize("omni_server", tts_async_chunk_server_params, indirect=True)
def test_voice_clone_zh_002(omni_server, openai_client) -> None:
"""
@@ -110,6 +112,7 @@ def test_voice_clone_zh_002(omni_server, openai_client) -> None:
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4644")
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
def test_voice_clone_en_001(omni_server, openai_client) -> None:
"""
@@ -33,7 +33,7 @@ test_params = [
]
@pytest.mark.full_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
@@ -60,7 +60,7 @@ def _write_synthetic_dreamzero_videos(client_mod, video_dir: Path) -> None:
_write_synthetic_video(video_dir / file_name, client_mod.cv2, channel=channel)
@pytest.mark.full_model
@pytest.mark.slow
@pytest.mark.diffusion
@pytest.mark.distributed_cuda
@hardware_test(res={"cuda": "H100"}, num_cards=2)
@@ -15,7 +15,7 @@ from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
pytestmark = [pytest.mark.full_model, pytest.mark.omni]
pytestmark = [pytest.mark.slow, pytest.mark.omni]
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
@@ -38,7 +38,7 @@ def _get_diffusion_feature_cases(model: str):
]
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.diffusion
@pytest.mark.parametrize(
"omni_server",
@@ -19,7 +19,7 @@ from tests.helpers.runtime import (
dummy_messages_from_mix_data,
)
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
pytestmark = [pytest.mark.diffusion, pytest.mark.slow]
FOUR_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "L4"}, num_cards=4)
POSITIVE_PROMPT = "A cat sitting on a windowsill"
@@ -19,7 +19,7 @@ import pytest
from tests.helpers.mark import hardware_marks
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
pytestmark = [pytest.mark.diffusion, pytest.mark.slow]
MODEL = "black-forest-labs/FLUX.2-dev"
PROMPT = "A cinematic mountain landscape at sunrise, dramatic clouds, ultra-detailed, realistic photography."
@@ -8,7 +8,7 @@ import pytest
from tests.helpers.media import generate_synthetic_image
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
pytestmark = [pytest.mark.diffusion, pytest.mark.slow]
EDIT_PROMPT = "Transform this modern, geometrist image into a Vincent van Gogh style impressionist painting."
NEGATIVE_PROMPT = "blurry, low quality, modern, geometrist"
@@ -1,10 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E Online tests for GLM-TTS model with text input and audio output.
E2E expansion tests for GLM-TTS online serving (nightly CI).
These tests verify the /v1/audio/speech endpoint works correctly with
the GLM-TTS two-stage pipeline (AR + DiT).
These tests verify the /v1/audio/speech endpoint with the GLM-TTS two-stage pipeline.
"""
import os
@@ -19,16 +18,14 @@ from tests.helpers.media import load_test_audio_data_url
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
pytestmark = [pytest.mark.slow, pytest.mark.tts]
MODEL = os.environ.get("GLM_TTS_MODEL_PATH", "zai-org/GLM-TTS")
REF_TEXT = "他当时还跟线下其他的站姐吵架,然后,打架进局子了。"
DEPLOY_CONFIG = get_deploy_config_path("glm_tts.yaml")
# Official GLM-TTS reference audio and transcript. Vendored under tests/assets/
# so Buildkite does not depend on raw.githubusercontent.com during E2E.
REF_AUDIO_URL = load_test_audio_data_url("glm_tts/jiayan_zh.wav")
SYNC_EXTRA_ARGS = [
"--trust-remote-code",
"--disable-log-stats",
@@ -65,16 +62,7 @@ tts_async_chunk_server_params = [
]
# GLM-TTS is a zero-shot voice cloning model (always requires ref_audio).
# There is no text-only / non-clone path — the official implementation
# asserts prompt_feat.shape[1] != 0 in token2wav_with_cache and every
# example prompt includes a reference audio. CosyVoice3 follows the same
# pattern: all tests are voice_clone_*.
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", tts_sync_server_params, indirect=True)
def test_voice_clone_zh_sync(omni_server, openai_client) -> None:
"""
@@ -83,9 +71,6 @@ def test_voice_clone_zh_sync(omni_server, openai_client) -> None:
Input Modal: text + ref_audio + ref_text
Output Modal: audio
Input Setting: stream=False
Uses the official jiayan_zh.wav reference audio from the upstream
GLM-TTS repository (real human speech matching REF_TEXT).
"""
request_config = {
"model": omni_server.model,
@@ -98,9 +83,7 @@ def test_voice_clone_zh_sync(omni_server, openai_client) -> None:
openai_client.send_audio_speech_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", tts_async_chunk_server_params, indirect=True)
def test_voice_clone_zh_async_chunk(omni_server, openai_client) -> None:
"""
@@ -121,9 +104,7 @@ def test_voice_clone_zh_async_chunk(omni_server, openai_client) -> None:
openai_client.send_audio_speech_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", tts_sync_server_params, indirect=True)
def test_models_endpoint(omni_server, openai_client) -> None:
"""Test the /v1/models endpoint returns loaded model."""
@@ -14,7 +14,7 @@ import pytest
from tests.helpers.mark import hardware_marks
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
pytestmark = [pytest.mark.diffusion, pytest.mark.slow]
MODEL = "HiDream-ai/HiDream-I1-Full"
PROMPT = "A cinematic mountain landscape at sunrise, dramatic clouds, ultra-detailed, realistic photography."
@@ -46,6 +46,7 @@ def _get_hidream_i1_image_feature_cases(model: str):
_get_hidream_i1_image_feature_cases(MODEL),
indirect=True,
)
@pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4662")
def test_hidream_i1_image(
omni_server: OmniServer,
openai_client: OpenAIClientHandler,
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
End-to-end online tests for higgs-audio v2 against /v1/audio/speech.
E2E Online expansion tests for higgs-audio v2 against /v1/audio/speech.
v1 scope is plain text -> 24 kHz speech plus shallow voice clone via
ref_audio + ref_text (inline) or voice=<name> (after POST /v1/audio/voices).
@@ -31,6 +31,8 @@ from tests.helpers.media import load_test_audio_data_url
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
pytestmark = [pytest.mark.slow, pytest.mark.tts]
MODEL = "bosonai/higgs-audio-v2-generation-3B-base"
STAGE_CONFIG = get_deploy_config_path("higgs_audio_v2.yaml")
SERVER_ARGS = ["--trust-remote-code", "--disable-log-stats"]
@@ -69,9 +71,7 @@ _REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know w
class TestHiggsAudioV2OnlineHappyPath:
"""Plain-text -> audio happy paths against the live HTTP server."""
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_plain_text_wav(self, omni_server, openai_client) -> None:
"""Single non-streaming WAV request — covers the canonical TTS happy path."""
openai_client.send_audio_speech_request(
@@ -85,9 +85,7 @@ class TestHiggsAudioV2OnlineHappyPath:
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_plain_text_pcm_streaming(self, omni_server, openai_client) -> None:
"""Streaming PCM via the shared-memory connector's codec_streaming path.
@@ -106,9 +104,7 @@ class TestHiggsAudioV2OnlineHappyPath:
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_plain_text_with_max_new_tokens(self, omni_server, openai_client) -> None:
"""max_new_tokens is one of the few extra fields the higgs validator accepts."""
openai_client.send_audio_speech_request(
@@ -123,9 +119,7 @@ class TestHiggsAudioV2OnlineHappyPath:
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_concurrent_plain_text(self, omni_server, openai_client) -> None:
"""Three concurrent non-streaming requests — guards the per-slot audio state."""
openai_client.send_audio_speech_request(
@@ -154,9 +148,7 @@ class TestHiggsAudioV2OnlineHappyPath:
class TestHiggsAudioV2OnlineValidatorRejections:
"""Out-of-scope fields must come back as 4xx with a higgs-named message."""
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_rejects_ref_audio_without_ref_text(self, omni_server, openai_client) -> None:
"""Voice clone needs the transcript too — half-supplied is a 4xx."""
openai_client.send_audio_speech_request(
@@ -171,9 +163,7 @@ class TestHiggsAudioV2OnlineValidatorRejections:
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_rejects_ref_text_without_ref_audio(self, omni_server, openai_client) -> None:
"""Symmetric guard: ref_text alone is not enough — must come with ref_audio."""
openai_client.send_audio_speech_request(
@@ -188,9 +178,7 @@ class TestHiggsAudioV2OnlineValidatorRejections:
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_rejects_task_type(self, omni_server, openai_client) -> None:
openai_client.send_audio_speech_request(
{
@@ -204,9 +192,7 @@ class TestHiggsAudioV2OnlineValidatorRejections:
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_rejects_language_override(self, omni_server, openai_client) -> None:
openai_client.send_audio_speech_request(
{
@@ -220,9 +206,7 @@ class TestHiggsAudioV2OnlineValidatorRejections:
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_rejects_multi_speaker_tag_in_text(self, omni_server, openai_client) -> None:
openai_client.send_audio_speech_request(
{
@@ -235,9 +219,7 @@ class TestHiggsAudioV2OnlineValidatorRejections:
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_rejects_empty_input(self, omni_server, openai_client) -> None:
openai_client.send_audio_speech_request(
{
@@ -262,9 +244,7 @@ class TestHiggsAudioV2OnlineVoiceClone:
:meth:`HiggsAudioV2TalkerForConditionalGeneration._maybe_apply_ref_audio_substitution`.
"""
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voice_clone_basic(self, omni_server, openai_client) -> None:
"""Single non-streaming WAV request driven by the qwen3_tts ref clip."""
openai_client.send_audio_speech_request(
@@ -280,9 +260,7 @@ class TestHiggsAudioV2OnlineVoiceClone:
}
)
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voice_clone_pcm_streaming(self, omni_server, openai_client) -> None:
"""Voice clone over the streaming PCM path."""
openai_client.send_audio_speech_request(
+21 -97
View File
@@ -60,8 +60,9 @@ class TestHiggsAudioV3OnlineHappyPath:
"""Plain-text -> audio happy paths against the live HTTP server."""
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_plain_text_wav(self, omni_server, openai_client) -> None:
"""Single non-streaming WAV request - canonical TTS happy path."""
openai_client.send_audio_speech_request(
@@ -71,13 +72,12 @@ class TestHiggsAudioV3OnlineHappyPath:
"stream": False,
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_plain_text_with_max_new_tokens(self, omni_server, openai_client) -> None:
"""``max_new_tokens`` is one of the few extra fields the v3 validator accepts."""
openai_client.send_audio_speech_request(
@@ -87,31 +87,33 @@ class TestHiggsAudioV3OnlineHappyPath:
"stream": False,
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"max_new_tokens": 500,
"max_new_tokens": 32,
"min_audio_bytes": _MIN_AUDIO_BYTES,
}
)
@pytest.mark.skip(reason="issue#4411")
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_concurrent_plain_text(self, omni_server, openai_client) -> None:
"""Three concurrent non-streaming requests - guards per-slot audio state and Stage-0 PC under batching."""
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "It was the night before my birthday.",
"input": "it was the night before my birthday",
"stream": False,
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
},
request_num=3,
)
@pytest.mark.core_model
@pytest.mark.skip(reason="issue#4411")
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_plain_text_pcm_streaming(self, omni_server, openai_client) -> None:
"""Streaming PCM via talker2code2wav_async_chunk + the connector's codec_streaming path.
@@ -135,15 +137,14 @@ class TestHiggsAudioV3OnlineHappyPath:
"stream": True,
"response_format": "pcm",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
"min_hnr_db": 0.0,
}
)
@pytest.mark.skip(reason="issue#4411")
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_concurrent_pcm_streaming(self, omni_server, openai_client) -> None:
"""Three concurrent streaming requests - guards per-request frame cursors
in ``talker2code2wav_async_chunk`` and per-slot delay-pattern state under batched AR.
@@ -161,7 +162,6 @@ class TestHiggsAudioV3OnlineHappyPath:
"stream": True,
"response_format": "pcm",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
"min_hnr_db": -2.0,
},
request_num=3,
@@ -185,7 +185,7 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_emotion_and_expressive(self, omni_server, openai_client) -> None:
"""Delivery tokens (emotion + expressive_high) at the start of input."""
openai_client.send_audio_speech_request(
@@ -201,7 +201,7 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_style_whispering(self, omni_server, openai_client) -> None:
"""Style token at the start - ``whispering``."""
openai_client.send_audio_speech_request(
@@ -217,7 +217,7 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_prosody_speed_and_pitch(self, omni_server, openai_client) -> None:
"""Two prosody tokens at the start - slow speed plus low pitch."""
openai_client.send_audio_speech_request(
@@ -235,7 +235,7 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_pause_mid_text(self, omni_server, openai_client) -> None:
"""Positional pause token placed inline between two clauses."""
openai_client.send_audio_speech_request(
@@ -251,7 +251,7 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_sfx_with_onomatopoeia(self, omni_server, openai_client) -> None:
"""SFX token paired with its written onomatopoeia (``<|sfx:laughter|>Hehe``)."""
openai_client.send_audio_speech_request(
@@ -274,8 +274,9 @@ class TestHiggsAudioV3OnlineVoiceClone:
"""Voice clone via the two payload shapes the model serves."""
@pytest.mark.core_model
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voice_clone_ref_audio_ref_text(self, omni_server, openai_client) -> None:
"""Canonical vllm-omni voice clone via ``ref_audio`` + ``ref_text``."""
openai_client.send_audio_speech_request(
@@ -287,13 +288,12 @@ class TestHiggsAudioV3OnlineVoiceClone:
"stream": False,
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voice_clone_references_alias(self, omni_server, openai_client) -> None:
"""BosonAI cookbook payload: ``references=[{audio_path, text}]``.
@@ -309,81 +309,5 @@ class TestHiggsAudioV3OnlineVoiceClone:
"stream": False,
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
}
)
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
class TestHiggsAudioV3OnlineValidatorRejections:
"""Out-of-scope shapes must come back as 4xx.
These cases use ``send_audio_speech_http_request`` (raw HTTP POST against
``/v1/audio/speech``) rather than the OpenAI SDK helper because:
- We need the failure-path matcher; the SDK helper asserts success.
- The cookbook ``references`` field is not in the OpenAI SDK schema, so
the SDK strips it before the request leaves the client and the server
sees a plain TTS request that returns 200. Raw HTTP keeps the JSON body
intact so the validator actually fires.
"""
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_rejects_empty_input(self, omni_server, openai_client) -> None:
"""Empty text input - validator should reject before reaching the engine."""
openai_client.send_audio_speech_http_request(
{
"json": {
"model": omni_server.model,
"input": "",
"response_format": "wav",
},
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"err_code": (400, 422),
"err_message": ("input", "empty"),
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_rejects_multi_reference_payload(self, omni_server, openai_client) -> None:
"""``references[]`` with more than one entry - multi-shot voice clone is not supported."""
openai_client.send_audio_speech_http_request(
{
"json": {
"model": omni_server.model,
"input": "Hello world.",
"references": [
{"audio_path": _REF_AUDIO_URL, "text": _REF_TEXT},
{"audio_path": _REF_AUDIO_URL, "text": _REF_TEXT},
],
"response_format": "wav",
},
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"err_code": (400, 422),
"err_message": "references",
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_rejects_conflicting_ref_audio_and_references(self, omni_server, openai_client) -> None:
"""``ref_audio`` and ``references`` cannot both be set to different values."""
openai_client.send_audio_speech_http_request(
{
"json": {
"model": omni_server.model,
"input": "Hello world.",
"ref_audio": _REF_AUDIO_URL,
"references": [{"audio_path": "https://example.com/other.wav"}],
"response_format": "wav",
},
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"err_code": (400, 422),
"err_message": "mutually exclusive",
}
)
@@ -1,75 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Tests of common diffusion feature combinations in online serving mode
for HunyuanVideo-1.5-T2V (480p).
Coverage (H100, since model cannot fit L4):
- CacheDiT + Layerwise CPU offloading (1 GPU)
- CacheDiT + TP=2 + VAE patch parallel=2 (2 GPUs)
"""
import pytest
from tests.helpers.mark import hardware_marks
from tests.helpers.runtime import (
OmniServer,
OmniServerParams,
OpenAIClientHandler,
)
PROMPT = "A cat walking across a sunlit garden, cinematic lighting, slow motion."
NEGATIVE_PROMPT = "low quality, blurry, distorted"
MODEL = "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v"
SINGLE_CARD_MARKS = hardware_marks(res={"cuda": "H100"})
def _get_diffusion_feature_cases(model: str):
"""Return L4 diffusion feature cases for HunyuanVideo-1.5.
Designed for 2x H100 environment per issue #1832.
"""
return [
# (1 GPU) CacheDiT + Layerwise CPU offloading
pytest.param(
OmniServerParams(
model=model,
),
id="default",
marks=SINGLE_CARD_MARKS,
),
]
@pytest.mark.advanced_model
@pytest.mark.core_model
@pytest.mark.diffusion
@pytest.mark.parametrize(
"omni_server",
_get_diffusion_feature_cases(MODEL),
indirect=True,
)
def test_text_to_video_001(
omni_server: OmniServer,
openai_client: OpenAIClientHandler,
):
"""L4 diffusion feature coverage for HunyuanVideo-1.5-T2V on H100."""
form_data = {
"prompt": PROMPT,
"negative_prompt": NEGATIVE_PROMPT,
"height": 480,
"width": 640,
"num_frames": 5,
"num_inference_steps": 2,
"guidance_scale": 6.0,
"seed": 42,
}
request_config = {
"model": omni_server.model,
"form_data": form_data,
}
openai_client.send_video_diffusion_request(request_config)
@@ -14,7 +14,7 @@ import pytest
from tests.helpers.mark import hardware_marks
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
pytestmark = [pytest.mark.diffusion, pytest.mark.slow]
PROMPT = "A cat walking across a sunlit garden, cinematic lighting, slow motion."
NEGATIVE_PROMPT = "low quality, blurry, distorted"
@@ -1,69 +0,0 @@
"""
Recommended tests of diffusion features that are available in online serving mode
and are supported by the following model:
- LongCat-Image: text-to-image with single prompt input
Coverage:
- CPU offloading (model-level sequential offload via --enable-cpu-offload)
- Cache-DiT
- SP (Ulysses)
This validates:
- Successful image generation at the expected 768x1344 resolution with recommended feature combinations
"""
import pytest
from tests.helpers.mark import hardware_marks
from tests.helpers.runtime import (
OmniServer,
OmniServerParams,
OpenAIClientHandler,
dummy_messages_from_mix_data,
)
TEXT_TO_IMAGE_PROMPT = (
"A cinematic illustration of a cat typing on a silver laptop, soft window light, highly detailed."
)
NEGATIVE_PROMPT = "blurry, low quality, distorted, oversaturated"
SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
def _get_diffusion_feature_cases(model: str):
"""Return diffusion feature cases for LongCat-Image."""
return [
pytest.param(
OmniServerParams(
model=model,
),
id="default",
marks=SINGLE_CARD_FEATURE_MARKS,
)
]
@pytest.mark.advanced_model
@pytest.mark.core_model
@pytest.mark.diffusion
@pytest.mark.parametrize(
"omni_server",
_get_diffusion_feature_cases("meituan-longcat/LongCat-Image"),
indirect=True,
)
def test_longcat_image(omni_server: OmniServer, openai_client: OpenAIClientHandler):
"""Test the recommended feature combinations for LongCat-Image."""
messages = dummy_messages_from_mix_data(content_text=TEXT_TO_IMAGE_PROMPT)
request_config = {
"model": omni_server.model,
"messages": messages,
"extra_body": {
"height": 768,
"width": 1344,
"num_inference_steps": 2,
"negative_prompt": NEGATIVE_PROMPT,
"guidance_scale": 4.0,
"seed": 42,
},
}
openai_client.send_diffusion_request(request_config)
@@ -1,69 +0,0 @@
"""
Recommended tests of diffusion features that are available in online serving mode
and are supported by the following model:
- LongCat-Image-Edit: image-to-image edit with a single image + single edit prompt input
Coverage:
- CPU offloading (model-level sequential offload via --enable-cpu-offload)
- Cache-DiT
- SP (Ulysses)
This validates:
- Successful image generation at the expected 1024x1024 resolution with recommended feature combinations
"""
import pytest
from tests.helpers.mark import hardware_marks
from tests.helpers.media import generate_synthetic_image
from tests.helpers.runtime import (
OmniServer,
OmniServerParams,
OpenAIClientHandler,
dummy_messages_from_mix_data,
)
EDIT_PROMPT = "Transform this modern image into a cinematic animation style with vibrant colors and soft lighting."
NEGATIVE_PROMPT = "blurry, low quality, distorted, oversaturated"
SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
def _get_diffusion_feature_cases(model: str):
"""Return diffusion feature cases for LongCat-Image-Edit."""
return [
pytest.param(
OmniServerParams(
model=model,
),
id="default",
marks=SINGLE_CARD_FEATURE_MARKS,
)
]
@pytest.mark.advanced_model
@pytest.mark.core_model
@pytest.mark.diffusion
@pytest.mark.parametrize(
"omni_server",
_get_diffusion_feature_cases("meituan-longcat/LongCat-Image-Edit"),
indirect=True,
)
def test_longcat_image_edit(omni_server: OmniServer, openai_client: OpenAIClientHandler):
"""Test the recommended feature combinations for LongCat-Image-Edit."""
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}"
messages = dummy_messages_from_mix_data(image_data_url=image_data_url, content_text=EDIT_PROMPT)
request_config = {
"model": omni_server.model,
"messages": messages,
"extra_body": {
"height": 1024,
"width": 1024,
"num_inference_steps": 2,
"negative_prompt": NEGATIVE_PROMPT,
"guidance_scale": 4.0,
"seed": 42,
},
}
openai_client.send_diffusion_request(request_config)
@@ -3,6 +3,7 @@ Recommended tests of diffusion features that are available in online serving mod
and are supported by the following model:
- LongCat-Image-Edit: image-to-image edit with a single image + single edit prompt input
Coverage:
- Default smoke (1 GPU)
- CPU offloading (model-level sequential offload via --enable-cpu-offload)
- Cache-DiT
- SP (Ulysses)
@@ -17,7 +18,7 @@ from tests.helpers.mark import hardware_marks
from tests.helpers.media import generate_synthetic_image
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
pytestmark = [pytest.mark.diffusion, pytest.mark.slow]
EDIT_PROMPT = "Transform this modern image into a cinematic animation style with vibrant colors and soft lighting."
NEGATIVE_PROMPT = "blurry, low quality, distorted, oversaturated"
@@ -25,9 +26,17 @@ SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
PARALLEL_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"}, num_cards=2)
MODEL = "meituan-longcat/LongCat-Image-Edit"
def _get_diffusion_feature_cases(model: str):
"""Return diffusion feature cases for LongCat-Image-Edit."""
return [
pytest.param(
OmniServerParams(model=model),
id="default",
marks=SINGLE_CARD_FEATURE_MARKS,
),
pytest.param(
OmniServerParams(
model=model,
@@ -54,7 +63,7 @@ def _get_diffusion_feature_cases(model: str):
@pytest.mark.parametrize(
"omni_server",
_get_diffusion_feature_cases("meituan-longcat/LongCat-Image-Edit"),
_get_diffusion_feature_cases(MODEL),
indirect=True,
)
def test_longcat_image_edit(omni_server: OmniServer, openai_client: OpenAIClientHandler):
@@ -3,6 +3,7 @@ Recommended tests of diffusion features that are available in online serving mod
and are supported by the following model:
- LongCat-Image: text-to-image with single prompt input
Coverage:
- Default smoke (1 GPU)
- CPU offloading (model-level sequential offload via --enable-cpu-offload)
- Cache-DiT
- SP (Ulysses)
@@ -16,7 +17,7 @@ import pytest
from tests.helpers.mark import hardware_marks
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
pytestmark = [pytest.mark.diffusion, pytest.mark.slow]
TEXT_TO_IMAGE_PROMPT = (
"A cinematic illustration of a cat typing on a silver laptop, soft window light, highly detailed."
@@ -25,10 +26,17 @@ NEGATIVE_PROMPT = "blurry, low quality, distorted, oversaturated"
SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
PARALLEL_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"}, num_cards=2)
MODEL = "meituan-longcat/LongCat-Image"
def _get_diffusion_feature_cases(model: str):
"""Return diffusion feature cases for LongCat-Image."""
return [
pytest.param(
OmniServerParams(model=model),
id="default",
marks=SINGLE_CARD_FEATURE_MARKS,
),
pytest.param(
OmniServerParams(
model=model,
@@ -55,7 +63,7 @@ def _get_diffusion_feature_cases(model: str):
@pytest.mark.parametrize(
"omni_server",
_get_diffusion_feature_cases("meituan-longcat/LongCat-Image"),
_get_diffusion_feature_cases(MODEL),
indirect=True,
)
def test_longcat_image(omni_server: OmniServer, openai_client: OpenAIClientHandler):
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E Online tests for MiMo-Audio model with audio/text input and audio output.
E2E expansion tests for MiMo-Audio online serving (nightly Omni · Function Test with H100).
"""
import os
@@ -17,6 +17,8 @@ from vllm_omni.model_executor.model_loader.weight_utils import (
download_weights_from_hf_specific,
)
pytestmark = [pytest.mark.slow, pytest.mark.omni]
MIMO_AUDIO_TOKENIZER_REPO = "XiaomiMiMo/MiMo-Audio-Tokenizer"
CHAT_TEMPLATE_PATH = str(
Path(__file__).parent.parent.parent.parent / "examples" / "online_serving" / "mimo_audio" / "chat_template.jinja"
@@ -39,16 +41,11 @@ def download_tokenizer():
return local_path
# Guard module-level setup so test collection doesn't fail in environments
# where the model cache is read-only or models aren't available.
try:
stage_configs = [get_deploy_config_path("mimo_audio.yaml")]
tokenizer_path = download_tokenizer()
os.environ["MIMO_AUDIO_TOKENIZER_PATH"] = tokenizer_path
# --load-format dummy applies to every stage pipeline-wide, avoiding a
# per-stage yaml rewrite (the old approach wrote a tempfile + atexit-unlink
# which raced with CI's process lifecycle).
test_params = [
OmniServerParams(
model=model,
@@ -60,7 +57,7 @@ try:
]
except Exception as exc:
pytest.skip(
f"MiMo-Audio online serving tests skipped: module setup failed ({type(exc).__name__}: {exc})",
f"MiMo-Audio expansion tests skipped: module setup failed ({type(exc).__name__}: {exc})",
allow_module_level=True,
)
@@ -78,45 +75,7 @@ def get_max_batch_size(size_type="few"):
return batch_sizes.get(size_type, 5)
@pytest.mark.advanced_model
@pytest.mark.core_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
@pytest.mark.skip(reason="CI failed 8571")
def test_audio_to_text_audio_001(omni_server, openai_client) -> None:
"""
Test audio and text input processing and text/audio output generation via OpenAI API.
Deploy Setting: default yaml
Input Modal: text + audio
Output Modal: text + audio
Input Setting: stream=True
Datasets: single request
"""
audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1, sample_rate=24000)['base64']}"
messages = dummy_messages_from_mix_data(
audio_data_url=audio_data_url,
content_text=get_prompt("audio"),
)
request_config = {
"model": omni_server.model,
"messages": messages,
"stream": True,
"sampling_params_list": [{"max_tokens": 64}, {"max_tokens": 64}],
"key_words": {
"audio": ["test"],
},
}
# Test single completion
openai_client.send_omni_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_to_text_001(omni_server, openai_client) -> None:
"""
@@ -137,3 +96,34 @@ def test_text_to_text_001(omni_server, openai_client) -> None:
}
openai_client.send_omni_request(request_config, request_num=get_max_batch_size())
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
@pytest.mark.skip(reason="CI failed 8571")
def test_audio_to_text_audio_001(omni_server, openai_client) -> None:
"""
Test audio and text input processing and text/audio output generation via OpenAI API.
Deploy Setting: default yaml
Input Modal: text + audio
Output Modal: text + audio
Input Setting: stream=True
Datasets: single request
"""
audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1, sample_rate=24000)['base64']}"
messages = dummy_messages_from_mix_data(
audio_data_url=audio_data_url,
content_text=get_prompt("audio"),
)
request_config = {
"model": omni_server.model,
"messages": messages,
"stream": True,
"sampling_params_list": [{"max_tokens": 64}, {"max_tokens": 64}],
"key_words": {
"audio": ["test"],
},
}
openai_client.send_omni_request(request_config)
@@ -1,21 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E online serving tests for Ming-omni-tts (dense) model.
E2E expansion tests for Ming-omni-tts online serving (nightly CI).
Tests text-to-audio via /v1/audio/speech endpoint.
"""
import os
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
pytestmark = [pytest.mark.advanced_model, pytest.mark.omni]
pytestmark = [
pytest.mark.slow,
pytest.mark.tts,
pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4704"),
]
MODEL = "inclusionAI/Ming-omni-tts-0.5B"
DEPLOY_CONFIG = get_deploy_config_path("ming_tts.yaml")
@@ -3,8 +3,8 @@
"""
E2E online tests for the full MOSS-TTS family via /v1/audio/speech.
The offline suite (``tests/e2e/offline_inference/test_moss_tts.py`` and
``test_moss_tts_v1_5.py``) covers the engine path; this file covers the
The offline suite (``tests/e2e/offline_inference/test_moss_tts_*_expansion.py``)
covers the engine path; this file covers the
*serving* path (``serving_speech.py`` → ``_detect_moss_variant`` → the
delay/realtime prompt builders), which the Nano online test does not exercise.
@@ -30,11 +30,17 @@ from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
# TODO: Fix this test
# pytestmark = [pytest.mark.full_model, pytest.mark.tts]
pytestmark = [
pytest.mark.slow,
pytest.mark.tts,
pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4700"),
]
MODEL = "OpenMOSS-Team/MOSS-TTS-Realtime"
REF_AUDIO_URL = "https://raw.githubusercontent.com/OpenMOSS/MOSS-TTS/HEAD/assets/audio/reference_zh_1.wav"
# Voice-clone output is not reliably transcribed by the Whisper check used at
# full_model run_level; assert non-trivial WAV payload size instead.
_MIN_AUDIO_BYTES = 10_000
@pytest.fixture(scope="session")
@@ -94,6 +100,10 @@ def test_text_to_audio_001(omni_server, openai_client, ref_audio_data_url) -> No
Output Modal: audio (24 kHz, WAV)
Input Setting: stream=False
Datasets: single request
NOTE: ``min_audio_bytes`` skips Whisper transcript similarity — cloned
voice output is often empty or mismatched under ASR without indicating a
real serving regression.
"""
request_config = {
"model": omni_server.model,
@@ -101,6 +111,7 @@ def test_text_to_audio_001(omni_server, openai_client, ref_audio_data_url) -> No
"stream": False,
"response_format": "wav",
"ref_audio": ref_audio_data_url,
"min_audio_bytes": _MIN_AUDIO_BYTES,
}
openai_client.send_audio_speech_request(request_config)
@@ -144,6 +155,8 @@ def test_text_to_audio_003_chinese(omni_server, openai_client, ref_audio_data_ur
Output Modal: audio (24 kHz, WAV)
Input Setting: stream=False
Datasets: single request
NOTE: same ``min_audio_bytes`` rationale as test_text_to_audio_001.
"""
request_config = {
"model": omni_server.model,
@@ -151,6 +164,7 @@ def test_text_to_audio_003_chinese(omni_server, openai_client, ref_audio_data_ur
"stream": False,
"response_format": "wav",
"ref_audio": ref_audio_data_url,
"min_audio_bytes": _MIN_AUDIO_BYTES,
}
openai_client.send_audio_speech_request(request_config)
@@ -24,9 +24,9 @@ from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
# ``omni`` for all tests; 001/002 also get ``full_model`` via ``TestMossTtsNanoFull`` (003 is core+advanced only, no full_model).
# ``omni`` for all tests; 001/002 also get ``slow`` via ``TestMossTtsNanoFull`` (003 is core+advanced only, no slow).
pytestmark = [
pytest.mark.full_model,
pytest.mark.slow,
pytest.mark.tts,
]
@@ -14,7 +14,7 @@ from tests.helpers.runtime import (
dummy_messages_from_mix_data,
)
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
pytestmark = [pytest.mark.slow, pytest.mark.diffusion]
# L4: 4 GPUs + TP=4; XPU B60: 2 cards (use num_cards={"cuda": 4, "xpu": 4} if needed)
FOUR_CARD_MARKS = hardware_marks(
@@ -55,7 +55,7 @@ def get_max_batch_size(size_type="few"):
return batch_sizes.get(size_type, 5)
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.omni
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
@@ -93,7 +93,7 @@ def test_mix_to_text_audio_001(omni_server, openai_client) -> None:
openai_client.send_omni_request(request_config)
@pytest.mark.advanced_model
@pytest.mark.slow
@pytest.mark.omni
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_text_to_text_001(omni_server, openai_client) -> None:
@@ -15,10 +15,15 @@ import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
MODEL = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"
_STAGE_CONFIG = modify_stage_config(
get_deploy_config_path("qwen3_tts.yaml"),
updates={"stages": {0: {"default_sampling_params.max_tokens": 500}}},
)
def get_prompt(prompt_type="text"):
"""Text prompt for text-to-audio tests (same as test_qwen3_omni - beijing test case)."""
@@ -38,7 +43,7 @@ tts_server_params = [
pytest.param(
OmniServerParams(
model=MODEL,
stage_config_path=get_deploy_config_path("qwen3_tts.yaml"),
stage_config_path=_STAGE_CONFIG,
server_args=["--trust-remote-code"],
),
id="async_chunk",
-61
View File
@@ -1,61 +0,0 @@
"""
Tests for Stable Diffusion 3.5 medium model.
"""
import pytest
from tests.helpers.mark import hardware_marks
from tests.helpers.runtime import (
OmniServer,
OmniServerParams,
OpenAIClientHandler,
dummy_messages_from_mix_data,
)
FOUR_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "L4"}, num_cards=4)
POSITIVE_PROMPT = "A serene mountain landscape at sunset"
NEGATIVE_PROMPT = "blurry, low quality, distorted"
# For now, we only test one or two good configurations for this model to keep
# the CI light. The test cases focus on the most common feature combinations
# that provide good performance improvements.
def _get_diffusion_feature_cases(model: str):
return [
# Cache-DiT + CFG Parallel + Tensor Parallel
pytest.param(
OmniServerParams(
model=model,
),
id="default",
marks=FOUR_CARD_FEATURE_MARKS,
),
]
@pytest.mark.advanced_model
@pytest.mark.core_model
@pytest.mark.diffusion
@pytest.mark.parametrize(
"omni_server",
_get_diffusion_feature_cases(
model="stabilityai/stable-diffusion-3.5-medium",
),
indirect=True,
)
def test_text_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler):
messages = dummy_messages_from_mix_data(content_text=POSITIVE_PROMPT)
request_config = {
"model": omni_server.model,
"messages": messages,
"extra_body": {
"height": 1024,
"width": 1024,
"num_inference_steps": 28,
"negative_prompt": NEGATIVE_PROMPT,
"guidance_scale": 4.5,
"seed": 42,
},
}
openai_client.send_diffusion_request(request_config)
@@ -12,7 +12,7 @@ from tests.helpers.runtime import (
dummy_messages_from_mix_data,
)
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
pytestmark = [pytest.mark.diffusion, pytest.mark.slow]
FOUR_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "L4"}, num_cards=4)
POSITIVE_PROMPT = "A serene mountain landscape at sunset"
@@ -1,368 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""
E2E tests for Step-Audio2 online serving with concurrent requests.
Tests:
1. Single request via API
2. Concurrent throughput (1/2/4/8 concurrent requests)
3. Latency percentiles (p50/p95/p99)
"""
import asyncio
import os
import socket
import subprocess
import sys
import time
from pathlib import Path
import aiohttp
import numpy as np
import pytest
from vllm.utils.network_utils import get_open_port
MODEL = "stepfun-ai/Step-Audio-2-mini"
STAGE_CONFIG = str(Path(__file__).parent / "stage_configs" / "step_audio2_ci.yaml")
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
class OmniServer:
"""Omniserver for vLLM-Omni tests."""
def __init__(
self,
model: str,
serve_args: list[str],
*,
env_dict: dict[str, str] | None = None,
) -> None:
self.model = model
self.serve_args = serve_args
self.env_dict = env_dict
self.proc: subprocess.Popen | None = None
self.host = "127.0.0.1"
self.port = get_open_port()
def _start_server(self) -> None:
"""Start the vLLM-Omni server subprocess."""
env = os.environ.copy()
env["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
if self.env_dict is not None:
env.update(self.env_dict)
cmd = [
sys.executable,
"-m",
"vllm_omni.entrypoints.cli.main",
"serve",
self.model,
"--omni",
"--host",
self.host,
"--port",
str(self.port),
] + self.serve_args
print(f"Launching OmniServer with: {' '.join(cmd)}")
self.proc = subprocess.Popen(
cmd,
env=env,
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
)
max_wait = 600 # 10 minutes
start_time = time.time()
while time.time() - start_time < max_wait:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1)
result = sock.connect_ex((self.host, self.port))
if result == 0:
print(f"Server ready on {self.host}:{self.port}")
return
except Exception:
pass
time.sleep(2)
raise RuntimeError(f"Server failed to start within {max_wait} seconds")
def __enter__(self):
self._start_server()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.proc:
self.proc.terminate()
try:
self.proc.wait(timeout=30)
except subprocess.TimeoutExpired:
self.proc.kill()
self.proc.wait()
def create_dummy_audio_base64(duration_sec: float = 5.0, sample_rate: int = 16000) -> str:
"""Create dummy audio as base64."""
import base64
import io
import wave
num_samples = int(duration_sec * sample_rate)
t = np.linspace(0, duration_sec, num_samples, dtype=np.float32)
audio = (0.5 * np.sin(2 * np.pi * 440 * t) * 32767).astype(np.int16)
buffer = io.BytesIO()
with wave.open(buffer, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
wav_file.writeframes(audio.tobytes())
return base64.b64encode(buffer.getvalue()).decode("utf-8")
def create_s2st_request(audio_base64: str) -> dict:
"""Create S2ST request payload."""
# Use audio_url format with base64 data URL
audio_url = f"data:audio/wav;base64,{audio_base64}"
return {
"model": MODEL,
"messages": [
{"role": "system", "content": "请仔细聆听这段语音,然后复述其内容。"},
{
"role": "user",
"content": [
{
"type": "audio_url",
"audio_url": {"url": audio_url},
},
{
"type": "text",
"text": "<tts_start>",
},
],
},
],
"max_tokens": 512,
"temperature": 0.7,
"top_p": 0.9,
}
async def send_request(session: aiohttp.ClientSession, url: str, payload: dict) -> tuple[float, int, str]:
"""Send single request and return (latency, num_tokens, response_text)."""
start = time.perf_counter()
async with session.post(url, json=payload) as response:
result = await response.json()
latency = time.perf_counter() - start
if "choices" in result and len(result["choices"]) > 0:
text = result["choices"][0].get("message", {}).get("content", "")
tokens = result.get("usage", {}).get("completion_tokens", len(text.split()))
else:
text = str(result)
tokens = 0
return latency, tokens, text
async def benchmark_concurrent(
url: str,
payload: dict,
num_concurrent: int,
num_requests: int,
warmup: int = 2,
) -> dict:
"""Run concurrent benchmark with true sustained concurrency.
Uses semaphore to maintain exactly num_concurrent requests in flight at all times,
rather than batch-and-wait approach. Measures real wall clock time.
"""
semaphore = asyncio.Semaphore(num_concurrent)
async def bounded_request(session: aiohttp.ClientSession):
async with semaphore:
return await send_request(session, url, payload)
async with aiohttp.ClientSession() as session:
if warmup > 0:
print(f" Warming up ({warmup} requests)...", end=" ", flush=True)
for _ in range(warmup):
try:
await send_request(session, url, payload)
except Exception as e:
print(f"Warmup failed: {e}")
print("done")
print(f" Running {num_requests} requests with concurrency={num_concurrent}...", end=" ", flush=True)
wall_start = time.perf_counter()
tasks = [bounded_request(session) for _ in range(num_requests)]
results = await asyncio.gather(*tasks, return_exceptions=True)
wall_time = time.perf_counter() - wall_start
print(f"done ({wall_time:.2f}s)")
all_latencies = []
all_tokens = []
errors = 0
for result in results:
if isinstance(result, Exception):
print(f" Request failed: {result}")
errors += 1
continue
latency, tokens, _ = result
all_latencies.append(latency)
all_tokens.append(tokens)
if not all_latencies:
return {"error": "All requests failed"}
latencies = np.array(all_latencies)
total_tokens = sum(all_tokens)
successful_requests = len(all_latencies)
return {
"num_concurrent": num_concurrent,
"num_requests": num_requests,
"successful_requests": successful_requests,
"errors": errors,
"wall_time": wall_time,
"p50_latency": float(np.percentile(latencies, 50)),
"p95_latency": float(np.percentile(latencies, 95)),
"p99_latency": float(np.percentile(latencies, 99)),
"mean_latency": float(np.mean(latencies)),
"min_latency": float(np.min(latencies)),
"max_latency": float(np.max(latencies)),
"throughput_req_per_sec": successful_requests / wall_time,
"throughput_tokens_per_sec": total_tokens / wall_time,
}
class TestStepAudio2OnlineServing:
"""Online serving tests for Step-Audio2."""
@pytest.fixture(scope="class")
def server(self):
"""Start vLLM-Omni server."""
with OmniServer(MODEL, ["--stage-configs-path", STAGE_CONFIG]) as omni_server:
yield f"http://{omni_server.host}:{omni_server.port}/v1/chat/completions"
@pytest.mark.asyncio
async def test_single_request(self, server):
"""Test single request works."""
audio_base64 = create_dummy_audio_base64(duration_sec=2.0)
payload = create_s2st_request(audio_base64)
async with aiohttp.ClientSession() as session:
latency, tokens, text = await send_request(session, server, payload)
assert latency > 0
assert tokens >= 0
print(f"Single request: latency={latency:.2f}s, tokens={tokens}")
@pytest.mark.asyncio
@pytest.mark.parametrize("num_concurrent", [1, 2, 4])
async def test_concurrent_throughput(self, server, num_concurrent):
"""Test concurrent throughput at different concurrency levels."""
audio_base64 = create_dummy_audio_base64(duration_sec=5.0)
payload = create_s2st_request(audio_base64)
num_requests = num_concurrent * 3
stats = await benchmark_concurrent(server, payload, num_concurrent, num_requests)
assert "error" not in stats
print(f"\nConcurrency={num_concurrent}:")
print(f" p50={stats['p50_latency']:.2f}s, p95={stats['p95_latency']:.2f}s")
print(f" throughput={stats['throughput_req_per_sec']:.2f} req/s")
async def run_throughput_benchmark(
server_url: str,
audio_duration: float = 5.0,
concurrency_levels: list[int] = [1, 2, 4, 8],
requests_per_level: int = 10,
warmup: int = 2,
):
"""Run full throughput benchmark."""
audio_base64 = create_dummy_audio_base64(duration_sec=audio_duration)
payload = create_s2st_request(audio_base64)
print(f"{'=' * 70}")
print("Step-Audio2 Online Serving Throughput Benchmark")
print(f"{'=' * 70}")
print(f"Server: {server_url}")
print(f"Audio duration: {audio_duration}s")
print(f"Requests per level: {requests_per_level}")
print(f"Warmup requests: {warmup}")
print(f"Concurrency levels: {concurrency_levels}")
print(f"{'=' * 70}")
results = []
for num_concurrent in concurrency_levels:
print(f"\n[Concurrency={num_concurrent}]")
stats = await benchmark_concurrent(server_url, payload, num_concurrent, requests_per_level, warmup)
results.append(stats)
if "error" not in stats:
print(
f" Result: p50={stats['p50_latency']:.2f}s, "
f"p95={stats['p95_latency']:.2f}s, "
f"throughput={stats['throughput_req_per_sec']:.2f} req/s, "
f"{stats['throughput_tokens_per_sec']:.1f} tok/s"
)
print(f"\n{'=' * 80}")
print("SUMMARY")
print(f"{'=' * 80}")
print(
f"{'Conc':<6} {'Reqs':<6} {'Err':<5} {'Wall(s)':<9} {'p50':<8} {'p95':<8} {'p99':<8} {'Req/s':<8} {'Tok/s':<8}"
)
print("-" * 80)
for stats in results:
if "error" in stats:
print(f"{stats.get('num_concurrent', '?'):<6} ERROR: {stats['error']}")
continue
print(
f"{stats['num_concurrent']:<6} "
f"{stats['successful_requests']:<6} "
f"{stats['errors']:<5} "
f"{stats['wall_time']:<9.2f} "
f"{stats['p50_latency']:<8.2f} "
f"{stats['p95_latency']:<8.2f} "
f"{stats['p99_latency']:<8.2f} "
f"{stats['throughput_req_per_sec']:<8.2f} "
f"{stats['throughput_tokens_per_sec']:<8.1f}"
)
print("=" * 80)
return results
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Step-Audio2 Online Serving Benchmark")
parser.add_argument(
"--server-url", type=str, default="http://localhost:8000/v1/chat/completions", help="vLLM server URL"
)
parser.add_argument("--audio-duration", type=float, default=5.0, help="Input audio duration in seconds")
parser.add_argument("--concurrency", type=str, default="1,2,4,8", help="Comma-separated concurrency levels to test")
parser.add_argument("--requests-per-level", type=int, default=10, help="Number of requests per concurrency level")
parser.add_argument("--warmup", type=int, default=2, help="Number of warmup requests before each benchmark")
args = parser.parse_args()
concurrency_levels = [int(x) for x in args.concurrency.split(",")]
asyncio.run(
run_throughput_benchmark(
args.server_url,
args.audio_duration,
concurrency_levels,
args.requests_per_level,
args.warmup,
)
)
@@ -0,0 +1,132 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E expansion tests for Step-Audio2 online serving (weekly CI).
Tests speech-to-speech translation via /v1/chat/completions.
Request shape follows examples/online_serving/step_audio2/openai_chat_completion_client.py.
"""
import os
from pathlib import Path
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.media import generate_synthetic_audio
from tests.helpers.runtime import OmniServerParams
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
MODEL = "stepfun-ai/Step-Audio-2-mini"
STAGE_CONFIG = str(Path(__file__).parent / "stage_configs" / "step_audio2_ci.yaml")
SAMPLE_RATE = 16000
SEED = 42
# pyttsx3 synthetic input phrase; Step-Audio2 text output includes codec tokens
# (<audio_NNNN>) so we validate audio only, not text↔audio similarity.
SYNTH_PHRASE = "how are you"
TEST_PARAMS = [
pytest.param(
OmniServerParams(
model=MODEL,
stage_config_path=STAGE_CONFIG,
env_dict={"VLLM_IMAGE_FETCH_TIMEOUT": "60"},
),
id="step_audio2",
)
]
pytestmark = [
pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True),
pytest.mark.slow,
pytest.mark.tts,
]
def _synthetic_audio_base64(duration_sec: int = 2) -> str:
return generate_synthetic_audio(
duration_sec,
1,
SAMPLE_RATE,
phrase_text=SYNTH_PHRASE,
)["base64"]
def _default_sampling_params_list() -> list[dict]:
return [
{
"temperature": 0.7,
"top_p": 0.9,
"top_k": -1,
"max_tokens": 256,
"seed": SEED,
"detokenize": True,
"repetition_penalty": 1.05,
},
{
"temperature": 0.0,
"top_p": 1.0,
"top_k": -1,
"max_tokens": 1,
"seed": SEED,
"detokenize": False,
},
]
def _build_s2st_request_config(omni_server, audio_base64: str) -> dict:
"""Build audio-to-audio (S2ST) chat completion request config."""
audio_url = f"data:audio/wav;base64,{audio_base64}"
return {
"model": omni_server.model,
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are an audio processing assistant. Listen and repeat the audio content.",
}
],
},
{
"role": "user",
"content": [
{"type": "audio_url", "audio_url": {"url": audio_url}},
{"type": "text", "text": "Please listen to this audio and repeat its content."},
],
},
{"role": "assistant", "content": "<tts_start>"},
],
# Thinker text includes undetokenized audio codec placeholders; assert audio only.
"modalities": ["audio"],
"audio_ref_text": SYNTH_PHRASE,
"sampling_params_list": _default_sampling_params_list(),
"extra_body": {
"continue_final_message": True,
"add_generation_prompt": False,
},
}
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_single_s2st_request(omni_server, openai_client) -> None:
"""Test single speech-to-speech request via chat completions API."""
request_config = _build_s2st_request_config(
omni_server,
_synthetic_audio_base64(duration_sec=2),
)
openai_client.send_omni_request(request_config)
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("request_num", [2, 4])
def test_concurrent_s2st_requests(omni_server, openai_client, request_num: int) -> None:
"""Test concurrent speech-to-speech requests."""
request_config = _build_s2st_request_config(
omni_server,
_synthetic_audio_base64(duration_sec=2),
)
responses = openai_client.send_omni_request(request_config, request_num=request_num)
assert len(responses) == request_num
@@ -1,180 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E Online tests for Voxtral TTS model with text input and audio output.
These tests verify the /v1/audio/speech endpoint works correctly with
actual model inference, not mocks.
"""
import os
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
MODEL = "mistralai/Voxtral-4B-TTS-2603"
STAGE_CONFIG = get_deploy_config_path("voxtral_tts.yaml")
EXTRA_ARGS = ["--trust-remote-code", "--enforce-eager", "--disable-log-stats"]
TEST_PARAMS = [OmniServerParams(model=MODEL, stage_config_path=STAGE_CONFIG, server_args=EXTRA_ARGS)]
def verify_wav_audio(content: bytes) -> bool:
"""Verify that content is valid WAV audio data."""
if len(content) < 44:
return False
return content[:4] == b"RIFF" and content[8:12] == b"WAVE"
# Minimum expected audio size for a short sentence (~1 second of 24kHz 16-bit mono WAV)
_MIN_AUDIO_BYTES_BASIC = 10000
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
class TestVoxtralTTSFixedVoice:
"""E2E tests for Voxtral TTS model."""
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_speech_english_basic(self, omni_server, openai_client) -> None:
"""Test basic English TTS generation."""
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Hello, how are you?",
"voice": "casual_female",
"language": "English",
"response_format": "wav",
"timeout": 120.0,
"min_audio_bytes": _MIN_AUDIO_BYTES_BASIC,
}
)
@pytest.mark.core_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_speech_english_streaming(self, omni_server, openai_client) -> None:
"""Test basic streaming English TTS generation (PCM via streaming API)."""
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Hello, how are you?",
"voice": "casual_female",
"language": "English",
"stream": True,
"response_format": "pcm",
"timeout": 120.0,
"min_audio_bytes": _MIN_AUDIO_BYTES_BASIC,
}
)
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_speech_different_voices(self, omni_server, openai_client) -> None:
"""Test TTS with different voice presets."""
voices = ["casual_female", "neutral_male"]
for voice in voices:
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Testing voice selection.",
"voice": voice,
"language": "English",
"response_format": "wav",
"timeout": 120.0,
}
)
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_speech_speed(self, omni_server, openai_client) -> None:
"""Request with speed parameters"""
speeds = [0.5, 1, 1.5, 2, 2.5]
for speed in speeds:
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "The boy was there when the sun rose.",
"voice": "casual_female",
"language": "English",
"response_format": "wav",
"timeout": 120.0,
"speed": speed,
}
)
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_speech_instructions(self, omni_server, openai_client) -> None:
"""Request with instructions parameters"""
instructions = [
"Speak formally",
"Speak angrily",
"Deliver with a sad voice",
"Speak with a chirpy happy voice",
]
for instruction in instructions:
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "The boy was there when the sun rose.",
"voice": "casual_female",
"language": "English",
"response_format": "wav",
"timeout": 120.0,
"instructions": instruction,
}
)
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_speech_response_formats(self, omni_server, openai_client) -> None:
"""Test TTS with different response formats"""
response_formats = ["wav", "mp3"]
for response_format in response_formats:
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Testing various response formats.",
"voice": "casual_male",
"language": "English",
"response_format": response_format,
"timeout": 120.0,
}
)
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_speech_batches(self, omni_server, openai_client) -> None:
"""Test TTS batches"""
items = [
{"input": "The birch canoe slid on the smooth planks."},
{"input": "Glue the sheet to the dark blue background."},
{"input": "It's easy to tell the depth of a well."},
{"input": "These days a chicken leg is a rare dish."},
{"input": "Rice is often served in round bowls."},
]
openai_client.send_audio_speech_batch_http_request(
{
"json": {
"model": omni_server.model,
"items": items,
"voice": "casual_male",
"language": "English",
"response_format": "wav",
},
"timeout": 120.0,
}
)
@@ -0,0 +1,163 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E tests for Voxtral TTS online serving.
Smoke tests (core_model + advanced_model) and expansion scenarios (slow)
for the /v1/audio/speech endpoint.
"""
import os
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import get_deploy_config_path
MODEL = "mistralai/Voxtral-4B-TTS-2603"
STAGE_CONFIG = get_deploy_config_path("voxtral_tts.yaml")
EXTRA_ARGS = ["--trust-remote-code", "--enforce-eager", "--disable-log-stats"]
TEST_PARAMS = [OmniServerParams(model=MODEL, stage_config_path=STAGE_CONFIG, server_args=EXTRA_ARGS)]
_MIN_AUDIO_BYTES_BASIC = 10000
pytestmark = [
pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True),
pytest.mark.slow,
pytest.mark.tts,
]
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_speech_english_basic(omni_server, openai_client) -> None:
"""Test basic English TTS generation."""
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "how are you",
"voice": "casual_female",
"language": "English",
"response_format": "wav",
"timeout": 120.0,
"min_audio_bytes": _MIN_AUDIO_BYTES_BASIC,
}
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_speech_english_streaming(omni_server, openai_client) -> None:
"""Test basic streaming English TTS generation (PCM via streaming API)."""
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Hello, how are you?",
"voice": "casual_female",
"language": "English",
"stream": True,
"response_format": "pcm",
"timeout": 120.0,
"min_audio_bytes": _MIN_AUDIO_BYTES_BASIC,
}
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_speech_different_voices(omni_server, openai_client) -> None:
"""Test TTS with different voice presets."""
voices = ["casual_female", "neutral_male"]
for voice in voices:
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Testing voice selection.",
"voice": voice,
"language": "English",
"response_format": "wav",
"timeout": 120.0,
}
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_speech_speed(omni_server, openai_client) -> None:
"""Request with speed parameters."""
speeds = [0.5, 1, 1.5, 2, 2.5]
for speed in speeds:
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "The boy was there when the sun rose.",
"voice": "casual_female",
"language": "English",
"response_format": "wav",
"timeout": 120.0,
"speed": speed,
}
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_speech_instructions(omni_server, openai_client) -> None:
"""Request with instructions parameters."""
instructions = [
"Speak formally",
"Speak angrily",
"Deliver with a sad voice",
"Speak with a chirpy happy voice",
]
for instruction in instructions:
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "The boy was there when the sun rose.",
"voice": "casual_female",
"language": "English",
"response_format": "wav",
"timeout": 120.0,
"instructions": instruction,
}
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_speech_response_formats(omni_server, openai_client) -> None:
"""Test TTS with different response formats."""
response_formats = ["wav", "mp3"]
for response_format in response_formats:
openai_client.send_audio_speech_request(
{
"model": omni_server.model,
"input": "Testing various response formats.",
"voice": "casual_male",
"language": "English",
"response_format": response_format,
"timeout": 120.0,
}
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_speech_batches(omni_server, openai_client) -> None:
"""Test TTS batches."""
items = [
{"input": "The birch canoe slid on the smooth planks."},
{"input": "Glue the sheet to the dark blue background."},
{"input": "It's easy to tell the depth of a well."},
{"input": "These days a chicken leg is a rare dish."},
{"input": "Rice is often served in round bowls."},
]
openai_client.send_audio_speech_batch_http_request(
{
"json": {
"model": omni_server.model,
"items": items,
"voice": "casual_male",
"language": "English",
"response_format": "wav",
},
"timeout": 120.0,
}
)
@@ -29,7 +29,7 @@ class TestOpenAIAudioListEndpoints:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_list_voices_endpoint(self, omni_server) -> None:
"""``GET /v1/audio/voices`` returns a non-empty ``voices`` list."""
url = f"http://{omni_server.host}:{omni_server.port}/v1/audio/voices"
@@ -45,7 +45,7 @@ class TestOpenAIAudioListEndpoints:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_models_endpoint(self, omni_server) -> None:
"""``GET /v1/models`` returns at least one model."""
url = f"http://{omni_server.host}:{omni_server.port}/v1/models"
@@ -214,27 +214,6 @@ def _assert_realtime_accuracy(result: dict) -> None:
class TestQwen3OmniRealtimeWebSocket:
@pytest.mark.core_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@pytest.mark.parametrize("omni_server", realtime_async_chunk_server_params, indirect=True)
def test_streaming_audio_input_pcm_output_async_chunk_smoke(self, omni_server) -> None:
"""Ready CI: async_chunk on, no send delay, smoke only."""
pcm16 = _synthetic_pcm16_input()
result = asyncio.run(
_run_realtime_audio_roundtrip(
omni_server.host,
omni_server.port,
omni_server.model,
pcm16,
chunk_ms=100,
send_delay_ms=0,
)
)
_assert_realtime_smoke(result)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
@@ -125,7 +125,7 @@ class TestSpeechBatchE2E:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", default_server_params, indirect=True)
def test_batch_single_item(self, omni_server) -> None:
"""Batch with a single item works correctly."""
@@ -147,7 +147,7 @@ class TestSpeechBatchE2E:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", default_server_params, indirect=True)
def test_batch_per_item_voice_override(self, omni_server) -> None:
"""Per-item voice overrides the batch-level default."""
@@ -175,7 +175,7 @@ class TestSpeechBatchE2E:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", default_server_params, indirect=True)
def test_batch_multiple_languages(self, omni_server) -> None:
"""Batch items with different languages per item."""
@@ -200,7 +200,7 @@ class TestSpeechBatchE2E:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", default_server_params, indirect=True)
def test_batch_whisper_transcription(self, omni_server) -> None:
"""Whisper transcription of batch output matches input text."""
@@ -235,7 +235,7 @@ class TestSpeechBatchE2E:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", default_server_params, indirect=True)
def test_batch_result_indices_ordered(self, omni_server) -> None:
"""Result indices match the input item order."""
@@ -262,7 +262,7 @@ class TestSpeechBatchValidation:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", default_server_params, indirect=True)
def test_batch_empty_items_rejected(self, omni_server) -> None:
"""Empty items list returns a 4xx error."""
@@ -276,7 +276,7 @@ class TestSpeechBatchValidation:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", default_server_params, indirect=True)
def test_batch_exceeds_max_items(self, omni_server) -> None:
"""Batch exceeding 32 items returns 400 error."""
@@ -334,7 +334,7 @@ class TestSpeechBatchSize2:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", batch2_server_params, indirect=True)
def test_batch2_produces_valid_audio(self, omni_server) -> None:
"""Batch of 2 items with batched engine produces valid audio."""
@@ -371,7 +371,7 @@ class TestSpeechBatchSize2:
@pytest.mark.advanced_model
@pytest.mark.tts
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("omni_server", batch2_server_params, indirect=True)
def test_batch2_vs_sequential_timing(self, omni_server) -> None:
"""Compare batch-of-2 vs 2 sequential single requests.
+47 -44
View File
@@ -272,69 +272,72 @@ def test_output_kind_is_preserved_with_explicit_sampling_params(output_kind):
# End to end tests for ensuring internal manipulation of request ID
# in diffusion / Omni models don't leak back to the user.
#
# NOTE: It seems like we currently need the shutdowns here, otherwise
# running the tests sequentially seems to leave a zombie process in diffusion
# that can OOM the Omni tests.
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.omni
@pytest.mark.asyncio
@pytest.mark.parametrize(
"req_id",
["my-req-1", "img_gen-abc123", "chatcmpl-xyz"],
ids=["plain", "prefixed-img", "prefixed-chat"],
)
async def test_diffusion_generate_preserves_request_id(req_id):
"""Ensure diffusion model requests don't leak internal UUID-suffixed req id back to user."""
engine = AsyncOmni(model=DIFFUSION_MODEL)
try:
async for output in engine.generate("a white cat", request_id=req_id):
assert output.request_id == req_id
finally:
engine.shutdown()
# One AsyncOmni per test function (all cases in a single asyncio loop) to avoid
# repeated cold starts. Do not use class/module-scoped engine fixtures here:
# pytest-asyncio uses a function-scoped event loop by default, so reusing an
# engine across tests can hang on the second generate() call.
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.omni
@pytest.mark.asyncio
@pytest.mark.parametrize(
"req_id",
["my-req-1", "img_gen-abc123", "chatcmpl-xyz"],
ids=["plain", "prefixed-img", "prefixed-chat"],
)
async def test_omni_generate_preserves_request_id(req_id):
"""Ensure omni model requests don't leak internal UUID-suffixed req id back to user."""
engine = AsyncOmni(model=OMNI_MODEL, stage_configs_path=OMNI_STAGE_CONFIG)
try:
async for output in engine.generate(
"Say hello in one word.",
request_id=req_id,
output_modalities=["text"],
):
assert output.request_id == req_id
finally:
engine.shutdown()
# Covers:
# * plain client ids (``my-req-1``)
# * OpenAI-style prefixed ids (``img_gen-*``, ``chatcmpl-*``) that AsyncOmni
# suffixes internally for engine routing — streamed outputs must still echo
# the caller-visible id, not the internal UUID-suffixed id
# * empty ``request_id`` — server assigns a non-empty id for the caller
_DIFFUSION_REQ_IDS = ["my-req-1", "img_gen-abc123", "chatcmpl-xyz"]
_OMNI_REQ_IDS = ["my-req-1", "img_gen-abc123", "chatcmpl-xyz"]
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.omni
@pytest.mark.asyncio
async def test_diffusion_generate_empty_request_id():
"""Empty request_id should get a generated internal ID, not stay empty (diffusion)"""
async def test_diffusion_generate_request_id():
"""Diffusion E2E request-id contract (``riverclouds/qwen_image_random``).
Scenarios (one engine, sequential ``generate`` calls):
- plain id ``my-req-1``
- image-style prefix ``img_gen-abc123``
- chat-style prefix ``chatcmpl-xyz``
- empty ``request_id`` → output id is non-empty (auto-assigned)
Each streaming output must expose the user-supplied id unchanged; internal
UUID suffixing must not leak into ``output.request_id``.
"""
engine = AsyncOmni(model=DIFFUSION_MODEL)
try:
for req_id in _DIFFUSION_REQ_IDS:
async for output in engine.generate("a white cat", request_id=req_id):
assert output.request_id == req_id
async for output in engine.generate("a white cat", request_id=""):
assert output.request_id != ""
finally:
engine.shutdown()
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.omni
@pytest.mark.asyncio
async def test_omni_generate_empty_request_id():
"""Empty request_id should get a generated internal ID, not stay empty (omni)"""
async def test_omni_generate_request_id():
"""Omni E2E request-id contract (``Qwen/Qwen2.5-Omni-7B``, thinker-only stage).
Same scenarios as ``test_diffusion_generate_request_id``:
- plain id ``my-req-1``
- image-style prefix ``img_gen-abc123``
- chat-style prefix ``chatcmpl-xyz``
- empty ``request_id`` → output id is non-empty (auto-assigned)
Text modality only; asserts caller-visible ids are preserved across the
multi-stage orchestrator path on H100.
"""
engine = AsyncOmni(model=OMNI_MODEL, stage_configs_path=OMNI_STAGE_CONFIG)
try:
for req_id in _OMNI_REQ_IDS:
async for output in engine.generate(
"Say hello in one word.",
request_id=req_id,
output_modalities=["text"],
):
assert output.request_id == req_id
async for output in engine.generate(
"Say hello in one word.",
request_id="",
+95 -20
View File
@@ -17,6 +17,7 @@ import soundfile as sf
from PIL import Image
from tests.helpers.media import (
convert_audio_bytes_to_text,
cosine_similarity_text,
)
@@ -374,8 +375,15 @@ def _estimate_voice_gender_from_audio(audio_bytes: bytes) -> str:
return "unknown"
def _assert_preset_voice_gender_from_audio(audio_bytes: bytes | None, voice_name: str | None) -> None:
def _assert_preset_voice_gender_from_audio(
audio_bytes: bytes | None,
voice_name: str | None,
*,
response_format: str | None = None,
) -> None:
"""If ``voice_name`` matches a known preset, assert classifier gender matches (skip when unknown)."""
if response_format == "pcm":
return
if not voice_name or not audio_bytes:
return
key = str(voice_name).lower()
@@ -432,6 +440,61 @@ def _assert_pcm_int16_speech_hnr(audio_bytes: bytes, min_hnr_db: float = _MIN_PC
)
def _response_has_audio_output(response: Any) -> bool:
if response.audio_bytes:
return len(response.audio_bytes) > 0
if isinstance(getattr(response, "audio_content", None), str) and response.audio_content.strip():
return True
audio_data = getattr(response, "audio_data", None)
return bool(audio_data)
def _omni_assertion_needs_audio_transcript(request_config: dict[str, Any], run_level: str) -> bool:
if run_level not in {"advanced_model", "full_model"}:
return False
modalities = request_config.get("modalities", ["text", "audio"])
if "audio" not in modalities:
return False
keywords_dict = request_config.get("key_words", {}) or {}
if keywords_dict.get("audio") and "text" not in modalities:
return True
if request_config.get("audio_ref_text"):
return True
return "text" in modalities
def _speech_assertion_needs_audio_transcript(request_config: dict[str, Any], run_level: str) -> bool:
if run_level not in {"advanced_model", "full_model"}:
return False
if request_config.get("response_format") == "pcm":
return False
return bool(request_config.get("input"))
def _resolve_audio_transcript(
response: Any,
request_config: dict[str, Any],
run_level: str,
*,
speech_api: bool,
) -> str | None:
"""Run Whisper only when this run_level / request_config needs a transcript for assertions."""
needs = (
_speech_assertion_needs_audio_transcript(request_config, run_level)
if speech_api
else _omni_assertion_needs_audio_transcript(request_config, run_level)
)
if not needs:
return None
existing = getattr(response, "audio_content", None)
if isinstance(existing, str) and existing.strip():
return existing
audio_bytes = getattr(response, "audio_bytes", None)
if not audio_bytes:
return None
return convert_audio_bytes_to_text(audio_bytes)
def assert_omni_response(response: Any, request_config: dict[str, Any], run_level):
"""
Validate response results.
@@ -447,15 +510,18 @@ def assert_omni_response(response: Any, request_config: dict[str, Any], run_leve
modalities = request_config.get("modalities", ["text", "audio"])
if run_level in {"advanced_model", "full_model"}:
transcript = _resolve_audio_transcript(response, request_config, run_level, speech_api=False)
# Verify output success
if "audio" in modalities:
assert response.audio_content is not None, "No audio output is generated"
print(f"audio content is: {response.audio_content}")
assert _response_has_audio_output(response), "No audio output is generated"
if transcript is not None:
print(f"audio content is: {transcript}")
speaker = request_config.get("speaker")
if speaker:
_assert_preset_voice_gender_from_audio(
response.audio_bytes,
speaker,
response_format=request_config.get("response_format"),
)
if "text" in modalities:
assert response.text_content is not None, "No text output is generated"
@@ -474,7 +540,8 @@ def assert_omni_response(response: Any, request_config: dict[str, Any], run_leve
)
else:
if keywords:
audio_lower = response.audio_content.lower()
assert transcript is not None, "No audio transcript for keyword validation"
audio_lower = transcript.lower()
assert any(str(kw).lower() in audio_lower for kw in keywords), (
"The output does not contain any of the keywords."
)
@@ -484,7 +551,7 @@ def assert_omni_response(response: Any, request_config: dict[str, Any], run_leve
audio_ref_text = request_config.get("audio_ref_text")
similarity_threshold = request_config.get("similarity_threshold", 0.8)
if "text" in modalities:
transcript = (response.audio_content or "").strip()
assert transcript is not None, "No audio transcript for similarity validation"
text_output = (response.text_content or "").strip()
# For very short outputs (e.g. one-word answers), n-gram cosine
# similarity with length penalty is unreliable because Whisper
@@ -513,8 +580,9 @@ def assert_omni_response(response: Any, request_config: dict[str, Any], run_leve
print(f"similarity is: {similarity}")
assert similarity > similarity_threshold, "The audio content is not same as the text"
if audio_ref_text:
assert transcript is not None, "No audio transcript for reference-text validation"
audio_similarity = cosine_similarity_text(
response.audio_content.lower(),
transcript.strip().lower(),
str(audio_ref_text).lower(),
)
assert audio_similarity > similarity_threshold, (
@@ -557,8 +625,6 @@ def assert_audio_speech_response(response: Any, request_config: dict[str, Any],
req_fmt = request_config.get("response_format")
if req_fmt == "pcm" and response.audio_bytes:
min_hnr_db = float(request_config.get("min_hnr_db", _MIN_PCM_SPEECH_HNR_DB))
_assert_pcm_int16_speech_hnr(response.audio_bytes, min_hnr_db=min_hnr_db)
if response.audio_format:
assert "pcm" in response.audio_format.lower(), (
f"Expected audio/pcm content-type, got {response.audio_format!r}"
@@ -566,18 +632,27 @@ def assert_audio_speech_response(response: Any, request_config: dict[str, Any],
elif req_fmt == "wav" and response.audio_format:
assert req_fmt in response.audio_format
if run_level in {"advanced_model", "full_model"} and req_fmt != "pcm":
expected_text = request_config.get("input")
if expected_text:
transcript = (response.audio_content or "").strip()
print(f"audio content is: {transcript}")
print(f"input text is: {expected_text}")
similarity = cosine_similarity_text(transcript.lower(), expected_text.lower())
print(f"Cosine similarity: {similarity:.3f}")
assert similarity > 0.9, (
f"Transcript doesn't match input: similarity={similarity:.2f}, transcript='{transcript}'"
)
_assert_preset_voice_gender_from_audio(response.audio_bytes, request_config.get("voice"))
if run_level in {"advanced_model", "full_model"}:
if req_fmt == "pcm" and response.audio_bytes:
min_hnr_db = float(request_config.get("min_hnr_db", _MIN_PCM_SPEECH_HNR_DB))
_assert_pcm_int16_speech_hnr(response.audio_bytes, min_hnr_db=min_hnr_db)
transcript = _resolve_audio_transcript(response, request_config, run_level, speech_api=True)
if transcript is not None:
expected_text = request_config.get("input")
if expected_text:
print(f"audio content is: {transcript}")
print(f"input text is: {expected_text}")
similarity = cosine_similarity_text(transcript.strip().lower(), str(expected_text).lower())
print(f"Cosine similarity: {similarity:.3f}")
assert similarity > 0.9, (
f"Transcript doesn't match input: similarity={similarity:.2f}, transcript='{transcript}'"
)
_assert_preset_voice_gender_from_audio(
response.audio_bytes,
request_config.get("voice"),
response_format=request_config.get("response_format"),
)
def assert_diffusion_response(response: "DiffusionResponse", request_config: dict[str, Any], run_level: str = None):
+1
View File
@@ -201,6 +201,7 @@ def run_pre_test_cleanup() -> None:
wait_for_gpu_memory_to_clear(
devices=list(range(num_gpus)),
threshold_ratio=0.05,
timeout_s=60,
)
except Exception as e:
print(f"Pre-test cleanup note: {e}")
+9 -54
View File
@@ -45,7 +45,6 @@ from tests.helpers.assertions import (
from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup
from tests.helpers.media import (
_merge_base64_audio_to_segment,
convert_audio_bytes_to_text,
decode_b64_image,
)
from vllm_omni.config.stage_config import resolve_deploy_yaml
@@ -898,16 +897,13 @@ class OpenAIClientHandler:
if details := getattr(chunk.usage, "prompt_tokens_details", None):
result.cached_tokens = details.cached_tokens
audio_content = None
if audio_data:
merged_seg = _merge_base64_audio_to_segment(audio_data)
wav_buf = BytesIO()
merged_seg.export(wav_buf, format="wav")
result.audio_bytes = wav_buf.getvalue()
audio_content = convert_audio_bytes_to_text(result.audio_bytes)
result.text_content = text_content
result.audio_data = audio_data
result.audio_content = audio_content
result.e2e_latency = time.perf_counter() - wall_start
result.success = True
except Exception as e:
@@ -932,12 +928,9 @@ class OpenAIClientHandler:
result.prompt_tokens = usage.prompt_tokens
if details := getattr(usage, "prompt_tokens_details", None):
result.cached_tokens = details.cached_tokens
audio_content = None
if audio_data:
result.audio_bytes = base64.b64decode(audio_data)
audio_content = convert_audio_bytes_to_text(result.audio_bytes)
result.text_content = text_content
result.audio_content = audio_content
result.e2e_latency = time.perf_counter() - wall_start
if chat_completion.choices and chat_completion.choices[0].logprobs is not None:
result.logprobs = chat_completion.choices[0].logprobs.content
@@ -1587,6 +1580,8 @@ class OpenAIClientHandler:
extra_body["mm_processor_kwargs"] = mm
if "sampling_params_list" in request_config:
extra_body["sampling_params_list"] = request_config["sampling_params_list"]
if request_config.get("extra_body"):
extra_body.update(request_config["extra_body"])
create_kwargs: dict[str, Any] = {
"model": request_config.get("model"),
@@ -1648,8 +1643,8 @@ class OpenAIClientHandler:
Process streaming /v1/audio/speech responses into an OmniResponse.
This mirrors _process_stream_omni_response but operates on low-level
audio bytes and produces an OmniResponse with audio_content filled
from Whisper transcription.
audio bytes. Whisper transcription runs in assert_audio_speech_response
when the run_level requires it.
"""
result = OmniResponse()
@@ -1685,14 +1680,9 @@ class OpenAIClientHandler:
raise TypeError(f"Unsupported audio speech streaming response type: {type(response)}")
raw_bytes = bytes(data)
if response_format == "pcm":
transcript = None
else:
transcript = convert_audio_bytes_to_text(raw_bytes)
# Populate OmniResponse.
result.audio_bytes = raw_bytes
result.audio_content = transcript
result.e2e_latency = time.perf_counter() - wall_start
result.success = True
result.audio_format = getattr(response, "response", None)
@@ -1725,13 +1715,7 @@ class OpenAIClientHandler:
else:
raise TypeError(f"Unsupported audio speech response type: {type(response)}")
if response_format == "pcm":
transcript = None
else:
transcript = convert_audio_bytes_to_text(raw_bytes)
result.audio_bytes = raw_bytes
result.audio_content = transcript
result.e2e_latency = time.perf_counter() - wall_start
result.success = True
result.audio_format = getattr(response, "response", None)
@@ -2796,25 +2780,6 @@ class OmniRunnerHandler:
# ---------------------------------------------------------------------------
def _core_model_stage_config_path_with_dummy_load_format(stage_config_path: str | None, run_level: str) -> str | None:
"""For ``core_model`` runs, patch every stage in the deploy YAML to ``load_format: dummy``."""
if run_level != "core_model" or stage_config_path is None:
return stage_config_path
from tests.helpers.stage_config import modify_stage_config
with open(stage_config_path, encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
new_schema_stages = cfg.get("stages")
stage_key = "stages" if new_schema_stages is not None else "stage_args"
update_path = "load_format" if new_schema_stages is not None else "engine_args.load_format"
stage_entries = cfg.get(stage_key, [])
stage_ids = [stage["stage_id"] for stage in stage_entries if "stage_id" in stage]
return modify_stage_config(
stage_config_path,
updates={stage_key: {stage_id: {update_path: "dummy"} for stage_id in stage_ids}},
)
def iter_omni_server(
request: Any,
run_level: str,
@@ -2822,25 +2787,13 @@ def iter_omni_server(
omni_fixture_lock: threading.Lock,
) -> Generator[Any, Any, None]:
"""Start/stop an Omni HTTP server; used by ``omni_server`` / ``omni_server_function`` fixtures."""
from tests.helpers.stage_config import modify_stage_config
from tests.helpers.stage_config import stage_config_path_for_run_level
with omni_fixture_lock:
params: OmniServerParams = request.param
model = model_prefix + params.model
port = params.port
stage_config_path = params.stage_config_path
if run_level in {"advanced_model", "full_model"} and stage_config_path is not None:
with open(stage_config_path, encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
new_schema_stages = cfg.get("stages")
stage_key = "stages" if new_schema_stages is not None else "stage_args"
delete_path = "load_format" if new_schema_stages is not None else "engine_args.load_format"
stage_entries = cfg.get(stage_key, [])
stage_ids = [stage["stage_id"] for stage in stage_entries if "stage_id" in stage]
stage_config_path = modify_stage_config(
stage_config_path,
deletes={stage_key: {stage_id: [delete_path] for stage_id in stage_ids}},
)
stage_config_path = stage_config_path_for_run_level(params.stage_config_path, run_level)
server_args = params.server_args or []
if params.use_omni and params.stage_init_timeout is not None:
@@ -2905,6 +2858,8 @@ def iter_omni_runner(
omni_fixture_lock: threading.Lock,
) -> Generator[Any, None, None]:
"""Yield an :class:`OmniRunner`; used by ``omni_runner`` / ``omni_runner_function`` fixtures."""
from tests.helpers.stage_config import stage_config_path_for_run_level
with omni_fixture_lock:
param = request.param
if not isinstance(param, (tuple, list)) or len(param) not in (2, 3):
@@ -2918,7 +2873,7 @@ def iter_omni_runner(
else:
model, stage_config_path, extra = param[0], param[1], param[2]
extra_omni_kwargs = dict(extra) if extra is not None else {}
stage_config_path = _core_model_stage_config_path_with_dummy_load_format(stage_config_path, run_level)
stage_config_path = stage_config_path_for_run_level(stage_config_path, run_level)
model = model_prefix + model
with OmniRunner(model, seed=42, stage_configs_path=stage_config_path, **extra_omni_kwargs) as runner:
print("OmniRunner started successfully")
+74 -1
View File
@@ -627,7 +627,80 @@ def get_deploy_config_path(rel_path: str) -> str:
return str(_DEPLOY_DIR / rel_path)
def _get_config_value_by_path(config_dict: dict, path: str) -> Any:
"""Read a dot-separated path from a nested dict (e.g. ``engine_args.load_format``)."""
current: Any = config_dict
for key in path.split("."):
if not isinstance(current, dict) or key not in current:
return None
current = current[key]
return current
def _stage_load_format_paths(stage_config_path: str) -> tuple[str, str, list[int]]:
"""Return ``(stage_key, load_format_field_path, stage_ids)`` for a deploy YAML."""
with open(stage_config_path, encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
new_schema_stages = cfg.get("stages")
stage_key = "stages" if new_schema_stages is not None else "stage_args"
load_format_path = "load_format" if new_schema_stages is not None else "engine_args.load_format"
stage_entries = cfg.get(stage_key, [])
stage_ids = [stage["stage_id"] for stage in stage_entries if "stage_id" in stage]
return stage_key, load_format_path, stage_ids
def _add_dummy_load_format(
stage_config_path: str | None,
run_level: str,
) -> str | None:
"""For ``core_model`` runs, patch every stage in the deploy YAML to ``load_format: dummy``."""
if run_level != "core_model" or stage_config_path is None:
return stage_config_path
stage_key, load_format_path, stage_ids = _stage_load_format_paths(stage_config_path)
return modify_stage_config(
stage_config_path,
updates={stage_key: {stage_id: {load_format_path: "dummy"} for stage_id in stage_ids}},
)
def _delete_dummy_load_format(
stage_config_path: str | None,
run_level: str,
) -> str | None:
"""For ``advanced_model`` / ``full_model``, strip ``load_format: dummy`` so real weights load."""
if run_level not in {"advanced_model", "full_model"} or stage_config_path is None:
return stage_config_path
stage_key, load_format_path, _stage_ids = _stage_load_format_paths(stage_config_path)
with open(stage_config_path, encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
stage_entries = cfg.get(stage_key, [])
deletes: dict[int, list[str]] = {}
for stage in stage_entries:
stage_id = stage.get("stage_id")
if stage_id is None:
continue
if _get_config_value_by_path(stage, load_format_path) == "dummy":
deletes[stage_id] = [load_format_path]
if not deletes:
return stage_config_path
return modify_stage_config(
stage_config_path,
deletes={stage_key: deletes},
)
def stage_config_path_for_run_level(stage_config_path: str | None, run_level: str) -> str | None:
"""Apply run-level deploy YAML tweaks for weight loading (dummy vs real)."""
if run_level in {"advanced_model", "full_model"}:
return _delete_dummy_load_format(stage_config_path, run_level)
return _add_dummy_load_format(stage_config_path, run_level)
__all__ = [
"modify_stage_config",
"get_deploy_config_path",
"modify_stage_config",
"stage_config_path_for_run_level",
]
@@ -0,0 +1,151 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for Step-Audio2 thinker processor, encoder, and token handling."""
import numpy as np
import pytest
import torch
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def create_dummy_audio(sample_rate: int = 16000, duration_sec: float = 1.0) -> tuple[np.ndarray, int]:
"""Create a dummy audio signal (sine wave) for testing."""
t = np.linspace(0, duration_sec, int(sample_rate * duration_sec), dtype=np.float32)
audio = 0.5 * np.sin(2 * np.pi * 440 * t)
return audio, sample_rate
def test_processor_loads_correctly():
"""Test that Step-Audio2 processor can be imported and initialized."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
StepAudio2MultiModalProcessor,
StepAudio2ProcessingInfo,
StepAudio2Processor,
)
assert StepAudio2MultiModalProcessor is not None
assert StepAudio2ProcessingInfo is not None
assert StepAudio2Processor is not None
def test_audio_preprocessing():
"""Test that audio preprocessing produces correct tensor shapes."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
log_mel_spectrogram,
padding_mels,
)
audio, _sample_rate = create_dummy_audio(sample_rate=16000, duration_sec=1.0)
mel = log_mel_spectrogram(audio)
assert mel.ndim == 2, f"Expected 2D tensor, got {mel.ndim}D"
assert mel.shape[0] == 128, f"Expected 128 mel bins, got {mel.shape[0]}"
mels = [mel, mel]
padded_mels, _mel_lens = padding_mels(mels)
assert padded_mels.ndim == 3, f"Expected 3D tensor, got {padded_mels.ndim}D"
assert padded_mels.shape[0] == 2, f"Expected batch size 2, got {padded_mels.shape[0]}"
assert padded_mels.shape[1] == 128, f"Expected 128 mel bins, got {padded_mels.shape[1]}"
def test_feature_length_calculation():
"""Test that audio feature length calculation is correct."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
calculate_audio_feature_length,
)
assert calculate_audio_feature_length(1000) == 125
assert calculate_audio_feature_length(100) > 0
assert calculate_audio_feature_length(1) >= 1
def test_token_config_constants():
"""Test that token configuration constants are set correctly."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_constants import (
DEFAULT_TOKEN_CONFIG,
STEP_AUDIO2_AUDIO_END,
STEP_AUDIO2_AUDIO_PATCH_TOKEN_ID,
STEP_AUDIO2_AUDIO_START,
STEP_AUDIO2_AUDIO_VOCAB_SIZE,
STEP_AUDIO2_TEXT_MAX,
)
assert STEP_AUDIO2_TEXT_MAX < STEP_AUDIO2_AUDIO_START, "Text tokens should come before audio tokens"
assert STEP_AUDIO2_AUDIO_END == STEP_AUDIO2_AUDIO_START + STEP_AUDIO2_AUDIO_VOCAB_SIZE - 1
assert STEP_AUDIO2_AUDIO_PATCH_TOKEN_ID > STEP_AUDIO2_TEXT_MAX
assert STEP_AUDIO2_AUDIO_PATCH_TOKEN_ID < STEP_AUDIO2_AUDIO_START
assert DEFAULT_TOKEN_CONFIG.text_max == STEP_AUDIO2_TEXT_MAX
assert DEFAULT_TOKEN_CONFIG.audio_start == STEP_AUDIO2_AUDIO_START
def test_mm_field_config_structure():
"""Test that multimodal field config is properly structured for vLLM batching."""
from vllm.multimodal.inputs import MultiModalFieldConfig
audio_lens = torch.tensor([100, 200, 150], dtype=torch.int32)
field_config = MultiModalFieldConfig.flat_from_sizes("audio", audio_lens)
assert field_config is not None
def test_audio_encoder_output_shape():
"""Test that audio encoder produces correct output shapes."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
Adaptor,
AudioEncoder,
)
encoder = AudioEncoder(n_mels=128, n_ctx=1500, n_state=512, n_head=8, n_layer=6)
batch_size = 2
n_mels = 128
time_steps = 400
x = torch.randn(batch_size, n_mels, time_steps)
x_len = torch.tensor([time_steps, time_steps // 2], dtype=torch.int32)
encoded, _encoded_lens = encoder(x, x_len)
assert encoded.ndim == 3, f"Expected 3D tensor, got {encoded.ndim}D"
assert encoded.shape[0] == batch_size
assert encoded.shape[2] == 512
adapter = Adaptor(n_state=512, n_hidden=4096, kernel_size=3, stride=2)
adapted = adapter(encoded)
assert adapted.ndim == 3
assert adapted.shape[0] == batch_size
assert adapted.shape[2] == 4096
def test_token_separation():
"""Test that token separation works correctly."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
StepAudio2ThinkerForConditionalGeneration,
)
token_ids = [100, 200, 151700, 300, 151800, 400]
text_tokens, audio_tokens = StepAudio2ThinkerForConditionalGeneration.separate_tokens(token_ids)
assert text_tokens == [100, 200, 300, 400]
assert audio_tokens == [4, 104]
def test_has_audio_output():
"""Test detection of audio tokens in output."""
from vllm_omni.model_executor.models.step_audio2.step_audio2_thinker import (
StepAudio2ThinkerForConditionalGeneration,
)
text_only = [100, 200, 300]
assert not StepAudio2ThinkerForConditionalGeneration.has_audio_output(text_only)
with_audio = [100, 200, 151700, 300]
assert StepAudio2ThinkerForConditionalGeneration.has_audio_output(with_audio)