[Test] Unify L2/L3 test layout, Buildkite steps, and test helpers (#2556)
Signed-off-by: wangyu <410167048@qq.com> Signed-off-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com> Signed-off-by: wangyu <53896905+yenuo26@users.noreply.github.com> Co-authored-by: Huang, Zeyu <11222265+fhfuih@users.noreply.github.com>
This commit is contained in:
1 parent
b85833eeec
commit
6f2ad7b403
134 files changed
+4904
-4185
No files matched your search
@@ -9,16 +9,7 @@ steps:
|
||||
- export VLLM_ROCM_USE_AITER=0
|
||||
# ignore test_teacache_extractors.py because it use rocm gemm kernel from vLLM
|
||||
# that is not supported on CPU
|
||||
- "timeout 20m pytest -v -s -m 'core_model and cpu' --ignore=tests/diffusion/cache/test_teacache_extractors.py --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml"
|
||||
|
||||
- label: "Diffusion Model Test"
|
||||
agent_pool: mi325_2
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- timeout 20m pytest -s -v tests/e2e/offline_inference/test_t2i_model.py -m "advanced_model and diffusion" --run-level "advanced_model"
|
||||
- "timeout 60m pytest -v -s -m 'core_model and cpu' --ignore=tests/diffusion/cache/test_teacache_extractors.py --cov=vllm_omni --cov-report=term-missing:skip-covered --cov-report=xml"
|
||||
|
||||
- label: "Diffusion Images API LoRA E2E"
|
||||
agent_pool: mi325_1
|
||||
@@ -26,7 +17,7 @@ steps:
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_images_generations_lora.py
|
||||
- timeout 20m pytest -s -v tests/diffusion/lora/test_images_generations_lora.py
|
||||
|
||||
- label: "Diffusion Model CPU offloading Test"
|
||||
agent_pool: mi325_1
|
||||
@@ -39,9 +30,9 @@ steps:
|
||||
- |
|
||||
timeout 20m bash -c '
|
||||
set +e
|
||||
pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py
|
||||
pytest -s -v tests/diffusion/offloader/test_diffusion_cpu_offload.py
|
||||
EXIT1=\$?
|
||||
pytest -s -v tests/e2e/offline_inference/test_diffusion_layerwise_offload.py
|
||||
pytest -s -v tests/diffusion/offloader/test_diffusion_layerwise_offload.py
|
||||
EXIT2=\$?
|
||||
exit \$((EXIT1 | EXIT2))
|
||||
'
|
||||
@@ -76,7 +67,7 @@ steps:
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- timeout 20m pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py
|
||||
- timeout 20m pytest -s -v tests/diffusion/distributed/test_sequence_parallel.py
|
||||
- timeout 20m pytest -s -v tests/diffusion/distributed/test_ulysses_uaa_perf.py
|
||||
|
||||
# merge-only tests
|
||||
@@ -86,7 +77,15 @@ steps:
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/offline_inference/test_zimage_parallelism.py
|
||||
- pytest -s -v tests/diffusion/distributed/test_zimage_parallelism.py
|
||||
|
||||
- label: "Diffusion Model Test"
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- timeout 30m pytest -s -v tests/e2e/offline_inference/test_z_image.py
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
agent_pool: mi325_2
|
||||
@@ -155,17 +154,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"
|
||||
'
|
||||
|
||||
- label: "Diffusion Image Edit Test"
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_image_gen_edit.py
|
||||
|
||||
# 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
|
||||
@@ -180,7 +168,7 @@ steps:
|
||||
# - export VLLM_LOGGING_LEVEL=DEBUG
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
# - timeout 30m pytest -s -v tests/e2e/offline_inference/test_bagel_text2img.py -m "advanced_model" --run-level "advanced_model" -k "shared_memory" -k "rocm"
|
||||
# - timeout 30m pytest -s -v tests/e2e/offline_inference/test_bagel.py -m "advanced_model" --run-level "advanced_model" -k "shared_memory" -k "rocm"
|
||||
|
||||
# - label: "Bagel Img2Img Model Test (2/3)"
|
||||
# agent_pool: mi325_1
|
||||
@@ -193,7 +181,7 @@ steps:
|
||||
# - export VLLM_LOGGING_LEVEL=DEBUG
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
# - timeout 30m pytest -s -v tests/e2e/offline_inference/test_bagel_img2img.py -m "advanced_model" --run-level "advanced_model" -k "rocm"
|
||||
# - timeout 30m pytest -s -v tests/e2e/offline_inference/test_bagel.py -m "advanced_model" --run-level "advanced_model" -k "rocm"
|
||||
|
||||
# - label: "Bagel Online Serving Test (3/3)"
|
||||
# agent_pool: mi325_1
|
||||
@@ -207,7 +195,7 @@ steps:
|
||||
# - export VLLM_LOGGING_LEVEL=DEBUG
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
# - timeout 40m pytest -s -v tests/e2e/online_serving/test_bagel_online.py -m "advanced_model" --run-level "advanced_model" -k "rocm"
|
||||
# - timeout 40m pytest -s -v tests/e2e/online_serving/test_bagel.py -m "advanced_model" --run-level "advanced_model" -k "rocm"
|
||||
|
||||
- label: "Voxtral-TTS E2E Test"
|
||||
agent_pool: mi325_1
|
||||
|
||||
@@ -9,7 +9,7 @@ steps:
|
||||
- export VLLM_ROCM_USE_AITER=0
|
||||
# ignore test_teacache_extractors.py because it use rocm gemm kernel from vLLM
|
||||
# that is not supported on CPU
|
||||
- "timeout 20m pytest -vvvv -s -m 'core_model and cpu' --ignore=tests/diffusion/cache/test_teacache_extractors.py --cov=vllm_omni --cov-branch --cov-report=term-missing --cov-report=html --cov-report=xml"
|
||||
- "timeout 60m pytest -v -s -m 'core_model and cpu' --ignore=tests/diffusion/cache/test_teacache_extractors.py --cov=vllm_omni --cov-report=term-missing:skip-covered --cov-report=xml"
|
||||
|
||||
- label: "Voxtral TTS CUDA Unit Test"
|
||||
agent_pool: mi325_1
|
||||
@@ -19,21 +19,13 @@ steps:
|
||||
commands:
|
||||
- timeout 10m pytest -s -v tests/model_executor/models/voxtral_tts/test_cuda_graph_acoustic_transformer.py
|
||||
|
||||
- label: "Diffusion Model Test"
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- timeout 30m pytest -s -v tests/e2e/offline_inference/test_t2i_model.py -m "core_model and diffusion" --run-level "core_model"
|
||||
|
||||
- label: "Diffusion Batching Test"
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- timeout 20m pytest -s -v tests/e2e/offline_inference/test_qwen_image_diffusion_batching.py -m "core_model and diffusion" --run-level "core_model"
|
||||
- timeout 20m pytest -s -v tests/diffusion/batching/test_diffusion_batching.py -m "core_model and diffusion" --run-level "core_model"
|
||||
|
||||
- label: "Custom Pipeline Test"
|
||||
agent_pool: mi325_1
|
||||
@@ -54,9 +46,9 @@ steps:
|
||||
- |
|
||||
timeout 20m bash -c '
|
||||
set +e
|
||||
pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py
|
||||
pytest -s -v tests/diffusion/offloader/test_diffusion_cpu_offload.py
|
||||
EXIT1=\$?
|
||||
pytest -s -v tests/e2e/offline_inference/test_diffusion_layerwise_offload.py
|
||||
pytest -s -v tests/diffusion/offloader/test_diffusion_layerwise_offload.py
|
||||
EXIT2=\$?
|
||||
exit \$((EXIT1 | EXIT2))
|
||||
'
|
||||
@@ -73,17 +65,6 @@ steps:
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - timeout 20m pytest -s -v tests/e2e/offline_inference/test_stable_audio_expansion.py -m "advanced_model and diffusion and L4" --run-level advanced_model
|
||||
|
||||
- label: "AudioX Online Test"
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_audiox_online.py
|
||||
|
||||
- label: "Diffusion Cache Backend Test"
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
@@ -103,7 +84,7 @@ steps:
|
||||
commands:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- timeout 20m pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py -m core_model
|
||||
- timeout 20m pytest -s -v tests/diffusion/distributed/test_sequence_parallel.py -m core_model
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
agent_pool: mi325_2
|
||||
@@ -175,17 +156,6 @@ steps:
|
||||
pytest -s -v tests/e2e/offline_inference/test_voxtral_tts.py -m "advanced_model" --run-level "advanced_model"
|
||||
'
|
||||
|
||||
- label: "Diffusion Image Edit Test"
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- timeout 20m pytest -s -v tests/e2e/online_serving/test_image_gen_edit.py
|
||||
|
||||
# TODO: Bagel test on ROCm is very unstable. @tjtanaa
|
||||
# Need to debug before reneable numerical changes across large PRs
|
||||
# - label: "Bagel Text2Img Model Test"
|
||||
@@ -199,7 +169,7 @@ steps:
|
||||
# - export VLLM_LOGGING_LEVEL=DEBUG
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
# - timeout 30m pytest -s -v tests/e2e/offline_inference/test_bagel_text2img.py -m "core_model" --run-level "core_model" -k "rocm"
|
||||
# - timeout 30m pytest -s -v tests/e2e/offline_inference/test_bagel.py -m "core_model" --run-level "core_model" -k "rocm"
|
||||
|
||||
# - label: "Bagel Img2Img Model Test"
|
||||
# agent_pool: mi325_1
|
||||
@@ -212,7 +182,7 @@ steps:
|
||||
# - export VLLM_LOGGING_LEVEL=DEBUG
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
# - timeout 30m pytest -s -v tests/e2e/offline_inference/test_bagel_img2img.py -m "core_model" --run-level "core_model" -k "rocm"
|
||||
# - timeout 30m pytest -s -v tests/e2e/offline_inference/test_bagel.py -m "core_model" --run-level "core_model" -k "rocm"
|
||||
|
||||
# - label: "Bagel Online Serving Test"
|
||||
# agent_pool: mi325_1
|
||||
@@ -226,7 +196,7 @@ steps:
|
||||
# - export VLLM_LOGGING_LEVEL=DEBUG
|
||||
# - export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
# - export VLLM_ROCM_USE_AITER_RMSNORM=0
|
||||
# - timeout 40m pytest -s -v tests/e2e/online_serving/test_bagel_online.py -m "core_model" --run-level "core_model" -k "rocm"
|
||||
# - timeout 40m pytest -s -v tests/e2e/online_serving/test_bagel.py -m "core_model" --run-level "core_model" -k "rocm"
|
||||
|
||||
- label: "CosyVoice3-TTS E2E Test"
|
||||
agent_pool: mi325_1
|
||||
@@ -236,5 +206,5 @@ steps:
|
||||
commands:
|
||||
- |
|
||||
timeout 20m bash -c '
|
||||
pytest -s -v tests/e2e/online_serving/test_cosyvoice3_tts.py -m "core_model" --run-level "core_model"
|
||||
pytest -s -v tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py -m "full_model" --run-level "core_model"
|
||||
'
|
||||
@@ -8,7 +8,7 @@ steps:
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- pytest -s -v tests/e2e/offline_inference/test_t2i_model.py
|
||||
- pytest -s -v tests/e2e/offline_inference/test_z_image.py
|
||||
|
||||
- label: "Diffusion Images API LoRA E2E"
|
||||
timeout_in_minutes: 30
|
||||
@@ -20,7 +20,7 @@ steps:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/online_serving/test_images_generations_lora.py
|
||||
- pytest -s -v tests/diffusion/lora/test_images_generations_lora.py
|
||||
|
||||
- label: "Diffusion Model CPU offloading Test"
|
||||
timeout_in_minutes: 20
|
||||
@@ -32,8 +32,8 @@ steps:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_diffusion_cpu_offload.py
|
||||
- pytest -s -v tests/e2e/offline_inference/test_diffusion_layerwise_offload.py
|
||||
- pytest -s -v tests/diffusion/offloader/test_diffusion_cpu_offload.py
|
||||
- pytest -s -v tests/diffusion/offloader/test_diffusion_layerwise_offload.py
|
||||
|
||||
- label: "Diffusion Cache Backend Test"
|
||||
timeout_in_minutes: 15
|
||||
@@ -57,7 +57,7 @@ steps:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_sequence_parallel.py -m core_model
|
||||
- pytest -s -v tests/diffusion/distributed/test_sequence_parallel.py -m core_model
|
||||
|
||||
- label: "Diffusion Tensor Parallelism Test"
|
||||
timeout_in_minutes: 20
|
||||
@@ -67,7 +67,7 @@ steps:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/offline_inference/test_zimage_parallelism.py
|
||||
- pytest -s -v tests/diffusion/distributed/test_zimage_parallelism.py
|
||||
|
||||
- label: "Diffusion GPU Worker Test"
|
||||
timeout_in_minutes: 20
|
||||
@@ -106,19 +106,6 @@ steps:
|
||||
- pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py
|
||||
|
||||
|
||||
- label: "Diffusion Image Edit Test"
|
||||
timeout_in_minutes: 15
|
||||
agent_pool: mi325_1
|
||||
depends_on: amd-build
|
||||
mirror_hardwares: [amdproduction]
|
||||
grade: Blocking
|
||||
commands:
|
||||
- export GPU_ARCHS=gfx942
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- pytest -s -v tests/e2e/online_serving/test_image_gen_edit.py
|
||||
|
||||
|
||||
- label: "Omni Sleep Mode Test"
|
||||
timeout_in_minutes: 40
|
||||
agent_pool: mi325_2
|
||||
@@ -130,4 +117,4 @@ steps:
|
||||
- export VLLM_LOGGING_LEVEL=DEBUG
|
||||
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
|
||||
- export VLLM_TEST_CLEAN_GPU_MEMORY="1"
|
||||
- pytest -s -v tests/e2e/offline_inference/test_omni_sleep_mode.py -m "advanced_model and omni and MI325" --run-level "advanced_model"
|
||||
- pytest -s -v tests/entrypoints/test_omni_sleep_mode.py -m "advanced_model and omni and MI325" --run-level "advanced_model"
|
||||
+701
-430
File diff suppressed because it is too large.
Load diff
@@ -218,7 +218,7 @@ steps:
|
||||
- label: ":full_moon: Omni · Multi-Replica Startup Test with 4x H100"
|
||||
timeout_in_minutes: 45
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py -m "core_model" --run-level "core_model"
|
||||
- pytest -s -v tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py -m "full_model" --run-level "core_model"
|
||||
agents:
|
||||
queue: "mithril-h100-pool"
|
||||
plugins:
|
||||
@@ -262,7 +262,7 @@ steps:
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
|
||||
- pytest -s -v tests/e2e/ -m "full_model and L4 and omni" -k "not test_qwen2_5" --run-level "full_model" --ignore=tests/e2e/accuracy
|
||||
- pytest -s -v tests/e2e/ -m "full_model and L4 and tts" --run-level "full_model" --ignore=tests/e2e/accuracy
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.12xlarge instance on AWS, has 4 L4 GPU
|
||||
plugins:
|
||||
@@ -333,7 +333,7 @@ steps:
|
||||
build.pull_request.labels includes "diffusion-x2iat-test"
|
||||
steps:
|
||||
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with H100"
|
||||
timeout_in_minutes: 120
|
||||
timeout_in_minutes: 150
|
||||
commands:
|
||||
- pytest -sv tests/e2e/ -k "not test_wan and not test_bagel_expansion and not hunyuan" -m "full_model and diffusion and H100" --run-level "full_model" --ignore=tests/e2e/accuracy
|
||||
agents:
|
||||
@@ -749,7 +749,7 @@ steps:
|
||||
timeout_in_minutes: 180
|
||||
commands:
|
||||
- pytest -s -v tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py -m full_model --run-level full_model
|
||||
- pytest -s -v tests/e2e/accuracy/test_ltx2_3_video_similarity.py -m advanced_model --run-level advanced_model
|
||||
- pytest -s -v tests/e2e/accuracy/test_ltx2_3_video_similarity.py -m full_model --run-level full_model
|
||||
- pytest -s -v tests/e2e/accuracy/test_diffusers_backend_similarity.py -k '2v_matches_diffusers' -m full_model --run-level full_model
|
||||
agents:
|
||||
queue: "mithril-h100-pool"
|
||||
@@ -832,6 +832,65 @@ steps:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- group: ":card_index_dividers: Diffusion · Quantization Test"
|
||||
depends_on: upload-nightly-pipeline
|
||||
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test" || build.pull_request.labels includes "diffusion-x2iat-test"
|
||||
steps:
|
||||
- label: ":full_moon: Quantization Test with H100"
|
||||
timeout_in_minutes: 90
|
||||
commands:
|
||||
- timeout 30m pytest -sv tests/diffusion/quantization -m 'full_model and cuda 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: 1
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
- name: hf-cache
|
||||
mountPath: /root/.cache/huggingface
|
||||
env:
|
||||
- name: HF_HOME
|
||||
value: /root/.cache/huggingface
|
||||
- name: HF_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hf-token-secret
|
||||
key: token
|
||||
nodeSelector:
|
||||
node.kubernetes.io/instance-type: gpu-h100-sxm
|
||||
volumes:
|
||||
- name: devshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
- name: hf-cache
|
||||
hostPath:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: ":full_moon: Quantization Test with L4"
|
||||
timeout_in_minutes: 60
|
||||
commands:
|
||||
- pytest -sv tests/diffusion/quantization -m 'full_model and cuda and L4' --run-level "full_model"
|
||||
agents:
|
||||
queue: "gpu_1_queue" # g6.4xlarge instance on AWS, has 1 L4 GPU
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
- "HF_TOKEN"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: ":bar_chart: Testcase Statistics"
|
||||
key: nightly-testcase-statistics
|
||||
timeout_in_minutes: 120
|
||||
@@ -849,7 +908,7 @@ steps:
|
||||
- image: 936637512419.dkr.ecr.us-west-2.amazonaws.com/vllm-ci-pull-through-cache/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 2
|
||||
nvidia.com/gpu: 1
|
||||
volumeMounts:
|
||||
- name: devshm
|
||||
mountPath: /dev/shm
|
||||
|
||||
+450
-548
File diff suppressed because it is too large.
Load diff
@@ -63,7 +63,7 @@ def _find_repo_root(start: Path) -> Path:
|
||||
|
||||
REPO_ROOT = _find_repo_root(Path(__file__).resolve())
|
||||
DEFAULT_STAGE_ASYNC = REPO_ROOT / "vllm_omni" / "model_executor" / "stage_configs" / "voxcpm_async_chunk.yaml"
|
||||
DEFAULT_STAGE_SYNC = REPO_ROOT / "vllm_omni" / "model_executor" / "stage_configs" / "voxcpm.yaml"
|
||||
DEFAULT_STAGE_SYNC = REPO_ROOT / "vllm_omni" / "deploy" / "voxcpm.yaml"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -152,6 +152,8 @@ vllm_omni/ tests/
|
||||
│ │ │ │ └── test_lora_manager.py ✅
|
||||
│ ├── models/ │ ├── models/
|
||||
│ │ ├── qwen_image/ │ │ ├── qwen_image/ (e2e coverage)
|
||||
│ │ ├── ovis_image/ │ │ ├── ovis_image/
|
||||
│ │ │ └── ... │ │ │ └── test_ovis_image.py ✅
|
||||
│ │ ├── z_image/ │ │ └── z_image/
|
||||
│ │ └── ... │ │ └── test_zimage_tp_constraints.py ✅
|
||||
│ └── worker/ │ └── worker/
|
||||
@@ -232,15 +234,13 @@ vllm_omni/ tests/
|
||||
│ ├── test_qwen3_omni.py
|
||||
│ ├── test_qwen3_omni_expansion.py
|
||||
│ ├── test_mimo_audio.py
|
||||
│ ├── test_image_gen_edit.py
|
||||
│ └── test_images_generations_lora.py
|
||||
└── offline_inference/ ✅
|
||||
├── test_qwen2_5_omni.py
|
||||
├── test_qwen3_omni.py
|
||||
├── test_bagel_text2img.py
|
||||
├── test_t2i_model.py
|
||||
├── test_t2v_model.py
|
||||
├── test_ovis_image.py
|
||||
├── test_z_image.py
|
||||
├── test_wan22.py
|
||||
├── test_zimage_tensor_parallel.py
|
||||
├── test_cache_dit.py
|
||||
├── test_teacache.py
|
||||
|
||||
@@ -80,7 +80,7 @@ Our test scripts use the pytest framework. First, please use `git clone https://
|
||||
|
||||
```bash
|
||||
cd tests
|
||||
pytest -s -v -m "full_model and omni and H100" --run-level=full_model
|
||||
pytest -s -v -m "full_model and (omni or tts) and H100" --run-level=full_model
|
||||
```
|
||||
|
||||
=== "L5 level"
|
||||
|
||||
@@ -11,7 +11,8 @@ Defined in `pyproject.toml`:
|
||||
| `advanced_model` | L3 tests (run on each merge to main) |
|
||||
| `full_model` | L4 tests (run nightly) |
|
||||
| `diffusion` | Diffusion model tests |
|
||||
| `omni` | Omni model tests |
|
||||
| `omni` | Omni multimodal model tests |
|
||||
| `tts` | TTS model tests |
|
||||
| `cache` | Cache backend tests |
|
||||
| `parallel` | Parallelism/distributed tests |
|
||||
| `cpu` | Tests that run on CPU |
|
||||
|
||||
@@ -54,6 +54,8 @@ vllm_omni/ tests/
|
||||
│ │ │ │ └── test_lora_manager.py ✅
|
||||
│ ├── models/ │ ├── models/
|
||||
│ │ ├── qwen_image/ │ │ ├── qwen_image/ (e2e coverage)
|
||||
│ │ ├── ovis_image/ │ │ ├── ovis_image/
|
||||
│ │ │ └── ... │ │ │ └── test_ovis_image.py ✅
|
||||
│ │ ├── z_image/ │ │ └── z_image/
|
||||
│ │ └── ... │ │ └── test_zimage_tp_constraints.py ✅
|
||||
│ └── worker/ │ └── worker/
|
||||
@@ -134,15 +136,13 @@ vllm_omni/ tests/
|
||||
│ ├── test_qwen3_omni.py
|
||||
│ ├── test_qwen3_omni_expansion.py
|
||||
│ ├── test_mimo_audio.py
|
||||
│ ├── test_image_gen_edit.py
|
||||
│ └── test_images_generations_lora.py
|
||||
└── offline_inference/ ✅
|
||||
├── test_qwen2_5_omni.py
|
||||
├── test_qwen3_omni.py
|
||||
├── test_bagel_text2img.py
|
||||
├── test_t2i_model.py
|
||||
├── test_t2v_model.py
|
||||
├── test_ovis_image.py
|
||||
├── test_z_image.py
|
||||
├── test_wan22.py
|
||||
├── test_zimage_tensor_parallel.py
|
||||
├── test_cache_dit.py
|
||||
├── test_teacache.py
|
||||
|
||||
@@ -37,7 +37,7 @@ def run_e2e():
|
||||
parser.add_argument(
|
||||
"--stage-config",
|
||||
type=str,
|
||||
default="vllm_omni/model_executor/stage_configs/omnivoice.yaml",
|
||||
default="vllm_omni/deploy/omnivoice.yaml",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--text",
|
||||
|
||||
@@ -11,11 +11,11 @@ import soundfile as sf
|
||||
import torch
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
from vllm_omni import AsyncOmni, Omni
|
||||
from vllm_omni.engine.arg_utils import nullify_stage_engine_defaults
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
DEFAULT_SYNC_STAGE_CONFIG = REPO_ROOT / "vllm_omni" / "model_executor" / "stage_configs" / "voxcpm.yaml"
|
||||
DEFAULT_SYNC_STAGE_CONFIG = get_deploy_config_path("voxcpm.yaml")
|
||||
|
||||
|
||||
def _build_prompt(args) -> dict[str, Any]:
|
||||
@@ -167,7 +167,7 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--stage-configs-path",
|
||||
type=str,
|
||||
default=str(DEFAULT_SYNC_STAGE_CONFIG),
|
||||
default=DEFAULT_SYNC_STAGE_CONFIG,
|
||||
help=("Stage config path. Use voxcpm.yaml for non-streaming or voxcpm_async_chunk.yaml for streaming."),
|
||||
)
|
||||
parser.add_argument("--text", type=str, required=True, help="Input text for synthesis.")
|
||||
|
||||
@@ -17,7 +17,7 @@ case "$MODE" in
|
||||
STAGE_CONFIG="vllm_omni/model_executor/stage_configs/voxcpm_async_chunk.yaml"
|
||||
;;
|
||||
sync)
|
||||
STAGE_CONFIG="vllm_omni/model_executor/stage_configs/voxcpm.yaml"
|
||||
STAGE_CONFIG="vllm_omni/deploy/voxcpm.yaml"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown mode: $MODE"
|
||||
|
||||
@@ -60,6 +60,9 @@ dev = [
|
||||
"zhconv>=1.4.2",
|
||||
"scipy>=1.10.0",
|
||||
"funasr>=1.0.0",
|
||||
"lpips>=0.1.4", # for quantization fp8
|
||||
"orjson>=3.11.7", # to make v1/embeddings API fast
|
||||
"voxcpm>=2.0.2" # for voxcpm model test
|
||||
]
|
||||
|
||||
demo = [
|
||||
@@ -182,6 +185,7 @@ markers = [
|
||||
# function module markers
|
||||
"diffusion: Diffusion model tests",
|
||||
"omni: Omni model tests",
|
||||
"tts: TTS model tests",
|
||||
"cache: Cache backend tests",
|
||||
"parallel: Parallelism/distributed tests",
|
||||
"example: Doc example code tests",
|
||||
|
||||
@@ -8,6 +8,14 @@ Root pytest entrypoint for the vLLM-Omni test suite.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Before ``pytest_plugins`` and before any other test path imports vLLM, pin op
|
||||
# registration order (see :func:`tests.model_executor.helpers.bootstrap_vllm_layer_custom_op_modules`).
|
||||
# Subdir ``conftest`` hooks can run after other tests are collected/imported, which is
|
||||
# too late and can trigger duplicate ``vllm::flashinfer_rotary_embedding`` (etc.) errors.
|
||||
from tests.model_executor.helpers import bootstrap_vllm_layer_custom_op_modules
|
||||
|
||||
bootstrap_vllm_layer_custom_op_modules()
|
||||
|
||||
pytest_plugins = (
|
||||
"tests.helpers.fixtures.env",
|
||||
"tests.helpers.fixtures.log",
|
||||
|
||||
@@ -17,7 +17,7 @@ from tests.dfx.conftest import (
|
||||
)
|
||||
from tests.helpers.runtime import OmniServer
|
||||
|
||||
pytestmark = [pytest.mark.full_model, pytest.mark.omni]
|
||||
pytestmark = [pytest.mark.full_model]
|
||||
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
@@ -35,6 +35,7 @@ benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchm
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True)
|
||||
def test_stability_qwen3_omni(omni_server, stability_benchmark_params):
|
||||
|
||||
@@ -35,6 +35,7 @@ benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchm
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.tts
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True)
|
||||
def test_stability_qwen3_tts(omni_server, stability_benchmark_params):
|
||||
|
||||
@@ -35,6 +35,7 @@ benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchm
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True)
|
||||
def test_stability_qwen_image(omni_server, stability_benchmark_params):
|
||||
|
||||
@@ -35,6 +35,7 @@ benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchm
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
@pytest.mark.parametrize("stability_benchmark_params", benchmark_indices, indirect=True)
|
||||
def test_stability_wan22(omni_server, stability_benchmark_params):
|
||||
|
||||
+2
-2
@@ -15,13 +15,13 @@ that concurrent async requests are handled correctly.
|
||||
|
||||
Usage (standalone):
|
||||
|
||||
python tests/e2e/offline_inference/test_qwen_image_diffusion_batching.py \
|
||||
python tests/diffusion/batching/test_diffusion_batching.py \
|
||||
--model <model_name_or_path> \
|
||||
--num-prompts 8
|
||||
|
||||
Or via pytest:
|
||||
|
||||
pytest tests/e2e/offline_inference/test_qwen_image_diffusion_batching.py -s
|
||||
pytest tests/diffusion/batching/test_diffusion_batching.py -s
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
File renamed without changes.
+4
-1
@@ -148,6 +148,10 @@ EP_TEST_CONFIG = [
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards={"cuda": TOTAL_CARD_NUM})
|
||||
@pytest.mark.parametrize("model_name", MODELS)
|
||||
def test_ep(model_name):
|
||||
device_count = current_omni_platform.get_device_count()
|
||||
if device_count < TOTAL_CARD_NUM:
|
||||
pytest.skip(f"Requires at least {TOTAL_CARD_NUM} GPUs, got {device_count}")
|
||||
|
||||
baseline_cache: dict[str, InferenceResult] = {}
|
||||
results: list[dict] = []
|
||||
# baseline_key = PROMPT + "_" + str(DEFAULT_HEIGHT) + "*" + str(DEFAULT_WIDTH) + "_" + str(DEFAULT_SEED) + "_" + str(DEFAULT_STEPS)
|
||||
@@ -157,7 +161,6 @@ def test_ep(model_name):
|
||||
# enable_expert_parallel=False,
|
||||
# warmup=True,
|
||||
# )
|
||||
device_count = current_omni_platform.get_device_count()
|
||||
|
||||
print("\n" + "=" * 90)
|
||||
print(f"Sequence Parallel Test - Model: {model_name}")
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+56
-60
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -7,7 +6,6 @@ import pytest
|
||||
import torch
|
||||
from safetensors.torch import save_file
|
||||
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
@@ -17,16 +15,15 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
|
||||
|
||||
|
||||
# This test is specific to Z-Image LoRA behavior. Keep it focused on a single
|
||||
# model to reduce runtime and avoid extra downloads.
|
||||
models = ["Tongyi-MAI/Z-Image-Turbo"]
|
||||
omni_runner_params = [(model_name, None) for model_name in models]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
def test_diffusion_model(model_name: str, tmp_path: Path):
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.parametrize("omni_runner", omni_runner_params, indirect=True)
|
||||
def test_diffusion_model(tmp_path: Path, omni_runner):
|
||||
def _extract_images(outputs: list[OmniRequestOutput]):
|
||||
if not outputs:
|
||||
raise ValueError("Empty outputs from Omni.generate()")
|
||||
@@ -75,62 +72,61 @@ def test_diffusion_model(model_name: str, tmp_path: Path):
|
||||
)
|
||||
return str(adapter_dir)
|
||||
|
||||
with OmniRunner(model_name) as runner:
|
||||
m = runner.omni
|
||||
# high resolution may cause OOM on L4
|
||||
height = 256
|
||||
width = 256
|
||||
prompt = "a photo of a cat sitting on a laptop keyboard"
|
||||
runner = omni_runner
|
||||
m = runner.omni
|
||||
# high resolution may cause OOM on L4
|
||||
height = 256
|
||||
width = 256
|
||||
prompt = "a photo of a cat sitting on a laptop keyboard"
|
||||
|
||||
outputs = m.generate(
|
||||
prompt,
|
||||
OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(current_omni_platform.device_type).manual_seed(42),
|
||||
num_outputs_per_prompt=1,
|
||||
),
|
||||
)
|
||||
images = _extract_images(outputs)
|
||||
outputs = m.generate(
|
||||
prompt,
|
||||
OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(current_omni_platform.device_type).manual_seed(42),
|
||||
num_outputs_per_prompt=1,
|
||||
),
|
||||
)
|
||||
images = _extract_images(outputs)
|
||||
|
||||
assert len(images) == 1
|
||||
# check image size
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
assert len(images) == 1
|
||||
# check image size
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
|
||||
# Real LoRA E2E: generate again with a real on-disk PEFT adapter and
|
||||
# verify that output changes.
|
||||
if model_name == "Tongyi-MAI/Z-Image-Turbo":
|
||||
from vllm_omni.lora.request import LoRARequest
|
||||
from vllm_omni.lora.utils import stable_lora_int_id
|
||||
# Real LoRA E2E: generate again with a real on-disk PEFT adapter and
|
||||
# verify that output changes.
|
||||
from vllm_omni.lora.request import LoRARequest
|
||||
from vllm_omni.lora.utils import stable_lora_int_id
|
||||
|
||||
lora_dir = _write_zimage_lora(tmp_path / "zimage_lora")
|
||||
lora_request = LoRARequest(
|
||||
lora_name="test",
|
||||
lora_int_id=stable_lora_int_id(lora_dir),
|
||||
lora_path=lora_dir,
|
||||
)
|
||||
outputs_lora = m.generate(
|
||||
prompt,
|
||||
OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(current_omni_platform.device_type).manual_seed(42),
|
||||
num_outputs_per_prompt=1,
|
||||
lora_request=lora_request,
|
||||
lora_scale=2.0,
|
||||
),
|
||||
)
|
||||
images_lora = _extract_images(outputs_lora)
|
||||
assert len(images_lora) == 1
|
||||
assert images_lora[0].width == width
|
||||
assert images_lora[0].height == height
|
||||
lora_dir = _write_zimage_lora(tmp_path / "zimage_lora")
|
||||
lora_request = LoRARequest(
|
||||
lora_name="test",
|
||||
lora_int_id=stable_lora_int_id(lora_dir),
|
||||
lora_path=lora_dir,
|
||||
)
|
||||
outputs_lora = m.generate(
|
||||
prompt,
|
||||
OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(current_omni_platform.device_type).manual_seed(42),
|
||||
num_outputs_per_prompt=1,
|
||||
lora_request=lora_request,
|
||||
lora_scale=2.0,
|
||||
),
|
||||
)
|
||||
images_lora = _extract_images(outputs_lora)
|
||||
assert len(images_lora) == 1
|
||||
assert images_lora[0].width == width
|
||||
assert images_lora[0].height == height
|
||||
|
||||
import numpy as np
|
||||
import numpy as np
|
||||
|
||||
diff = np.abs(np.array(images[0], dtype=np.int16) - np.array(images_lora[0], dtype=np.int16)).mean()
|
||||
assert diff > 0.0
|
||||
diff = np.abs(np.array(images[0], dtype=np.int16) - np.array(images_lora[0], dtype=np.int16)).mean()
|
||||
assert diff > 0.0
|
||||
+14
-16
@@ -23,12 +23,11 @@ from PIL import Image
|
||||
from safetensors.torch import save_file
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniServer
|
||||
from tests.helpers.runtime import OmniServer, OmniServerParams
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
MODEL = "Tongyi-MAI/Z-Image-Turbo"
|
||||
DIFFUSION_INIT_TIMEOUT_S = 900
|
||||
|
||||
|
||||
PROMPT = "a photo of a cat sitting on a laptop keyboard"
|
||||
@@ -36,20 +35,18 @@ SIZE = "256x256"
|
||||
SEED = 42
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def omni_server():
|
||||
with OmniServer(
|
||||
MODEL,
|
||||
[
|
||||
"--num-gpus",
|
||||
"1",
|
||||
"--stage-init-timeout",
|
||||
str(DIFFUSION_INIT_TIMEOUT_S),
|
||||
"--init-timeout",
|
||||
str(DIFFUSION_INIT_TIMEOUT_S),
|
||||
],
|
||||
) as server:
|
||||
yield server
|
||||
server_params = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
server_args=[
|
||||
"--num-gpus",
|
||||
"1",
|
||||
],
|
||||
),
|
||||
id="zimage_turbo_lora",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _write_zimage_lora(adapter_dir: Path, *, q_scale: float = 0.0, k_scale: float = 0.0, v_scale: float = 0.0):
|
||||
@@ -159,6 +156,7 @@ def _basic_payload() -> dict:
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "L4", "rocm": "MI325", "xpu": "B60"})
|
||||
@pytest.mark.parametrize("omni_server", server_params, indirect=True)
|
||||
def test_images_generations_per_request_lora_switching(omni_server: OmniServer, tmp_path: Path) -> None:
|
||||
# Base generation.
|
||||
base_img = _post_images(omni_server, _basic_payload())
|
||||
+24
-10
@@ -1,7 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Tests for Ovis Image model pipeline.
|
||||
Tests for Ovis Image model pipeline (unit-style with heavy mocks, plus a small
|
||||
real transformer forward for coverage).
|
||||
|
||||
Strategy:
|
||||
1. `mock_dependencies` fixture mocks heavy external components (VAE, Scheduler, TextEncoder)
|
||||
to allow fast testing of the pipeline logic without downloading weights.
|
||||
- Mocks are configured to return tensors on the correct device.
|
||||
@@ -18,10 +21,6 @@ from pytest_mock import MockerFixture
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig, TransformerConfig
|
||||
|
||||
# Mock the OvisImageTransformer2DModel to avoid complex init if needed,
|
||||
# or let it run if it's lightweight. It's likely not lightweight.
|
||||
# Better to mock the transformer forwarding to return random noise.
|
||||
from vllm_omni.diffusion.distributed.utils import get_local_device
|
||||
from vllm_omni.diffusion.models.ovis_image.pipeline_ovis_image import OvisImagePipeline
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
@@ -150,6 +149,8 @@ def ovis_pipeline(mocker: MockerFixture, mock_dependencies, monkeypatch: pytest.
|
||||
return pipeline
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_interface_compliance(ovis_pipeline):
|
||||
"""Verify methods required by vllm-omni framework."""
|
||||
assert hasattr(ovis_pipeline, "load_weights")
|
||||
@@ -159,6 +160,8 @@ def test_interface_compliance(ovis_pipeline):
|
||||
# assert hasattr(ovis_pipeline, "vae") # Ovis uses VAE
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_basic_generation(ovis_pipeline):
|
||||
"""Test the forward pass logic."""
|
||||
# Setup request
|
||||
@@ -184,8 +187,18 @@ def test_basic_generation(ovis_pipeline):
|
||||
assert ovis_pipeline.transformer.call_count > 0
|
||||
|
||||
|
||||
def test_guidance_scale(ovis_pipeline):
|
||||
"""Test that classifier-free guidance path is taken when scale > 1.0."""
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_guidance_scale(ovis_pipeline, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Test that classifier-free guidance path is taken when scale > 1.0.
|
||||
|
||||
Unit tests do not initialize the CFG process group; stub world size 1 / rank 0 so
|
||||
``predict_noise_maybe_with_cfg`` uses the sequential (non-CFG-parallel) branch.
|
||||
"""
|
||||
_cfg_parallel = "vllm_omni.diffusion.distributed.cfg_parallel"
|
||||
monkeypatch.setattr(f"{_cfg_parallel}.get_classifier_free_guidance_world_size", lambda: 1)
|
||||
monkeypatch.setattr(f"{_cfg_parallel}.get_classifier_free_guidance_rank", lambda: 0)
|
||||
|
||||
req = OmniDiffusionRequest(
|
||||
prompts=[
|
||||
{
|
||||
@@ -205,6 +218,8 @@ def test_guidance_scale(ovis_pipeline):
|
||||
assert ovis_pipeline.transformer.call_count >= 2
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.cpu
|
||||
def test_resolution_check(ovis_pipeline):
|
||||
"""Test resolution divisible validation logic if present."""
|
||||
# Pass odd resolution
|
||||
@@ -223,8 +238,7 @@ def test_resolution_check(ovis_pipeline):
|
||||
assert output is not None
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.advanced_model
|
||||
@hardware_test(res={"cuda": "L4", "rocm": "MI325"})
|
||||
def test_real_transformer_init_and_forward(mocker: MockerFixture):
|
||||
"""Test the real OvisImageTransformer2DModel initialization and forward pass for coverage."""
|
||||
File renamed without changes.
+17
-2
@@ -1,8 +1,8 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
|
||||
from tests.e2e.offline_inference.test_diffusion_cpu_offload import check_audio_determinism
|
||||
from tests.helpers.env import DeviceMemoryMonitor
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
@@ -30,6 +30,21 @@ IMAGE_VIDEO_MODELS_PARAMS = {
|
||||
}
|
||||
|
||||
|
||||
def check_audio_determinism(audio1, audio2, atol=1e-2):
|
||||
device = current_omni_platform.device_type
|
||||
if isinstance(audio1, np.ndarray):
|
||||
audio1 = torch.from_numpy(audio1).to(device)
|
||||
if isinstance(audio2, np.ndarray):
|
||||
audio2 = torch.from_numpy(audio2).to(device)
|
||||
|
||||
if not torch.allclose(audio1, audio2, atol=atol):
|
||||
diff = torch.abs(audio1 - audio2)
|
||||
print(f"Max difference: {diff.max().item()}")
|
||||
print(f"Mean difference: {diff.mean().item()}")
|
||||
raise AssertionError(f"Audio outputs differ beyond tolerance atol={atol}")
|
||||
return True
|
||||
|
||||
|
||||
def run_inference(
|
||||
model_name: str,
|
||||
layerwise_offload: bool = False,
|
||||
@@ -54,7 +69,7 @@ def run_inference(
|
||||
) as runner:
|
||||
current_omni_platform.reset_peak_memory_stats()
|
||||
|
||||
# Refer to tests/e2e/offline_inference/test_t2v_model.py
|
||||
# Refer to tests/e2e/offline_inference/test_wan22.py
|
||||
# Use minimal settings for testing
|
||||
output = runner.omni.generate(
|
||||
"A cat sitting on a table",
|
||||
+29
-8
@@ -42,7 +42,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.core_model, pytest.mark.diffusion]
|
||||
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
@@ -85,14 +85,27 @@ def _generate_single_stage_image(
|
||||
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image"
|
||||
assert hasattr(first_output, "request_output") and first_output.request_output
|
||||
req_out = first_output.request_output[0]
|
||||
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
|
||||
images = req_out.images
|
||||
if hasattr(first_output, "images") and first_output.images:
|
||||
images = first_output.images
|
||||
else:
|
||||
assert hasattr(first_output, "request_output") and first_output.request_output
|
||||
request_output = first_output.request_output
|
||||
if isinstance(request_output, list):
|
||||
req_out = request_output[0]
|
||||
else:
|
||||
req_out = request_output
|
||||
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
|
||||
images = req_out.images
|
||||
assert len(images) >= 1
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
|
||||
peak_mem_mb = getattr(first_output, "peak_memory_mb", None)
|
||||
if peak_mem_mb is not None:
|
||||
peak_mem = float(peak_mem_mb) / 1024.0
|
||||
else:
|
||||
peak_mem = torch.accelerator.max_memory_allocated() / (1024**3)
|
||||
|
||||
return images, peak_mem
|
||||
|
||||
|
||||
@@ -143,7 +156,10 @@ def _generate_bagel_image(
|
||||
generated_image = images[0]
|
||||
break
|
||||
if hasattr(req_output, "request_output") and req_output.request_output:
|
||||
for stage_out in req_output.request_output:
|
||||
stage_outputs = req_output.request_output
|
||||
if not isinstance(stage_outputs, list):
|
||||
stage_outputs = [stage_outputs]
|
||||
for stage_out in stage_outputs:
|
||||
if hasattr(stage_out, "images") and stage_out.images:
|
||||
generated_image = stage_out.images[0]
|
||||
break
|
||||
@@ -155,8 +171,11 @@ def _generate_bagel_image(
|
||||
|
||||
# Check LLM stage output — should have finish_reason=stop (not length)
|
||||
for req_output in omni_outputs:
|
||||
if hasattr(req_output, "request_output"):
|
||||
for stage_out in req_output.request_output:
|
||||
if hasattr(req_output, "request_output") and req_output.request_output:
|
||||
stage_outputs = req_output.request_output
|
||||
if not isinstance(stage_outputs, list):
|
||||
stage_outputs = [stage_outputs]
|
||||
for stage_out in stage_outputs:
|
||||
if hasattr(stage_out, "outputs"):
|
||||
for comp_out in stage_out.outputs:
|
||||
if hasattr(comp_out, "finish_reason"):
|
||||
@@ -218,6 +237,7 @@ def test_single_stage_qwen_image_fp8():
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"})
|
||||
@pytest.mark.skip(reason="This model is not authorized on Hugging Face Hub yet")
|
||||
def test_single_stage_flux_fp8():
|
||||
"""FLUX.1-dev with FP8 generates valid images."""
|
||||
images, _ = _generate_single_stage_image(
|
||||
@@ -232,6 +252,7 @@ def test_single_stage_flux_fp8():
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100"})
|
||||
@pytest.mark.skip(reason="This model is not authorized on Hugging Face Hub yet")
|
||||
def test_single_stage_flux_fp8_uses_less_memory():
|
||||
"""FP8 should use less peak memory than BF16 for FLUX.1-dev."""
|
||||
_, mem_bf16 = _generate_single_stage_image(
|
||||
@@ -66,7 +66,7 @@ QUALITY_CONFIGS = [
|
||||
quantization="fp8",
|
||||
task="t2i",
|
||||
prompt="a cup of coffee on a wooden table, morning light",
|
||||
max_lpips=0.10,
|
||||
max_lpips=0.15,
|
||||
num_inference_steps=20,
|
||||
),
|
||||
QualityTestConfig(
|
||||
@@ -201,11 +201,20 @@ def _unload(omni):
|
||||
_marks = hardware_marks(res={"cuda": "H100"})
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
def _quality_param(c: QualityTestConfig):
|
||||
marks = list(_marks)
|
||||
if c.id == "fp8_qwen_image":
|
||||
marks.append(
|
||||
pytest.mark.skip(reason="Qwen-Image FP8 quality gate temporarily disabled (see CI / issue tracker).")
|
||||
)
|
||||
return pytest.param(c, id=c.id, marks=marks)
|
||||
|
||||
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[pytest.param(c, id=c.id, marks=_marks) for c in QUALITY_CONFIGS],
|
||||
[_quality_param(c) for c in QUALITY_CONFIGS],
|
||||
)
|
||||
def test_quantization_quality(config: QualityTestConfig):
|
||||
"""Validate that quantized output stays within LPIPS threshold of BF16."""
|
||||
|
||||
+3
-40
@@ -2,15 +2,10 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
End-to-end test for Bagel text2img generation.
|
||||
End-to-end test for Bagel text2img with the Mooncake inter-stage connector.
|
||||
|
||||
This test validates that the Bagel model generates images that match
|
||||
expected reference pixel values within a ±5 tolerance.
|
||||
|
||||
Equivalent to running:
|
||||
python3 examples/offline_inference/bagel/end2end.py \
|
||||
--prompts "A futuristic city skyline at twilight, cyberpunk style" \
|
||||
--modality text2img --step 15
|
||||
Validates image output against reference pixels (±5) in advanced_model runs.
|
||||
Shared-memory connector coverage lives in `test_bagel_shared_memory_connector.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -30,9 +25,7 @@ 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
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
BAGEL_CI_DEPLOY = get_deploy_config_path("ci/bagel.yaml")
|
||||
BAGEL_MOONCAKE_CI_DEPLOY = get_deploy_config_path("ci/bagel_mooncake.yaml")
|
||||
|
||||
# Reference pixel data extracted from the known-good output image
|
||||
@@ -52,20 +45,6 @@ REFERENCE_PIXELS = [
|
||||
{"position": (256, 256), "rgb": (167, 156, 150)},
|
||||
]
|
||||
|
||||
if current_omni_platform.is_rocm():
|
||||
REFERENCE_PIXELS = [
|
||||
{"position": (100, 100), "rgb": (115, 113, 94)},
|
||||
{"position": (400, 50), "rgb": (159, 160, 144)},
|
||||
{"position": (700, 100), "rgb": (164, 151, 123)},
|
||||
{"position": (150, 400), "rgb": (120, 121, 107)},
|
||||
{"position": (512, 512), "rgb": (165, 133, 127)},
|
||||
{"position": (700, 400), "rgb": (217, 130, 66)},
|
||||
{"position": (100, 700), "rgb": (191, 168, 152)},
|
||||
{"position": (400, 700), "rgb": (130, 96, 77)},
|
||||
{"position": (700, 700), "rgb": (247, 203, 140)},
|
||||
{"position": (256, 256), "rgb": (167, 156, 150)},
|
||||
]
|
||||
|
||||
# Maximum allowed difference per color channel
|
||||
PIXEL_TOLERANCE = 10
|
||||
|
||||
@@ -193,22 +172,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"})
|
||||
def test_bagel_text2img_shared_memory_connector(run_level):
|
||||
"""Test Bagel text2img with shared memory connector."""
|
||||
config_path = _resolve_deploy_config(BAGEL_CI_DEPLOY, run_level)
|
||||
with OmniRunner(
|
||||
"ByteDance-Seed/BAGEL-7B-MoT",
|
||||
stage_configs_path=config_path,
|
||||
) as runner:
|
||||
generated_image = _generate_bagel_image(runner.omni)
|
||||
if run_level == "advanced_model":
|
||||
_validate_pixels(generated_image)
|
||||
|
||||
|
||||
def _wait_for_port(host: str, port: int, timeout: int = 30) -> bool:
|
||||
"""Wait for a port to become available.
|
||||
|
||||
+97
-9
@@ -2,18 +2,17 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
End-to-end test for Bagel img2img generation.
|
||||
End-to-end tests for Bagel with shared memory connector: img2img and text2img.
|
||||
|
||||
This test validates that the Bagel model generates images from an input image
|
||||
and text prompt that match expected reference pixel values within a ±10 tolerance.
|
||||
|
||||
Equivalent to running:
|
||||
python3 examples/offline_inference/bagel/end2end.py \
|
||||
--prompts "Change the grass color to red" \
|
||||
--modality img2img --step 15 \
|
||||
--image-path 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
|
||||
- img2img: validates output vs reference pixels within a ±10 tolerance.
|
||||
- text2img: validates output vs reference pixels within a ±5 tolerance
|
||||
(equivalent to `examples/offline_inference/bagel/end2end.py` with
|
||||
text2img modality and 15 steps).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
import socket
|
||||
from typing import Any
|
||||
|
||||
@@ -60,10 +59,43 @@ if current_omni_platform.is_rocm():
|
||||
{"position": (256, 256), "rgb": (181, 201, 221)},
|
||||
]
|
||||
|
||||
# text2img reference pixels (aligned with offline `bagel/end2end.py` text2img, 15 steps)
|
||||
# "Generated with seed=52, num_inference_steps=15,
|
||||
# prompt='A futuristic city skyline at twilight, cyberpunk style'"
|
||||
TEXT2IMG_REFERENCE_PIXELS = [
|
||||
{"position": (100, 100), "rgb": (115, 113, 94)},
|
||||
{"position": (400, 50), "rgb": (159, 160, 144)},
|
||||
{"position": (700, 100), "rgb": (164, 151, 123)},
|
||||
{"position": (150, 400), "rgb": (120, 121, 107)},
|
||||
{"position": (512, 512), "rgb": (165, 133, 127)},
|
||||
{"position": (700, 400), "rgb": (217, 130, 66)},
|
||||
{"position": (100, 700), "rgb": (191, 168, 152)},
|
||||
{"position": (400, 700), "rgb": (130, 96, 77)},
|
||||
{"position": (700, 700), "rgb": (247, 203, 140)},
|
||||
{"position": (256, 256), "rgb": (167, 156, 150)},
|
||||
]
|
||||
|
||||
if current_omni_platform.is_rocm():
|
||||
TEXT2IMG_REFERENCE_PIXELS = [
|
||||
{"position": (100, 100), "rgb": (115, 113, 94)},
|
||||
{"position": (400, 50), "rgb": (159, 160, 144)},
|
||||
{"position": (700, 100), "rgb": (164, 151, 123)},
|
||||
{"position": (150, 400), "rgb": (120, 121, 107)},
|
||||
{"position": (512, 512), "rgb": (165, 133, 127)},
|
||||
{"position": (700, 400), "rgb": (217, 130, 66)},
|
||||
{"position": (100, 700), "rgb": (191, 168, 152)},
|
||||
{"position": (400, 700), "rgb": (130, 96, 77)},
|
||||
{"position": (700, 700), "rgb": (247, 203, 140)},
|
||||
{"position": (256, 256), "rgb": (167, 156, 150)},
|
||||
]
|
||||
|
||||
PIXEL_TOLERANCE = 10
|
||||
TEXT2IMG_PIXEL_TOLERANCE = 5
|
||||
|
||||
DEFAULT_PROMPT = "<|fim_middle|><|im_start|>Change the grass color to red<|im_end|>"
|
||||
|
||||
TEXT2IMG_DEFAULT_PROMPT = "<|im_start|>A cute cat<|im_end|>"
|
||||
|
||||
EXPECTED_OUTPUT_SIZE = (1024, 672)
|
||||
|
||||
|
||||
@@ -184,6 +216,42 @@ def _generate_bagel_img2img(
|
||||
return generated_image
|
||||
|
||||
|
||||
def _generate_bagel_text2img(omni: Omni, prompt: str = TEXT2IMG_DEFAULT_PROMPT) -> Image.Image:
|
||||
"""Generate an image using Bagel text2img with configured parameters."""
|
||||
params_list = _configure_sampling_params(omni)
|
||||
|
||||
omni_outputs = list(
|
||||
omni.generate(
|
||||
prompts=[{"prompt": prompt, "modalities": ["image"]}],
|
||||
sampling_params_list=params_list,
|
||||
)
|
||||
)
|
||||
|
||||
generated_image = _extract_generated_image(omni_outputs)
|
||||
assert generated_image is not None, "No images generated"
|
||||
assert generated_image.size == (1024, 1024), f"Expected 1024x1024, got {generated_image.size}"
|
||||
|
||||
return generated_image
|
||||
|
||||
|
||||
def _generate_bagel_text2img(omni: Omni, prompt: str = TEXT2IMG_DEFAULT_PROMPT) -> Image.Image:
|
||||
"""Generate an image using Bagel text2img with configured parameters."""
|
||||
params_list = _configure_sampling_params(omni)
|
||||
|
||||
omni_outputs = list(
|
||||
omni.generate(
|
||||
prompts=[{"prompt": prompt, "modalities": ["image"]}],
|
||||
sampling_params_list=params_list,
|
||||
)
|
||||
)
|
||||
|
||||
generated_image = _extract_generated_image(omni_outputs)
|
||||
assert generated_image is not None, "No images generated"
|
||||
assert generated_image.size == (1024, 1024), f"Expected 1024x1024, got {generated_image.size}"
|
||||
|
||||
return generated_image
|
||||
|
||||
|
||||
def _resolve_deploy_config(config_path: str, run_level: str) -> str:
|
||||
"""Resolve deploy config based on run level.
|
||||
|
||||
@@ -218,3 +286,23 @@ def test_bagel_img2img_shared_memory_connector(run_level):
|
||||
generated_image = _generate_bagel_img2img(runner.omni, input_image)
|
||||
if run_level == "advanced_model":
|
||||
_validate_pixels(generated_image)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_bagel_text2img_shared_memory_connector(run_level):
|
||||
"""Test Bagel text2img with shared memory connector."""
|
||||
config_path = _resolve_deploy_config(BAGEL_CI_DEPLOY, run_level)
|
||||
with OmniRunner(
|
||||
"ByteDance-Seed/BAGEL-7B-MoT",
|
||||
stage_configs_path=config_path,
|
||||
) as runner:
|
||||
generated_image = _generate_bagel_text2img(runner.omni)
|
||||
if run_level == "advanced_model":
|
||||
_validate_pixels(
|
||||
generated_image,
|
||||
reference_pixels=TEXT2IMG_REFERENCE_PIXELS,
|
||||
tolerance=TEXT2IMG_PIXEL_TOLERANCE,
|
||||
)
|
||||
@@ -282,7 +282,7 @@ def _run_with_custom_transformer(model: str, output_dir: Path) -> list[Image.Ima
|
||||
return frames
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.benchmark
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.skipif(
|
||||
@@ -381,7 +381,7 @@ def _run_vllm_omni_serving(model: str, output_dir: Path) -> list[Image.Image]:
|
||||
return frames
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.benchmark
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.skipif(
|
||||
|
||||
+12
-16
@@ -2,44 +2,40 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
# ruff: noqa: E402
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from vllm_omni import Omni
|
||||
|
||||
# Prefer a tiny/random checkpoint for CI.
|
||||
# Override in CI if needed: AUDIOX_TEST_MODEL=<model-or-local-path>
|
||||
models = [os.environ.get("AUDIOX_TEST_MODEL", "zhangj1an/audiox_random")]
|
||||
|
||||
# (model, stage_configs_path, extra_omni_kwargs) for ``omni_runner`` indirect parametrize
|
||||
_OMNI_RUNNER_PARAMS = [(m, None, {"model_class_name": "AudioXPipeline"}) for m in models]
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.parametrize("omni_runner", _OMNI_RUNNER_PARAMS, indirect=True, ids=models),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "L4", "xpu": "B60"})
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
def test_audiox_model(model_name: str):
|
||||
m = Omni(model=model_name, model_class_name="AudioXPipeline")
|
||||
|
||||
def test_audiox_model(omni_runner: OmniRunner) -> None:
|
||||
# Keep runtime short for CI.
|
||||
seconds_total = 2.0
|
||||
# AudioXPipeline always emits 44.1 kHz stereo (advertised via class-level
|
||||
# ``audio_sample_rate``); the trimmed output should match this rate.
|
||||
sample_rate = 44100
|
||||
|
||||
outputs = m.generate(
|
||||
outputs = omni_runner.omni.generate(
|
||||
prompts={"prompt": "A dog barking in a quiet park."},
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
num_inference_steps=4,
|
||||
@@ -0,0 +1,110 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
End-to-end tests for Bagel text2text and img2text (understanding) tasks.
|
||||
|
||||
These tests validate that the Bagel multistage pipeline correctly generates
|
||||
text output for understanding tasks, matching reference results.
|
||||
|
||||
Equivalent to running:
|
||||
python3 examples/offline_inference/bagel/end2end.py \
|
||||
--modality text2text \
|
||||
--prompts "Where is the capital of France?"
|
||||
|
||||
python3 examples/offline_inference/bagel/end2end.py \
|
||||
--modality img2text \
|
||||
--prompts "Please describe this image" \
|
||||
--image-path 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
import pytest
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
MODEL_NAME = "ByteDance-Seed/BAGEL-7B-MoT"
|
||||
STAGE_CONFIG = get_deploy_config_path("ci/bagel.yaml")
|
||||
|
||||
REFERENCE_TEXT_TEXT2TEXT = "The capital of France is Paris."
|
||||
REFERENCE_TEXT_IMG2TEXT = (
|
||||
"This is a photo of a wooden boardwalk or pathway that leads through "
|
||||
"tall green grass. The path appears to be in a natural setting, possibly "
|
||||
"a wetland or marsh area. The sky above is blue with some scattered "
|
||||
"clouds, suggesting it might be a sunny day. The overall scene looks "
|
||||
"peaceful and serene."
|
||||
)
|
||||
|
||||
|
||||
# (model, stage_config_path, extra_omni_kwargs) for ``@pytest.mark.parametrize("omni_runner", ..., indirect=True)``
|
||||
_OMNI_RUNNER_PARAM = (MODEL_NAME, STAGE_CONFIG)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.advanced_model,
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
|
||||
def _extract_text(omni_outputs: list) -> str:
|
||||
"""Extract generated text from OmniRequestOutput list."""
|
||||
for req_output in omni_outputs:
|
||||
ro = getattr(req_output, "request_output", None)
|
||||
if ro and getattr(ro, "outputs", None):
|
||||
return "".join(getattr(o, "text", "") or "" for o in ro.outputs)
|
||||
return ""
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_bagel_text2text(run_level, omni_runner: OmniRunner) -> None:
|
||||
"""Test Bagel text2text produces correct text output."""
|
||||
omni = omni_runner.omni
|
||||
prompt = "<|im_start|>user\nWhere is the capital of France?<|im_end|>\n<|im_start|>assistant\n"
|
||||
params_list = omni.default_sampling_params_list
|
||||
omni_outputs = list(
|
||||
omni.generate(
|
||||
prompts=[{"prompt": prompt, "modalities": ["text"]}],
|
||||
sampling_params_list=params_list,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(omni_outputs) > 0, "No outputs returned"
|
||||
text = _extract_text(omni_outputs)
|
||||
assert len(text) > 0, "Generated text is empty"
|
||||
|
||||
if run_level == "advanced_model":
|
||||
assert text == REFERENCE_TEXT_TEXT2TEXT, f"Text mismatch: expected {REFERENCE_TEXT_TEXT2TEXT!r}, got {text!r}"
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_bagel_img2text(run_level, omni_runner: OmniRunner) -> None:
|
||||
"""Test Bagel img2text produces correct text output."""
|
||||
input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB")
|
||||
omni = omni_runner.omni
|
||||
prompt = "<|im_start|>user\n<|image_pad|>\nPlease describe this image<|im_end|>\n<|im_start|>assistant\n"
|
||||
params_list = omni.default_sampling_params_list
|
||||
omni_outputs = list(
|
||||
omni.generate(
|
||||
prompts=[
|
||||
{
|
||||
"prompt": prompt,
|
||||
"multi_modal_data": {"image": input_image},
|
||||
"modalities": ["text"],
|
||||
}
|
||||
],
|
||||
sampling_params_list=params_list,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(omni_outputs) > 0, "No outputs returned"
|
||||
text = _extract_text(omni_outputs)
|
||||
assert len(text) > 0, "Generated text is empty"
|
||||
|
||||
if run_level in ["advanced_model", "full_model"]:
|
||||
assert "wooden boardwalk" in text.lower(), f"Text mismatch: expected 'wooden boardwalk' in {text!r}"
|
||||
+44
-51
@@ -2,7 +2,9 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
End-to-end test for BAGEL LoRA support (Stage 1 / DiT).
|
||||
Expanded end-to-end test for BAGEL in offline mode.
|
||||
|
||||
This test file primarily covers end-to-end tests for LoRA support (Stage 1 / DiT).
|
||||
|
||||
Validates that LoRA adapters are correctly loaded, applied with controllable
|
||||
scale, and cleanly deactivated. Uses a synthetic rank-1 adapter targeting the
|
||||
@@ -32,7 +34,7 @@ from safetensors.torch import save_file
|
||||
|
||||
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
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.lora.request import LoRARequest
|
||||
from vllm_omni.lora.utils import stable_lora_int_id
|
||||
@@ -41,26 +43,15 @@ MODEL = "ByteDance-Seed/BAGEL-7B-MoT"
|
||||
BAGEL_STAGE_CONFIG = get_deploy_config_path("ci/bagel.yaml")
|
||||
DEFAULT_PROMPT = "<|im_start|>A cute cat<|im_end|>"
|
||||
|
||||
# (model, stage_configs_path) for ``@pytest.mark.parametrize("omni_runner", ..., indirect=True)``
|
||||
_OMNI_RUNNER_PARAM = (MODEL, BAGEL_STAGE_CONFIG)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers (reused from test_bagel_text2img.py patterns)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _resolve_deploy_config(config_path: str, run_level: str) -> str:
|
||||
if run_level == "advanced_model":
|
||||
return modify_stage_config(
|
||||
config_path,
|
||||
deletes={
|
||||
"stages": {
|
||||
0: ["load_format"],
|
||||
1: ["load_format"],
|
||||
}
|
||||
},
|
||||
)
|
||||
return config_path
|
||||
|
||||
|
||||
def _configure_sampling_params(omni: Omni, num_inference_steps: int = 10) -> list[OmniSamplingParams]:
|
||||
params_list = omni.default_sampling_params_list
|
||||
if len(params_list) > 1:
|
||||
@@ -146,50 +137,52 @@ def _make_file_lora_request(adapter_dir: Path) -> LoRARequest:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_bagel_lora_scale_and_deactivation(run_level, tmp_path):
|
||||
def test_bagel_lora_scale_and_deactivation(omni_runner: OmniRunner, tmp_path) -> None:
|
||||
"""Validate LoRA effect, bounded perturbation, and clean deactivation."""
|
||||
config_path = _resolve_deploy_config(BAGEL_STAGE_CONFIG, run_level)
|
||||
with OmniRunner(MODEL, stage_configs_path=config_path) as runner:
|
||||
omni = runner.omni
|
||||
lora_request = _make_file_lora_request(tmp_path / "bagel_lora")
|
||||
omni = omni_runner.omni
|
||||
lora_request = _make_file_lora_request(tmp_path / "bagel_lora")
|
||||
|
||||
# 1) Baseline (no LoRA)
|
||||
baseline = _generate_bagel_image(omni)
|
||||
# 1) Baseline (no LoRA)
|
||||
baseline = _generate_bagel_image(omni)
|
||||
|
||||
# 2) LoRA with scale=1.0
|
||||
img_1x = _generate_bagel_image_with_lora(omni, lora_request, lora_scale=1.0)
|
||||
# 2) LoRA with scale=1.0
|
||||
img_1x = _generate_bagel_image_with_lora(omni, lora_request, lora_scale=1.0)
|
||||
|
||||
# 3) LoRA with scale=2.0
|
||||
img_2x = _generate_bagel_image_with_lora(omni, lora_request, lora_scale=2.0)
|
||||
# 3) LoRA with scale=2.0
|
||||
img_2x = _generate_bagel_image_with_lora(omni, lora_request, lora_scale=2.0)
|
||||
|
||||
# 4) No LoRA again (deactivation)
|
||||
restored = _generate_bagel_image(omni)
|
||||
# 4) No LoRA again (deactivation)
|
||||
restored = _generate_bagel_image(omni)
|
||||
|
||||
baseline_arr = np.array(baseline, dtype=np.int16)
|
||||
img_1x_arr = np.array(img_1x, dtype=np.int16)
|
||||
img_2x_arr = np.array(img_2x, dtype=np.int16)
|
||||
restored_arr = np.array(restored, dtype=np.int16)
|
||||
baseline_arr = np.array(baseline, dtype=np.int16)
|
||||
img_1x_arr = np.array(img_1x, dtype=np.int16)
|
||||
img_2x_arr = np.array(img_2x, dtype=np.int16)
|
||||
restored_arr = np.array(restored, dtype=np.int16)
|
||||
|
||||
diff_1x = np.abs(baseline_arr - img_1x_arr).mean()
|
||||
diff_2x = np.abs(baseline_arr - img_2x_arr).mean()
|
||||
diff_restored = np.abs(baseline_arr - restored_arr).mean()
|
||||
diff_1x = np.abs(baseline_arr - img_1x_arr).mean()
|
||||
diff_2x = np.abs(baseline_arr - img_2x_arr).mean()
|
||||
diff_restored = np.abs(baseline_arr - restored_arr).mean()
|
||||
|
||||
# (a) Adapter has visible effect at both scales
|
||||
assert diff_1x > 0.5, f"LoRA scale=1.0 had no visible effect: diff={diff_1x}"
|
||||
assert diff_2x > 0.5, f"LoRA scale=2.0 had no visible effect: diff={diff_2x}"
|
||||
# (a) Adapter has visible effect at both scales
|
||||
assert diff_1x > 0.5, f"LoRA scale=1.0 had no visible effect: diff={diff_1x}"
|
||||
assert diff_2x > 0.5, f"LoRA scale=2.0 had no visible effect: diff={diff_2x}"
|
||||
|
||||
# (b) Different scales produce different outputs
|
||||
assert not np.isclose(diff_1x, diff_2x, atol=1.0), (
|
||||
f"LoRA scale has no effect: diff_1x={diff_1x:.2f}, diff_2x={diff_2x:.2f}"
|
||||
)
|
||||
# (b) Different scales produce different outputs
|
||||
assert not np.isclose(diff_1x, diff_2x, atol=1.0), (
|
||||
f"LoRA scale has no effect: diff_1x={diff_1x:.2f}, diff_2x={diff_2x:.2f}"
|
||||
)
|
||||
|
||||
# (c) Output is not corrupted (scale=2.0 can produce ~2x the diff of scale=1.0)
|
||||
assert diff_1x < 80, f"LoRA output looks corrupted: diff_1x={diff_1x}"
|
||||
assert diff_2x < 120, f"LoRA output looks corrupted: diff_2x={diff_2x}"
|
||||
# (c) Output is not corrupted (scale=2.0 can produce ~2x the diff of scale=1.0)
|
||||
assert diff_1x < 80, f"LoRA output looks corrupted: diff_1x={diff_1x}"
|
||||
assert diff_2x < 120, f"LoRA output looks corrupted: diff_2x={diff_2x}"
|
||||
|
||||
# (d) Deactivation fully restores base model
|
||||
assert diff_restored == 0.0, f"Base model not restored after LoRA deactivation: diff={diff_restored}"
|
||||
# (d) Deactivation fully restores base model
|
||||
assert diff_restored == 0.0, f"Base model not restored after LoRA deactivation: diff={diff_restored}"
|
||||
@@ -1,135 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
End-to-end tests for Bagel text2text and img2text (understanding) tasks.
|
||||
|
||||
These tests validate that the Bagel multistage pipeline correctly generates
|
||||
text output for understanding tasks, matching reference results.
|
||||
|
||||
Equivalent to running:
|
||||
python3 examples/offline_inference/bagel/end2end.py \
|
||||
--modality text2text \
|
||||
--prompts "Where is the capital of France?"
|
||||
|
||||
python3 examples/offline_inference/bagel/end2end.py \
|
||||
--modality img2text \
|
||||
--prompts "Please describe this image" \
|
||||
--image-path 2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
import pytest
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
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
|
||||
|
||||
MODEL_NAME = "ByteDance-Seed/BAGEL-7B-MoT"
|
||||
STAGE_CONFIG = get_deploy_config_path("ci/bagel.yaml")
|
||||
|
||||
REFERENCE_TEXT_TEXT2TEXT = "The capital of France is Paris."
|
||||
|
||||
REFERENCE_TEXT_IMG2TEXT = (
|
||||
"This is a photo of a wooden boardwalk or pathway that leads through "
|
||||
"tall green grass. The path appears to be in a natural setting, possibly "
|
||||
"a wetland or marsh area. The sky above is blue with some scattered "
|
||||
"clouds, suggesting it might be a sunny day. The overall scene looks "
|
||||
"peaceful and serene."
|
||||
)
|
||||
|
||||
|
||||
def _resolve_deploy_config(config_path: str, run_level: str) -> str:
|
||||
"""Strip load_format: dummy for advanced_model (real weights)."""
|
||||
if run_level == "advanced_model":
|
||||
return modify_stage_config(
|
||||
config_path,
|
||||
deletes={
|
||||
"stages": {
|
||||
0: ["load_format"],
|
||||
1: ["load_format"],
|
||||
}
|
||||
},
|
||||
)
|
||||
return config_path
|
||||
|
||||
|
||||
def _extract_text(omni_outputs: list) -> str:
|
||||
"""Extract generated text from OmniRequestOutput list."""
|
||||
for req_output in omni_outputs:
|
||||
ro = getattr(req_output, "request_output", None)
|
||||
if ro and getattr(ro, "outputs", None):
|
||||
return "".join(getattr(o, "text", "") or "" for o in ro.outputs)
|
||||
return ""
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_bagel_text2text(run_level):
|
||||
"""Test Bagel text2text produces correct text output."""
|
||||
config_path = _resolve_deploy_config(STAGE_CONFIG, run_level)
|
||||
with OmniRunner(
|
||||
MODEL_NAME,
|
||||
stage_configs_path=config_path,
|
||||
) as runner:
|
||||
omni = runner.omni
|
||||
prompt = "<|im_start|>user\nWhere is the capital of France?<|im_end|>\n<|im_start|>assistant\n"
|
||||
params_list = omni.default_sampling_params_list
|
||||
omni_outputs = list(
|
||||
omni.generate(
|
||||
prompts=[{"prompt": prompt, "modalities": ["text"]}],
|
||||
sampling_params_list=params_list,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(omni_outputs) > 0, "No outputs returned"
|
||||
text = _extract_text(omni_outputs)
|
||||
assert len(text) > 0, "Generated text is empty"
|
||||
|
||||
if run_level == "advanced_model":
|
||||
assert text == REFERENCE_TEXT_TEXT2TEXT, (
|
||||
f"Text mismatch: expected {REFERENCE_TEXT_TEXT2TEXT!r}, got {text!r}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_bagel_img2text(run_level):
|
||||
"""Test Bagel img2text produces correct text output."""
|
||||
input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB")
|
||||
config_path = _resolve_deploy_config(STAGE_CONFIG, run_level)
|
||||
with OmniRunner(
|
||||
MODEL_NAME,
|
||||
stage_configs_path=config_path,
|
||||
stage_init_timeout=300,
|
||||
) as runner:
|
||||
omni = runner.omni
|
||||
prompt = "<|im_start|>user\n<|image_pad|>\nPlease describe this image<|im_end|>\n<|im_start|>assistant\n"
|
||||
params_list = omni.default_sampling_params_list
|
||||
omni_outputs = list(
|
||||
omni.generate(
|
||||
prompts=[
|
||||
{
|
||||
"prompt": prompt,
|
||||
"multi_modal_data": {"image": input_image},
|
||||
"modalities": ["text"],
|
||||
}
|
||||
],
|
||||
sampling_params_list=params_list,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(omni_outputs) > 0, "No outputs returned"
|
||||
text = _extract_text(omni_outputs)
|
||||
assert len(text) > 0, "Generated text is empty"
|
||||
|
||||
if run_level == "advanced_model":
|
||||
assert text == REFERENCE_TEXT_IMG2TEXT, f"Text mismatch: expected {REFERENCE_TEXT_IMG2TEXT!r}, got {text!r}"
|
||||
@@ -1,77 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
System test for cache-dit backend.
|
||||
|
||||
This test verifies that cache-dit acceleration works correctly with diffusion models.
|
||||
It uses minimal settings to keep test time short for CI.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
# Use random weights model for testing
|
||||
models = ["riverclouds/qwen_image_random"]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.cache
|
||||
@hardware_test(res={"cuda": "L4", "rocm": "MI325", "xpu": "B60"})
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
def test_cache_dit(model_name: str):
|
||||
"""Test cache-dit backend with diffusion model."""
|
||||
# Configure cache-dit with minimal settings for fast testing
|
||||
cache_config = {
|
||||
"Fn_compute_blocks": 1,
|
||||
"Bn_compute_blocks": 0,
|
||||
"max_warmup_steps": 2, # Minimal warmup for fast test
|
||||
"residual_diff_threshold": 0.24,
|
||||
"max_continuous_cached_steps": 3,
|
||||
}
|
||||
with OmniRunner(
|
||||
model_name,
|
||||
cache_backend="cache_dit",
|
||||
cache_config=cache_config,
|
||||
) as runner:
|
||||
# Use minimal settings for fast testing
|
||||
height = 256
|
||||
width = 256
|
||||
num_inference_steps = 4 # Minimal steps for fast test
|
||||
|
||||
outputs = runner.omni.generate(
|
||||
"a photo of a cat sitting on a laptop keyboard",
|
||||
OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=num_inference_steps,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(current_omni_platform.device_type).manual_seed(42),
|
||||
num_outputs_per_prompt=1, # Single output for speed
|
||||
),
|
||||
)
|
||||
# Extract images from request_output['images']
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image"
|
||||
if not hasattr(first_output, "request_output") or not first_output.request_output:
|
||||
raise ValueError("No request_output found in OmniRequestOutput")
|
||||
|
||||
req_out = first_output.request_output
|
||||
if not isinstance(req_out, OmniRequestOutput) or not hasattr(req_out, "images"):
|
||||
raise ValueError("Invalid request_output structure or missing 'images' key")
|
||||
|
||||
images = req_out.images
|
||||
|
||||
# Verify generation succeeded
|
||||
assert images is not None
|
||||
assert len(images) == 1
|
||||
# Check image size
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
+59
-65
@@ -6,8 +6,9 @@ Offline E2E smoke test for CosyVoice3 zero-shot reference inference.
|
||||
This test uses the official upstream zero-shot prompt text/audio pair and
|
||||
verifies a stable reference recipe:
|
||||
- config-derived top_p/top_k and token-length ratios
|
||||
- model EOS token as the stop token
|
||||
- model EOS token as the stop token for stage-0 (talker) sampling
|
||||
- a conservative repetition penalty to avoid degenerate loops
|
||||
- stage-1 (code2wav) completion reports a normal terminal ``finish_reason``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -23,18 +24,19 @@ from huggingface_hub import snapshot_download
|
||||
from vllm.sampling_params import SamplingParams
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.media import test_asset_path
|
||||
from tests.helpers.media import get_asset_path
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
from vllm_omni.model_executor.models.cosyvoice3.config import CosyVoice3Config
|
||||
from vllm_omni.model_executor.models.cosyvoice3.tokenizer import get_qwen_tokenizer
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
|
||||
MODEL = "FunAudioLLM/Fun-CosyVoice3-0.5B-2512"
|
||||
MODEL_DIR_ENV = "VLLM_OMNI_COSYVOICE3_MODEL_DIR"
|
||||
|
||||
# Vendored under tests/assets/cosyvoice3/ so the test does not depend on
|
||||
# raw.githubusercontent.com being reachable from CI runners.
|
||||
REFERENCE_PROMPT_WAV_PATH = test_asset_path("cosyvoice3/zero_shot_prompt.wav")
|
||||
REFERENCE_PROMPT_WAV_PATH = get_asset_path("cosyvoice3/zero_shot_prompt.wav")
|
||||
REFERENCE_PROMPT_TEXT = "You are a helpful assistant.<|endofprompt|>希望你以后能够做的比我还好呦。"
|
||||
REFERENCE_SYNTH_TEXT = (
|
||||
"CosyVoice is undergoing a comprehensive upgrade, providing more accurate, "
|
||||
@@ -44,12 +46,6 @@ REFERENCE_STAGE0_TEMPERATURE = 1.0
|
||||
REFERENCE_STAGE0_REPETITION_PENALTY = 2.0
|
||||
|
||||
|
||||
ASYNC_CHUNK_MODES = [
|
||||
pytest.param(False, id="sync"),
|
||||
pytest.param(True, id="async_chunk"),
|
||||
]
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _load_reference_prompt_wav() -> tuple[np.ndarray, int]:
|
||||
audio, sr = sf.read(str(REFERENCE_PROMPT_WAV_PATH), dtype="float32", always_2d=False)
|
||||
@@ -120,18 +116,6 @@ def _concat_audio(audio_val) -> np.ndarray:
|
||||
return audio_np.reshape(-1)
|
||||
|
||||
|
||||
def _get_stage_engine_outputs(omni_runner: OmniRunner, stage_id: int):
|
||||
stage_list = getattr(omni_runner.omni, "stage_list", None)
|
||||
if stage_list is not None:
|
||||
return getattr(stage_list[stage_id], "engine_outputs", None) or []
|
||||
|
||||
stage_clients = getattr(getattr(omni_runner.omni, "engine", None), "stage_clients", None)
|
||||
if stage_clients is not None:
|
||||
return getattr(stage_clients[stage_id], "engine_outputs", None) or []
|
||||
|
||||
raise AttributeError("Unable to locate stage outputs on Omni runner")
|
||||
|
||||
|
||||
def _build_reference_inputs(prompt_audio: tuple[np.ndarray, int]) -> list[dict[str, object]]:
|
||||
return [
|
||||
{
|
||||
@@ -143,57 +127,67 @@ def _build_reference_inputs(prompt_audio: tuple[np.ndarray, int]) -> list[dict[s
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
# (model, stage_configs_path, extra_omni_kwargs) for ``@pytest.mark.parametrize("omni_runner", ..., indirect=True)``
|
||||
_cosy_deployment = get_deploy_config_path("cosyvoice3.yaml")
|
||||
_cosy_model_path = str(_resolve_model_dir())
|
||||
_OMNI_RUNNER_PARAMS = [
|
||||
pytest.param(
|
||||
(_cosy_model_path, _cosy_deployment, {"async_chunk": False}),
|
||||
id="sync",
|
||||
),
|
||||
pytest.param(
|
||||
(_cosy_model_path, _cosy_deployment, {"async_chunk": True}),
|
||||
id="async_chunk",
|
||||
),
|
||||
]
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.tts,
|
||||
pytest.mark.parametrize("omni_runner", _OMNI_RUNNER_PARAMS, indirect=True),
|
||||
]
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("async_chunk", ASYNC_CHUNK_MODES)
|
||||
def test_cosyvoice3_offline_reference_zero_shot(async_chunk: bool) -> None:
|
||||
def test_cosyvoice3_offline_reference_zero_shot(omni_runner: OmniRunner) -> None:
|
||||
"""CosyVoice3 zero-shot reference inference should stop cleanly and produce sane audio."""
|
||||
prompt_audio, prompt_sr = _load_reference_prompt_wav()
|
||||
model_dir = _resolve_model_dir()
|
||||
expected_stop_token = int(CosyVoice3Config().llm["eos_token_id"])
|
||||
|
||||
with OmniRunner(
|
||||
str(model_dir),
|
||||
seed=42,
|
||||
stage_configs_path=get_deploy_config_path("cosyvoice3.yaml"),
|
||||
async_chunk=async_chunk,
|
||||
stage_init_timeout=300,
|
||||
) as omni_runner:
|
||||
sampling_params_list = omni_runner.get_default_sampling_params_list()
|
||||
sampling_params_list[0] = _reference_zero_shot_stage0_sampling(text=REFERENCE_SYNTH_TEXT)
|
||||
sampling_params_list = omni_runner.get_default_sampling_params_list()
|
||||
sampling_params_list[0] = _reference_zero_shot_stage0_sampling(text=REFERENCE_SYNTH_TEXT)
|
||||
|
||||
outputs = omni_runner.omni.generate(_build_reference_inputs((prompt_audio, prompt_sr)), sampling_params_list)
|
||||
outputs = omni_runner.omni.generate(_build_reference_inputs((prompt_audio, prompt_sr)), sampling_params_list)
|
||||
|
||||
assert outputs, "No outputs returned"
|
||||
audio_mm = outputs[0].multimodal_output
|
||||
assert "audio" in audio_mm, "No audio output found"
|
||||
assert outputs, "No outputs returned"
|
||||
audio_mm = outputs[0].multimodal_output
|
||||
assert "audio" in audio_mm, "No audio output found"
|
||||
|
||||
audio = _concat_audio(audio_mm["audio"])
|
||||
assert audio.size > 0, "Generated audio is empty"
|
||||
audio = _concat_audio(audio_mm["audio"])
|
||||
assert audio.size > 0, "Generated audio is empty"
|
||||
|
||||
sr_val = audio_mm.get("sr", 24000)
|
||||
if isinstance(sr_val, list) and sr_val:
|
||||
sr_val = sr_val[-1]
|
||||
if hasattr(sr_val, "item"):
|
||||
sr_val = sr_val.item()
|
||||
sr = int(sr_val)
|
||||
assert sr == 24000, f"Unexpected sample_rate={sr}"
|
||||
sr_val = audio_mm.get("sr", 24000)
|
||||
if isinstance(sr_val, list) and sr_val:
|
||||
sr_val = sr_val[-1]
|
||||
if hasattr(sr_val, "item"):
|
||||
sr_val = sr_val.item()
|
||||
sr = int(sr_val)
|
||||
assert sr == 24000, f"Unexpected sample_rate={sr}"
|
||||
|
||||
duration_s = audio.size / sr
|
||||
assert 2.8 <= duration_s <= 8.8, f"Unexpected duration={duration_s:.3f}s (samples={audio.size}, sr={sr})"
|
||||
duration_s = audio.size / sr
|
||||
assert 2.8 <= duration_s <= 8.8, f"Unexpected duration={duration_s:.3f}s (samples={audio.size}, sr={sr})"
|
||||
|
||||
stage0_outputs = _get_stage_engine_outputs(omni_runner, 0)
|
||||
if stage0_outputs:
|
||||
completion = stage0_outputs[0].outputs[0]
|
||||
finish_reason = getattr(completion, "finish_reason", None)
|
||||
stop_reason = getattr(completion, "stop_reason", None)
|
||||
num_tokens = len(getattr(completion, "token_ids", []) or [])
|
||||
|
||||
assert finish_reason == "stop", f"Stage-0 finish_reason={finish_reason}, expected 'stop'"
|
||||
assert int(stop_reason) == expected_stop_token, (
|
||||
f"Stage-0 stop_reason={stop_reason}, expected {expected_stop_token}"
|
||||
)
|
||||
assert 80 <= num_tokens <= 220, f"Stage-0 num_tokens={num_tokens}, expected sane stop-bound range"
|
||||
else:
|
||||
assert async_chunk, "Stage-0 produced no engine outputs"
|
||||
# Code2wav is ``final_output`` (stage 1); ``Omni.generate`` yields its ``request_output`` / completions.
|
||||
pipeline_out = OmniRequestOutput.unwrap_result(outputs[0])
|
||||
assert pipeline_out.stage_id == 1, f"expected final stage 1, got {pipeline_out.stage_id}"
|
||||
assert pipeline_out.final_output_type == "audio", (
|
||||
f"expected audio final_output_type, got {pipeline_out.final_output_type!r}"
|
||||
)
|
||||
ro = pipeline_out.request_output
|
||||
assert ro is not None, "stage 1 should include request_output"
|
||||
completions = getattr(ro, "outputs", None) or []
|
||||
assert completions, "stage 1 request_output should include at least one completion"
|
||||
completion = completions[0]
|
||||
finish_reason = getattr(completion, "finish_reason", None)
|
||||
assert finish_reason == "stop", f"Stage-1 finish_reason={finish_reason}, expected 'stop'"
|
||||
token_ids = getattr(completion, "token_ids", None) or []
|
||||
if token_ids:
|
||||
assert 1 <= len(token_ids) <= 4096, f"Stage-1 num_tokens={len(token_ids)}, unexpected range"
|
||||
+4
-3
@@ -4,7 +4,7 @@
|
||||
E2E offline smoke tests for Dynin-Omni.
|
||||
|
||||
- model: "snu-aidas/Dynin-Omni"
|
||||
- stage config: tests/e2e/stage_configs/dynin_omni_ci.yaml
|
||||
- stage config: vllm_omni/deploy/dynin_omni_ci.yaml
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,13 +19,14 @@ import torch
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
_DEFAULT_DYNIN_CONFIG_PATH: Path | None = None
|
||||
_DEFAULT_STAGE_CONFIG_PATH = _REPO_ROOT / "tests" / "e2e" / "stage_configs" / "dynin_omni_ci.yaml"
|
||||
_DEFAULT_STAGE_CONFIG_PATH = Path(get_deploy_config_path("dynin_omni_ci.yaml"))
|
||||
|
||||
models = ["snu-aidas/Dynin-Omni"]
|
||||
stage_configs = [str(_DEFAULT_STAGE_CONFIG_PATH)]
|
||||
@@ -34,7 +35,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.core_model,
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.omni,
|
||||
pytest.mark.parametrize("omni_runner", test_params, indirect=True),
|
||||
]
|
||||
@@ -1,58 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
End-to-end test for ErnieImage text-to-image generation.
|
||||
|
||||
Equivalent to running:
|
||||
vllm serve baidu/ERNIE-Image --omni
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "baidu/ERNIE-Image"
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
def test_ernie_image_text_to_image():
|
||||
with OmniRunner(
|
||||
MODEL,
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
tensor_parallel_size=2,
|
||||
),
|
||||
enable_cpu_offload=True,
|
||||
) as runner:
|
||||
omni_outputs = list(
|
||||
runner.omni.generate(
|
||||
prompts=["A photo of a cat sitting on a laptop"],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=4.0,
|
||||
seed=42,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(omni_outputs) > 0
|
||||
output = omni_outputs[0]
|
||||
images = None
|
||||
if output.images:
|
||||
images = output.images
|
||||
elif hasattr(output, "request_output") and output.request_output:
|
||||
for stage_out in output.request_output:
|
||||
if hasattr(stage_out, "images") and stage_out.images:
|
||||
images = stage_out.images
|
||||
break
|
||||
|
||||
assert images is not None
|
||||
assert len(images) > 0
|
||||
assert isinstance(images[0], Image.Image)
|
||||
assert images[0].size == (512, 512)
|
||||
@@ -0,0 +1,52 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
End-to-end test for ErnieImage text-to-image generation.
|
||||
|
||||
Equivalent to running:
|
||||
vllm serve baidu/ERNIE-Image --omni
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
MODEL = "baidu/ERNIE-Image"
|
||||
|
||||
# (model, stage_configs_path, extra_omni_kwargs) for ``omni_runner`` indirect parametrize
|
||||
_OMNI_RUNNER_PARAM = (
|
||||
MODEL,
|
||||
None,
|
||||
{
|
||||
"parallel_config": DiffusionParallelConfig(
|
||||
tensor_parallel_size=2,
|
||||
),
|
||||
"enable_cpu_offload": True,
|
||||
},
|
||||
)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=2)
|
||||
def test_ernie_image_text_to_image(omni_runner_handler: OmniRunnerHandler) -> None:
|
||||
request_config = {
|
||||
"model": omni_runner_handler.runner.model_name,
|
||||
"prompt": "A photo of a cat sitting on a laptop",
|
||||
"sampling_params": OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=4.0,
|
||||
seed=42,
|
||||
),
|
||||
}
|
||||
omni_runner_handler.send_diffusion_request(request_config)
|
||||
@@ -1,37 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""Tests for Flux1 Schnell."""
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "black-forest-labs/FLUX.1-schnell"
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
def test_flux_schnell_text_to_image():
|
||||
"""Test FLUX.1-schnell text-to-image generation."""
|
||||
omni = Omni(model=MODEL)
|
||||
|
||||
omni_outputs = list(
|
||||
omni.generate(
|
||||
prompts=["A photo of a cat sitting on a laptop"],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_inference_steps=2,
|
||||
seed=42,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(omni_outputs) > 0
|
||||
images = omni_outputs[0].images
|
||||
assert len(images) == 1
|
||||
assert isinstance(images[0], Image.Image)
|
||||
assert images[0].size == (512, 512)
|
||||
+39
-23
@@ -5,11 +5,16 @@
|
||||
These tests require:
|
||||
- A CUDA GPU
|
||||
- The quantized model checkpoint (vllm-project-org/FLUX.1-dev-AutoRound-w4a16)
|
||||
|
||||
``test_flux_autoround_w4a16_generates_image`` uses the shared ``omni_runner`` fixture
|
||||
(quantized model). ``test_flux_autoround_w4a16_memory_savings`` must load two different
|
||||
checkpoints in sequence, so it keeps explicit ``OmniRunner`` context managers.
|
||||
"""
|
||||
|
||||
import gc
|
||||
import os as _os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
|
||||
@@ -21,6 +26,10 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
pytestmark = pytest.mark.skip(
|
||||
reason="Current Hugging Face account is not yet authorized to access this model.",
|
||||
)
|
||||
|
||||
QUANTIZED_MODEL = "vllm-project-org/FLUX.1-dev-AutoRound-w4a16"
|
||||
BASELINE_MODEL = "black-forest-labs/FLUX.1-dev"
|
||||
|
||||
@@ -32,6 +41,27 @@ HEIGHT = 256
|
||||
WIDTH = 256
|
||||
NUM_STEPS = 2 # minimal for smoke-test
|
||||
|
||||
# Indirect param: (model_id, stage_config_path, extra OmniRunner / Omni kwargs)
|
||||
_OMNI_RUNNER_PARAM = (QUANTIZED_MODEL, None, {"enforce_eager": True})
|
||||
|
||||
|
||||
def _sampling_params() -> OmniDiffusionSamplingParams:
|
||||
return OmniDiffusionSamplingParams(
|
||||
height=HEIGHT,
|
||||
width=WIDTH,
|
||||
num_inference_steps=NUM_STEPS,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(device=current_omni_platform.device_type).manual_seed(42),
|
||||
)
|
||||
|
||||
|
||||
def _first_request_images(outputs) -> list:
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image"
|
||||
req_out = first_output.request_output
|
||||
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
|
||||
return req_out.images
|
||||
|
||||
|
||||
def _generate_image(model_name: str, **extra_kwargs) -> tuple[list, float]:
|
||||
"""Load a FLUX model, generate one image, return (images, peak_memory_mb)."""
|
||||
@@ -44,25 +74,12 @@ def _generate_image(model_name: str, **extra_kwargs) -> tuple[list, float]:
|
||||
|
||||
with OmniRunner(model_name, enforce_eager=True, **extra_kwargs) as runner:
|
||||
current_omni_platform.reset_peak_memory_stats()
|
||||
outputs = runner.omni.generate(
|
||||
"a photo of a cat sitting on a laptop keyboard",
|
||||
OmniDiffusionSamplingParams(
|
||||
height=HEIGHT,
|
||||
width=WIDTH,
|
||||
num_inference_steps=NUM_STEPS,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(device=current_omni_platform.device_type).manual_seed(42),
|
||||
),
|
||||
)
|
||||
outputs = runner.omni.generate("a photo of a cat sitting on a laptop keyboard", _sampling_params())
|
||||
|
||||
peak = monitor.peak_used_mb
|
||||
monitor.stop()
|
||||
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image"
|
||||
req_out = first_output.request_output
|
||||
assert isinstance(req_out, OmniRequestOutput) and hasattr(req_out, "images")
|
||||
images = req_out.images
|
||||
images = _first_request_images(outputs)
|
||||
|
||||
gc.collect()
|
||||
current_omni_platform.empty_cache()
|
||||
@@ -70,26 +87,25 @@ def _generate_image(model_name: str, **extra_kwargs) -> tuple[list, float]:
|
||||
return images, peak
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
def test_flux_autoround_w4a16_generates_image():
|
||||
@pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)
|
||||
def test_flux_autoround_w4a16_generates_image(omni_runner: OmniRunner):
|
||||
"""Load the W4A16 quantized FLUX model and verify it produces a valid image."""
|
||||
images, _ = _generate_image(QUANTIZED_MODEL)
|
||||
outputs = omni_runner.omni.generate("a photo of a cat sitting on a laptop keyboard", _sampling_params())
|
||||
images = _first_request_images(outputs)
|
||||
|
||||
assert len(images) >= 1, "Expected at least one generated image"
|
||||
img = images[0]
|
||||
assert img.width == WIDTH, f"Expected width {WIDTH}, got {img.width}"
|
||||
assert img.height == HEIGHT, f"Expected height {HEIGHT}, got {img.height}"
|
||||
|
||||
# Sanity: image should not be blank (all-zero)
|
||||
import numpy as np
|
||||
|
||||
arr = np.array(img)
|
||||
assert arr.std() > 1.0, "Generated image appears blank (std ≈ 0)"
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
def test_flux_autoround_w4a16_memory_savings():
|
||||
@@ -98,7 +114,7 @@ def test_flux_autoround_w4a16_memory_savings():
|
||||
The W4A16 model should use meaningfully less memory than the
|
||||
BF16/FP16 baseline since weights are 4-bit instead of 16-bit.
|
||||
"""
|
||||
quant_images, quant_peak = _generate_image(QUANTIZED_MODEL)
|
||||
_, quant_peak = _generate_image(QUANTIZED_MODEL)
|
||||
cleanup_dist_env_and_memory()
|
||||
_, baseline_peak = _generate_image(BASELINE_MODEL)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Tests for FluxKontext model pipeline.
|
||||
|
||||
FluxKontext is a text-to-image and image-to-image diffusion model that supports:
|
||||
- Text-to-image generation
|
||||
- Image editing with text guidance
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
from tests.helpers.runtime import OmniRunnerHandler
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "black-forest-labs/FLUX.1-Kontext-dev"
|
||||
|
||||
_OMNI_RUNNER_PARAM = (
|
||||
MODEL,
|
||||
None,
|
||||
{
|
||||
"parallel_config": DiffusionParallelConfig(tensor_parallel_size=2),
|
||||
"enable_cpu_offload": False,
|
||||
},
|
||||
)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
|
||||
def _sampling_512() -> OmniDiffusionSamplingParams:
|
||||
return OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_inference_steps=2,
|
||||
seed=42,
|
||||
)
|
||||
|
||||
|
||||
def test_flux_kontext_text_to_image(omni_runner_handler: OmniRunnerHandler):
|
||||
"""Test FluxKontext text-to-image generation with real model."""
|
||||
request_config = {
|
||||
"model": MODEL,
|
||||
"prompt": "A photo of a cat sitting on a laptop",
|
||||
"sampling_params": _sampling_512(),
|
||||
}
|
||||
omni_runner_handler.send_diffusion_request(request_config)
|
||||
|
||||
|
||||
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")
|
||||
request_config = {
|
||||
"prompt": "Transform this image into a Vincent van Gogh style painting",
|
||||
"multi_modal_data": {"img2img": input_image},
|
||||
"modalities": ["img2img"],
|
||||
"sampling_params": _sampling_512(),
|
||||
}
|
||||
omni_runner_handler.send_diffusion_request(request_config)
|
||||
@@ -0,0 +1,34 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""Tests for Flux1 Schnell."""
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.helpers.runtime import OmniRunnerHandler
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "black-forest-labs/FLUX.1-schnell"
|
||||
|
||||
_OMNI_RUNNER_PARAM = (MODEL, None)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
|
||||
def test_flux_schnell_text_to_image(omni_runner_handler: OmniRunnerHandler):
|
||||
"""Test FLUX.1-schnell text-to-image generation."""
|
||||
request_config = {
|
||||
"model": MODEL,
|
||||
"prompt": "A photo of a cat sitting on a laptop",
|
||||
"sampling_params": OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_inference_steps=2,
|
||||
seed=42,
|
||||
),
|
||||
}
|
||||
omni_runner_handler.send_diffusion_request(request_config)
|
||||
@@ -4,31 +4,26 @@
|
||||
"""
|
||||
End-to-end test for Flux2 Klein inpainting.
|
||||
|
||||
Inpainting uses ``omni_runner_handler.send_diffusion_request`` with
|
||||
``multi_modal_data`` containing ``image`` and ``mask_image``; see
|
||||
:meth:`OmniRunnerHandler.send_diffusion_request` in ``tests.helpers.runtime``.
|
||||
"""
|
||||
|
||||
# ruff: noqa: E402
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from vllm_omni.entrypoints.omni import Omni
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import DiffusionResponse, OmniRunnerHandler
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
|
||||
|
||||
MODEL = "black-forest-labs/FLUX.2-klein-4B"
|
||||
|
||||
_OMNI_RUNNER_PARAM = (MODEL, None)
|
||||
|
||||
pytestmark = [pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)]
|
||||
|
||||
_HEIGHT = 512
|
||||
_WIDTH = 512
|
||||
_NUM_INFERENCE_STEPS = 4
|
||||
@@ -49,50 +44,56 @@ def _create_test_inputs(color: tuple = (100, 150, 200)):
|
||||
return _create_test_image(_WIDTH, _HEIGHT, color), _create_test_mask(_WIDTH, _HEIGHT)
|
||||
|
||||
|
||||
def _extract_images_from_output(outputs: list) -> list[Image.Image]:
|
||||
images = []
|
||||
for req_output in outputs:
|
||||
if hasattr(req_output, "images") and req_output.images:
|
||||
images.extend(req_output.images)
|
||||
elif hasattr(req_output, "request_output") and req_output.request_output:
|
||||
stage_out = req_output.request_output
|
||||
if isinstance(stage_out, OmniRequestOutput) and hasattr(stage_out, "images"):
|
||||
images.extend(stage_out.images)
|
||||
elif isinstance(stage_out, list):
|
||||
for s in stage_out:
|
||||
if hasattr(s, "images") and s.images:
|
||||
images.extend(s.images)
|
||||
return images
|
||||
def _images_from_response(response: DiffusionResponse) -> list[Image.Image]:
|
||||
if isinstance(response.images[0], list):
|
||||
return [f for fr in response.images for f in fr]
|
||||
return list(response.images)
|
||||
|
||||
|
||||
def _send_inpaint_with_generator(
|
||||
omni_runner_handler: OmniRunnerHandler, prompt: str, input_image, mask_image, generator: torch.Generator
|
||||
) -> DiffusionResponse:
|
||||
return omni_runner_handler.send_diffusion_request(
|
||||
{
|
||||
"model": MODEL,
|
||||
"prompt": prompt,
|
||||
"multi_modal_data": {"image": input_image, "mask_image": mask_image},
|
||||
"sampling_params": OmniDiffusionSamplingParams(
|
||||
height=_HEIGHT,
|
||||
width=_WIDTH,
|
||||
num_inference_steps=_NUM_INFERENCE_STEPS,
|
||||
guidance_scale=0.0,
|
||||
generator=generator,
|
||||
num_outputs_per_prompt=1,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Regression test for https://github.com/vllm-project/vllm-omni/issues/3097
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
def test_flux2_klein_can_accept_text_inputs():
|
||||
model = Omni(model=MODEL)
|
||||
outputs = model.generate(
|
||||
"a cup of coffee on the table",
|
||||
OmniDiffusionSamplingParams(num_inference_steps=2, seed=42),
|
||||
def test_flux2_klein_can_accept_text_inputs(omni_runner_handler: OmniRunnerHandler):
|
||||
omni_runner_handler.send_diffusion_request(
|
||||
{
|
||||
"model": MODEL,
|
||||
"prompt": "a cup of coffee on the table",
|
||||
"sampling_params": OmniDiffusionSamplingParams(num_inference_steps=2, seed=42),
|
||||
}
|
||||
)
|
||||
assert len(outputs[0].images) == 1
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
def test_flux2_klein_inpaint_basic():
|
||||
m = None
|
||||
try:
|
||||
m = Omni(model=MODEL)
|
||||
input_image, mask_image = _create_test_inputs()
|
||||
|
||||
outputs = m.generate(
|
||||
prompts=[
|
||||
{
|
||||
"prompt": "Fill in the masked area with a beautiful garden",
|
||||
"multi_modal_data": {"image": input_image, "mask_image": mask_image},
|
||||
}
|
||||
],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_flux2_klein_inpaint_basic(omni_runner_handler: OmniRunnerHandler):
|
||||
input_image, mask_image = _create_test_inputs()
|
||||
omni_runner_handler.send_diffusion_request(
|
||||
{
|
||||
"model": MODEL,
|
||||
"prompt": "Fill in the masked area with a beautiful garden",
|
||||
"multi_modal_data": {"image": input_image, "mask_image": mask_image},
|
||||
"sampling_params": OmniDiffusionSamplingParams(
|
||||
height=_HEIGHT,
|
||||
width=_WIDTH,
|
||||
num_inference_steps=_NUM_INFERENCE_STEPS,
|
||||
@@ -100,128 +101,45 @@ def test_flux2_klein_inpaint_basic():
|
||||
generator=torch.Generator(current_omni_platform.device_type).manual_seed(42),
|
||||
num_outputs_per_prompt=1,
|
||||
),
|
||||
)
|
||||
|
||||
images = _extract_images_from_output(list(outputs))
|
||||
assert len(images) == 1
|
||||
assert images[0].size == (_WIDTH, _HEIGHT)
|
||||
finally:
|
||||
if m is not None and hasattr(m, "close"):
|
||||
m.close()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
def test_flux2_klein_inpaint_deterministic():
|
||||
m = None
|
||||
try:
|
||||
m = Omni(model=MODEL)
|
||||
input_image, mask_image = _create_test_inputs()
|
||||
seed = 12345
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_flux2_klein_inpaint_deterministic(omni_runner_handler: OmniRunnerHandler):
|
||||
input_image, mask_image = _create_test_inputs()
|
||||
seed = 12345
|
||||
|
||||
gen1 = torch.Generator(current_omni_platform.device_type).manual_seed(seed)
|
||||
gen2 = torch.Generator(current_omni_platform.device_type).manual_seed(seed)
|
||||
gen1 = torch.Generator(current_omni_platform.device_type).manual_seed(seed)
|
||||
gen2 = torch.Generator(current_omni_platform.device_type).manual_seed(seed)
|
||||
|
||||
outputs1 = m.generate(
|
||||
prompts=[
|
||||
{
|
||||
"prompt": "A red flower in a field",
|
||||
"multi_modal_data": {"image": input_image, "mask_image": mask_image},
|
||||
}
|
||||
],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=_HEIGHT,
|
||||
width=_WIDTH,
|
||||
num_inference_steps=_NUM_INFERENCE_STEPS,
|
||||
guidance_scale=0.0,
|
||||
generator=gen1,
|
||||
num_outputs_per_prompt=1,
|
||||
),
|
||||
)
|
||||
r1 = _send_inpaint_with_generator(omni_runner_handler, "A red flower in a field", input_image, mask_image, gen1)
|
||||
r2 = _send_inpaint_with_generator(omni_runner_handler, "A red flower in a field", input_image, mask_image, gen2)
|
||||
|
||||
outputs2 = m.generate(
|
||||
prompts=[
|
||||
{
|
||||
"prompt": "A red flower in a field",
|
||||
"multi_modal_data": {"image": input_image, "mask_image": mask_image},
|
||||
}
|
||||
],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=_HEIGHT,
|
||||
width=_WIDTH,
|
||||
num_inference_steps=_NUM_INFERENCE_STEPS,
|
||||
guidance_scale=0.0,
|
||||
generator=gen2,
|
||||
num_outputs_per_prompt=1,
|
||||
),
|
||||
)
|
||||
images1 = _images_from_response(r1)
|
||||
images2 = _images_from_response(r2)
|
||||
|
||||
images1 = _extract_images_from_output(list(outputs1))
|
||||
images2 = _extract_images_from_output(list(outputs2))
|
||||
|
||||
assert len(images1) == 1
|
||||
assert len(images2) == 1
|
||||
|
||||
assert list(images1[0].getdata()) == list(images2[0].getdata()), (
|
||||
"Same input with same seed should produce identical output. "
|
||||
"This is critical for offline/online consistency."
|
||||
)
|
||||
finally:
|
||||
if m is not None and hasattr(m, "close"):
|
||||
m.close()
|
||||
assert list(images1[0].getdata()) == list(images2[0].getdata()), (
|
||||
"Same input with same seed should produce identical output. This is critical for offline/online consistency."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
def test_flux2_klein_inpaint_different_seeds_different_output():
|
||||
m = None
|
||||
try:
|
||||
m = Omni(model=MODEL)
|
||||
input_image, mask_image = _create_test_inputs()
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_flux2_klein_inpaint_different_seeds_different_output(omni_runner_handler: OmniRunnerHandler):
|
||||
input_image, mask_image = _create_test_inputs()
|
||||
|
||||
gen1 = torch.Generator(current_omni_platform.device_type).manual_seed(42)
|
||||
gen2 = torch.Generator(current_omni_platform.device_type).manual_seed(99999)
|
||||
gen1 = torch.Generator(current_omni_platform.device_type).manual_seed(42)
|
||||
gen2 = torch.Generator(current_omni_platform.device_type).manual_seed(99999)
|
||||
|
||||
outputs1 = m.generate(
|
||||
prompts=[
|
||||
{
|
||||
"prompt": "A beautiful landscape",
|
||||
"multi_modal_data": {"image": input_image, "mask_image": mask_image},
|
||||
}
|
||||
],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=_HEIGHT,
|
||||
width=_WIDTH,
|
||||
num_inference_steps=_NUM_INFERENCE_STEPS,
|
||||
guidance_scale=0.0,
|
||||
generator=gen1,
|
||||
num_outputs_per_prompt=1,
|
||||
),
|
||||
)
|
||||
r1 = _send_inpaint_with_generator(omni_runner_handler, "A beautiful landscape", input_image, mask_image, gen1)
|
||||
r2 = _send_inpaint_with_generator(omni_runner_handler, "A beautiful landscape", input_image, mask_image, gen2)
|
||||
|
||||
outputs2 = m.generate(
|
||||
prompts=[
|
||||
{
|
||||
"prompt": "A beautiful landscape",
|
||||
"multi_modal_data": {"image": input_image, "mask_image": mask_image},
|
||||
}
|
||||
],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=_HEIGHT,
|
||||
width=_WIDTH,
|
||||
num_inference_steps=_NUM_INFERENCE_STEPS,
|
||||
guidance_scale=0.0,
|
||||
generator=gen2,
|
||||
num_outputs_per_prompt=1,
|
||||
),
|
||||
)
|
||||
images1 = _images_from_response(r1)
|
||||
images2 = _images_from_response(r2)
|
||||
|
||||
images1 = _extract_images_from_output(list(outputs1))
|
||||
images2 = _extract_images_from_output(list(outputs2))
|
||||
|
||||
assert len(images1) == 1
|
||||
assert len(images2) == 1
|
||||
|
||||
different_pixel_count = sum(1 for p1, p2 in zip(images1[0].getdata(), images2[0].getdata()) if p1 != p2)
|
||||
assert different_pixel_count > 0, "Different seeds should produce different outputs"
|
||||
finally:
|
||||
if m is not None and hasattr(m, "close"):
|
||||
m.close()
|
||||
different_pixel_count = sum(1 for p1, p2 in zip(images1[0].getdata(), images2[0].getdata()) if p1 != p2)
|
||||
assert different_pixel_count > 0, "Different seeds should produce different outputs"
|
||||
@@ -1,107 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Tests for FluxKontext model pipeline.
|
||||
|
||||
FluxKontext is a text-to-image and image-to-image diffusion model that supports:
|
||||
- Text-to-image generation
|
||||
- Image editing with text guidance
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.diffusion.data import DiffusionParallelConfig
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "black-forest-labs/FLUX.1-Kontext-dev"
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
def test_flux_kontext_text_to_image():
|
||||
"""Test FluxKontext text-to-image generation with real model."""
|
||||
with OmniRunner(
|
||||
MODEL,
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
tensor_parallel_size=2,
|
||||
),
|
||||
enable_cpu_offload=False,
|
||||
) as runner:
|
||||
omni_outputs = list(
|
||||
runner.omni.generate(
|
||||
prompts=["A photo of a cat sitting on a laptop"],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_inference_steps=2,
|
||||
seed=42,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(omni_outputs) > 0
|
||||
output = omni_outputs[0]
|
||||
images = None
|
||||
if output.images:
|
||||
images = output.images
|
||||
elif hasattr(output, "request_output") and output.request_output:
|
||||
for stage_out in output.request_output:
|
||||
if hasattr(stage_out, "images") and stage_out.images:
|
||||
images = stage_out.images
|
||||
break
|
||||
|
||||
assert images is not None
|
||||
assert len(images) > 0
|
||||
assert isinstance(images[0], Image.Image)
|
||||
assert images[0].size == (512, 512)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
def test_flux_kontext_image_edit():
|
||||
"""Test FluxKontext image-to-image editing with real model."""
|
||||
input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB")
|
||||
with OmniRunner(
|
||||
MODEL,
|
||||
parallel_config=DiffusionParallelConfig(
|
||||
tensor_parallel_size=2,
|
||||
),
|
||||
enable_cpu_offload=False,
|
||||
) as runner:
|
||||
omni_outputs = list(
|
||||
runner.omni.generate(
|
||||
prompts=[
|
||||
{
|
||||
"prompt": "Transform this image into a Vincent van Gogh style painting",
|
||||
"multi_modal_data": {"img2img": input_image},
|
||||
"modalities": ["img2img"],
|
||||
}
|
||||
],
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_inference_steps=2,
|
||||
seed=42,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(omni_outputs) > 0
|
||||
output = omni_outputs[0]
|
||||
images = None
|
||||
if output.images:
|
||||
images = output.images
|
||||
elif hasattr(output, "request_output") and output.request_output:
|
||||
for stage_out in output.request_output:
|
||||
if hasattr(stage_out, "images") and stage_out.images:
|
||||
images = stage_out.images
|
||||
break
|
||||
|
||||
assert images is not None
|
||||
assert len(images) > 0
|
||||
assert isinstance(images[0], Image.Image)
|
||||
assert images[0].size == (512, 512)
|
||||
File renamed without changes.
+61
-51
@@ -13,6 +13,28 @@ from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "SII-GAIR/daVinci-MagiHuman-Base-1080p"
|
||||
|
||||
# (model, stage_config_path, extra_omni_kwargs) — see ``omni_runner`` in tests.helpers.fixtures.runtime
|
||||
_OMNI_RUNNER_PARAM = (
|
||||
MODEL,
|
||||
None,
|
||||
{
|
||||
"init_timeout": 1200,
|
||||
"tensor_parallel_size": 2,
|
||||
},
|
||||
)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
_SKIP_ISSUE_3236 = pytest.mark.skip(
|
||||
reason="https://github.com/vllm-project/vllm-omni/issues/3236",
|
||||
)
|
||||
|
||||
|
||||
def _validate_mp4(video_bytes: bytes, min_frames: int = 10) -> None:
|
||||
"""Validate that the MP4 contains meaningful video and audio tracks."""
|
||||
@@ -38,17 +60,10 @@ def _validate_mp4(video_bytes: bytes, min_frames: int = 10) -> None:
|
||||
container.close()
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@_SKIP_ISSUE_3236
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
def test_magi_human_e2e(run_level):
|
||||
def test_magi_human_e2e(omni_runner: OmniRunner):
|
||||
"""End-to-end test for MagiHuman generating video and audio."""
|
||||
if run_level != "advanced_model":
|
||||
pytest.skip("MagiHuman e2e test requires advanced_model run level with real weights.")
|
||||
|
||||
model_path = "SII-GAIR/daVinci-MagiHuman-Base-1080p"
|
||||
|
||||
prompt = (
|
||||
"A young woman with long, wavy golden blonde hair and bright blue eyes, "
|
||||
"wearing a fitted ivory silk blouse with a delicate lace collar, sits "
|
||||
@@ -88,54 +103,49 @@ def test_magi_human_e2e(run_level):
|
||||
},
|
||||
)
|
||||
|
||||
with OmniRunner(
|
||||
model_path,
|
||||
init_timeout=1200,
|
||||
tensor_parallel_size=2,
|
||||
) as runner:
|
||||
omni = runner.omni
|
||||
outputs = list(
|
||||
omni.generate(
|
||||
prompts=[prompt],
|
||||
sampling_params_list=[sampling_params],
|
||||
)
|
||||
omni = omni_runner.omni
|
||||
outputs = list(
|
||||
omni.generate(
|
||||
prompts=[prompt],
|
||||
sampling_params_list=[sampling_params],
|
||||
)
|
||||
)
|
||||
|
||||
assert len(outputs) > 0, "No outputs returned"
|
||||
first = outputs[0]
|
||||
assert len(outputs) > 0, "No outputs returned"
|
||||
first = outputs[0]
|
||||
|
||||
assert hasattr(first, "images") and first.images, "No video frames in output"
|
||||
video_frames = first.images[0]
|
||||
assert isinstance(video_frames, np.ndarray), f"Expected numpy array, got {type(video_frames)}"
|
||||
assert video_frames.ndim == 4, f"Expected 4D array (T,H,W,3), got shape {video_frames.shape}"
|
||||
assert hasattr(first, "images") and first.images, "No video frames in output"
|
||||
video_frames = first.images[0]
|
||||
assert isinstance(video_frames, np.ndarray), f"Expected numpy array, got {type(video_frames)}"
|
||||
assert video_frames.ndim == 4, f"Expected 4D array (T,H,W,3), got shape {video_frames.shape}"
|
||||
|
||||
mm = first.multimodal_output
|
||||
assert mm, "multimodal_output is empty or missing"
|
||||
mm = first.multimodal_output
|
||||
assert mm, "multimodal_output is empty or missing"
|
||||
|
||||
audio_waveform = mm.get("audio")
|
||||
assert audio_waveform is not None, "No audio waveform in multimodal_output"
|
||||
audio_waveform = mm.get("audio")
|
||||
assert audio_waveform is not None, "No audio waveform in multimodal_output"
|
||||
|
||||
audio_sample_rate = mm.get("audio_sample_rate")
|
||||
assert audio_sample_rate is not None, (
|
||||
"audio_sample_rate not found in multimodal_output; model post-process must propagate it"
|
||||
)
|
||||
assert isinstance(audio_sample_rate, (int, float)), (
|
||||
f"audio_sample_rate should be numeric, got {type(audio_sample_rate)}"
|
||||
)
|
||||
assert int(audio_sample_rate) > 0, f"audio_sample_rate must be positive, got {audio_sample_rate}"
|
||||
audio_sample_rate = mm.get("audio_sample_rate")
|
||||
assert audio_sample_rate is not None, (
|
||||
"audio_sample_rate not found in multimodal_output; model post-process must propagate it"
|
||||
)
|
||||
assert isinstance(audio_sample_rate, (int, float)), (
|
||||
f"audio_sample_rate should be numeric, got {type(audio_sample_rate)}"
|
||||
)
|
||||
assert int(audio_sample_rate) > 0, f"audio_sample_rate must be positive, got {audio_sample_rate}"
|
||||
|
||||
fps = mm.get("fps")
|
||||
assert fps is not None, "fps not found in multimodal_output; model post-process must propagate it"
|
||||
assert isinstance(fps, (int, float)), f"fps should be numeric, got {type(fps)}"
|
||||
assert int(fps) > 0, f"fps must be positive, got {fps}"
|
||||
fps = mm.get("fps")
|
||||
assert fps is not None, "fps not found in multimodal_output; model post-process must propagate it"
|
||||
assert isinstance(fps, (int, float)), f"fps should be numeric, got {type(fps)}"
|
||||
assert int(fps) > 0, f"fps must be positive, got {fps}"
|
||||
|
||||
video_bytes = mux_video_audio_bytes(
|
||||
video_frames,
|
||||
audio_waveform,
|
||||
fps=float(fps),
|
||||
audio_sample_rate=int(audio_sample_rate),
|
||||
)
|
||||
assert isinstance(video_bytes, bytes), f"Expected MP4 bytes, got {type(video_bytes)}"
|
||||
assert len(video_bytes) > 1000, f"MP4 too small ({len(video_bytes)} bytes)"
|
||||
video_bytes = mux_video_audio_bytes(
|
||||
video_frames,
|
||||
audio_waveform,
|
||||
fps=float(fps),
|
||||
audio_sample_rate=int(audio_sample_rate),
|
||||
)
|
||||
assert isinstance(video_bytes, bytes), f"Expected MP4 bytes, got {type(video_bytes)}"
|
||||
assert len(video_bytes) > 1000, f"MP4 too small ({len(video_bytes)} bytes)"
|
||||
|
||||
_validate_mp4(video_bytes)
|
||||
_validate_mp4(video_bytes)
|
||||
@@ -1,212 +0,0 @@
|
||||
"""
|
||||
End-to-end test for MammothModa2 text-to-image generation.
|
||||
|
||||
Verifies that the AR->DiT pipeline produces an image tensor whose pixel values
|
||||
match a golden reference.
|
||||
|
||||
Model weights: env var ``MAMMOTHMODA2_MODEL_PATH``
|
||||
(default: <repo_root>/MammothModa2-Preview)
|
||||
Stage config: env var ``MAMMOTHMODA2_T2I_STAGE_CONFIG``
|
||||
(default: vllm_omni/model_executor/stage_configs/mammoth_moda2.yaml)
|
||||
|
||||
Golden pixel file: ``tests/e2e/offline_inference/fixtures/mammoth_moda2_t2i_golden.json``
|
||||
Regenerate with: ``UPDATE_GOLDEN=1 pytest tests/e2e/offline_inference/test_mammoth_moda2.py``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm.sampling_params import SamplingParams
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "1"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
_IMAGE_TOKEN_ID = 151655 # "<|image_pad|>"
|
||||
_VIDEO_TOKEN_ID = 151656 # "<|video_pad|>"
|
||||
_VISION_START_TOKEN_ID = 151652 # "<|vision_start|>"
|
||||
_VISION_END_TOKEN_ID = 151653 # "<|vision_end|>"
|
||||
_AR_PATCH_SIZE = 16
|
||||
|
||||
_STAGE_CONFIGS_DIR = Path(__file__).resolve().parents[3] / "vllm_omni" / "model_executor" / "stage_configs"
|
||||
MODEL_PATH = os.environ.get(
|
||||
"MAMMOTHMODA2_MODEL_PATH",
|
||||
str(Path(__file__).resolve().parents[3] / "MammothModa2-Preview"),
|
||||
)
|
||||
T2I_STAGE_CONFIG = os.environ.get(
|
||||
"MAMMOTHMODA2_T2I_STAGE_CONFIG",
|
||||
str(_STAGE_CONFIGS_DIR / "mammoth_moda2.yaml"),
|
||||
)
|
||||
|
||||
# Golden pixel reference file. Set UPDATE_GOLDEN=1 to regenerate.
|
||||
_GOLDEN_T2I_PATH = Path(__file__).parent / "fixtures" / "mammoth_moda2_t2i_golden.json"
|
||||
# Fixed sampling coordinates: (channel, row_fraction, col_fraction)
|
||||
# Covers corners, centre, and mid-edges across all 3 channels.
|
||||
_PIXEL_SAMPLE_COORDS = [
|
||||
(0, 0.0, 0.0),
|
||||
(0, 0.5, 0.5),
|
||||
(0, 1.0, 1.0),
|
||||
(0, 0.25, 0.75),
|
||||
(1, 0.0, 1.0),
|
||||
(1, 0.5, 0.0),
|
||||
(1, 0.75, 0.25),
|
||||
(1, 1.0, 0.5),
|
||||
(2, 0.0, 0.5),
|
||||
(2, 0.5, 1.0),
|
||||
(2, 0.75, 0.75),
|
||||
(2, 1.0, 0.0),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
def _load_t2i_gen_config(model_dir: str) -> dict:
|
||||
cfg_path = Path(model_dir) / "t2i_generation_config.json"
|
||||
if not cfg_path.exists():
|
||||
pytest.skip(f"t2i_generation_config.json not found at {cfg_path}")
|
||||
with cfg_path.open() as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _format_t2i_prompt(user_prompt: str, ar_width: int, ar_height: int) -> str:
|
||||
return (
|
||||
"<|im_start|>system\nYou are a helpful image generator.<|im_end|>\n"
|
||||
f"<|im_start|>user\n{user_prompt}<|im_end|>\n"
|
||||
"<|im_start|>assistant\n"
|
||||
f"<|image start|>{ar_width}*{ar_height}<|image token|>"
|
||||
)
|
||||
|
||||
|
||||
def _sample_pixels(img_tensor: torch.Tensor) -> list[float]:
|
||||
"""Sample pixel values at fixed fractional coordinates from a (C, H, W) tensor."""
|
||||
t = img_tensor.float().clamp(0.0, 1.0)
|
||||
if t.ndim == 4:
|
||||
t = t[0] # unbatch
|
||||
C, H, W = t.shape
|
||||
values = []
|
||||
for c, rh, rw in _PIXEL_SAMPLE_COORDS:
|
||||
ri = min(int(rh * (H - 1)), H - 1)
|
||||
ci = min(int(rw * (W - 1)), W - 1)
|
||||
values.append(round(float(t[c, ri, ci]), 6))
|
||||
return values
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# End-to-end test
|
||||
# ---------------------------------------------------------------------------
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
def test_mammothmoda2_t2i_e2e():
|
||||
"""
|
||||
End-to-end text-to-image generation with MammothModa2 (AR -> DiT).
|
||||
|
||||
Verifies:
|
||||
- Omni pipeline initialises with the two-stage YAML config.
|
||||
- DiT stage outputs an image tensor with the correct shape.
|
||||
- A fixed set of pixel values matches a golden reference
|
||||
(regenerate with ``UPDATE_GOLDEN=1``).
|
||||
"""
|
||||
if not Path(MODEL_PATH).exists():
|
||||
pytest.skip(f"Model weights not found at {MODEL_PATH}")
|
||||
if not Path(T2I_STAGE_CONFIG).exists():
|
||||
pytest.skip(f"Stage config not found at {T2I_STAGE_CONFIG}")
|
||||
|
||||
gen_cfg = _load_t2i_gen_config(MODEL_PATH)
|
||||
eol_token_id = int(gen_cfg["eol_token_id"])
|
||||
visual_start = int(gen_cfg["visual_token_start_id"])
|
||||
visual_end = int(gen_cfg["visual_token_end_id"])
|
||||
|
||||
height, width = 256, 256 # small for CI speed
|
||||
ar_height, ar_width = height // _AR_PATCH_SIZE, width // _AR_PATCH_SIZE
|
||||
expected_grid_tokens = ar_height * (ar_width + 1)
|
||||
|
||||
prompt_text = "A cat sitting on a laptop keyboard"
|
||||
formatted_prompt = _format_t2i_prompt(prompt_text, ar_width, ar_height)
|
||||
|
||||
with OmniRunner(MODEL_PATH, stage_configs_path=T2I_STAGE_CONFIG, trust_remote_code=True) as runner:
|
||||
omni = runner.omni
|
||||
# Greedy / deterministic sampling so pixel values are reproducible.
|
||||
ar_sampling = SamplingParams(
|
||||
temperature=0.0,
|
||||
top_k=1,
|
||||
max_tokens=max(1, expected_grid_tokens + 1),
|
||||
detokenize=False,
|
||||
)
|
||||
dit_sampling = SamplingParams(temperature=0.0, max_tokens=1, detokenize=False)
|
||||
|
||||
outputs = list(
|
||||
omni.generate(
|
||||
[
|
||||
{
|
||||
"prompt": formatted_prompt,
|
||||
"additional_information": {
|
||||
"omni_task": ["t2i"],
|
||||
"ar_width": [ar_width],
|
||||
"ar_height": [ar_height],
|
||||
"eol_token_id": [eol_token_id],
|
||||
"visual_token_start_id": [visual_start],
|
||||
"visual_token_end_id": [visual_end],
|
||||
"image_height": [height],
|
||||
"image_width": [width],
|
||||
"num_inference_steps": [2],
|
||||
"text_guidance_scale": [1.0],
|
||||
"cfg_range": [0.0, 1.0],
|
||||
"visual_ids": [
|
||||
_IMAGE_TOKEN_ID,
|
||||
_VIDEO_TOKEN_ID,
|
||||
_VISION_START_TOKEN_ID,
|
||||
_VISION_END_TOKEN_ID,
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
[ar_sampling, dit_sampling],
|
||||
)
|
||||
)
|
||||
|
||||
assert len(outputs) > 0, "Pipeline produced no outputs"
|
||||
|
||||
# Find the image tensor produced by the DiT stage and compare pixels.
|
||||
found_image = False
|
||||
for out in outputs:
|
||||
ro_list = getattr(out, "request_output", out)
|
||||
if not isinstance(ro_list, list):
|
||||
ro_list = [ro_list]
|
||||
for ro in ro_list:
|
||||
completion_outputs = getattr(ro, "outputs", None)
|
||||
if not isinstance(completion_outputs, list):
|
||||
continue
|
||||
for completion in completion_outputs:
|
||||
mm = getattr(completion, "multimodal_output", None)
|
||||
if not (isinstance(mm, dict) and "image" in mm):
|
||||
continue
|
||||
img_list = mm["image"] if isinstance(mm["image"], list) else [mm["image"]]
|
||||
for img_tensor in img_list:
|
||||
assert isinstance(img_tensor, torch.Tensor), f"Expected image tensor, got {type(img_tensor)}"
|
||||
assert img_tensor.ndim in (3, 4), f"Expected 3D or 4D image tensor, got {img_tensor.ndim}D"
|
||||
|
||||
sampled = _sample_pixels(img_tensor)
|
||||
|
||||
if os.environ.get("UPDATE_GOLDEN"):
|
||||
_GOLDEN_T2I_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_GOLDEN_T2I_PATH.write_text(json.dumps({"pixels": sampled}, indent=2))
|
||||
print(f"\nGolden file written to {_GOLDEN_T2I_PATH}")
|
||||
elif _GOLDEN_T2I_PATH.exists():
|
||||
golden = json.loads(_GOLDEN_T2I_PATH.read_text())["pixels"]
|
||||
for i, (got, exp) in enumerate(zip(sampled, golden)):
|
||||
assert abs(got - exp) < 1e-4, f"Pixel {i} mismatch: got {got}, expected {exp}"
|
||||
|
||||
found_image = True
|
||||
|
||||
assert found_image, "No image tensor found in pipeline output"
|
||||
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
End-to-end test for MammothModa2 text-to-image generation.
|
||||
|
||||
Verifies that the AR->DiT pipeline produces an image tensor whose pixel values
|
||||
match a golden reference.
|
||||
|
||||
Model Hub repo id: ``bytedance-research/MammothModa2-Preview``.
|
||||
Stage config: ``get_deploy_config_path("mammoth_moda2.yaml")`` → ``vllm_omni/deploy/mammoth_moda2.yaml``
|
||||
|
||||
Golden pixel file: ``tests/e2e/offline_inference/fixtures/mammoth_moda2_t2i_golden.json``
|
||||
Regenerate with: ``UPDATE_GOLDEN=1 pytest tests/e2e/offline_inference/test_mammoth_moda2_expansion.py``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from huggingface_hub import snapshot_download
|
||||
from vllm.sampling_params 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
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
_IMAGE_TOKEN_ID = 151655 # "<|image_pad|>"
|
||||
_VIDEO_TOKEN_ID = 151656 # "<|video_pad|>"
|
||||
_VISION_START_TOKEN_ID = 151652 # "<|vision_start|>"
|
||||
_VISION_END_TOKEN_ID = 151653 # "<|vision_end|>"
|
||||
_AR_PATCH_SIZE = 16
|
||||
|
||||
MODEL_PATH = "bytedance-research/MammothModa2-Preview"
|
||||
T2I_STAGE_CONFIG = get_deploy_config_path("mammoth_moda2.yaml")
|
||||
|
||||
_OMNI_RUNNER_PARAM = (MODEL_PATH, T2I_STAGE_CONFIG)
|
||||
|
||||
# Golden pixel reference file. Set UPDATE_GOLDEN=1 to regenerate.
|
||||
_GOLDEN_T2I_PATH = Path(__file__).parent / "fixtures" / "mammoth_moda2_t2i_golden.json"
|
||||
# Fixed sampling coordinates: (channel, row_fraction, col_fraction)
|
||||
# Covers corners, centre, and mid-edges across all 3 channels.
|
||||
_PIXEL_SAMPLE_COORDS = [
|
||||
(0, 0.0, 0.0),
|
||||
(0, 0.5, 0.5),
|
||||
(0, 1.0, 1.0),
|
||||
(0, 0.25, 0.75),
|
||||
(1, 0.0, 1.0),
|
||||
(1, 0.5, 0.0),
|
||||
(1, 0.75, 0.25),
|
||||
(1, 1.0, 0.5),
|
||||
(2, 0.0, 0.5),
|
||||
(2, 0.5, 1.0),
|
||||
(2, 0.75, 0.75),
|
||||
(2, 1.0, 0.0),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
def _load_t2i_gen_config(repo_id: str) -> dict:
|
||||
weights_dir = Path(snapshot_download(repo_id))
|
||||
cfg_path = weights_dir / "t2i_generation_config.json"
|
||||
if not cfg_path.exists():
|
||||
pytest.skip(f"t2i_generation_config.json not found at {cfg_path}")
|
||||
with cfg_path.open() as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _format_t2i_prompt(user_prompt: str, ar_width: int, ar_height: int) -> str:
|
||||
return (
|
||||
"<|im_start|>system\nYou are a helpful image generator.<|im_end|>\n"
|
||||
f"<|im_start|>user\n{user_prompt}<|im_end|>\n"
|
||||
"<|im_start|>assistant\n"
|
||||
f"<|image start|>{ar_width}*{ar_height}<|image token|>"
|
||||
)
|
||||
|
||||
|
||||
def _sample_pixels(img_tensor: torch.Tensor) -> list[float]:
|
||||
"""Sample pixel values at fixed fractional coordinates from a (C, H, W) tensor."""
|
||||
t = img_tensor.float().clamp(0.0, 1.0)
|
||||
if t.ndim == 4:
|
||||
t = t[0] # unbatch
|
||||
C, H, W = t.shape
|
||||
values = []
|
||||
for c, rh, rw in _PIXEL_SAMPLE_COORDS:
|
||||
ri = min(int(rh * (H - 1)), H - 1)
|
||||
ci = min(int(rw * (W - 1)), W - 1)
|
||||
values.append(round(float(t[c, ri, ci]), 6))
|
||||
return values
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# End-to-end test
|
||||
# ---------------------------------------------------------------------------
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.diffusion,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/3201")
|
||||
@hardware_test(res={"cuda": "H100"})
|
||||
def test_mammothmoda2_t2i_e2e(omni_runner: OmniRunner):
|
||||
"""
|
||||
End-to-end text-to-image generation with MammothModa2 (AR -> DiT).
|
||||
|
||||
Verifies:
|
||||
- Omni pipeline initialises with the two-stage YAML config.
|
||||
- DiT stage outputs an image tensor with the correct shape.
|
||||
- A fixed set of pixel values matches a golden reference
|
||||
(regenerate with ``UPDATE_GOLDEN=1``).
|
||||
"""
|
||||
gen_cfg = _load_t2i_gen_config(MODEL_PATH)
|
||||
eol_token_id = int(gen_cfg["eol_token_id"])
|
||||
visual_start = int(gen_cfg["visual_token_start_id"])
|
||||
visual_end = int(gen_cfg["visual_token_end_id"])
|
||||
|
||||
height, width = 256, 256 # small for CI speed
|
||||
ar_height, ar_width = height // _AR_PATCH_SIZE, width // _AR_PATCH_SIZE
|
||||
expected_grid_tokens = ar_height * (ar_width + 1)
|
||||
|
||||
prompt_text = "A cat sitting on a laptop keyboard"
|
||||
formatted_prompt = _format_t2i_prompt(prompt_text, ar_width, ar_height)
|
||||
|
||||
omni = omni_runner.omni
|
||||
ar_sampling = SamplingParams(
|
||||
temperature=0.0,
|
||||
top_k=1,
|
||||
max_tokens=max(1, expected_grid_tokens + 1),
|
||||
detokenize=False,
|
||||
)
|
||||
dit_sampling = SamplingParams(temperature=0.0, max_tokens=1, detokenize=False)
|
||||
|
||||
outputs = list(
|
||||
omni.generate(
|
||||
[
|
||||
{
|
||||
"prompt": formatted_prompt,
|
||||
"additional_information": {
|
||||
"omni_task": ["t2i"],
|
||||
"ar_width": [ar_width],
|
||||
"ar_height": [ar_height],
|
||||
"eol_token_id": [eol_token_id],
|
||||
"visual_token_start_id": [visual_start],
|
||||
"visual_token_end_id": [visual_end],
|
||||
"image_height": [height],
|
||||
"image_width": [width],
|
||||
"num_inference_steps": [2],
|
||||
"text_guidance_scale": [1.0],
|
||||
"cfg_range": [0.0, 1.0],
|
||||
"visual_ids": [
|
||||
_IMAGE_TOKEN_ID,
|
||||
_VIDEO_TOKEN_ID,
|
||||
_VISION_START_TOKEN_ID,
|
||||
_VISION_END_TOKEN_ID,
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
[ar_sampling, dit_sampling],
|
||||
)
|
||||
)
|
||||
|
||||
assert len(outputs) > 0, "Pipeline produced no outputs"
|
||||
|
||||
found_image = False
|
||||
for out in outputs:
|
||||
ro_list = getattr(out, "request_output", out)
|
||||
if not isinstance(ro_list, list):
|
||||
ro_list = [ro_list]
|
||||
for ro in ro_list:
|
||||
completion_outputs = getattr(ro, "outputs", None)
|
||||
if not isinstance(completion_outputs, list):
|
||||
continue
|
||||
for completion in completion_outputs:
|
||||
mm = getattr(completion, "multimodal_output", None)
|
||||
if not (isinstance(mm, dict) and "image" in mm):
|
||||
continue
|
||||
img_list = mm["image"] if isinstance(mm["image"], list) else [mm["image"]]
|
||||
for img_tensor in img_list:
|
||||
assert isinstance(img_tensor, torch.Tensor), f"Expected image tensor, got {type(img_tensor)}"
|
||||
assert img_tensor.ndim in (3, 4), f"Expected 3D or 4D image tensor, got {img_tensor.ndim}D"
|
||||
|
||||
sampled = _sample_pixels(img_tensor)
|
||||
|
||||
if os.environ.get("UPDATE_GOLDEN"):
|
||||
_GOLDEN_T2I_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_GOLDEN_T2I_PATH.write_text(json.dumps({"pixels": sampled}, indent=2))
|
||||
print(f"\nGolden file written to {_GOLDEN_T2I_PATH}")
|
||||
elif _GOLDEN_T2I_PATH.exists():
|
||||
golden = json.loads(_GOLDEN_T2I_PATH.read_text())["pixels"]
|
||||
for i, (got, exp) in enumerate(zip(sampled, golden)):
|
||||
assert abs(got - exp) < 1e-4, f"Pixel {i} mismatch: got {got}, expected {exp}"
|
||||
|
||||
found_image = True
|
||||
|
||||
assert found_image, "No image tensor found in pipeline output"
|
||||
+7
-7
@@ -63,7 +63,7 @@ def test_text_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
prompt = build_prompt("请详细介绍鹦鹉的生活习性。")
|
||||
request_config = {"prompts": prompt, "modalities": ["text"]}
|
||||
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@@ -80,7 +80,7 @@ def test_image_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
prompt = build_prompt(f"{IMAGE_TOKEN}Describe this image briefly.")
|
||||
request_config = {"prompts": prompt, "images": image, "modalities": ["text"]}
|
||||
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@@ -99,7 +99,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
prompt = build_prompt(f"{AUDIO_TOKEN}Please recognize the language of this speech and transcribe it. Format: oral.")
|
||||
request_config = {"prompts": prompt, "audios": audio, "modalities": ["text"]}
|
||||
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@@ -116,7 +116,7 @@ def test_video_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
prompt = build_prompt(f"{VIDEO_TOKEN}Describe what is happening in this video.")
|
||||
request_config = {"prompts": prompt, "videos": video, "modalities": ["text"]}
|
||||
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@@ -136,7 +136,7 @@ def test_mixed_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
prompt = build_prompt(f"{IMAGE_TOKEN}{AUDIO_TOKEN}Describe the image and transcribe the audio.")
|
||||
request_config = {"prompts": prompt, "images": image, "audios": audio, "modalities": ["text"]}
|
||||
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@@ -152,7 +152,7 @@ def test_text_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||
prompt = build_prompt("请简单介绍一下北京。")
|
||||
request_config = {"prompts": prompt, "modalities": ["audio"]}
|
||||
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@@ -169,4 +169,4 @@ def test_image_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||
prompt = build_prompt(f"{IMAGE_TOKEN}Describe this image briefly.")
|
||||
request_config = {"prompts": prompt, "images": image, "modalities": ["audio"]}
|
||||
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
+40
-34
@@ -18,9 +18,25 @@ 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
|
||||
from vllm_omni import Omni
|
||||
|
||||
MODEL_NAME = "OpenMOSS-Team/MOSS-TTS-Nano"
|
||||
STAGE_CONFIG = get_deploy_config_path("moss_tts_nano.yaml")
|
||||
|
||||
# (model, stage_configs_path, extra_omni_kwargs) for ``omni_runner`` indirect parametrize
|
||||
_OMNI_RUNNER_PARAM = (
|
||||
MODEL_NAME,
|
||||
STAGE_CONFIG,
|
||||
)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.tts,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
SAMPLE_RATE = 48000
|
||||
REF_AUDIO_URL = "https://raw.githubusercontent.com/OpenMOSS/MOSS-TTS-Nano/main/assets/audio/zh_1.wav"
|
||||
|
||||
@@ -90,74 +106,64 @@ def _build_request(
|
||||
|
||||
def _collect_audio(omni: Omni, request: dict) -> tuple[torch.Tensor, int]:
|
||||
"""Run a single request and return (waveform, sample_rate)."""
|
||||
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 multimodal_output to be non-None"
|
||||
audio = mm.get("audio")
|
||||
sr = mm.get("sr")
|
||||
assert audio is not None, "Expected 'audio' key in multimodal_output"
|
||||
assert isinstance(audio, torch.Tensor), f"audio should be Tensor, got {type(audio)}"
|
||||
return audio.cpu(), int(sr.item()) if sr is not None else SAMPLE_RATE
|
||||
# Omni.generate returns list[OmniRequestOutput] by default (py_generator=False).
|
||||
for omni_out in omni.generate(request, DEFAULT_SAMPLING):
|
||||
mm = omni_out.multimodal_output
|
||||
assert mm is not None, "Expected multimodal_output to be non-None"
|
||||
audio = mm.get("audio")
|
||||
sr = mm.get("sr")
|
||||
assert audio is not None, "Expected 'audio' key in multimodal_output"
|
||||
assert isinstance(audio, torch.Tensor), f"audio should be Tensor, got {type(audio)}"
|
||||
return audio.cpu(), int(sr.item()) if sr is not None else SAMPLE_RATE
|
||||
raise AssertionError("No stage outputs received")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def omni_engine():
|
||||
"""Module-scoped Omni engine to avoid re-loading the model for each test."""
|
||||
return Omni(model=MODEL_NAME, stage_init_timeout=180)
|
||||
|
||||
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
def test_moss_tts_nano_english(omni_engine, ref_audio_path):
|
||||
"""English voice_clone mode (no prompt_text) produces non-empty 48 kHz audio."""
|
||||
def test_moss_tts_nano_english(omni_runner: OmniRunner, ref_audio_path) -> None:
|
||||
"""English TTS produces non-empty 48 kHz stereo audio."""
|
||||
req = _build_request("Hello, this is a short voice cloning demo for testing.", ref_audio_path)
|
||||
audio, sr = _collect_audio(omni_engine, req)
|
||||
audio, sr = _collect_audio(omni_runner.omni, req)
|
||||
|
||||
assert sr == SAMPLE_RATE, f"Expected sample_rate={SAMPLE_RATE}, got {sr}"
|
||||
assert audio.numel() > 0, "Audio tensor should not be empty"
|
||||
assert not torch.all(audio == 0), "Audio should not be all-zeros (silence)"
|
||||
|
||||
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
def test_moss_tts_nano_chinese(omni_engine, ref_audio_path):
|
||||
"""Chinese voice_clone mode produces non-empty audio."""
|
||||
def test_moss_tts_nano_chinese(omni_runner: OmniRunner, ref_audio_path) -> None:
|
||||
"""Chinese TTS produces non-empty audio."""
|
||||
req = _build_request("你好,这是语音合成测试。", ref_audio_path)
|
||||
audio, sr = _collect_audio(omni_engine, req)
|
||||
audio, sr = _collect_audio(omni_runner.omni, req)
|
||||
|
||||
assert sr == SAMPLE_RATE
|
||||
assert audio.numel() > 0
|
||||
assert not torch.all(audio == 0)
|
||||
|
||||
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
def test_moss_tts_nano_deterministic(omni_engine, ref_audio_path):
|
||||
def test_moss_tts_nano_deterministic(omni_runner: OmniRunner, ref_audio_path) -> None:
|
||||
"""Same seed produces identical waveforms."""
|
||||
req = _build_request("Reproducible output test.", ref_audio_path, seed=123)
|
||||
audio1, _ = _collect_audio(omni_engine, req)
|
||||
audio2, _ = _collect_audio(omni_engine, req)
|
||||
audio1, _ = _collect_audio(omni_runner.omni, req)
|
||||
audio2, _ = _collect_audio(omni_runner.omni, req)
|
||||
|
||||
assert audio1.shape == audio2.shape, "Waveform shapes should match with same seed"
|
||||
assert torch.allclose(audio1, audio2, atol=1e-4), "Waveforms should match with same seed"
|
||||
|
||||
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
def test_moss_tts_nano_batch(omni_engine, ref_audio_path):
|
||||
def test_moss_tts_nano_batch(omni_runner: OmniRunner, ref_audio_path) -> None:
|
||||
"""Batch of two requests returns audio for each."""
|
||||
requests = [
|
||||
_build_request("First request.", ref_audio_path),
|
||||
_build_request("Second request.", ref_audio_path),
|
||||
]
|
||||
results = []
|
||||
for stage_outputs in omni_engine.generate(requests, [DEFAULT_SAMPLING] * 2):
|
||||
for req_output in stage_outputs.request_output:
|
||||
mm = req_output.outputs[0].multimodal_output
|
||||
assert mm is not None
|
||||
results.append(mm["audio"].cpu())
|
||||
# Single-stage pipeline: one SamplingParams object is broadcast to all prompts.
|
||||
for omni_out in omni_runner.omni.generate(requests, DEFAULT_SAMPLING):
|
||||
mm = omni_out.multimodal_output
|
||||
assert mm is not None
|
||||
results.append(mm["audio"].cpu())
|
||||
|
||||
assert len(results) == 2, f"Expected 2 outputs, got {len(results)}"
|
||||
for i, audio in enumerate(results):
|
||||
@@ -1,159 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
End-to-end tests for Omni Sleep Mode across various model architectures.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
from vllm import SamplingParams
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "ByteDance-Seed/BAGEL-7B-MoT"
|
||||
MODEL_DIFF = "riverclouds/qwen_image_random"
|
||||
|
||||
|
||||
def get_ack_info(ack, key, default=None):
|
||||
if hasattr(ack, key):
|
||||
return getattr(ack, key)
|
||||
if isinstance(ack, dict):
|
||||
return ack.get(key, default)
|
||||
return default
|
||||
|
||||
|
||||
def get_dynamic_devices(stage_idx, num_stages, tp_size):
|
||||
total_gpus = torch.accelerator.device_count()
|
||||
gpus_per_stage = tp_size
|
||||
start_idx = stage_idx * gpus_per_stage
|
||||
if start_idx + gpus_per_stage > total_gpus:
|
||||
start_idx = start_idx % total_gpus
|
||||
device_ids = [str(start_idx + i) for i in range(gpus_per_stage)]
|
||||
return ",".join(device_ids)
|
||||
|
||||
|
||||
# Test 1: Diffusion Model (2-Stage BAGEL)
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.parametrize("tp_size", [1])
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1)
|
||||
@pytest.mark.asyncio
|
||||
async def test_diffusion_model_sleep_tp(tp_size):
|
||||
num_gpus = torch.accelerator.device_count()
|
||||
if num_gpus < tp_size:
|
||||
pytest.skip(f"Skipping TP={tp_size}")
|
||||
|
||||
engine_args = {
|
||||
"model": MODEL,
|
||||
"enable_sleep_mode": True,
|
||||
"tensor_parallel_size": tp_size,
|
||||
"enforce_eager": True,
|
||||
"trust_remote_code": True,
|
||||
"dtype": "bfloat16",
|
||||
"gpu_memory_utilization": 0.5,
|
||||
}
|
||||
|
||||
engine = AsyncOmni(**engine_args, stage_init_timeout=1200)
|
||||
try:
|
||||
# BAGEL requires 2 params
|
||||
diff_sp = OmniDiffusionSamplingParams(num_inference_steps=2, height=256, width=256)
|
||||
llm_sp = SamplingParams()
|
||||
|
||||
# Warmup
|
||||
async for _ in engine.generate("test", sampling_params=[llm_sp, diff_sp]):
|
||||
pass
|
||||
|
||||
# Sleep all
|
||||
acks = await engine.sleep(level=2)
|
||||
statuses = [get_ack_info(ack, "status") for ack in acks]
|
||||
assert all(s == "SUCCESS" for s in statuses), f"Sleep failed. Statuses: {statuses}"
|
||||
|
||||
# Wakeup & Verify
|
||||
await engine.wake_up()
|
||||
async for _ in engine.generate("verify", sampling_params=[llm_sp, diff_sp]):
|
||||
pass
|
||||
|
||||
print(f"Diffusion TP={tp_size} Lifecycle OK")
|
||||
finally:
|
||||
engine.shutdown()
|
||||
|
||||
|
||||
# Test 2: Multi-stage Manual Config
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.parametrize("tp_size", [1, 2])
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
||||
@pytest.mark.asyncio
|
||||
async def test_multistage_sleep_h100(tp_size):
|
||||
num_gpus = torch.accelerator.device_count()
|
||||
if num_gpus < tp_size * 2:
|
||||
pytest.skip("Not enough GPUs")
|
||||
|
||||
stages = []
|
||||
for i in range(2):
|
||||
devs = get_dynamic_devices(i, 2, tp_size)
|
||||
stages.append(
|
||||
{
|
||||
"stage_id": i,
|
||||
"stage_type": "llm" if i == 0 else "diffusion",
|
||||
"runtime": {"process": True, "devices": devs},
|
||||
"engine_args": {
|
||||
"model": MODEL,
|
||||
"model_stage": "thinker" if i == 0 else "base",
|
||||
"tensor_parallel_size": tp_size,
|
||||
"gpu_memory_utilization": 0.4,
|
||||
"dtype": "bfloat16",
|
||||
"enable_sleep_mode": True,
|
||||
"trust_remote_code": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
connectors = [{"src_stage_id": 0, "dst_stage_id": 1, "connector_type": "queue"}]
|
||||
|
||||
engine = AsyncOmni(
|
||||
model=MODEL, stages=stages, connectors=connectors, enable_sleep_mode=True, stage_init_timeout=1200
|
||||
)
|
||||
try:
|
||||
sp = OmniDiffusionSamplingParams(num_inference_steps=2)
|
||||
async for _ in engine.generate("warmup", sampling_params=[SamplingParams(), sp]):
|
||||
pass
|
||||
|
||||
acks = await engine.sleep(stage_ids=[0, 1], level=2)
|
||||
assert len(acks) == 2
|
||||
|
||||
await engine.wake_up(stage_ids=[0, 1])
|
||||
async for _ in engine.generate("verify", sampling_params=[SamplingParams(), sp]):
|
||||
pass
|
||||
finally:
|
||||
engine.shutdown()
|
||||
|
||||
|
||||
# Test 3: Pure Diffusion Single-Stage
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.parametrize("tp_size", [1, 2])
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
|
||||
@pytest.mark.asyncio
|
||||
async def test_pure_diffusion_scenario(tp_size):
|
||||
engine_args = {
|
||||
"model": MODEL_DIFF,
|
||||
"enable_sleep_mode": True,
|
||||
"tensor_parallel_size": tp_size,
|
||||
"enforce_eager": True,
|
||||
"dtype": "bfloat16",
|
||||
"gpu_memory_utilization": 0.5,
|
||||
}
|
||||
|
||||
engine = AsyncOmni(**engine_args, stage_init_timeout=1200)
|
||||
try:
|
||||
await engine.sleep(level=1)
|
||||
await engine.wake_up()
|
||||
async for _ in engine.generate("test", sampling_params=[SamplingParams()]):
|
||||
pass
|
||||
print("Pure Diffusion OK")
|
||||
finally:
|
||||
engine.shutdown()
|
||||
+22
-21
@@ -11,27 +11,35 @@ import os
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
MODEL = "k2-fsa/OmniVoice"
|
||||
STAGE_CONFIG = get_deploy_config_path("omnivoice.yaml")
|
||||
|
||||
# (model, stage_config_path, extra_omni_kwargs) — see ``omni_runner`` in tests.helpers.fixtures.runtime
|
||||
_OMNI_RUNNER_PARAM = (
|
||||
MODEL,
|
||||
STAGE_CONFIG,
|
||||
{
|
||||
"trust_remote_code": True,
|
||||
"log_stats": True,
|
||||
},
|
||||
)
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.tts,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
|
||||
def get_stage_config():
|
||||
return str(
|
||||
Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / "omnivoice.yaml"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_omnivoice_text_to_audio() -> None:
|
||||
def test_omnivoice_text_to_audio(omni_runner: OmniRunner) -> None:
|
||||
"""
|
||||
Test OmniVoice text-to-audio generation via offline Omni runner.
|
||||
Deploy Setting: omnivoice.yaml (enforce_eager=true)
|
||||
@@ -40,17 +48,10 @@ def test_omnivoice_text_to_audio() -> None:
|
||||
"""
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
with OmniRunner(
|
||||
MODEL,
|
||||
stage_configs_path=get_stage_config(),
|
||||
trust_remote_code=True,
|
||||
log_stats=True,
|
||||
) as runner:
|
||||
prompts = {"prompt": "Hello, this is a test for text to audio."}
|
||||
prompts = {"prompt": "Hello, this is a test for text to audio."}
|
||||
sampling_params_list = [OmniDiffusionSamplingParams()]
|
||||
|
||||
sampling_params_list = [OmniDiffusionSamplingParams()]
|
||||
|
||||
outputs = list(runner.omni.generate(prompts, sampling_params_list=sampling_params_list))
|
||||
outputs = list(omni_runner.omni.generate(prompts, sampling_params_list=sampling_params_list))
|
||||
|
||||
assert len(outputs) > 0, "No outputs generated"
|
||||
|
||||
+10
-15
@@ -22,6 +22,11 @@ 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.omni,
|
||||
]
|
||||
|
||||
QUANTIZED_MODEL = "Intel/Qwen2.5-Omni-7B-int4-AutoRound"
|
||||
BASELINE_MODEL = "Qwen/Qwen2.5-Omni-7B"
|
||||
|
||||
@@ -56,8 +61,6 @@ quant_params = [(QUANTIZED_MODEL, stage_config)]
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@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):
|
||||
@@ -66,7 +69,7 @@ def test_text_to_text(omni_runner, omni_runner_handler):
|
||||
"prompts": "What is the capital of China?",
|
||||
"modalities": ["text"],
|
||||
}
|
||||
response = omni_runner_handler.send_request(request_config)
|
||||
response = omni_runner_handler.send_omni_request(request_config)
|
||||
assert response.success, f"Request failed: {response.error_message}"
|
||||
assert response.text_content and len(response.text_content.strip()) > 0
|
||||
|
||||
@@ -76,8 +79,6 @@ def test_text_to_text(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@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):
|
||||
@@ -91,7 +92,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler):
|
||||
"audios": (audio, 16000),
|
||||
"modalities": ["text"],
|
||||
}
|
||||
response = omni_runner_handler.send_request(request_config)
|
||||
response = omni_runner_handler.send_omni_request(request_config)
|
||||
assert response.success, f"Request failed: {response.error_message}"
|
||||
assert response.text_content and len(response.text_content.strip()) > 0
|
||||
|
||||
@@ -101,8 +102,6 @@ def test_audio_to_text(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@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):
|
||||
@@ -114,7 +113,7 @@ def test_image_to_text(omni_runner, omni_runner_handler):
|
||||
"images": image,
|
||||
"modalities": ["text"],
|
||||
}
|
||||
response = omni_runner_handler.send_request(request_config)
|
||||
response = omni_runner_handler.send_omni_request(request_config)
|
||||
assert response.success, f"Request failed: {response.error_message}"
|
||||
assert response.text_content and len(response.text_content.strip()) > 0
|
||||
|
||||
@@ -124,8 +123,6 @@ def test_image_to_text(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@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):
|
||||
@@ -137,7 +134,7 @@ def test_video_to_text(omni_runner, omni_runner_handler):
|
||||
"videos": video,
|
||||
"modalities": ["text"],
|
||||
}
|
||||
response = omni_runner_handler.send_request(request_config)
|
||||
response = omni_runner_handler.send_omni_request(request_config)
|
||||
assert response.success, f"Request failed: {response.error_message}"
|
||||
assert response.text_content and len(response.text_content.strip()) > 0
|
||||
|
||||
@@ -147,8 +144,6 @@ def test_video_to_text(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@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):
|
||||
@@ -166,5 +161,5 @@ def test_mix_to_audio(omni_runner, omni_runner_handler):
|
||||
"audios": (audio, 16000),
|
||||
"modalities": ["audio"],
|
||||
}
|
||||
response = omni_runner_handler.send_request(request_config)
|
||||
response = omni_runner_handler.send_omni_request(request_config)
|
||||
assert response.success, f"Request failed: {response.error_message}"
|
||||
@@ -78,7 +78,7 @@ def test_mix_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||
}
|
||||
|
||||
# Test single completion
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@@ -98,4 +98,4 @@ def test_text_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
request_config = {"prompts": get_question("text_only"), "modalities": ["text"]}
|
||||
|
||||
# Test single completion
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
@@ -61,4 +61,4 @@ def test_video_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||
request_config = {"prompts": get_question(), "videos": video, "modalities": ["audio"]}
|
||||
|
||||
# Test single completion
|
||||
omni_runner_handler.send_request(request_config)
|
||||
omni_runner_handler.send_omni_request(request_config)
|
||||
+15
-12
@@ -22,6 +22,15 @@ 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.omni,
|
||||
]
|
||||
|
||||
_SKIP_ISSUE_3195 = pytest.mark.skip(
|
||||
reason="https://github.com/vllm-project/vllm-omni/issues/3195",
|
||||
)
|
||||
|
||||
QUANTIZED_MODEL = "Intel/Qwen3-Omni-30B-A3B-Instruct-int4-AutoRound"
|
||||
BASELINE_MODEL = "Qwen/Qwen3-Omni-30B-A3B-Instruct"
|
||||
|
||||
@@ -69,8 +78,7 @@ quant_params = [(QUANTIZED_MODEL, stage_config)]
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_ISSUE_3195
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||
def test_text_to_text(omni_runner, omni_runner_handler):
|
||||
@@ -89,8 +97,7 @@ def test_text_to_text(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_ISSUE_3195
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||
def test_audio_to_text(omni_runner, omni_runner_handler):
|
||||
@@ -114,8 +121,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_ISSUE_3195
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||
def test_image_to_text(omni_runner, omni_runner_handler):
|
||||
@@ -137,8 +143,7 @@ def test_image_to_text(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_ISSUE_3195
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||
def test_video_to_text(omni_runner, omni_runner_handler):
|
||||
@@ -160,8 +165,7 @@ def test_video_to_text(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_ISSUE_3195
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||
def test_video_to_audio(omni_runner, omni_runner_handler):
|
||||
@@ -182,8 +186,7 @@ def test_video_to_audio(omni_runner, omni_runner_handler):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_ISSUE_3195
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=2)
|
||||
@pytest.mark.parametrize("omni_runner", quant_params, indirect=True)
|
||||
def test_mix_to_audio(omni_runner, omni_runner_handler):
|
||||
@@ -68,7 +68,7 @@ def get_prompt():
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_runner", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_runner, omni_runner_handler) -> None:
|
||||
|
||||
@@ -64,7 +64,7 @@ def get_prompt():
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_runner", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_runner, omni_runner_handler) -> None:
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
# Match unprefixed HF id even when MODEL_PREFIX is set (omni_runner resolves full path).
|
||||
_QWEN_IMAGE_RANDOM_ID = "riverclouds/qwen_image_random"
|
||||
|
||||
|
||||
def _is_qwen_image_random(model_path: str) -> bool:
|
||||
return model_path.rstrip("/").endswith(_QWEN_IMAGE_RANDOM_ID)
|
||||
|
||||
|
||||
models = ["Tongyi-MAI/Z-Image-Turbo", "riverclouds/qwen_image_random"]
|
||||
|
||||
# Modelscope can't find riverclouds/qwen_image_random
|
||||
# TODO: When NPU support is ready, remove this branch.
|
||||
if current_omni_platform.is_npu():
|
||||
models = ["Tongyi-MAI/Z-Image-Turbo", "Qwen/Qwen-Image"]
|
||||
|
||||
# omni_runner expects (model, stage_configs_path); single-stage diffusion has no YAML.
|
||||
test_params = [(m, None) for m in models]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "L4", "rocm": "MI325", "xpu": "B60"}, num_cards={"cuda": 1, "rocm": 1, "xpu": 2})
|
||||
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
||||
def test_diffusion_model(omni_runner, run_level):
|
||||
resolved = omni_runner.model_name
|
||||
if run_level == "core_model" and not _is_qwen_image_random(resolved):
|
||||
pytest.skip()
|
||||
|
||||
if run_level == "advanced_model" and _is_qwen_image_random(resolved):
|
||||
pytest.skip()
|
||||
|
||||
# high resolution may cause OOM on L4
|
||||
height = 256
|
||||
width = 256
|
||||
sampling = OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=0.0,
|
||||
generator=torch.Generator(current_omni_platform.device_type).manual_seed(42),
|
||||
num_outputs_per_prompt=2,
|
||||
)
|
||||
|
||||
# OmniRunner.generate() is typed for list[TextPrompt]; diffusion uses Omni.generate(str, ...).
|
||||
outputs = omni_runner.omni.generate(
|
||||
"a photo of a cat sitting on a laptop keyboard",
|
||||
sampling,
|
||||
)
|
||||
|
||||
# Extract images from request_output['images']
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image"
|
||||
if not hasattr(first_output, "request_output") or not first_output.request_output:
|
||||
raise ValueError("No request_output found in OmniRequestOutput")
|
||||
|
||||
req_out = first_output.request_output
|
||||
if not isinstance(req_out, OmniRequestOutput) or not hasattr(req_out, "images"):
|
||||
raise ValueError("Invalid request_output structure or missing 'images' key")
|
||||
|
||||
images = req_out.images
|
||||
|
||||
assert len(images) == 2
|
||||
# check image size
|
||||
assert images[0].width == width
|
||||
assert images[0].height == height
|
||||
images[0].save("image_output.png")
|
||||
@@ -1,56 +0,0 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
from vllm_omni.outputs import OmniRequestOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
models = ["Wan-AI/Wan2.2-T2V-A14B-Diffusers"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", models)
|
||||
def test_video_diffusion_model(model_name: str):
|
||||
with OmniRunner(
|
||||
model_name,
|
||||
boundary_ratio=0.875,
|
||||
flow_shift=5.0,
|
||||
) as runner:
|
||||
# Use minimal settings for testing
|
||||
# num_frames must satisfy: num_frames % vae_scale_factor_temporal == 1
|
||||
# For Wan2.2, vae_scale_factor_temporal=4, so valid values are 5, 9, 13, 17, ...
|
||||
height = 480
|
||||
width = 640
|
||||
num_frames = 5
|
||||
outputs = runner.omni.generate(
|
||||
prompts="A cat sitting on a table",
|
||||
sampling_params_list=OmniDiffusionSamplingParams(
|
||||
height=height,
|
||||
width=width,
|
||||
num_frames=num_frames,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=1.0,
|
||||
generator=torch.Generator(current_omni_platform.device_type).manual_seed(42),
|
||||
),
|
||||
)
|
||||
first_output = outputs[0]
|
||||
assert first_output.final_output_type == "image"
|
||||
if not hasattr(first_output, "request_output") or not first_output.request_output:
|
||||
raise ValueError("No request_output found in OmniRequestOutput")
|
||||
|
||||
req_out = first_output.request_output
|
||||
if not isinstance(req_out, OmniRequestOutput) or not hasattr(req_out, "images"):
|
||||
raise ValueError("Invalid request_output structure or missing 'images' key")
|
||||
|
||||
frames = req_out.images[0]
|
||||
|
||||
assert frames is not None
|
||||
assert hasattr(frames, "shape")
|
||||
# frames shape: (batch, num_frames, height, width, channels)
|
||||
assert frames.shape[1] == num_frames
|
||||
assert frames.shape[2] == height
|
||||
assert frames.shape[3] == width
|
||||
@@ -41,7 +41,8 @@ def _extract_audio(multimodal_output: dict) -> torch.Tensor:
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voxcpm2_zero_shot_001(voxcpm2_engine):
|
||||
"""Test zero-shot TTS produces valid audio output."""
|
||||
@@ -54,7 +55,8 @@ def test_voxcpm2_zero_shot_001(voxcpm2_engine):
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voxcpm2_voice_clone_002(voxcpm2_engine):
|
||||
"""Test voice cloning with a reference audio file.
|
||||
@@ -95,7 +97,8 @@ def test_voxcpm2_voice_clone_002(voxcpm2_engine):
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@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):
|
||||
"""Regression: prefill+decode mixed batch must not crash (PR #2903)."""
|
||||
|
||||
+12
-69
@@ -3,44 +3,28 @@
|
||||
|
||||
"""E2E test for VoxCPM offline inference."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import tests.helpers.runtime as omni_runtime
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from vllm_omni.model_executor.models.voxcpm.voxcpm_runtime_utils import (
|
||||
prepare_voxcpm_hf_config_dir,
|
||||
resolve_voxcpm_model_dir,
|
||||
)
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
VOXCPM_MODEL = os.environ.get("VOXCPM_MODEL", "OpenBMB/VoxCPM1.5")
|
||||
STAGE_CONFIG = str(
|
||||
Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / "voxcpm.yaml"
|
||||
)
|
||||
VOXCPM_MODEL = "OpenBMB/VoxCPM1.5"
|
||||
STAGE_CONFIG = get_deploy_config_path("voxcpm.yaml")
|
||||
SAMPLE_RATE = 24000
|
||||
|
||||
# (model, stage_config_path) for ``@pytest.mark.parametrize("omni_runner", ..., indirect=True)``
|
||||
_OMNI_RUNNER_PARAM = (VOXCPM_MODEL, STAGE_CONFIG)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_npu_cleanup_for_voxcpm(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Limit the NPU cleanup workaround to this VoxCPM test module only."""
|
||||
original_cleanup = omni_runtime.cleanup_dist_env_and_memory
|
||||
|
||||
def _safe_cleanup() -> None:
|
||||
try:
|
||||
original_cleanup()
|
||||
except RuntimeError as exc:
|
||||
if "Allocator for npu is not a DeviceAllocator" in str(exc):
|
||||
return
|
||||
raise
|
||||
|
||||
monkeypatch.setattr(omni_runtime, "cleanup_dist_env_and_memory", _safe_cleanup)
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.tts,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
|
||||
def _build_prompt(text: str) -> dict[str, Any]:
|
||||
@@ -94,50 +78,9 @@ def _extract_final_multimodal_output(outputs) -> dict[str, Any]:
|
||||
raise AssertionError("No multimodal audio output found in VoxCPM generate results")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def voxcpm_model_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> str:
|
||||
model_dir = resolve_voxcpm_model_dir(VOXCPM_MODEL)
|
||||
|
||||
hf_config_env = os.environ.get("VLLM_OMNI_VOXCPM_HF_CONFIG_PATH")
|
||||
if hf_config_env:
|
||||
hf_config_dir = Path(hf_config_env).expanduser()
|
||||
else:
|
||||
hf_config_dir = tmp_path / "voxcpm_hf_config"
|
||||
|
||||
if not (hf_config_dir / "config.json").exists():
|
||||
prepare_voxcpm_hf_config_dir(model_dir, hf_config_dir)
|
||||
|
||||
monkeypatch.setenv("VLLM_OMNI_VOXCPM_HF_CONFIG_PATH", str(hf_config_dir))
|
||||
return str(model_dir)
|
||||
|
||||
|
||||
def test_prepare_voxcpm_hf_config_dir(tmp_path: Path):
|
||||
model_dir = tmp_path / "model"
|
||||
model_dir.mkdir()
|
||||
(model_dir / "config.json").write_text(json.dumps({"hidden_size": 1024}), encoding="utf-8")
|
||||
(model_dir / "generation_config.json").write_text(json.dumps({"do_sample": False}), encoding="utf-8")
|
||||
|
||||
hf_config_dir = prepare_voxcpm_hf_config_dir(model_dir, tmp_path / "voxcpm_hf_config")
|
||||
|
||||
prepared_config = json.loads((hf_config_dir / "config.json").read_text(encoding="utf-8"))
|
||||
assert prepared_config["model_type"] == "voxcpm"
|
||||
assert prepared_config["architectures"] == ["VoxCPMForConditionalGeneration"]
|
||||
assert (hf_config_dir / "generation_config.json").exists()
|
||||
|
||||
|
||||
def test_resolve_voxcpm_model_dir_local_path(tmp_path: Path):
|
||||
model_dir = tmp_path / "OpenBMB" / "VoxCPM1.5"
|
||||
model_dir.mkdir(parents=True)
|
||||
|
||||
assert resolve_voxcpm_model_dir(str(model_dir)) == model_dir
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voxcpm_zero_shot_001(voxcpm_model_path: str):
|
||||
with OmniRunner(voxcpm_model_path, stage_configs_path=STAGE_CONFIG) as runner:
|
||||
outputs = list(runner.omni.generate(_build_prompt("Hello, this is a VoxCPM offline inference test.")))
|
||||
def test_voxcpm_zero_shot_001(omni_runner: OmniRunner) -> None:
|
||||
outputs = list(omni_runner.omni.generate(_build_prompt("Hello, this is a VoxCPM offline inference test.")))
|
||||
|
||||
assert outputs, "No outputs returned"
|
||||
|
||||
@@ -31,7 +31,7 @@ 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, modify_stage_config
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
from vllm_omni.entrypoints.async_omni import AsyncOmni
|
||||
|
||||
MODEL = "mistralai/Voxtral-4B-TTS-2603"
|
||||
@@ -42,6 +42,14 @@ 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).
|
||||
_OMNI_RUNNER_PARAM = (
|
||||
MODEL,
|
||||
STAGE_CONFIG,
|
||||
{"enforce_eager": True},
|
||||
)
|
||||
|
||||
|
||||
def _compose_request(model_name: str, text: str, voice: str) -> dict:
|
||||
"""Build the TTS input dict using mistral tokenizer."""
|
||||
@@ -58,83 +66,60 @@ def _compose_request(model_name: str, text: str, voice: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _resolve_stage_config(run_level: str) -> str:
|
||||
"""Resolve stage config: strip load_format for advanced_model (real weights)."""
|
||||
if run_level == "advanced_model":
|
||||
return modify_stage_config(
|
||||
STAGE_CONFIG,
|
||||
deletes={
|
||||
"stages": {
|
||||
0: ["load_format"],
|
||||
1: ["load_format"],
|
||||
}
|
||||
},
|
||||
)
|
||||
return STAGE_CONFIG
|
||||
@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)
|
||||
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
|
||||
inputs = _compose_request(MODEL, TEST_TEXT, VOICE)
|
||||
|
||||
sampling_params = SamplingParams(max_tokens=2500)
|
||||
sampling_params_list = [sampling_params, sampling_params]
|
||||
|
||||
outputs = list(omni.generate([inputs], sampling_params_list))
|
||||
|
||||
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)
|
||||
if mm and "audio" in mm:
|
||||
audio_data = mm["audio"]
|
||||
break
|
||||
|
||||
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):
|
||||
audio_tensor = audio_data
|
||||
else:
|
||||
audio_tensor = torch.tensor(audio_data)
|
||||
|
||||
audio_array = audio_tensor.float().cpu().numpy()
|
||||
|
||||
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.omni
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_voxtral_tts_offline_basic(run_level):
|
||||
"""Test basic Voxtral TTS offline inference with a voice preset."""
|
||||
stage_config = _resolve_stage_config(run_level)
|
||||
|
||||
with OmniRunner(
|
||||
MODEL,
|
||||
stage_configs_path=stage_config,
|
||||
enforce_eager=True,
|
||||
) as runner:
|
||||
omni = runner.omni
|
||||
inputs = _compose_request(MODEL, TEST_TEXT, VOICE)
|
||||
|
||||
sampling_params = SamplingParams(max_tokens=2500)
|
||||
sampling_params_list = [sampling_params, sampling_params]
|
||||
|
||||
outputs = list(omni.generate([inputs], sampling_params_list))
|
||||
|
||||
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)
|
||||
if mm and "audio" in mm:
|
||||
audio_data = mm["audio"]
|
||||
break
|
||||
|
||||
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):
|
||||
audio_tensor = audio_data
|
||||
else:
|
||||
audio_tensor = torch.tensor(audio_data)
|
||||
|
||||
audio_array = audio_tensor.float().cpu().numpy()
|
||||
|
||||
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.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
def test_voxtral_tts_offline_streaming(run_level):
|
||||
def test_voxtral_tts_offline_streaming():
|
||||
"""Test AsyncOmni streaming inference for Voxtral TTS."""
|
||||
|
||||
async def _run():
|
||||
stage_config = _resolve_stage_config(run_level)
|
||||
|
||||
async_omni = AsyncOmni(
|
||||
model=MODEL,
|
||||
stage_configs_path=stage_config,
|
||||
stage_configs_path=STAGE_CONFIG,
|
||||
stage_init_timeout=300,
|
||||
enforce_eager=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunnerHandler
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "Wan-AI/Wan2.2-T2V-A14B-Diffusers"
|
||||
PROMPT = "Two anthropomorphic cats in boxing gear on a spotlighted stage."
|
||||
NEGATIVE_PROMPT = "low quality, blurry, watermark, text"
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards={"cuda": 1, "rocm": 1})
|
||||
@pytest.mark.parametrize("omni_runner", [(MODEL, None)], indirect=True)
|
||||
def test_text_to_video_001(omni_runner_handler: OmniRunnerHandler):
|
||||
sampling = OmniDiffusionSamplingParams(
|
||||
height=512,
|
||||
width=512,
|
||||
num_frames=8,
|
||||
fps=8,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=4.0,
|
||||
seed=42,
|
||||
)
|
||||
request_config = {
|
||||
"model": MODEL,
|
||||
"prompt": PROMPT,
|
||||
"negative_prompt": NEGATIVE_PROMPT,
|
||||
"sampling_params": sampling,
|
||||
}
|
||||
omni_runner_handler.send_diffusion_request(request_config)
|
||||
@@ -0,0 +1,33 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunnerHandler
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
|
||||
MODEL = "Tongyi-MAI/Z-Image-Turbo"
|
||||
PROMPT = "A high-detail studio photo of an orange tabby cat sitting on a laptop keyboard."
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "L4", "rocm": "MI325", "xpu": "B60"}, num_cards={"cuda": 1, "rocm": 1, "xpu": 2})
|
||||
@pytest.mark.parametrize("omni_runner", [(MODEL, None)], indirect=True)
|
||||
def test_zimage(omni_runner_handler: OmniRunnerHandler):
|
||||
# high resolution may cause OOM on L4
|
||||
sampling = OmniDiffusionSamplingParams(
|
||||
height=256,
|
||||
width=256,
|
||||
num_inference_steps=2,
|
||||
guidance_scale=0.0,
|
||||
seed=42,
|
||||
num_outputs_per_prompt=2,
|
||||
)
|
||||
request_config = {
|
||||
"model": MODEL,
|
||||
"prompt": PROMPT,
|
||||
"sampling_params": sampling,
|
||||
}
|
||||
omni_runner_handler.send_diffusion_request(request_config)
|
||||
+1
-1
@@ -35,7 +35,7 @@ def _audiox_server_cases(model: str):
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.full_model
|
||||
@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:
|
||||
File renamed without changes.
+2
-6
@@ -19,6 +19,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.full_model, pytest.mark.tts]
|
||||
|
||||
MODEL = "FunAudioLLM/Fun-CosyVoice3-0.5B-2512"
|
||||
|
||||
# Official CosyVoice zero-shot prompt audio and its transcript. Vendored under
|
||||
@@ -64,8 +66,6 @@ tts_async_chunk_server_params = [
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_voice_clone_zh_001(omni_server, openai_client) -> None:
|
||||
@@ -88,8 +88,6 @@ def test_voice_clone_zh_001(omni_server, openai_client) -> None:
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_async_chunk_server_params, indirect=True)
|
||||
def test_voice_clone_zh_002(omni_server, openai_client) -> None:
|
||||
@@ -112,8 +110,6 @@ def test_voice_clone_zh_002(omni_server, openai_client) -> None:
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_voice_clone_en_001(omni_server, openai_client) -> None:
|
||||
@@ -7,13 +7,13 @@ Example online tests for Dynin-Omni model.
|
||||
import base64
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from vllm.assets.image import ImageAsset
|
||||
|
||||
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]
|
||||
|
||||
@@ -21,7 +21,7 @@ os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
MODEL = "snu-aidas/Dynin-Omni"
|
||||
STAGE_CONFIG = str(Path(__file__).parent.parent / "stage_configs" / "dynin_omni_ci.yaml")
|
||||
STAGE_CONFIG = get_deploy_config_path("dynin_omni_ci.yaml")
|
||||
|
||||
T2I_PROMPT = "A high quality detailed living room interior photo."
|
||||
T2S_PROMPT = "Please read this sentence naturally: Hello from online serving."
|
||||
|
||||
@@ -12,7 +12,12 @@ Coverage:
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_marks
|
||||
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler
|
||||
from tests.helpers.runtime import (
|
||||
OmniServer,
|
||||
OmniServerParams,
|
||||
OpenAIClientHandler,
|
||||
dummy_messages_from_mix_data,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
|
||||
|
||||
@@ -96,9 +101,10 @@ def _get_diffusion_feature_cases(model: str):
|
||||
indirect=True,
|
||||
)
|
||||
def test_flux2_klein(omni_server: OmniServer, openai_client: OpenAIClientHandler):
|
||||
messages = dummy_messages_from_mix_data(content_text=POSITIVE_PROMPT)
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"messages": [{"role": "user", "content": POSITIVE_PROMPT}],
|
||||
"messages": messages,
|
||||
"extra_body": {
|
||||
"height": 512,
|
||||
"width": 512,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Tests for Flux2 Klein; currently Dev is implemented separately,
|
||||
but ideally these models will fold together in the future.
|
||||
"""
|
||||
|
||||
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 cat sitting on a windowsill"
|
||||
NEGATIVE_PROMPT = "blurry, low quality"
|
||||
|
||||
|
||||
# For now, we only test a good configuration for this model to keep the CI
|
||||
# light, but in the near future may explore using random models to check
|
||||
# cross-feature compatibility more generally.
|
||||
def _get_diffusion_feature_cases(model: str):
|
||||
return [
|
||||
# FP8 / Hybrid sequence parallelism
|
||||
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="black-forest-labs/FLUX.2-klein-4B",
|
||||
),
|
||||
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": 512,
|
||||
"width": 512,
|
||||
"num_inference_steps": 2,
|
||||
"negative_prompt": NEGATIVE_PROMPT,
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42,
|
||||
},
|
||||
}
|
||||
|
||||
openai_client.send_diffusion_request(request_config)
|
||||
File renamed without changes.
@@ -0,0 +1,75 @@
|
||||
# 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)
|
||||
@@ -1,278 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E online serving test for Qwen-Image-Edit-2509 multi-image input.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import openai
|
||||
import pytest
|
||||
import requests
|
||||
from PIL import Image
|
||||
from vllm.assets.image import ImageAsset
|
||||
from vllm.utils.network_utils import get_open_port
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
# Increase timeout for downloading assets from S3 (default 5s is too short for CI)
|
||||
os.environ.setdefault("VLLM_IMAGE_FETCH_TIMEOUT", "60")
|
||||
|
||||
models = ["Qwen/Qwen-Image-Edit-2509"]
|
||||
test_params = models
|
||||
t2i_models = ["Tongyi-MAI/Z-Image-Turbo"]
|
||||
|
||||
|
||||
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__))), # Set working directory to vllm-omni root
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
# Wait for server to be ready
|
||||
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:
|
||||
try:
|
||||
os.killpg(self.proc.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
try:
|
||||
self.proc.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
os.killpg(self.proc.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
self.proc.wait()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def omni_server(request):
|
||||
"""Start vLLM-Omni server as a subprocess with actual model weights."""
|
||||
model = request.param
|
||||
with OmniServer(model, ["--num-gpus", "1"]) as server:
|
||||
yield server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(omni_server):
|
||||
"""OpenAI client for the running vLLM-Omni server."""
|
||||
return openai.OpenAI(
|
||||
base_url=f"http://{omni_server.host}:{omni_server.port}/v1",
|
||||
api_key="EMPTY",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def base64_encoded_images() -> list[str]:
|
||||
"""Base64 encoded PNG images for testing."""
|
||||
images = [
|
||||
ImageAsset("cherry_blossom").pil_image.convert("RGB"),
|
||||
ImageAsset("stop_sign").pil_image.convert("RGB"),
|
||||
]
|
||||
encoded: list[str] = []
|
||||
for img in images:
|
||||
with BytesIO() as buffer:
|
||||
img.save(buffer, format="PNG")
|
||||
encoded.append(base64.b64encode(buffer.getvalue()).decode("utf-8"))
|
||||
return encoded
|
||||
|
||||
|
||||
def dummy_messages_from_image_data(
|
||||
image_data_urls: list[str],
|
||||
content_text: str = "Combine these two images into one scene.",
|
||||
):
|
||||
"""Create messages with image data URLs for OpenAI API."""
|
||||
content = [{"type": "text", "text": content_text}]
|
||||
for image_url in image_data_urls:
|
||||
content.append({"type": "image_url", "image_url": {"url": image_url}})
|
||||
return [{"role": "user", "content": content}]
|
||||
|
||||
|
||||
def _extract_image_data_url(message_content) -> str:
|
||||
assert isinstance(message_content, list) and len(message_content) >= 1
|
||||
content_part = message_content[0]
|
||||
if isinstance(content_part, dict):
|
||||
image_url = content_part.get("image_url", {}).get("url", "")
|
||||
else:
|
||||
image_url_obj = getattr(content_part, "image_url", None)
|
||||
if isinstance(image_url_obj, dict):
|
||||
image_url = image_url_obj.get("url", "")
|
||||
else:
|
||||
image_url = getattr(image_url_obj, "url", "")
|
||||
assert isinstance(image_url, str) and image_url
|
||||
return image_url
|
||||
|
||||
|
||||
def _decode_data_url_to_image_bytes(data_url: str) -> bytes:
|
||||
assert data_url.startswith("data:image")
|
||||
_, b64_data = data_url.split(",", 1)
|
||||
return base64.b64decode(b64_data)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_i2i_multi_image_input_qwen_image_edit_2509(
|
||||
omni_server,
|
||||
base64_encoded_images: list[str],
|
||||
) -> None:
|
||||
"""Test multi-image input editing via OpenAI API with concurrent requests."""
|
||||
image_data_urls = [f"data:image/png;base64,{img}" for img in base64_encoded_images]
|
||||
messages = dummy_messages_from_image_data(image_data_urls)
|
||||
|
||||
barrier = threading.Barrier(2)
|
||||
results: list[tuple[int, int]] = []
|
||||
|
||||
def _call_chat(width: int, height: int) -> None:
|
||||
client = openai.OpenAI(
|
||||
base_url=f"http://{omni_server.host}:{omni_server.port}/v1",
|
||||
api_key="EMPTY",
|
||||
)
|
||||
barrier.wait()
|
||||
chat_completion = client.chat.completions.create(
|
||||
model=omni_server.model,
|
||||
messages=messages,
|
||||
extra_body={
|
||||
"height": height,
|
||||
"width": width,
|
||||
"num_inference_steps": 2,
|
||||
"guidance_scale": 0.0,
|
||||
"seed": 42,
|
||||
},
|
||||
)
|
||||
|
||||
assert len(chat_completion.choices) == 1
|
||||
choice = chat_completion.choices[0]
|
||||
assert choice.finish_reason == "stop"
|
||||
assert choice.message.role == "assistant"
|
||||
|
||||
image_data_url = _extract_image_data_url(choice.message.content)
|
||||
image_bytes = _decode_data_url_to_image_bytes(image_data_url)
|
||||
img = Image.open(BytesIO(image_bytes))
|
||||
img.load()
|
||||
results.append(img.size)
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=_call_chat, args=(1248, 832)),
|
||||
threading.Thread(target=_call_chat, args=(1024, 768)),
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
# TODO @ZJY
|
||||
# assert (1248, 832) in results
|
||||
# assert (1024, 768) in results
|
||||
|
||||
|
||||
@pytest.mark.parametrize("omni_server", t2i_models, indirect=True)
|
||||
def test_t2i_concurrent_requests_different_sizes(omni_server) -> None:
|
||||
"""Test /v1/images/generations concurrent requests with different sizes."""
|
||||
base_url = f"http://{omni_server.host}:{omni_server.port}"
|
||||
url = f"{base_url}/v1/images/generations"
|
||||
|
||||
barrier = threading.Barrier(2)
|
||||
results: list[tuple[int, int]] = []
|
||||
|
||||
def _call_generate(size: str) -> None:
|
||||
payload: dict[str, Any] = {
|
||||
"prompt": "cute cat playing with a ball",
|
||||
"n": 1,
|
||||
"size": size,
|
||||
"response_format": "b64_json",
|
||||
"num_inference_steps": 2,
|
||||
}
|
||||
barrier.wait()
|
||||
response = requests.post(url, json=payload, timeout=120)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
image_b64 = data["data"][0]["b64_json"]
|
||||
image_bytes = base64.b64decode(image_b64)
|
||||
img = Image.open(BytesIO(image_bytes))
|
||||
img.load()
|
||||
results.append(img.size)
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=_call_generate, args=("512x512",)),
|
||||
threading.Thread(target=_call_generate, args=("768x512",)),
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
assert (512, 512) in results
|
||||
assert (768, 512) in results
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
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)
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
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)
|
||||
@@ -53,7 +53,7 @@ try:
|
||||
OmniServerParams(
|
||||
model=model,
|
||||
stage_config_path=stage_config,
|
||||
server_args=["--chat-template", CHAT_TEMPLATE_PATH, "--load-format", "dummy"],
|
||||
server_args=["--chat-template", CHAT_TEMPLATE_PATH],
|
||||
)
|
||||
for model in models
|
||||
for stage_config in stage_configs
|
||||
@@ -68,7 +68,7 @@ except Exception as exc:
|
||||
def get_prompt(prompt_type="text_only"):
|
||||
prompts = {
|
||||
"text_only": "What is the capital of China? Answer in 20 words.",
|
||||
"audio": "What is recited in the audio?",
|
||||
"audio": "What one English word is repeated in the audio?",
|
||||
}
|
||||
return prompts.get(prompt_type, prompts["text_only"])
|
||||
|
||||
@@ -104,6 +104,7 @@ def test_audio_to_text_audio_001(omni_server, openai_client) -> None:
|
||||
"model": omni_server.model,
|
||||
"messages": messages,
|
||||
"stream": True,
|
||||
"sampling_params_list": [{"max_tokens": 64}, {"max_tokens": 64}],
|
||||
"key_words": {
|
||||
"audio": ["test"],
|
||||
},
|
||||
|
||||
+12
-13
@@ -21,6 +21,12 @@ from tests.helpers.stage_config import get_deploy_config_path
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
pytestmark = [pytest.mark.omni, pytest.mark.full_model]
|
||||
|
||||
_SKIP_NEED_4_H100_NOT_CI = pytest.mark.skip(
|
||||
reason="Requires 4x H100 GPUs; skipped in CI for now.",
|
||||
)
|
||||
|
||||
models = ["Jonathan1909/Ming-flash-omni-2.0"]
|
||||
|
||||
# Use thinker-only topology to test text outputs
|
||||
@@ -62,8 +68,7 @@ def get_max_batch_size(size_type="few"):
|
||||
return batch_sizes.get(size_type, 5)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_NEED_4_H100_NOT_CI
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_text_to_text_001(omni_server, openai_client) -> None:
|
||||
@@ -89,8 +94,7 @@ def test_text_to_text_001(omni_server, openai_client) -> None:
|
||||
openai_client.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_NEED_4_H100_NOT_CI
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_text_to_text_stream_001(omni_server, openai_client) -> None:
|
||||
@@ -116,8 +120,7 @@ def test_text_to_text_stream_001(omni_server, openai_client) -> None:
|
||||
openai_client.send_omni_request(request_config, request_num=get_max_batch_size())
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_NEED_4_H100_NOT_CI
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_image_to_text_001(omni_server, openai_client) -> None:
|
||||
@@ -144,8 +147,7 @@ def test_image_to_text_001(omni_server, openai_client) -> None:
|
||||
openai_client.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_NEED_4_H100_NOT_CI
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_audio_to_text_001(omni_server, openai_client) -> None:
|
||||
@@ -172,8 +174,7 @@ def test_audio_to_text_001(omni_server, openai_client) -> None:
|
||||
openai_client.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_NEED_4_H100_NOT_CI
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_video_to_text_001(omni_server, openai_client) -> None:
|
||||
@@ -200,9 +201,7 @@ def test_video_to_text_001(omni_server, openai_client) -> None:
|
||||
openai_client.send_omni_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@_SKIP_NEED_4_H100_NOT_CI
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
def test_mix_to_text_001(omni_server, openai_client) -> None:
|
||||
+7
-8
@@ -25,6 +25,13 @@ 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).
|
||||
pytestmark = [pytest.mark.full_model, pytest.mark.tts]
|
||||
|
||||
_SKIP_ISSUE_3168 = pytest.mark.skip(
|
||||
reason="https://github.com/vllm-project/vllm-omni/issues/3168",
|
||||
)
|
||||
|
||||
MODEL = "OpenMOSS-Team/MOSS-TTS-Nano"
|
||||
REF_AUDIO_URL = "https://raw.githubusercontent.com/OpenMOSS/MOSS-TTS-Nano/main/assets/audio/zh_1.wav"
|
||||
REF_AUDIO_TRANSCRIPT = "欢迎关注模思智能、上海创智学院与复旦大学自然语言处理实验室。"
|
||||
@@ -79,8 +86,6 @@ tts_server_params = [
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_server, openai_client, ref_audio_data_url) -> None:
|
||||
@@ -103,8 +108,6 @@ def test_text_to_audio_001(omni_server, openai_client, ref_audio_data_url) -> No
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_002(omni_server, openai_client, ref_audio_data_url) -> None:
|
||||
@@ -136,8 +139,6 @@ def test_text_to_audio_002(omni_server, openai_client, ref_audio_data_url) -> No
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_003(omni_server, openai_client, ref_audio_data_url) -> None:
|
||||
@@ -160,8 +161,6 @@ def test_text_to_audio_003(omni_server, openai_client, ref_audio_data_url) -> No
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_004_ref_text_ignored(omni_server, openai_client, ref_audio_data_url) -> None:
|
||||
@@ -1,201 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E Online tests for OmniVoice TTS model via /v1/audio/speech endpoint.
|
||||
|
||||
Tests verify that the OmniVoice model generates valid audio when
|
||||
accessed through the standard OpenAI-compatible speech API.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.media import generate_synthetic_audio
|
||||
from tests.helpers.runtime import OmniServerParams
|
||||
|
||||
try:
|
||||
from transformers import HiggsAudioV2TokenizerModel # noqa: F401
|
||||
|
||||
_HAS_VOICE_CLONE = True
|
||||
except ImportError:
|
||||
_HAS_VOICE_CLONE = False
|
||||
|
||||
MODEL = "k2-fsa/OmniVoice"
|
||||
|
||||
STAGE_CONFIG = str(
|
||||
Path(__file__).parent.parent.parent.parent / "vllm_omni" / "model_executor" / "stage_configs" / "omnivoice.yaml"
|
||||
)
|
||||
EXTRA_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--disable-log-stats",
|
||||
]
|
||||
TEST_PARAMS = [
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=STAGE_CONFIG,
|
||||
server_args=EXTRA_ARGS,
|
||||
)
|
||||
]
|
||||
|
||||
MIN_AUDIO_BYTES = 5000
|
||||
|
||||
|
||||
def _get_ref_audio_b64() -> str:
|
||||
"""Generate synthetic speech for reference audio.
|
||||
|
||||
Returns:
|
||||
Base64 data URL string (data:audio/wav;base64,...)
|
||||
"""
|
||||
audio_data = generate_synthetic_audio(duration=2, num_channels=1, sample_rate=24000)
|
||||
return f"data:audio/wav;base64,{audio_data['base64']}"
|
||||
|
||||
|
||||
def make_speech_request(
|
||||
host: str,
|
||||
port: int,
|
||||
text: str,
|
||||
timeout: float = 180.0,
|
||||
) -> httpx.Response:
|
||||
"""Make a request to the /v1/audio/speech endpoint for OmniVoice."""
|
||||
url = f"http://{host}:{port}/v1/audio/speech"
|
||||
payload = {"input": text}
|
||||
|
||||
with httpx.Client(timeout=timeout) as client:
|
||||
return client.post(url, json=payload)
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
|
||||
class TestOmniVoiceTTS:
|
||||
"""E2E tests for OmniVoice TTS model."""
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_speech_auto_voice(self, omni_server) -> None:
|
||||
"""Test auto voice TTS generation (text only, no reference audio)."""
|
||||
response = make_speech_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
text="Hello, this is a test of the OmniVoice text to speech system.",
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert response.headers.get("content-type") == "audio/wav"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV audio"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES, (
|
||||
f"Audio too small ({len(response.content)} bytes), expected > {MIN_AUDIO_BYTES}"
|
||||
)
|
||||
|
||||
|
||||
def make_voice_clone_request(
|
||||
host: str,
|
||||
port: int,
|
||||
text: str,
|
||||
ref_audio_b64: str,
|
||||
ref_text: str | None = None,
|
||||
timeout: float = 180.0,
|
||||
) -> httpx.Response:
|
||||
"""Make a voice cloning request to the /v1/audio/speech endpoint.
|
||||
|
||||
Args:
|
||||
host: Server host
|
||||
port: Server port
|
||||
text: Text to synthesize
|
||||
ref_audio_b64: Base64-encoded reference audio data URL
|
||||
ref_text: Optional transcript of reference audio
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
httpx.Response object
|
||||
"""
|
||||
url = f"http://{host}:{port}/v1/audio/speech"
|
||||
payload = {
|
||||
"input": text,
|
||||
"ref_audio": ref_audio_b64,
|
||||
}
|
||||
if ref_text:
|
||||
payload["ref_text"] = ref_text
|
||||
|
||||
with httpx.Client(timeout=timeout) as client:
|
||||
return client.post(url, json=payload)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _HAS_VOICE_CLONE, reason="Voice cloning requires transformers>=5.3.0")
|
||||
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
|
||||
class TestOmniVoiceVoiceCloning:
|
||||
"""E2E tests for OmniVoice voice cloning functionality."""
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voice_clone_ref_audio_only(self, omni_server) -> None:
|
||||
"""Test voice cloning with ref_audio only (x_vector mode)."""
|
||||
ref_audio_b64 = _get_ref_audio_b64()
|
||||
|
||||
response = make_voice_clone_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
text="Hello, this is a voice cloning test.",
|
||||
ref_audio_b64=ref_audio_b64,
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert response.headers.get("content-type") == "audio/wav"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV audio"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES, (
|
||||
f"Audio too small ({len(response.content)} bytes), expected > {MIN_AUDIO_BYTES}"
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voice_clone_ref_audio_and_text(self, omni_server) -> None:
|
||||
"""Test voice cloning with ref_audio and ref_text (in-context mode)."""
|
||||
ref_audio_b64 = _get_ref_audio_b64()
|
||||
ref_text = "This is the reference transcript."
|
||||
|
||||
response = make_voice_clone_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
text="Hello, this is a voice cloning test with in-context learning.",
|
||||
ref_audio_b64=ref_audio_b64,
|
||||
ref_text=ref_text,
|
||||
)
|
||||
|
||||
assert response.status_code == 200, f"Request failed: {response.text}"
|
||||
assert response.headers.get("content-type") == "audio/wav"
|
||||
assert verify_wav_audio(response.content), "Response is not valid WAV audio"
|
||||
assert len(response.content) > MIN_AUDIO_BYTES, (
|
||||
f"Audio too small ({len(response.content)} bytes), expected > {MIN_AUDIO_BYTES}"
|
||||
)
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voice_clone_invalid_ref_audio_format(self, omni_server) -> None:
|
||||
"""Test that invalid ref_audio format returns a clear error."""
|
||||
response = make_voice_clone_request(
|
||||
host=omni_server.host,
|
||||
port=omni_server.port,
|
||||
text="This should fail with invalid ref_audio.",
|
||||
ref_audio_b64="not_a_valid_uri",
|
||||
)
|
||||
|
||||
assert response.status_code in (400, 422), (
|
||||
f"Expected 400/422 for invalid ref_audio format, got {response.status_code}"
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E Online tests for OmniVoice TTS model via /v1/audio/speech endpoint.
|
||||
|
||||
Tests verify that the OmniVoice model generates valid audio when
|
||||
accessed through the standard OpenAI-compatible speech API.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
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
|
||||
|
||||
try:
|
||||
from transformers import HiggsAudioV2TokenizerModel # noqa: F401
|
||||
|
||||
_HAS_VOICE_CLONE = True
|
||||
except ImportError:
|
||||
_HAS_VOICE_CLONE = False
|
||||
|
||||
pytestmark = [pytest.mark.full_model, pytest.mark.tts]
|
||||
|
||||
MODEL = "k2-fsa/OmniVoice"
|
||||
|
||||
STAGE_CONFIG = get_deploy_config_path("omnivoice.yaml")
|
||||
EXTRA_ARGS = [
|
||||
"--trust-remote-code",
|
||||
"--disable-log-stats",
|
||||
]
|
||||
TEST_PARAMS = [
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=STAGE_CONFIG,
|
||||
server_args=EXTRA_ARGS,
|
||||
)
|
||||
]
|
||||
|
||||
# Lower this in ``request_config`` via ``min_audio_bytes`` if a run produces legitimately short WAVs.
|
||||
_DEFAULT_MIN_AUDIO_BYTES = 5000
|
||||
|
||||
|
||||
REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav")
|
||||
REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
|
||||
|
||||
|
||||
def get_prompt(prompt_type="text"):
|
||||
prompts = {
|
||||
"text": "The weather is nice today, perfect for a walk in the park.",
|
||||
}
|
||||
return prompts.get(prompt_type, prompts["text"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
|
||||
class TestOmniVoiceTTS:
|
||||
"""E2E tests for OmniVoice TTS model."""
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_speech_auto_voice(self, omni_server, openai_client) -> None:
|
||||
"""Test auto voice TTS generation (text only, no reference audio)."""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt("text"),
|
||||
"response_format": "wav",
|
||||
"timeout": 180.0,
|
||||
"min_audio_bytes": _DEFAULT_MIN_AUDIO_BYTES,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _HAS_VOICE_CLONE, reason="Voice cloning requires transformers>=5.3.0")
|
||||
@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True)
|
||||
class TestOmniVoiceVoiceCloning:
|
||||
"""E2E tests for OmniVoice voice cloning functionality."""
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voice_clone_ref_audio_only(self, omni_server, openai_client) -> None:
|
||||
"""Test voice cloning with ref_audio only (x_vector mode)."""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt("text"),
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"response_format": "wav",
|
||||
"timeout": 180.0,
|
||||
"min_audio_bytes": _DEFAULT_MIN_AUDIO_BYTES,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voice_clone_ref_audio_and_text(self, omni_server, openai_client) -> None:
|
||||
"""Test voice cloning with ref_audio and ref_text (in-context mode)."""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt("text"),
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"ref_text": REF_TEXT,
|
||||
"response_format": "wav",
|
||||
"timeout": 180.0,
|
||||
"min_audio_bytes": _DEFAULT_MIN_AUDIO_BYTES,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_voice_clone_invalid_ref_audio_format(self, omni_server, openai_client) -> None:
|
||||
"""Test that invalid ref_audio format returns a clear error."""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt("text"),
|
||||
"ref_audio": "not_a_valid_uri",
|
||||
"response_format": "wav",
|
||||
"timeout": 180.0,
|
||||
"status_code": (400, 422),
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
@@ -87,7 +87,7 @@ def _assert_audio_outputs(responses: list[OmniResponse], *, expect_text: bool) -
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
@@ -104,7 +104,7 @@ def test_text_only_batch_uses_multi_replica_talker(omni_server, openai_client) -
|
||||
_assert_text_outputs(responses)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
@@ -121,7 +121,7 @@ def test_text_to_audio_stream_batch_uses_multi_replica_vocoder(omni_server, open
|
||||
_assert_audio_outputs(responses, expect_text=False)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.full_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
|
||||
|
||||
@@ -20,6 +20,7 @@ from tests.helpers.runtime import OmniServerParams
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
|
||||
DEFAULT_AUDIO_SPEECH_TIMEOUT_S = 180.0
|
||||
|
||||
# Vendored under tests/assets/qwen3_tts/clone_2.wav so the server does not need
|
||||
# to fetch the reference audio over HTTPS at request time (CI runners in some
|
||||
@@ -56,7 +57,7 @@ tts_server_params = [
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_server, openai_client) -> None:
|
||||
@@ -73,6 +74,7 @@ def test_text_to_audio_001(omni_server, openai_client) -> None:
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": False,
|
||||
"timeout": DEFAULT_AUDIO_SPEECH_TIMEOUT_S,
|
||||
"response_format": "wav",
|
||||
"task_type": "Base",
|
||||
"voice": "clone",
|
||||
@@ -83,7 +85,7 @@ def test_text_to_audio_001(omni_server, openai_client) -> None:
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_002(omni_server, openai_client) -> None:
|
||||
@@ -99,6 +101,7 @@ def test_text_to_audio_002(omni_server, openai_client) -> None:
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt(),
|
||||
"stream": True,
|
||||
"timeout": DEFAULT_AUDIO_SPEECH_TIMEOUT_S,
|
||||
"response_format": "wav",
|
||||
"task_type": "Base",
|
||||
"voice": "clone",
|
||||
|
||||
@@ -16,7 +16,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.omni]
|
||||
pytestmark = [pytest.mark.full_model, pytest.mark.tts]
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
@@ -49,7 +49,7 @@ tts_server_params = [
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_001(omni_server, openai_client) -> None:
|
||||
@@ -75,7 +75,7 @@ def test_text_to_audio_001(omni_server, openai_client) -> None:
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@pytest.mark.tts
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_text_to_audio_002(omni_server, openai_client) -> None:
|
||||
|
||||
@@ -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.full_model, pytest.mark.tts]
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Online serving tests for ``Qwen/Qwen-Image`` (text-to-image).
|
||||
|
||||
- ``test_text_to_image_001``: single chat request, default server (``_get_default_case``).
|
||||
- ``test_batch_001``: concurrent prompts via ``send_diffusion_request([cfg0, cfg1, ...])`` — one
|
||||
dict per prompt; each entry carries its own ``messages`` / ``negative_prompt`` (see
|
||||
``TEST_PROMPTS``).
|
||||
- ``test_acceleration_feature_001``: ``advanced_model`` only — parametrized ``OmniServerParams``
|
||||
with cache/offload/parallel flags (see ``_get_diffusion_feature_cases``).
|
||||
|
||||
Full feature grids for Qwen-Image remain in ``test_qwen_image_expansion.py`` (L4).
|
||||
|
||||
From ``tests/``::
|
||||
|
||||
pytest -s -v e2e/online_serving/test_qwen_image.py -m "core_model and diffusion" --run-level=core_model
|
||||
pytest -s -v e2e/online_serving/test_qwen_image.py -m "advanced_model and diffusion" --run-level=advanced_model
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_marks
|
||||
from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
MODEL = "Qwen/Qwen-Image"
|
||||
T2I_PROMPT = "A photo of a cat sitting on a laptop keyboard, digital art style."
|
||||
NEGATIVE_PROMPT = "blurry, low quality"
|
||||
|
||||
SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
|
||||
PARALLEL_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"}, num_cards=2)
|
||||
|
||||
TEST_PROMPTS: list[dict[str, str]] = [
|
||||
{"prompt": "a cup of coffee on a table", "negative_prompt": "low resolution"},
|
||||
{"prompt": "a toy dinosaur on a sandy beach", "negative_prompt": "cinematic, realistic"},
|
||||
{"prompt": "a futuristic city skyline at sunset", "negative_prompt": "blurry, foggy"},
|
||||
{"prompt": "a bowl of fresh strawberries", "negative_prompt": "low detail"},
|
||||
{"prompt": "a medieval knight standing in the rain", "negative_prompt": "modern clothing"},
|
||||
{"prompt": "a cat wearing sunglasses lounging in a garden", "negative_prompt": "dark lighting"},
|
||||
{"prompt": "a spaceship flying above a volcano", "negative_prompt": "low contrast"},
|
||||
{"prompt": "a watercolor painting of a mountain lake", "negative_prompt": "photo, realistic"},
|
||||
]
|
||||
|
||||
|
||||
def _get_default_case(model: str):
|
||||
"""Return a single default ``OmniServerParams`` row (no extra ``server_args``)."""
|
||||
return [
|
||||
pytest.param(
|
||||
OmniServerParams(model=model),
|
||||
id="default",
|
||||
marks=SINGLE_CARD_FEATURE_MARKS,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize("omni_server", _get_default_case(MODEL), indirect=True)
|
||||
def test_text_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None:
|
||||
"""Default Qwen-Image T2I smoke (single ``default`` server config)."""
|
||||
messages = dummy_messages_from_mix_data(content_text=T2I_PROMPT)
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"messages": messages,
|
||||
"extra_body": {
|
||||
"height": 512,
|
||||
"width": 512,
|
||||
"num_inference_steps": 2,
|
||||
"negative_prompt": NEGATIVE_PROMPT,
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42,
|
||||
},
|
||||
}
|
||||
openai_client.send_diffusion_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize("omni_server", _get_default_case(MODEL), indirect=True)
|
||||
def test_batch_001(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None:
|
||||
"""Concurrent T2I: one ``request_config`` dict per prompt (``send_diffusion_request`` list mode)."""
|
||||
request_config = [
|
||||
{
|
||||
"model": omni_server.model,
|
||||
"messages": dummy_messages_from_mix_data(content_text=prompt["prompt"]),
|
||||
"extra_body": {
|
||||
"height": 512,
|
||||
"width": 512,
|
||||
"num_inference_steps": 2,
|
||||
"negative_prompt": prompt["negative_prompt"],
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42,
|
||||
},
|
||||
}
|
||||
for prompt in TEST_PROMPTS
|
||||
]
|
||||
openai_client.send_diffusion_request(request_config)
|
||||
@@ -0,0 +1,147 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""
|
||||
Online serving tests for the Qwen-Image-Edit family (image-to-image via chat completions).
|
||||
|
||||
- ``test_single_image_to_image_001``: ``Qwen/Qwen-Image-Edit`` — one reference image, fixed 512×512.
|
||||
- ``test_multi_images_to_image_001``: ``Qwen/Qwen-Image-Edit-2509`` — two reference images, fixed 512×512.
|
||||
- ``test_different_sizes_001``: ``Qwen/Qwen-Image-Edit-2509`` only, ``advanced_model`` — mixed input
|
||||
resolutions; ``extra_body`` uses per-output ``width``/``height`` lists; the test client sends one
|
||||
scalar-size request per list index in parallel and merges images (see ``OpenAIClientHandler.send_diffusion_request``).
|
||||
|
||||
``_get_diffusion_feature_cases`` registers a single ``default`` ``OmniServerParams`` row per model.
|
||||
|
||||
From ``tests/``::
|
||||
|
||||
pytest -s -v e2e/online_serving/test_qwen_image_edit.py -m "core_model and diffusion" --run-level=core_model
|
||||
pytest -s -v e2e/online_serving/test_qwen_image_edit.py -m "advanced_model and diffusion" --run-level=advanced_model
|
||||
"""
|
||||
|
||||
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, geometrist image into a Vincent van Gogh style impressionist painting."
|
||||
MULTI_EDIT_PROMPT = (
|
||||
"Transform the first image into a Dadaism collage art. "
|
||||
"Transform the second image into a Vincent van Gogh style painting. "
|
||||
"Then juxtapose the two transformed images into a single artwork for visual contrast."
|
||||
)
|
||||
NEGATIVE_PROMPT = "blurry, low quality, modern, geometrist"
|
||||
SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
|
||||
|
||||
|
||||
def _get_diffusion_feature_cases(model: str):
|
||||
"""Return one ``default`` ``OmniServerParams`` row for ``model`` (no extra ``server_args``)."""
|
||||
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("Qwen/Qwen-Image-Edit"),
|
||||
indirect=True,
|
||||
)
|
||||
def test_single_image_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler):
|
||||
"""Single-reference edit smoke for ``Qwen/Qwen-Image-Edit`` (CFG when negative prompt + ``true_cfg_scale`` > 1)."""
|
||||
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)
|
||||
|
||||
# CFG parallel is only activated when a negative prompt and true_cfg_scale > 1.0 are both present
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"messages": messages,
|
||||
"extra_body": {
|
||||
"height": 512,
|
||||
"width": 512,
|
||||
"num_inference_steps": 2,
|
||||
"negative_prompt": NEGATIVE_PROMPT,
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42,
|
||||
},
|
||||
}
|
||||
|
||||
openai_client.send_diffusion_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize(
|
||||
"omni_server",
|
||||
_get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2509"),
|
||||
indirect=True,
|
||||
)
|
||||
def test_multi_images_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler):
|
||||
"""Two-reference edit smoke for ``Qwen/Qwen-Image-Edit-2509``."""
|
||||
|
||||
image_data_url_list = [f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" for _ in range(2)]
|
||||
|
||||
messages = dummy_messages_from_mix_data(image_data_url=image_data_url_list, content_text=MULTI_EDIT_PROMPT)
|
||||
|
||||
# CFG parallel is only activated when a negative prompt and true_cfg_scale > 1.0 are both present
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"messages": messages,
|
||||
"extra_body": {
|
||||
"height": 512,
|
||||
"width": 512,
|
||||
"num_inference_steps": 2,
|
||||
"negative_prompt": NEGATIVE_PROMPT,
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42,
|
||||
},
|
||||
}
|
||||
|
||||
openai_client.send_diffusion_request(request_config)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize(
|
||||
"omni_server",
|
||||
_get_diffusion_feature_cases("Qwen/Qwen-Image-Edit-2509"),
|
||||
indirect=True,
|
||||
)
|
||||
def test_different_sizes_001(omni_server: OmniServer, openai_client: OpenAIClientHandler):
|
||||
"""Multi-reference edit with distinct input resolutions; per-output ``width``/``height`` as lists (client splits into concurrent scalar-size calls)."""
|
||||
image_data_url_list = [
|
||||
f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}",
|
||||
f"data:image/jpeg;base64,{generate_synthetic_image(384, 256)['base64']}",
|
||||
]
|
||||
|
||||
messages = dummy_messages_from_mix_data(image_data_url=image_data_url_list, content_text=MULTI_EDIT_PROMPT)
|
||||
|
||||
# List height/width: client sends 512×512 and 768×512 in parallel, merges to two images; assertions use lists.
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"messages": messages,
|
||||
"extra_body": {
|
||||
"height": [512, 768],
|
||||
"width": [512, 512],
|
||||
"num_inference_steps": 2,
|
||||
"negative_prompt": NEGATIVE_PROMPT,
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42,
|
||||
},
|
||||
}
|
||||
|
||||
openai_client.send_diffusion_request(request_config)
|
||||
@@ -0,0 +1,68 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
Online serving tests for ``Qwen/Qwen-Image-Layered`` (layer decomposition).
|
||||
|
||||
- ``test_feature``: default single-GPU smoke via ``_get_diffusion_feature_cases`` (one ``default`` case).
|
||||
- ``test_layered_output_image_count``: guard for issue #1969 (multi-layer image count).
|
||||
- ``test_empty_prompt``: guard for issue #1966 (empty prompt).
|
||||
|
||||
Broader parallel / cache feature matrices previously lived in ``test_qwen_image_layered_expansion.py``;
|
||||
that module is now a shim that points here.
|
||||
"""
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
MODEL = "Qwen/Qwen-Image-Layered"
|
||||
EDIT_PROMPT = "Decompose this image into layers."
|
||||
MULTI_EDIT_PROMPT = (
|
||||
"Transform the first image into a Dadaism collage art. "
|
||||
"Transform the second image into a Vincent van Gogh style painting. "
|
||||
"Then juxtapose the two transformed images into a single artwork for visual contrast."
|
||||
)
|
||||
NEGATIVE_PROMPT = "blurry, low quality, distorted"
|
||||
SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
|
||||
|
||||
|
||||
def _get_diffusion_feature_cases(model: str):
|
||||
"""Return a single default ``OmniServerParams`` row (no extra ``server_args``)."""
|
||||
return [
|
||||
pytest.param(
|
||||
OmniServerParams(model=model),
|
||||
id="default",
|
||||
marks=SINGLE_CARD_FEATURE_MARKS,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.diffusion
|
||||
@pytest.mark.parametrize("omni_server", _get_diffusion_feature_cases(MODEL), indirect=True)
|
||||
def test_single_image_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler):
|
||||
"""Default Qwen-Image-Layered smoke (single ``default`` server config)."""
|
||||
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": {
|
||||
"num_inference_steps": 2,
|
||||
"negative_prompt": NEGATIVE_PROMPT,
|
||||
"true_cfg_scale": 4.0,
|
||||
"seed": 42,
|
||||
},
|
||||
}
|
||||
|
||||
openai_client.send_diffusion_request(request_config)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
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)
|
||||
@@ -5,7 +5,12 @@ 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
|
||||
from tests.helpers.runtime import (
|
||||
OmniServer,
|
||||
OmniServerParams,
|
||||
OpenAIClientHandler,
|
||||
dummy_messages_from_mix_data,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model]
|
||||
|
||||
@@ -45,9 +50,10 @@ def _get_diffusion_feature_cases(model: str):
|
||||
indirect=True,
|
||||
)
|
||||
def test_sd3_medium(omni_server: OmniServer, openai_client: OpenAIClientHandler):
|
||||
messages = dummy_messages_from_mix_data(content_text=POSITIVE_PROMPT)
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"messages": [{"role": "user", "content": POSITIVE_PROMPT}],
|
||||
"messages": messages,
|
||||
"extra_body": {
|
||||
"height": 1024,
|
||||
"width": 1024,
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E online serving tests for the async video generation API.
|
||||
|
||||
These tests exercise the real `/v1/videos` job lifecycle against a running
|
||||
diffusion model, including create, retrieve, list, download, and delete flows.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniServer
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
os.environ["VLLM_TEST_CLEAN_GPU_MEMORY"] = "0"
|
||||
|
||||
MODEL = "Wan-AI/Wan2.2-T2V-A14B-Diffusers"
|
||||
VIDEO_POLL_INTERVAL_S = 2.0
|
||||
VIDEO_TIMEOUT_S = 900.0
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def video_server():
|
||||
"""Start a real video generation server for async video API tests."""
|
||||
with OmniServer(
|
||||
MODEL,
|
||||
[
|
||||
"--num-gpus",
|
||||
"1",
|
||||
"--boundary-ratio",
|
||||
"0.875",
|
||||
"--flow-shift",
|
||||
"5.0",
|
||||
"--disable-log-stats",
|
||||
],
|
||||
) as server:
|
||||
yield server
|
||||
|
||||
|
||||
def _video_api_url(server: OmniServer, suffix: str = "") -> str:
|
||||
return f"http://{server.host}:{server.port}/v1/videos{suffix}"
|
||||
|
||||
|
||||
def _multipart_fields(payload: dict[str, Any]) -> list[tuple[str, tuple[None, str]]]:
|
||||
return [(key, (None, str(value))) for key, value in payload.items() if value is not None]
|
||||
|
||||
|
||||
def _create_video_job(
|
||||
server: OmniServer,
|
||||
*,
|
||||
prompt: str | None = None,
|
||||
**overrides: Any,
|
||||
) -> requests.Response:
|
||||
payload: dict[str, Any] = {
|
||||
"prompt": prompt or f"video api e2e {uuid.uuid4().hex[:8]}",
|
||||
"width": 640,
|
||||
"height": 480,
|
||||
"num_frames": 5,
|
||||
"fps": 8,
|
||||
"num_inference_steps": 2,
|
||||
"guidance_scale": 1.0,
|
||||
"guidance_scale_2": 1.0,
|
||||
"boundary_ratio": 0.875,
|
||||
"flow_shift": 5.0,
|
||||
"seed": 42,
|
||||
}
|
||||
payload.update(overrides)
|
||||
return requests.post(
|
||||
_video_api_url(server),
|
||||
files=_multipart_fields(payload),
|
||||
timeout=VIDEO_TIMEOUT_S,
|
||||
)
|
||||
|
||||
|
||||
def _retrieve_video_job(server: OmniServer, video_id: str) -> requests.Response:
|
||||
return requests.get(_video_api_url(server, f"/{video_id}"), timeout=VIDEO_TIMEOUT_S)
|
||||
|
||||
|
||||
def _wait_for_video_status(server: OmniServer, video_id: str, expected_status: str) -> dict[str, Any]:
|
||||
deadline = time.time() + VIDEO_TIMEOUT_S
|
||||
last_payload: dict[str, Any] | None = None
|
||||
|
||||
while time.time() < deadline:
|
||||
response = _retrieve_video_job(server, video_id)
|
||||
assert response.status_code == 200, response.text
|
||||
last_payload = response.json()
|
||||
status = last_payload["status"]
|
||||
if status == expected_status:
|
||||
return last_payload
|
||||
if status == "failed":
|
||||
raise AssertionError(f"Video job {video_id} failed unexpectedly: {last_payload}")
|
||||
time.sleep(VIDEO_POLL_INTERVAL_S)
|
||||
|
||||
raise AssertionError(
|
||||
f"Timed out waiting for video job {video_id} to reach status={expected_status}. Last payload: {last_payload}"
|
||||
)
|
||||
|
||||
|
||||
def _wait_for_video_missing(server: OmniServer, video_id: str) -> None:
|
||||
deadline = time.time() + 30.0
|
||||
last_status: int | None = None
|
||||
|
||||
while time.time() < deadline:
|
||||
response = _retrieve_video_job(server, video_id)
|
||||
last_status = response.status_code
|
||||
if response.status_code == 404:
|
||||
return
|
||||
time.sleep(0.5)
|
||||
|
||||
raise AssertionError(f"Timed out waiting for video job {video_id} to disappear. Last status={last_status}")
|
||||
|
||||
|
||||
def _delete_video_job(server: OmniServer, video_id: str) -> requests.Response:
|
||||
return requests.delete(_video_api_url(server, f"/{video_id}"), timeout=VIDEO_TIMEOUT_S)
|
||||
|
||||
|
||||
def _delete_video_job_with_retry(server: OmniServer, video_id: str) -> requests.Response:
|
||||
deadline = time.time() + 30.0
|
||||
last_response: requests.Response | None = None
|
||||
|
||||
while time.time() < deadline:
|
||||
response = _delete_video_job(server, video_id)
|
||||
last_response = response
|
||||
if response.status_code != 409:
|
||||
return response
|
||||
time.sleep(1.0)
|
||||
|
||||
raise AssertionError(
|
||||
f"Timed out waiting to delete video job {video_id}. "
|
||||
f"Last response: {None if last_response is None else last_response.text}"
|
||||
)
|
||||
|
||||
|
||||
def _best_effort_delete(server: OmniServer, video_id: str) -> None:
|
||||
try:
|
||||
response = _delete_video_job_with_retry(server, video_id)
|
||||
if response.status_code not in (200, 404):
|
||||
print(f"Cleanup delete for {video_id} returned {response.status_code}: {response.text}")
|
||||
except Exception as exc:
|
||||
print(f"Cleanup delete for {video_id} failed: {exc}")
|
||||
|
||||
|
||||
def _assert_mp4_payload(content: bytes) -> None:
|
||||
assert len(content) > 32, f"Downloaded video payload is unexpectedly small: {len(content)} bytes"
|
||||
assert content[4:8] == b"ftyp", "Downloaded payload does not look like an MP4 file."
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_create_list_retrieve_download_video(video_server: OmniServer, tmp_path: Path) -> None:
|
||||
video_id: str | None = None
|
||||
try:
|
||||
create_response = _create_video_job(video_server, prompt="A small paper airplane gliding over a lake at dawn.")
|
||||
assert create_response.status_code == 200, create_response.text
|
||||
|
||||
created = create_response.json()
|
||||
video_id = created["id"]
|
||||
assert created["object"] == "video"
|
||||
assert created["status"] == "queued"
|
||||
assert created["model"] == MODEL
|
||||
|
||||
completed = _wait_for_video_status(video_server, video_id, "completed")
|
||||
assert completed["file_name"] is not None
|
||||
assert completed["completed_at"] is not None
|
||||
assert completed["progress"] == 100
|
||||
|
||||
retrieve_response = _retrieve_video_job(video_server, video_id)
|
||||
assert retrieve_response.status_code == 200, retrieve_response.text
|
||||
retrieved = retrieve_response.json()
|
||||
assert retrieved["id"] == video_id
|
||||
assert retrieved["status"] == "completed"
|
||||
|
||||
list_response = requests.get(_video_api_url(video_server), timeout=VIDEO_TIMEOUT_S)
|
||||
assert list_response.status_code == 200, list_response.text
|
||||
listed = list_response.json()
|
||||
assert listed["object"] == "list"
|
||||
assert any(item["id"] == video_id for item in listed["data"])
|
||||
|
||||
download_response = requests.get(
|
||||
_video_api_url(video_server, f"/{video_id}/content"),
|
||||
timeout=VIDEO_TIMEOUT_S,
|
||||
)
|
||||
assert download_response.status_code == 200, download_response.text
|
||||
assert download_response.headers["content-type"].startswith("video/mp4")
|
||||
assert completed["file_name"] in download_response.headers.get("content-disposition", "")
|
||||
_assert_mp4_payload(download_response.content)
|
||||
|
||||
output_path = tmp_path / completed["file_name"]
|
||||
output_path.write_bytes(download_response.content)
|
||||
assert output_path.stat().st_size == len(download_response.content)
|
||||
finally:
|
||||
if video_id is not None:
|
||||
_best_effort_delete(video_server, video_id)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_delete_video_while_queued_or_in_progress(video_server: OmniServer) -> None:
|
||||
create_response = _create_video_job(
|
||||
video_server,
|
||||
prompt="A toy boat sailing through fog.",
|
||||
num_frames=13,
|
||||
num_inference_steps=4,
|
||||
)
|
||||
assert create_response.status_code == 200, create_response.text
|
||||
|
||||
created = create_response.json()
|
||||
video_id = created["id"]
|
||||
assert created["status"] == "queued"
|
||||
|
||||
# Delete immediately so the job is still queued or just entering execution.
|
||||
delete_response = _delete_video_job_with_retry(video_server, video_id)
|
||||
assert delete_response.status_code == 200, delete_response.text
|
||||
deleted = delete_response.json()
|
||||
assert deleted["id"] == video_id
|
||||
assert deleted["deleted"] is True
|
||||
assert deleted["object"] == "video.deleted"
|
||||
|
||||
_wait_for_video_missing(video_server, video_id)
|
||||
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.diffusion
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"})
|
||||
def test_delete_video_after_processing(video_server: OmniServer) -> None:
|
||||
create_response = _create_video_job(
|
||||
video_server,
|
||||
prompt="A lantern drifting above a quiet forest.",
|
||||
seed=43,
|
||||
)
|
||||
assert create_response.status_code == 200, create_response.text
|
||||
|
||||
video_id = create_response.json()["id"]
|
||||
completed = _wait_for_video_status(video_server, video_id, "completed")
|
||||
assert completed["file_name"] is not None
|
||||
|
||||
delete_response = _delete_video_job_with_retry(video_server, video_id)
|
||||
assert delete_response.status_code == 200, delete_response.text
|
||||
deleted = delete_response.json()
|
||||
assert deleted["id"] == video_id
|
||||
assert deleted["deleted"] is True
|
||||
assert deleted["object"] == "video.deleted"
|
||||
|
||||
_wait_for_video_missing(video_server, video_id)
|
||||
|
||||
download_response = requests.get(
|
||||
_video_api_url(video_server, f"/{video_id}/content"),
|
||||
timeout=VIDEO_TIMEOUT_S,
|
||||
)
|
||||
assert download_response.status_code == 404, download_response.text
|
||||
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user