[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:
wangyuandHuang, Zeyu authored and GitHub committed 2026-05-08 14:12:23 +08:00
1 parent b85833eeec
commit 6f2ad7b403
134 files changed
+4904 -4185

No files matched your search

+17 -29
View File
@@ -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 -39
View File
@@ -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"
'
+7 -20
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+64 -5
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -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__)
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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"
+2 -1
View File
@@ -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 |
+4 -4
View File
@@ -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"
+4
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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):
@@ -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.
@@ -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}")
@@ -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
@@ -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())
@@ -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."""
@@ -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",
@@ -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."""
@@ -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.
@@ -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(
@@ -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,
+110
View File
@@ -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}"
@@ -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
@@ -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,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)
-37
View File
@@ -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)
@@ -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)
+79 -161
View File
@@ -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)
@@ -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"
@@ -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)
@@ -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()
@@ -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"
@@ -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)
@@ -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
+6 -3
View File
@@ -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)."""
@@ -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"
+54 -69
View File
@@ -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)
@@ -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:
@@ -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)
@@ -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)
+3 -2
View File
@@ -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"],
},
@@ -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:
@@ -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:
-201
View File
@@ -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"
+105
View File
@@ -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)
+61
View File
@@ -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